☰
微服务请求链路全解析:网关路由、服务发现与上下文透传
2026/10/8 3:46:05 网站建设 项目流程

面试官问「请求链路怎么走」,54 人共创的项目里最容易被问住的是这两段

我在很多次面试和被面试里发现,微服务项目的请求链路问题几乎必考,但大多数人只能说到「网关转发一下,服务之间Feign调用一下」就没了。尤其是刚从多人协作的大型项目里走出来的候选人,明明自己写过不少接口,可一旦被追问「从浏览器输入URL到最终数据库,这条路上每个环节做了什么,谁注册谁发现谁负载均衡,异常了怎么兜底」,往往卡在两个地方:一个是网关之后、服务实例之前这段路由与发现逻辑,另一个是跨服务的调用上下文传递。这两个点恰恰是54人规模项目里由于服务拆得细、协作链条长而被反复锤炼的部分。这篇文章我就结合自己在类似规模项目里的实际经验,把这两段链路掰开揉碎讲清楚,顺便聊聊面试官到底希望听到什么程度的回答。

先说下项目背景,方便你们对应代入。我参与过的一个项目组高峰时期有54个后端研发,微服务拆了接近40个,按业务域分了订单、库存、支付、用户、营销等小组。每个人对自己小组的模块很熟,但跨组调用时经常搞不清完整链路。这种规模下,代码仓库的PR(Pull Request)动辄涉及两三个服务,线上问题排查也常需要拉上四五个小组的人对日志。正因为这样,后来不管是内部晋升答辩还是外部面试,别人问请求链路时,能不能把「负载均衡发生在哪层」「服务实例列表从哪来」「TraceId怎么透传」「线程池里丢没丢上下文」讲清楚,几乎成了区分“只写接口”和“理解分布式”的分水岭。

1. 这个项目里的请求链路,为什么总被面试官盯上

面试官爱问请求链路,不是因为它新,而是因为它能像脑外科手术一样,一层层剖开候选人对分布式系统的真实理解。一个经典的请求链路问题通常是:用户在浏览器点击下单,到看到结果,整个过程发生了什么?这个问题听着简单,但里面藏了DNS解析、Nginx反向代理、网关路由、服务注册发现、负载均衡、序列化协议、线程切换、超时重试、事务一致性等多个考点。

在54人共创的项目里,链路还有一个额外特点:它不是一个「直线」而是一张「网」。比如发起一笔下单请求,对外暴露的可能是BFF(Backend for Frontend)层,BFF先调用订单服务,订单服务又要调库存服务、优惠券服务、用户服务。而这些服务本身又依赖Redis、MQ、MySQL、Elasticsearch等基础设施。每个环节都由不同小组维护,接口契约由各个团队自己定,线上问题时很难靠「猜」定位。

面试官问链路,真正想考察的有三个层面:

  • 广度:你是否知道从客户端到服务器,有哪些常见组件参与,顺序是什么。
  • 深度:你是否能说清楚某个关键组件的内部机制,比如网关怎么做路由、注册中心怎么做到服务发现、负载均衡策略怎么生效。
  • 实战性:当链路出现超时、报错、数据不一致时,你能否借助链路信息(TraceId、日志、Metrics)快速定位并解决。

很多候选人能在广度上拿分,一到深度就露馅。尤其是我前面提到的两段:一段是网关路由到具体服务实例的「最后一公里」,另一段是服务间调用时的「上下文透传」。这两段在纯业务开发里不一定天天手动配置,但出了问题一定要懂。下面分别展开。

2. 第一段容易卡壳:从网关到服务实例,谁在为每一次请求「导航」

2.1 网关层到底做了什么,不只是转发

