尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

微服务链路追踪实战:从traceId到全链路排查

微服务链路追踪实战:从traceId到全链路排查 很多微服务项目前期都是“有日志就行”单服务阶段一个接口报错翻一下日志就能定位。但服务数量一多尤其一个请求要经过网关、鉴权、订单、库存、支付好几个服务之后排查问题就变成了“先找请求路径再看具体节点”。链路追踪解决的核心问题不是“加一组监控”而是把分布式环境里散落在多个进程中的日志、耗时和异常串起来让一个请求从入口到出口的完整路径可以被还原。这也是为什么“微服务一多就必须做链路追踪”不是一句口号而是规模变大之后被现实逼出来的选择。真正开始排查线上问题的人都会发现大多数时候时间不是花在“改代码修Bug”上而是花在“这个请求到底经过了哪些服务、卡在哪一步”上。1. 服务一多原来的排查方式为什么失效1.1 请求路径从“可预期”变成“不可预期”单体架构时代一个请求进来逻辑清楚出错路径基本固定。查日志只需要看一个进程从上往下翻出错位置往往就在最后几行。到了微服务阶段一个接口的前后依赖关系会变得非常复杂。网关转发鉴权服务校验身份订单服务创建订单库存服务扣减库存支付服务发起支付中间可能还夹着消息队列、缓存、分布式任务和第三方接口。服务少的时候团队里两三个人还能靠经验记住主要调用路径。一旦服务数量涨到十个、二十个调用关系图会超出任何人的记忆范围。同一个接口在不同时段可能走不同分支某个分支慢会拖累整个接口某个新上线的服务可能临时修改了内部调用方式但这些变化不会主动通知所有下游使用者。我曾经处理过一个商品详情页变慢的问题。页面接口本身很简单但下面的子接口有十多个分支调用有的查缓存有的回源数据库有的调搜索服务有的调评价服务。某个评分服务在高峰期多了一个慢查询页面整体耗时从200毫秒涨到3秒。如果不看调用链你只能从页面最外层接口一层一层往下猜。1.2 日志分散只看“最后一个服务的报错”解决不了问题很多人一开始做分布式排查还是沿用单体的老经验先看报错再回推原因。这个思路在微服务环境里经常失效。原因在于报错往往出现在最下游的服务但真正的问题可能在上游。比如订单服务调用库存服务库存服务返回了一个“库存不足”的异常。看起来问题在库存侧但如果继续追会发现是因为订单服务传了一个错误的商品ID或者是上游请求里的店铺ID与商品维度不匹配。没有统一的traceId你想把订单服务和库存服务的日志按时间对齐起来难度极高。日志分散也是一个大问题。微服务的日志通常会落在不同虚拟机、不同容器、不同pod里。出了问题先要去日志平台里搜索再把多个服务的时间和节点拼起来。这个过程非常依赖运维系统做得好不好。如果日志平台本身没有把traceId作为索引字段排查一次像做一个手工拼图游戏。更关键的是日志记录的内容经常不在同一个时间粒度上。A服务记录的是入口时间B服务记录的是处理完成时间中间的网络耗时、排队时间和重试时间在单条日志里完全看不出来。1.3 微服务排障的真实成本大头不是“修”而是“找”做微服务时间长了你会发现一个规律真正修Bug的时间往往很短改代码可能只需要几分钟但“找到这个Bug到底发生在哪”可能需要几小时甚至几天。举一个很常见的场景某个下单接口突然变慢用户已经开始投诉。网关超时时间设置的是5秒现在大量请求超过5秒网关直接返回504。订单服务日志显示请求进来了但执行到一半就超时退出。库存服务日志显示某个查询数据库的SQL耗时接近4秒。你以为问题定位在数据库慢查询但继续查下去会发现是因为Redis里某个商品维度的缓存Key长时间未命中缓存回填逻辑又同时被多个线程触发数据库连接池被打满最后导致正常的库存查询被阻塞。这个过程中你至少要看三个服务的日志网关、订单服务、库存服务可能还要查Redis监控和数据库连接池监控。真正动手修只需要把缓存回填的并发限制改一下但定位这个位置可能需要好几个小时。我一般会建议团队做一个简单统计线上问题的平均定位耗时是多少。如果已经超过30分钟而且其中80%的时间是在“找路径”而不是“验证修复方案”那就说明没有链路追踪已经变成一个明显的效率瓶颈。2. 没有链路追踪时线上故障的三种典型形态2.1 接口超时你以为A挂了其实是B慢这是最典型的微服务故障形态。现象是用户请求超时直觉反应是某个服务宕机了。但实际查下来A服务没有宕机只是A服务还在等待B服务的响应。比如A服务接到请求后需要调用B服务获取用户信息。B服务本身没有挂但它依赖的数据库连接池资源不足每个请求排队等待了3秒。A服务自身的处理能力没有问题但因为下游响应慢在线程池资源固定且不会无限等待的情况下A服务的线程也被占满最终导致大量请求超时。只看A服务的监控时你看到的是线程池活跃数很高容易误判为“A服务需要扩容”。只有把调用链打开才能看到A服务的耗时大头在“下游调用等待”上B服务才是导致延迟的根源。2.2 数据不一致同一个操作在两个服务里结果不同微服务拆分之后一个业务流程往往要跨多个服务但跨服务之间并没有单体时代那种单库事务保障。比如订单状态更新成功库存没有扣减用户看到订单已支付但支付回调没有及时通知订单服务退款操作在订单系统成功在财务系统里却没有生成凭证。这类问题最麻烦的地方是每个服务都记录了成功日志你无法从单个服务判断到底是谁先谁后谁调用谁失败补偿逻辑有没有被触发。如果没有链路追踪你只能拿业务流水号或订单号在各个服务的数据库里手工比对效率非常低。有了调用链至少可以看到这个订单请求经过的完整节点以及每一步的返回结果和耗时判断范围能缩小很多。2.3 单个服务都正常但用户就是觉得慢还有一种情况很隐蔽不是某一个服务明显变慢而是所有服务的平均耗时都在正常范围内但用户从入口到出口的总耗时就很高。原因是服务之间的网络往返、序列化、反序列化、网关转发、负载均衡、以及多次的RPC调用叠加在一起。比如一个操作需要串行调用4个服务每个服务只花50毫秒看起来都不算慢但加上4次网络开销和等待时间总耗时可能超过500毫秒。如果再叠加一层网关和一次外部HTTP调用用户体感就会明显变差。这在单服务视角里是看不出来的。只有把整个调用链展示出来看到总耗时和各个Span的耗时占比才能意识到瓶颈不在任何单个节点而在于调用链路的“节点之间”。3. 链路追踪到底做了什么从traceId到完整调用链3.1 核心概念Trace、Span、traceId和parentId链路追踪有三个核心概念理解了它们后面看文档和排查问题都会顺畅很多。Trace指一次请求从入口到出口的完整过程。在调用链上它是一个树状结构根节点是入口请求一级一级往下展开。Span指Trace中的一个独立处理单元。一次HTTP请求、一次数据库访问、一次消息发送都可以是一个Span。每个Span记录了开始时间、结束时间、状态、操作名称、服务名称等关键信息。traceId是连接整个Trace的全局唯一标识。同一个请求经过的所有服务都会把traceId记录在自己的日志和Span里。这样无论日志散落在哪里只要按traceId搜索就能把整条请求串起来。parentId用来标记一个Span由哪个父Span发起。通过parentId系统才能还原出调用层级知道哪个节点先调用、哪个节点是它的下游。链路追踪系统的价值就是围绕traceId把分散的Span聚合成一棵调用树再统一展示起来。3.2 上下文是怎么传递的请求头、RPC Attachment和异步消息链路追踪能跑通关键前提是“traceId要跟着请求走”。在不同场景里传递的方式不一样。HTTP调用通常通过请求头传递。比如网关在入口生成traceId放到请求头里下游服务接收到后从请求头读取再继续传给更下游的服务。RPC调用比如Dubbo这类框架一般通过RPC的Attachment或附加参数传递。Spring Cloud Alibaba、Spring Cloud Sleuth这些组件已经封装好了自动透传逻辑但用得不对还是会有坑比如拦截器没有生效、附加上下文被覆盖、自定义ThreadLocal没有清理干净等。消息队列比如RocketMQ、Kafka一般会选择把traceId放到消息的Header里。消费端收到消息后从Header里取出traceId把消费逻辑也纳入同一条调用链里。这里最重要的认知是链路追踪不是“只有接入组件才有用”还要关注上下文在“创建新线程、跨进程、跨队列”时是否被正确传递。很多链路断掉不是链路追踪系统不行而是业务代码里上下文丢了。3.3 数据链路探针、上报、聚合、存储、展示链路追踪系统的完整工作流程通常包含五个环节。第一步探针或SDK。它可以是一个Java Agent也可以是一个第三方SDK。侵入程度各有不同核心作用是拦截关键调用生成Span。第二步上报。探针把生成的Span数据异步发送给Collector或收集端。这里要注意上报一般不能阻塞业务线程否则会反过来拖慢请求。第三步聚合。同一个traceId的Span会被收集端汇总通过parentId还原调用关系。第四步存储。聚合后的数据会写入存储层常见的包括Elasticsearch、MySQL、H2、ClickHouse等。生产环境一般会优先考虑长期存储能力较强的方案。第五步展示。通过Web界面查看调用链、服务拓扑、耗时分布、错误节点。开源工具里SkyWalking、Zipkin、Jaeger、Pinpoint、Cat都有各自的定位。SkyWalking在Java生态里比较流行使用Java Agent接入对代码侵入极低Zipkin和Jaeger轻量适合和Spring Cloud生态结合但要自己搞定采集器、存储和UIPinpoint对调用细节展示很丰富但资源占用通常偏高Cat偏向日志分析型需要前期投入工程化改造。选型不需要迷信哪个最强重点看团队的运维能力、存储设施和对“侵入程度”的容忍度。我个人会更关注一个问题接入之后能不能在不改业务代码的情况下停掉或降采样避免链路追踪变成生产环境的“另一种故障源”。3.4 只靠调用关系还不够要能看耗时、状态和异常点链路追踪如果只展示“A调用了B”那价值有限。真正能帮助排查问题的是每个Span的耗时、状态码、异常栈、成功失败标志以及缺陷调用有没有触发重试。举个例子一个接口调用了三次下游服务第一次失败后自动重试第二次成功。如果只展示调用链你看到的是一条成功的链路。但如果你把注意力放在耗时上会看到调用链里多了一个耗时很高的Span这个多余耗时就是一次失败重试造成的。这类信息在容量评估、超时设置和接口调优时非常关键。到了生产环境还要考虑采样问题。全量上报会带来很大的存储和网络开销。一般开发环境可以全量采样生产环境按流量设置采样率比如1%或10%具体要看你们对“可回溯性”的要求和数据量。低采样率能接受但不要低到“出问题根本查不到那一天的数据”的程度。4. 从零落地的实操路径先日志traceId再接入开源系统4.1 第一步先给日志统一加上traceId哪怕你的团队暂时没有精力接入完整链路追踪系统我也建议先做这一步让每个日志文件里都能看到traceId。具体做法不复杂在网关或所有请求入口处生成一个traceId放到SLF4J的MDCMapped Diagnostic Context里再在日志格式里加上这个字段比如%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] [%level] %logger - %msg%n。这样每一行日志里都会带上traceId后续在日志平台上按traceId搜索能把同一个请求经过的所有服务日志串起来。这里最容易忽略的是异步线程。接口里开启线程池做计算时子线程里的MDC上下文不会自动从父线程复制过去。如果你不处理子线程里打印的日志traceId就是空的链路又断了。常见处理方式是使用线程池装饰器在提交任务时把父线程的MDC内容复制到子线程任务执行完再恢复。4.2 第二步选一个开源链路追踪工具先跑通单机Demo日志里有了traceId是“靠日志串链路”。如果服务数量一多这个方式仍然很费劲因为要从日志平台里来回搜索耗时也高。这时候就该考虑接入链路追踪系统了。选型之前先想清楚几个前提团队主要技术栈是什么有没有统一的运维平台存储用什么能不能接受Agent方式接入Java技术栈里如果追求低侵入可以先试SkyWalking用Java Agent方式接入业务代码基本不用动。如果是Spring Cloud生态而且团队对中间件理解比较深也可以考虑Zipkin或Jaeger搭配Spring Cloud Sleuth或Micrometer Tracing使用。不管选哪个第一件事都是先在测试环境跑通一个最小Demo部署一个提供HTTP接口的服务手动发起几次请求确认在UI上能看到完整调用链、节点耗时和日志关联再去考虑跨服务传递和批量部署。4.3 第三步网关和RPC调用怎么透传上下文很多团队接入完链路追踪系统后发现了一个奇怪现象服务内部有调用链但跨服务就断了。最常见的原因是网关没有透传traceId或者RPC框架的上下文传递配置不对。网关本身就是流量入口它负责生成traceId并向下游透传。这里要注意网关不能每转发一次就重新生成一个traceId否则所有下游服务记录的traceId五花八门还是串不成链。生成规则应该是入口请求里如果已经带了traceId就继续沿用如果不带网关新生成一个写进请求头再一路传下去。在RPC调用里要关注框架是否已经把traceId放进Attachment或请求头里。如果框架自带支持通常不需要额外处理如果用的是自研RPC、历史遗留框架就要手动在拦截器里取traceId并传递。排查的时候优先看两个方向一个是网关到第一个服务的头部信息有没有带traceId另一个是服务间调用时框架有没有保留上游头部信息。4.4 第四步异步任务、消息队列和定时任务怎么衔接异步场景是最容易让链路追踪“断链”的地方。线程池、MQ Consumer、定时任务、回调如果不在这些入口处理好traceId的传递和恢复链路永远只会显示一半。消息队列是比较典型的情况。生产端在发送消息时把traceId放到消息的Header里消费端接收到消息后先从Header里取出traceId放入当前线程的MDC再执行业务逻辑。这样一条消息的消费处理也能挂到同一条调用链上。定时任务特殊一点它通常不是由用户请求触发而是由调度中心触发。这类场景要建立自己的“根Trace”可以为每次调度生成一个traceId调度任务里所有子任务都继承这个traceId方便事后回溯“某次定时任务为什么跑了很久、某个批次为什么处理失败”。很多团队在排查定时任务相关问题时会发现链路追踪里根本没有定时任务的数据原因就是当时没有做入口Trace初始化。4.5 第五步采样率、存储和保留策略要提前设计链路追踪接入后最怕的就是“存储压力过大”。生产环境建议先按业务量估算数据量。一个请求平均产生多少个Span日请求量有多少再乘上采样率就能粗略估算存储需求。如果日请求量达到百万级别全量采集会产生大量数据存储成本会迅速上升。这时候就要考虑降采样比如按百分比采样或者按接口重要性设置不同采样策略。我一般会建议开发环境全量采样方便排查预发环境按50%或100%采样用来做上线前的链路验证生产环境核心交易链路做较高采样日志类、查询类、非核心链路做低采样。具体数字不是死的要结合团队存储能力和诉求来调。采样率调低带来的问题是线上出故障时可能恰好没有采到那条请求。这对存储压力大的团队来说属于可接受的取舍但如果你所在的业务对可观测性要求很高建议宁可多花存储成本也至少保证核心交易链路的全量采样。5. 落地后最常见的问题和排查顺序5.1 链路只显示半截先怀疑上下文丢失链路追踪接入完成后最常见的现象是同一个请求前面几个服务有链路到某个服务之后就没有了。这种情况我首先会怀疑“请求在进入那个服务时traceId没有传递过去”。检查顺序一般是先看不完整链路的边界是在哪个服务。再看这个服务是通过什么方式被调用的HTTP还是RPC还是MQ。HTTP就查请求头里有没有带traceIdRPC就查Attachment里有没有MQ就查消息Header里有没有。确认调用方有没有做透传被调用方有没有从对应位置读取。最后确认这个服务里有没有异步线程中间有没有重启线程池、新开Thread、使用CompletableFuture并行调用。排查过程中很多问题不是出在框架而是业务代码里“手动开了线程、又没把MDC内容复制过去”。这种问题用单机日志不一定能发现因为日志能打印但traceId是缺的或者跟主链路对不上。5.2 traceId在日志里没打出来查MDC生命周期和线程隔离有时候链路系统里能看到Trace但日志文件里每一行traceId都是空的或者同一个请求在不同行里traceId不一样。这种情况基本是MDC的使用问题。重点检查三处日志格式里是否真的引入了%X{traceId}。traceId是否在拦截器或过滤器里放进了MDC并且有没有被后续代码提前清除。异步线程里有没有复制主线程的MDC上下文执行完之后有没有清理。MDC底层是ThreadLocal天然具备线程隔离性但正因如此在线程复用场景里如果不注意清理很容易出现“traceId串了”的现象本来没有traceId的任务复用了上一个任务残留的traceId。所以入线程时复制MDC出线程时清空MDC这两个步骤都要有。5.3 链路有了但看不出谁慢看Span父子关系和耗时占比链路追踪系统把Trace展示出来后很多人只会看最后的耗时然后凭感觉判断瓶颈。更可靠的判断方式是看整条链路的耗时分布。比如一个接口总耗时3秒但调用链里第一个Span显示网关耗时0.5秒订单服务耗时2秒库存服务耗时0.3秒。那重点就应该放在订单服务里继续看它的子Span到底是在查询数据库、调用外部HTTP、还是写消息队列消耗了大部分时间。还有一个容易被忽略的是“在等待”的时间。RPC调用的Span耗时通常会包含“发送请求到拿到响应”的整个过程其中既有下游执行耗时也有网络耗时和序列化耗时。如果发现这个Span耗时很大但是下游服务的实际执行时间很小那就要关注客户端连接池、网络抖动、超时重试等问题了。5.4 接入后性能下降检查探针、采样和高频接口CPU链路追踪系统本身也有开销尤其是在Java Agent场景下字节码增强和Span采集会带来一定的CPU、内存、网络消耗。接入之后如果发现服务性能下降优先看这些方面探针日志是否过多默认日志级别是不是DEBUG。高频接口是否没有做采样每个请求都生成大量Span。是否开启了过于细粒度的插件比如数据库调用、Redis调用、HTTP调用全部拦截导致Span数量爆炸。采集端上报的网络路径是否稳定如果上报链路阻塞探针内部缓冲区会积压带来内存压力。排查这类问题时先关闭非必要插件再降低采样率然后再看CPU和内存是否恢复正常。如果问题依旧就要去看探针自身运行时有没有报错、有没有和业务代码冲突。6. 什么时候不得不做什么时候可以再等等6.1 判断标准不是服务数量而是排障成本“微服务一多就必须做链路追踪”这句话里的“多”并没有一个绝对数字更多是看排障成本。我习惯用一组条件来判断指标大概状态建议动作服务数量5个以下团队小可以先做日志traceId链路追踪系统可选服务数量10到20个跨两三个团队建议正式接入链路追踪平均单次线上问题定位耗时超过30分钟建议尽快接入单条请求跨服务深度稳定超过3层建议接入跨团队协作排障频率每个月至少一次建议接入高峰流量下出现超时、数据不一致次数每周都有必须接入这些条件不要求全部满足。只要排障成本已经明显影响到线上恢复速度链路追踪就值得投入。6.2 轻量替代方案统一日志格式按traceId检索如果团队暂时没有人力运维完整的链路追踪系统可以先做一个轻量方案统一日志格式把所有服务的日志都输出成结构化JSON或带固定字段的文本traceId单独作为一个字段统一接入日志平台。这样做的好处是至少你能在出问题时按traceId搜索到一个请求经过的所有日志。排查路径仍然靠人工一步步串但比没有强很多。缺点也很明显不能自动生成调用拓扑不能自动还原Span耗时异常定位还是依赖经验和耐心。所以轻量方案适合服务数量还在可控范围、链路深度不深的阶段。一旦服务规模扩大、线上问题开始频繁出现还是要尽快切到正式链路追踪系统。6.3 分阶段落地不要想一口气做到位链路追踪项目如果一开始就追求大而全反而容易失败。我更建议分阶段往前走。第一阶段只做日志traceId。所有服务统一日志格式入口生成traceId并透传日志平台支持按traceId检索。第二阶段接一个开源链路追踪系统先在一两个核心服务上试点跑通跨服务调用链确认UI和存储逻辑都没问题。第三阶段逐步扩大到网关、RPC、MQ、定时任务等场景打开采样和告警把“按traceId查问题”变成团队默认的排查习惯。第四阶段再考虑把链路追踪和现有监控系统、告警系统、日志平台联动起来形成完整的可观测体系。这套路径的优点是每一步都有明确产出不会在接入阶段就把资源烧光。真正落地的时候最该盯住的不是“看起来全不全面”而是任何一个关键接口是否都能在几秒钟内还原完整调用链以及团队成员是不是已经养成了“先看链路再写结论”的习惯。很多项目最后不是死在技术选型上而是死在“出了故障却没人能快速说清请求到底卡在哪一步”。链路追踪的价值就是让排障从“靠经验猜”变成“靠数据查”。
返回列表