ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

微服务请求链路全解析:网关路由、服务发现与上下文透传

微服务请求链路全解析:网关路由、服务发现与上下文透传 面试官问「请求链路怎么走」54 人共创的项目里最容易被问住的是这两段我在很多次面试和被面试里发现微服务项目的请求链路问题几乎必考但大多数人只能说到「网关转发一下服务之间Feign调用一下」就没了。尤其是刚从多人协作的大型项目里走出来的候选人明明自己写过不少接口可一旦被追问「从浏览器输入URL到最终数据库这条路上每个环节做了什么谁注册谁发现谁负载均衡异常了怎么兜底」往往卡在两个地方一个是网关之后、服务实例之前这段路由与发现逻辑另一个是跨服务的调用上下文传递。这两个点恰恰是54人规模项目里由于服务拆得细、协作链条长而被反复锤炼的部分。这篇文章我就结合自己在类似规模项目里的实际经验把这两段链路掰开揉碎讲清楚顺便聊聊面试官到底希望听到什么程度的回答。先说下项目背景方便你们对应代入。我参与过的一个项目组高峰时期有54个后端研发微服务拆了接近40个按业务域分了订单、库存、支付、用户、营销等小组。每个人对自己小组的模块很熟但跨组调用时经常搞不清完整链路。这种规模下代码仓库的PRPull Request动辄涉及两三个服务线上问题排查也常需要拉上四五个小组的人对日志。正因为这样后来不管是内部晋升答辩还是外部面试别人问请求链路时能不能把「负载均衡发生在哪层」「服务实例列表从哪来」「TraceId怎么透传」「线程池里丢没丢上下文」讲清楚几乎成了区分“只写接口”和“理解分布式”的分水岭。1. 这个项目里的请求链路为什么总被面试官盯上面试官爱问请求链路不是因为它新而是因为它能像脑外科手术一样一层层剖开候选人对分布式系统的真实理解。一个经典的请求链路问题通常是用户在浏览器点击下单到看到结果整个过程发生了什么这个问题听着简单但里面藏了DNS解析、Nginx反向代理、网关路由、服务注册发现、负载均衡、序列化协议、线程切换、超时重试、事务一致性等多个考点。在54人共创的项目里链路还有一个额外特点它不是一个「直线」而是一张「网」。比如发起一笔下单请求对外暴露的可能是BFFBackend for Frontend层BFF先调用订单服务订单服务又要调库存服务、优惠券服务、用户服务。而这些服务本身又依赖Redis、MQ、MySQL、Elasticsearch等基础设施。每个环节都由不同小组维护接口契约由各个团队自己定线上问题时很难靠「猜」定位。面试官问链路真正想考察的有三个层面广度你是否知道从客户端到服务器有哪些常见组件参与顺序是什么。深度你是否能说清楚某个关键组件的内部机制比如网关怎么做路由、注册中心怎么做到服务发现、负载均衡策略怎么生效。实战性当链路出现超时、报错、数据不一致时你能否借助链路信息TraceId、日志、Metrics快速定位并解决。很多候选人能在广度上拿分一到深度就露馅。尤其是我前面提到的两段一段是网关路由到具体服务实例的「最后一公里」另一段是服务间调用时的「上下文透传」。这两段在纯业务开发里不一定天天手动配置但出了问题一定要懂。下面分别展开。2. 第一段容易卡壳从网关到服务实例谁在为每一次请求「导航」2.1 网关层到底做了什么不只是转发很多项目用Spring Cloud Gateway或Zuul作为统一入口。很多人以为网关就是「把请求转发到对应服务」其实网关做的事比这多得多。以我常用的Spring Cloud Gateway为例一次请求进入网关后要经历以下处理链路由匹配根据请求的path、method、header等条件匹配到预先配置的RouteDefinition。route的规则通常类似Path/api/order/**如果命中则进入后续过滤逻辑。过滤器链执行Gateway的过滤器分为全局过滤器和针对单个路由的过滤器。常见功能包括JWT或Token校验、签名校验、灰度发布标记、限流RequestRateLimiter、日志记录、请求体修改等。转发目标组装网关通过lb://order-service这种格式的URI将目标服务名交给LoadBalancer负载均衡器。此时还完全没有确定“到底发给哪台机器”。发出请求由负载均衡器从服务注册中心拿到可用实例列表选出一个实例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。常见做法是StripPrefix1去掉第一段前缀或者自定义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 链路追踪IDTraceId和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传给子线程并在子线程执行完清理。使用TransmittableThreadLocalTTL这种专门用于线程池上下文传递的类优化代码的侵入性很多公司直接将其整合到框架层。如果你能在面试中说清楚「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加CloudSleuth非常适合做链路接入。但遇到过的问题是Sleuth对异步场景的Context传播支持有限需要手动处理线程池。后来我们用SkyWalking因为它天然支持异步线程跨线程传播省了很多事。5.3 压测与故障注入提前暴露链路问题只有压测才能发现链路的真实极限。很多项目平时没问题一到双11就崩就是因为没提前做过全链路压测。我参与过的项目中每年大促前都会组织一次全链路压测流程大概是梳理核心链路比如下单、支付确定压测目标TPS。通过压测工具Jmeter或自研压测平台构造请求打入网关。监控各服务CPU、内存、RT、错误率以及MySQL慢查询、Redis大key等指标。找出瓶颈后扩容热点服务、优化SQL、调整线程池参数、增加缓存。故障注入也是很有价值的手段。比如我们会在测试环境随机杀掉某个库存服务实例看订单服务是否能够通过重试或降级方案保证核心请求继续处理或者给某个Feign接口人为加500ms延迟观察上游线程池是否有堆积、是否产生超时。这种演练能暴露出很多代码层面看不到的隐患比如线程池满了后非核心请求把核心请求的资源也占满了导致雪崩。这时候需要引入线程池隔离如Hystrix或Sentinel或者给不同接口配置不同信号量。6. 写在最后想清楚业务边界比背八股更重要我在实际带项目过程中最大的体会是链路不是「画个箭头」那么简单它本质上是一种职责划分。网关、注册中心、负载均衡、远程调用、异步消息、上下文透传每个环节都有它存在的理由但也有它不能越过的边界。比如网关可以做鉴权但不要把业务逻辑堆在网关里服务发现可以解决动态IP问题但不要把注册中心当存储引擎乱用Feign能简化调用但过度依赖同步调用会让整体链路变成“串行瀑布”任何一环慢了都会拖垮整个入口。当你们项目里几十人共同开发时链路的每一次调整都要经过评审和兼容性考虑否则你永远不会知道“我只是改了一个Feign包名为什么线上报错一晚上没停”。回到面试本身当面试官问「请求链路怎么走」时他要的不只是你能说出顺序而是希望看到你在面对真实复杂系统时的结构化思维先全局再关键点最后落到问题排查。把我上面提到的两段——网关到实例的动态路由以及跨服务上下文透传——彻底理解并能在自己的项目里找到对应案例你就有底气去回答。平时多看一眼网关的日志多去查一次TraceId多想想线程池里的用户信息会不会丢这些习惯比背一百道题都管用。
返回列表