很多项目用Spring Cloud Gateway或Zuul作为统一入口。很多人以为网关就是「把请求转发到对应服务」,其实网关做的事比这多得多。以我常用的Spring Cloud Gateway为例,一次请求进入网关后,要经历以下处理链:

  1. 路由匹配:根据请求的path、method、header等条件,匹配到预先配置的RouteDefinition。route的规则通常类似Path=/api/order/**,如果命中,则进入后续过滤逻辑。
  2. 过滤器链执行:Gateway的过滤器分为全局过滤器和针对单个路由的过滤器。常见功能包括:JWT或Token校验、签名校验、灰度发布标记、限流(RequestRateLimiter)、日志记录、请求体修改等。
  3. 转发目标组装:网关通过lb://order-service这种格式的URI,将目标服务名交给LoadBalancer(负载均衡器)。此时还完全没有确定“到底发给哪台机器”。
  4. 发出请求:由负载均衡器从服务注册中心拿到可用实例列表,选出一个实例IP,然后真正发起HTTP请求。

容易卡壳的点在于:很多人以为网关配置了uri: http://localhost:8080或uri: http://order-service就够了。但在生产环境,几乎不会直连某个固定地址,而是用lb://结合服务名。为什么?因为服务实例可能动态扩缩容,IP不固定。lb://前缀正是触发负载均衡机制的关键。

2.2 服务发现:注册中心如何保证「能找到」

当网关拿到order-service这个逻辑服务名,它会向注册中心(Nacos、Eureka或Consul)查询该服务名对应的可用实例列表。注册中心在项目里扮演的角色相当于「通讯录」:每个服务启动时将自身IP、端口、服务名注册进去,同时定时发送心跳来维持「在线」状态。

这里有个很细节的考点:网关和各个微服务在内存中会缓存一份实例列表,并不是每次请求都实时向注册中心拉取。以Nacos为例,客户端通过NacosWatch或NamingService订阅服务变化,本地维护一份host列表。这样设计是为了减少对注册中心的压力,但也带来了「缓存延迟」。如果某个服务实例宕机,且还没被注册中心判定为不健康,或客户端还没刷新本地缓存,就会有少量请求被转发到死掉的实例,造成短暂的连接失败。面试时能说出「注册中心有延迟感知,客户端有本地缓存,因此负载均衡有概率选到不健康节点,需要配合重试机制兜底」,面试官大概率会点头。

2.3 负载均衡算法:到底有几个「选人」策略

拿到实例列表后,负载均衡器要从中选一个。常见的算法有:

  • 轮询:按顺序轮流分配,简单粗暴,但没考虑机器性能差异。
  • 随机:随机选一个,部分场景下可能导致短时流量倾斜。
  • 最少连接数:选当前活跃连接最少的实例,适合长连接或请求处理时间不均匀的服务。
  • 哈希一致性:根据请求某个参数(如userId)哈希选择实例,保证同一用户的请求尽量落在同一台机器,便于本地缓存。

我项目中用的Spring Cloud LoadBalancer,默认是轮询,但可以自定义。真正生产环境里,如果下游服务有缓存或状态,通常会选基于key的哈希策略。但要注意:哈希策略可能会在某台机器下线后导致大量缓存失效,这就是一致性哈希要引入虚拟节点的原因。面试时能答出这些,说明你对负载均衡的理解不是停留在概念,而是踩过坑。

2.4 常见坑:路径重写、超时、鉴权透传

网关这层最容易出问题的地方,我列几个真实的:

  • 路径重写:前端调/api/order/list,网关需要将它转成/order/list发给订单服务,因为服务接口里可能没有/api前缀。如果StripPrefix配置不对,大概率404。常见做法是StripPrefix=1(去掉第一段前缀)或者自定义RewritePath。
  • 超时配置:网关默认的响应超时可能只有几百毫秒。订单服务如果依赖的第三方接口较慢,网关容易先返回504。所以网关的httpclient连接超时、响应超时要设成大于下游服务链路的整体超时,否则会因为「上游不理解下游」而误报。
  • 鉴权信息透传:用户登录后生成JWT,网关校验通过后,通常会把userId等信息解析出来,放入请求头(比如X-User-Id)再转发给下游。下游服务就无需再解析JWT,直接信任网关透传的header。但这里有个安全注意点:如果网关没有剔除客户端传入的伪造X-User-Id头,攻击者可能直接伪造身份。所以网关在转发前必须覆盖或移除内部Header。

面试官深挖时,如果你能主动提到「网关要对内外部Header做隔离」,这绝对是加分项,因为它体现了你的边界安全意识。

3. 第二段容易卡壳:服务间调用的链路透传与上下文,为什么总丢

3.1 Feign/RPC调用的本质:别把它当普通HTTP

在微服务架构中,服务间常用OpenFeign或Dubbo进行调用。很多同学天天写@FeignClient("stock-service"),然后调用接口,但没想过底层发生了什么。Feign本质上是一个「声明式HTTP客户端」,它会把接口方法解析成一个HTTP请求,并通过Client组件(默认是JDK的HttpURLConnection,生产环境常换成OkHttp或Apache HttpClient)发送出去。

重点来了:Feign调用时,目标地址并不是直接写在注解里的stock-service这个字符串。它会通过SpringCloudLoadBalancer的FeignBlockingLoadBalancerClient,先解析出服务名对应的实例列表,然后选择一个实例,拼出http://192.168.1.10:8080/xxx这样的URL再发请求。这个机制和网关转发非常相似,所以服务之间实际上是在「多次执行负载均衡」。

面试官问到这里时,最容易问:「如果服务A调用服务B超时,你会怎么排查?」很多人会直接说看B服务的日志。但实际还有可能A在选实例、建立连接、发送请求、等待响应的过程中就超时了。A的Feign配置包含连接超时和读超时,默认连接超时可能只有几秒,读超时更短。一旦B服务处理需要10秒,A就报Read timed out。这时候不是B挂了,而是A的Feign超时设置不合理。需要分清楚是哪一段超时。

3.2 链路追踪ID:TraceId和SpanId怎么跨服务传递

这是第二段链路中最经典的问题:请求从网关到订单服务,再调库存服务,三处日志怎么串起来?答案是链路追踪ID透传。

通常我们会在网关入口生成一个全局的TraceId(例如UUID或Snowflake ID),然后放入请求头(比如X-Trace-Id)。网关把该请求转发给下游服务时,下游服务会从request header里取出TraceId,放入自己的日志上下文里。当它再调用库存服务时,继续把这个TraceId透传过去。这样,整条请求经过的所有服务日志里都有同一个TraceId。排查时直接拿TraceId去各个系统检索,就能还原完整调用链。

但这里有个大坑:微服务里很多调用不是同步HTTP,而是异步消息(MQ)。比如订单创建成功后,会发送一个「订单创建事件」到Kafka或RocketMQ。消费者从MQ拿到消息时,本质上是另一个线程在处理,新的线程里没有原来的TraceId。如果之前没有把TraceId作为消息头的一部分发送,消费者日志里就找不到关联。所以我们在发送MQ消息时,需要把TraceId塞进消息体的ext字段或消息Header中;消费者消费时再取出,并重新设置到日志上下文。

另一个更隐蔽的坑是线程池异步编排。比如用CompletableFuture或ExecutorService做并行任务,子线程默认不会继承父线程的ThreadLocal变量。而日志框架(如Logback的MDC)正是基于ThreadLocal存储TraceId的。这时候子线程里打印的日志就没有TraceId,甚至可能是空值。解决方案有两个方向:

  • 手动在任务提交时,把父线程的MDC context(包含TraceId)传给子线程,并在子线程执行完清理。
  • 使用TransmittableThreadLocal(TTL)这种专门用于线程池上下文传递的类,优化代码的侵入性,很多公司直接将其整合到框架层。

如果你能在面试中说清楚「ThreadLocal为什么不能跨线程,以及TTL的原理是用装饰器包装Runable,在线程池提交任务时捕获父线程上下文,执行前重新塞入」,那基本在同龄人里稳了。

3.3 分布式调用中的幂等与重试

跨服务调用不可避免会遇到网络抖动,导致请求丢失或响应超时。为了保证最终一致性,我们经常会加「重试」。但重试会带来重复请求问题。所以在面试链路时,一定要能讲出幂等设计。

最常见的做法是唯一流水号:服务A调用服务B时,在请求体里带一个requestId,比如UUID。B服务在接口入口先查Redis或数据库,判断这个requestId是否已经处理过。如果处理过,直接返回上一次的结果。这个方案在支付、下单等场景中非常常见。

但在重试链路里还有一个容易被问到的点:重试次数和超时时间怎么配合。比如服务A调用服务B的接口,设置连接超时2秒,读超时5秒,重试2次。如果B真的处理很慢,A可能会因为重试而堆积大量请求,反而压垮B。所以更高阶的做法是结合「超时时间预算」和「流量控制」,必要时采取「快速失败」而非无限重试。在多人协作的项目里,往往由架构组统一规定Feign的最大重试次数、超时基线,避免各个服务组随意配置导致雪崩。

3.4 上下文传递还包含「业务身份」

除了TraceId,服务间调用还需要传递业务身份信息,比如当前用户ID、用户角色、租户ID、语言环境等。通常命名为X-User-Id、X-Tenant-Id。设计时要考虑:

  • 哪些Header是「可信的、仅内部传递」,哪些是「外部传入的、必须校验」。
  • 内部服务之间通过mTLS或内网环境保证安全,同时还需要把内部Header与外部Header隔离。
  • 在Spring Cloud中,可以通过RequestInterceptor来统一往Feign请求头里添加这些上下文,避免每个业务方法手动传参。

但很多项目没做这一步,于是业务代码里到处是userContext参数透传,非常恶心。如果一个线程池异步任务里需要当前用户信息,更麻烦,因为UserContext也是ThreadLocal。所以链路上下文处理,实际上包含了日志上下文、用户上下文、事务上下文、语言上下文等多维度内容。能在面试中把这些梳理清楚,说明你真的负责过跨服务功能。

4. 面试实战:从一次下单请求走通全线

为了让你们更直观地感受面试官想要的答案,我模拟一个真实面试场景。面试官问:「现在有一个下单请求,从点击按钮到返回成功,完整的过程你怎么讲?」你可以按下面的思路组织回答。

4.1 阶段一:客户端到网关

用户在浏览器点击「下单」按钮。浏览器发送HTTP POST请求到经过DNS解析后的域名,通常先到Nginx(或云负载均衡SLB)。Nginx负责终止SSL、做基础的安全防护、静态资源缓存,然后将动态请求反向代理到网关集群。这里要提一下Nginx的负载均衡,如果网关有多个节点,Nginx可以通过upstream配置轮询或ip_hash将请求分发到不同网关实例。当面试官追问「如果网关有状态怎么办」,你要回答网关本身应该是无状态的,session不应该放在本地内存,而应该放到Redis中。Spring Cloud Gateway可以集成Spring Session来把会话数据存到Redis,保证网关实例重启或扩缩容不影响用户会话。

请求到达网关后,先经过全局过滤器。一般我们会先做Token解析,从请求Header的Authorization中拿到JWT,验签后获取userId。校验通过后,网关把userId放入X-User-IdHeader,并移除外部传入的同名Header,防止伪造。然后根据/api/order/**匹配到订单服务的route,经过负载均衡选出一台订单服务实例,发起HTTP调用。

4.2 阶段二:订单服务内部处理

订单服务收到请求后,通过拦截器从X-User-IdHeader中解析出用户上下文,放入ThreadLocal。同时从X-Trace-IdHeader中取出链路ID,放入日志MDC。也就是说,从这一刻起,该服务内所有日志都会自动携带TraceId。然后订单服务开始业务处理:

  • 校验商品参数、用户状态;
  • 生成订单号(通常用雪花算法,保证全局唯一且趋势递增);
  • 保存订单主表到MySQL;
  • 发送「订单创建中」的状态到Redis或MQ。

但下单往往需要实时扣减库存,所以订单服务要调用库存服务。这时候订单服务通过Feign发起远程调用。Feign拦截器会从当前上下文中取出TraceId和UserId,添加到请求头,并带上必要的requestId(幂等键),组成真正的HTTP请求。

4.3 阶段三:库存服务返回与异常兜底

库存服务接收到Feign请求后,同样解析TraceId,写入日志MDC。扣减库存前,它先根据requestId在Redis里判断是否处理过。如果没有处理过,就执行扣数语句,同时记录处理结果;如果处理过,直接返回上次结果。扣数时加锁控制并发(比如分布式锁或数据库行锁),扣减完成后返回成功或失败。

订单服务拿到库存响应后,根据结果决定是否提交订单事务。如果扣减成功,则更新订单状态为「待支付」,返回给用户下单成功。如果库存不足或超时,则触发补偿流程:发送消息给MQ,由队列异步处理订单取消和库存回滚。这里还要提一点,如果Feign调用库存服务时发生超时异常,订单服务不能立刻认定库存服务失败,因为有可能库存服务已经扣减成功了,只是响应没回来。所以必须通过状态查询或者消息对账来确认最终结果。这种「超时后的不确定性」也是面试官最爱的追问点之一。

4.4 面试官追问环节怎么顶住

  • 「分布式事务怎么解决?」你可以回答:下单链路如果要求强一致,可以采用Seata的AT模式(类似两阶段提交);但更多场景用BASE理论,通过本地消息表+MQ实现最终一致性。然后说我们项目中订单和库存通过MQ解耦,订单服务先本地事务写入消息表,再异步发送MQ,库存服务消费后执行扣库存并幂等。
  • 「网关限流怎么做?」可以答:Spring Cloud Gateway基于Redis实现令牌桶限流,通过RequestRateLimiter过滤器配合KeyResolver,按用户维度或IP维度设置令牌桶容量和填充速率。超过阈值直接返回429。
  • 「如何发现实例下线了?」可以答:注册中心通过心跳检测,比如Nacos每5秒检查一次,15秒无心跳标记不健康,30秒移除;服务消费者通过subscribe机制感知变更并更新本地缓存。同时我们会在客户端做快速失败重试,避免下游故障影响本服务。

5. 排查链路问题的实战技巧:日志、追踪、压测三板斧

链路设计得再好,线上出问题时,没有一套排查手段也是白搭。下面分享我在项目里实际用过的三板斧,希望能帮你们避坑。

5.1 日志标准化:没有TraceId的日志都是废日志

在54人项目里,不同小组可能会用不同日志风格。有的喜欢打印参数值,有的不打异常栈,有的起个变量名五花八门。为了排查链路,我们做过一次规范化:所有服务接入统一日志框架,日志pattern固定包含[traceId, spanId, userId]字段。这样任何一个服务的日志都能直接用TraceId检索。这里的关键操作是:

  • 在网关入口生成TraceId,在启动类或Filter中放入MDC;
  • 在服务间的Feign调用、RestTemplate调用、Dubbo调用中,通过拦截器自动透传;
  • 在MQ消费者中,从消息Header取出TraceId并放入MDC;
  • 所有异步线程都需要特殊处理(用TTL或手动拷贝)。

如果你在面试中能说出「我在项目里推动过日志标准化,把原先分散的日志检索时间从半小时缩短到5分钟」,这比背一篇八股文要打动人得多。

5.2 链路追踪工具:SkyWalking / Zipkin怎么用

链路追踪工具本质是在各个服务节点埋点,通过上报gRPC或HTTP数据,把耗时和调用关系汇总到后端分析。以SkyWalking为例,它使用Java Agent字节码增强技术,无需修改业务代码即可实现自动埋点。它可以展示一个请求从网关到调用链路上每个节点的耗时、状态、SQL语句、异常信息。用起来很直观。实操中我建议重点关注几个指标:

  • Span耗时分布:找到耗时最多的Span,大概率就是瓶颈。
  • 上游调用下游的成功率:如果某个接口成功率低于99%,需要关注该下游是否存在抖动。
  • 调用拓扑变化:出现新节点或下线节点时是否影响整体链路。

Zipkin的使用类似,但通常需要结合自定义TracingFilter或Spring Cloud Sleuth。Sleuth会为每次请求生成TraceId和SpanId,并自动注入Feign的Header。如果你的项目是Spring Boot 2.x加Cloud,Sleuth非常适合做链路接入。但遇到过的问题是Sleuth对异步场景的Context传播支持有限,需要手动处理线程池。后来我们用SkyWalking,因为它天然支持异步线程跨线程传播,省了很多事。

5.3 压测与故障注入:提前暴露链路问题

只有压测才能发现链路的真实极限。很多项目平时没问题,一到双11就崩,就是因为没提前做过全链路压测。我参与过的项目中,每年大促前都会组织一次全链路压测,流程大概是:

  • 梳理核心链路(比如下单、支付),确定压测目标TPS。
  • 通过压测工具(Jmeter或自研压测平台)构造请求,打入网关。
  • 监控各服务CPU、内存、RT、错误率,以及MySQL慢查询、Redis大key等指标。
  • 找出瓶颈后扩容热点服务、优化SQL、调整线程池参数、增加缓存。

故障注入也是很有价值的手段。比如我们会在测试环境随机杀掉某个库存服务实例,看订单服务是否能够通过重试或降级方案保证核心请求继续处理;或者给某个Feign接口人为加500ms延迟,观察上游线程池是否有堆积、是否产生超时。这种演练能暴露出很多代码层面看不到的隐患,比如线程池满了后,非核心请求把核心请求的资源也占满了,导致雪崩。这时候需要引入线程池隔离(如Hystrix或Sentinel),或者给不同接口配置不同信号量。

6. 写在最后:想清楚业务边界,比背八股更重要

我在实际带项目过程中最大的体会是,链路不是「画个箭头」那么简单,它本质上是一种职责划分。网关、注册中心、负载均衡、远程调用、异步消息、上下文透传,每个环节都有它存在的理由,但也有它不能越过的边界。比如网关可以做鉴权,但不要把业务逻辑堆在网关里;服务发现可以解决动态IP问题,但不要把注册中心当存储引擎乱用;Feign能简化调用,但过度依赖同步调用会让整体链路变成“串行瀑布”,任何一环慢了都会拖垮整个入口。当你们项目里几十人共同开发时,链路的每一次调整都要经过评审和兼容性考虑,否则你永远不会知道“我只是改了一个Feign包名,为什么线上报错一晚上没停”。

回到面试本身,当面试官问「请求链路怎么走」时,他要的不只是你能说出顺序,而是希望看到你在面对真实复杂系统时的结构化思维:先全局,再关键点,最后落到问题排查。把我上面提到的两段——网关到实例的动态路由,以及跨服务上下文透传——彻底理解并能在自己的项目里找到对应案例,你就有底气去回答。平时多看一眼网关的日志,多去查一次TraceId,多想想线程池里的用户信息会不会丢,这些习惯比背一百道题都管用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询