一、一个跨服务请求的完整旅程
一次"下单"请求,从客户端发出,到网关路由、服务间RPC调用、MQ异步通知,经历5个服务节点、4种中间件——这就是分布式系统的日常。
图1-1:分布式请求全链路流程 — 一次下单请求从客户端到5个服务节点的完整路径
💻 生活类比:微服务 = 公司各部门
微服务 = 公司各部门(订单部 / 用户部 / 通知部),各自独立办公
Gateway 网关 = 公司前台,来访客户先到前台登记,前台根据需求指路到对应部门
RPC 调用 = 内线电话,部门之间直接通话解决需要协作的事情
MQ 消息队列 = 内部快递,不用等人签收,把文件放前台就行,对方有空再来取
Nacos 服务注册 = 公司通讯录,新员工入职登记、离职删记录、定期更新
请求流转关键代码行
1. 网关过滤 — GatewayFilterChain.filter() 请求经过前置过滤器链,做路由匹配、鉴权、限流
2. 负载均衡选择 — LoadBalancerClient.choose() 从 Nacos 拿到实例列表后选择一台机器
3. RPC 远程调用 — Feign.invoke() Order Service 通过 Feign 同步调用 User Service
4. 发送消息 — RabbitTemplate.convertAndSend() 订单创建后发消息到 MQ,异步通知
5. 消费消息 — @RabbitListener.onMessage() Notification Service 监听到消息后发送邮件/短信
// FilteringWebHandler.java — Gateway 过滤器链核心
public class FilteringWebHandler implements WebHandler {
private final List<GatewayFilter> filters;
public FilteringWebHandler(List<GatewayFilter> filters) {
this.filters = new ArrayList<>(filters);
this.filters.sort(AnnotationAwareOrderComparator.INSTANCE);
}
@Override
public Mono<Void> handle(ServerWebExchange exchange) {
// 1. 构建过滤器链,依次执行每个 GatewayFilter
return new DefaultGatewayFilterChain(filters)
.filter(exchange) // ← GatewayFilterChain.filter()
.then(() -> Mono.empty());
}
private static class DefaultGatewayFilterChain {
public Mono<Void> filter(ServerWebExchange exchange) {
// 2. 递归调用:当前 filter 执行后调用下一个
return filters.get(index).filter(exchange, next);
}
}
}
// 流程:RoutePredicate匹配 → FilterChain.filter() → 负载均衡 → 转发请求
二、服务注册与发现——服务之间怎么找到对方
在分布式系统中,服务实例随时可能上下线。Nacos 作为服务注册中心,让服务之间不需要硬编码 IP 地址,而是动态发现彼此。
图2-1:Nacos 服务注册与发现流程 — 注册、心跳、查询、下线通知完整生命周期
💻 生活类比:Nacos = 公司通讯录
新员工入职 = 服务启动,调用 register() 登记到通讯录
每天打卡 = 心跳上报 NacosHeartbeatTask.run(),不打就当离职了
查通讯录找人 = 服务查询 getInstances(),找到对方的位置
离职删记录 = 心跳超时后 deregister(),通讯录中删除,群发通知
有更新群发通知 = Nacos 推送变更,订阅者更新本地缓存
// NacosServiceRegistry.java — 服务注册核心代码
public class NacosServiceRegistry implements ServiceRegistry<Registration> {
private final NamingService namingService;
@Override
public void register(Registration registration) {
// 服务启动时注册实例到 Nacos
Instance instance = new Instance();
instance.setIp(registration.getHost());
instance.setPort(registration.getPort());
instance.setWeight(1.0);
instance.setHealthy(true);
namingService.registerInstance(serviceName, group, instance);
}
@Override
public void deregister(Registration registration) {
// 服务下线时注销实例
namingService.deregisterInstance(serviceName, group, instance);
}
}
// NacosHeartbeatTask.java — 心跳上报
public class NacosHeartbeatTask implements Runnable {
@Override
public void run() {
// 每5秒向 Nacos 发送心跳
if (!instance.isHealthy()) {
namingService.beat(serviceName, beatInfo);
}
}
}
💡 要点
服务实例有三种状态:健康(心跳正常)→ 不健康(15s未收到心跳,不路由但保留)→ 剔除(30s未收到心跳,从列表删除)
客户端会本地缓存实例列表,即使 Nacos 短暂不可用,服务间仍可通信(最终一致性)
三、负载均衡——请求到底打给哪台机器
一个服务部署了3台机器,请求来了给谁?负载均衡就是"前台分配客户"的策略——轮着来、抽签、能者多劳、还是看谁闲着就给谁。
图3-1:负载均衡决策流程 — 从实例列表中选择目标服务器的过程
💻 生活类比:负载均衡 = 前台分配客户
轮询 = 按顺序分,A、B、C、A、B、C... 公平但不管能力
随机 = 抽签决定,简单但不均匀
加权 = 能者多劳,配置高的机器分到更多请求
最少连接 = 看谁手上活少分给谁,最合理但开销大
// BaseLoadBalancer.java — 负载均衡核心
public class BaseLoadBalancer extends AbstractLoadBalancer {
private final IRule rule = new RoundRobinRule();
public Server chooseServer(Object key) {
// 委托给 IRule 策略来选择服务器
return rule.choose(key);
}
}
// RoundRobinRule.java — 轮询策略
public class RoundRobinRule extends AbstractLoadBalancerRule {
private AtomicInteger nextIndex = new AtomicInteger(0);
@Override
public Server choose(Object key) {
List<Server> servers = getLoadBalancer().getAllServers();
// 轮询:取余选择下一个服务器
int next = nextIndex.getAndIncrement() % servers.size();
return servers.get(next);
}
}
// WeightedResponseTimeRule.java — 加权策略
// 根据响应时间动态计算权重: 响应越快权重越高
| 策略 | 原理 | 适用场景 | 代码入口 |
| RoundRobinRule | 按顺序轮流分配 | 机器配置相同 | nextIndex % size |
| RandomRule | 随机选择一台 | 简单场景 | Random.nextInt() |
| WeightedResponseTimeRule | 响应越快权重越高 | 机器配置不同 | weightCalcTimer |
| BestAvailableRule | 选并发请求最少的 | 长连接场景 | server.getActiveRequests() |
四、熔断与限流——系统怎么自我保护
当下游服务崩溃或流量激增,系统需要"保险丝"自动断开、"闸门"控制流量,防止雪崩效应拖垮整个链路。
图4-1:熔断器状态机 — Closed → Open → Half-Open 三态循环
💻 生活类比:熔断 = 保险丝
CLOSED 正常营业 = 电路正常,电流畅通,计数器默默统计失败率
OPEN 暂停营业 = 电流太大,保险丝自动断开,所有请求直接拒绝(快速失败)
HALF-OPEN 试营业 = 过一会儿合上保险丝试试,放一个请求过去看看恢复没有
限流 = 闸门 = 每秒只放 N 个人进门,多了就在外面排队
图4-2:Sentinel 流控插槽链 — 请求经过 NodeSelector → Flow → Degrade 插槽
图4-3:四种限流算法对比 — 计数器、滑动窗口、令牌桶、漏桶
💻 生活类比:限流算法
令牌桶 = 游乐场排队取号,每秒放 N 个号,有号就能进,没号就等。前面没人时可以攒号突发进入
漏桶 = 漏水龙头,不管水多大,出口匀速流出,严格平滑
固定窗口 = 每小时限100人,简单但59分59秒和0分01秒可能瞬间200人
滑动窗口 = 任意1秒内限100人,更精确,解决临界突刺
// Sentinel SphU.entry() — 流控入口
public class SphU {
public static Entry entry(String name) throws BlockException {
// 1. 创建或获取 ProcessorSlotChain
ProcessorSlotChain chain = SlotChainProvider
.newSlotChain();
// 2. 依次执行: NodeSelector → Flow → Degrade
chain.entry(context, resourceWrapper, 1, false);
return entry;
}
}
// FlowSlot.java — 流量控制
public class FlowSlot extends AbstractLinkedProcessorSlot {
@Override
public void entry(...) throws BlockException {
// 检查流控规则: QPS / 线程数
FlowRuleChecker.checkFlow(rules, context, node);
// 通过 → fireEntry() 到下一个 slot
// 不通过 → 抛出 FlowException
fireEntry(context, resourceWrapper, obj, count, prioritized, args);
}
}
// CircuitBreaker.java — 熔断器状态机
public enum State {
CLOSED, // 正常放行
OPEN, // 快速拒绝
HALF_OPEN // 探测放行
}
五、链路追踪——一个请求经过了哪些服务
一个请求跨越5个服务,出问题了怎么定位?链路追踪给每个请求发一个"快递单号"(TraceId),每经过一个服务就记录一站(Span)。
图5-1:TraceId 传播流程 — 跨服务、跨协议的链路标识透传
图5-2:Span 树可视化 — Trace abc123 的时间线拆解
图5-3:Sleuth + Zipkin 架构 — 自动埋点、收集、存储、展示
💻 生活类比:链路追踪 = 快递物流追踪
TraceId = 快递单号,从寄件到收件,同一个单号贯穿始终
SpanId = 每个中转站编号,北京分拣中心、上海转运站、杭州派送点
Sleuth = 快递员自动记录,包裹每到一个站点自动扫描录入
Zipkin = 物流查询网站,输入快递单号,查看完整流转路径和耗时
// TracingFilter.java — Sleuth 自动埋点过滤器
public class TracingFilter implements Filter {
private final Tracer tracer;
@Override
public void doFilter(ServletRequest req, ServletResponse res,
FilterChain chain) throws IOException, ServletException {
// 1. 从请求头中提取 TraceId(如果有的话)
Span span = tracer.nextSpan()
.name("http:" + request.getRequestURI())
.start();
// 2. 将 TraceId 注入到响应头中(传播给下游)
try (Tracer.SpanInScope ws = tracer.withSpanInScope(span)) {
// 3. 注入 TraceId 到请求头,传递给下游服务
tracer.inject(span.context(), Format.Builtin.HTTP_HEADERS,
new HttpHeadersCarrier(request));
chain.doFilter(req, res);
} finally {
span.finish(); // 记录 Span 结束时间和耗时
}
}
}
// 关键传播路径:
// Gateway: Tracer.nextId() → 生成 TraceId
// Service A: Tracer.extract() → 读取 TraceId
// Feign/RPC: Tracer.inject() → 传播到下游
// MQ: TraceId 放入 Message Header → 消费者读取