FEATURED · 精选文章

微服务故障排查利器:全链路追踪从原理到实战

发布时间 / 2026/9/8 11:46:49
来源 / 创域科博编辑部
栏目 / 资讯中心
微服务故障排查利器:全链路追踪从原理到实战 把综艺节目里的名场面翻译成微服务故障现场大致是这样的一次流量波动后订单接口像荡秋千一样忽快忽慢批量查询时所有上游服务都拉一个公共基础服务来当垫脚石为了定位问题团队成员靠打电话、拉群、口头同步逐级确认和“追踪局靠打电话作弊”没有本质区别最后复盘时往往是那个被拉了最多垫脚石的服务承担了所有。综艺有剧本线上故障没有剧本。真正能让团队从这种状态里走出来的不是某个人多打几通电话而是一条贯穿所有调用环节、能被事后回放的全链路追踪系统。1. 先从“荡秋千”看分布式排查为什么这么难1.1 一次调用为什么会像荡秋千一样来回牵扯一个典型的互联网请求在微服务架构里很少只经过一个服务。用户点击下单请求先到网关再到订单服务订单服务要查询会员信息、扣减库存、调用支付、发送消息期间还会经过 Redis、数据库、消息队列和第三方接口。任何一个环节超时都可能触发上游重试重试又会让下游服务压力更大于是出现“越修越慢、越慢越查不出来”的抖动。这种现场让线性排查变得特别难。人脑习惯从单一服务日志出发按时间顺序读日志但分布式场景下同一批请求在不同机器上的时钟、日志级别、打印位置都不一样。某个服务的日志显示“调用库存接口超时”另一个服务的日志显示“库存接口本身正常”两边都没说谎但电话一打就变成了“到底谁有问题”的口水战。真正的事实需要通过一条把请求串起来的追踪数据来还原。1.2 打电话式排查的四个典型缺陷靠打电话和人工查日志定位问题在服务少、流量小的阶段勉强能跑通但一旦服务数量超过十个就会暴露出四个问题。第一信息失真。口头传递时时间点、错误码、参数会被简化甚至带偏常见结果是最后找到的“根因”根本不是最初的现象。第二范围不完整。每个人都只看自己负责的服务无法准确回答“这个请求到底经过了哪些服务”自然也没法判断是不是某个公共组件把链路拖住。第三无法回溯。故障发生时没有留下结构化的调用记录事后想复盘只能靠聊天记录和个人记忆很难形成能指导改进的证据。第四归因模糊。没有数据支撑时大家倾向于把问题算在“平时看起来最忙”或“被依赖最多”的服务头上这正是“总被拉来当垫脚石的服务承担了所有”的由来。与其靠人肉补全信息不如在请求入口处注入一个全局标记让每次调用自动记录“从哪里来、到那里去、花了多久、成功还是失败”。下面用一个最小 Demo 展示这套机制。对比项打电话式排查全链路追踪数据粒度口头描述、片段日志每次请求的完整调用树覆盖范围取决于参与者记忆所有接入服务的 Span日志关联人工对时间点trace_id 自动关联回溯能力聊天记录和个人记忆按 Trace 回放随时可查结果归因依赖讨论和投票依赖耗时、状态和调用关系2. 建立一条贯穿所有环节的追踪链路核心概念先对齐2.1 Trace、Span、Context 是什么先用人话说。Trace 是一条完整请求的“总账单”记录了这次请求从进入系统到返回给用户的全部过程。Span 是总账单上的每一笔明细对应一次具体操作比如调用一次支付接口、执行一条 SQL、消费一条消息。Context 则是贯穿所有明细的“订单号”它让每一笔明细都知道自己属于哪一次请求。在技术实现上每个 Span 都带有 trace_id、span_id、parent_span_id、开始时间、结束时间、属性和状态。Tracer 会根据这些信息把散落在不同进程里的 Span 拼成一棵调用树。最小结构大致是这样order-service /order └── pay-service /pay └── inventory-service /inventory根 Span 是 order-service 的 /order 请求pay-service 的 /pay 是它的子 Spaninventory-service 的 /inventory 又挂到 /pay 下面。在 Jaeger 或链路追踪 UI 中展示出来就是瀑布图每一行宽度代表耗时。把“荡秋千”里的每一个来回映射进去就能看到每一脚踩在哪个服务上。2.2 跨服务上下文传播怎么工作光有 Span 还不够。微服务调用会跨越进程边界必须让下游服务知道“我这次请求属于哪个 trace”这个机制叫上下文传播。目前使用最多的标准是 W3C Trace Context。上游服务发起 HTTP 请求时会在请求头里注入一个traceparent头下游服务读取这个头就能把新创建的 Span 挂到同一个 trace 下。一个traceparent值看起来像这样00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01各段含义可以按下表理解字段段示例值含义version00版本号当前常见为 00trace-id4bf92f3577b34da6a3ce929d0e0e4736全局唯一 trace IDparent-id00f067aa0ba902b7上游 Span IDflags01采样标记01 表示希望被采样使用 OpenTelemetry 的自动化 instrumentation 时Flask 端会自动提取这个头requests 端会自动注入这个头。也就是说只要两边都接入同一套 SDK上下文传播不需要手写 header 处理逻辑。这个细节非常关键很多调用链断掉都是因为某个服务用了普通 HTTP client 而没有接 instrumentation。2.3 追踪系统选型思路OpenTelemetry 负责采集、生成和导出 Trace 数据但数据最终要落到一个能查询、展示的系统里。常见选择有 Jaeger、Zipkin 和 SkyWalking。选型没有绝对最优关键看团队技术栈和现有基础设施。方案数据接入适合场景部署复杂度JaegerOTLP、Jaeger 原生协议需要灵活查看 Trace、轻量自建中单机 all-in-one 即可演示ZipkinZipkin v2、OTLP简单 Trace 展示低SkyWalkingSkyWalking 自有 Agent 协议Java 体系、需要 APM 指标和拓扑高通常需要 Agent 与后端集群下面 Demo 选择 Jaeger因为它支持 OTLP 标准协议Docker 一条命令就能启动UI 对瀑布图、服务名过滤和 trace 搜索都足够直观。3. 最小可复现 Demo三个服务模拟一次故障调用3.1 环境准备和项目结构这次 Demo 用三个 Flask 服务模拟一条真实的调用链order-service 接收用户请求调用 pay-servicepay-service 先做 1 秒的本地处理再调用 inventory-service。为了复现“垫脚石被冤枉”的场景真正的问题被故意放在 pay-service 的本地耗时里而 inventory-service 本身很快。如果只盯着服务监控很容易误以为库存服务是瓶颈通过 Trace可以看到耗时主要花在支付服务自身。准备环境Python 3.9 或更高版本Docker用于启动 Jaeger 服务pip 安装依赖目录结构trace-demo/ requirements.txt otel.py order_service.py pay_service.py inventory_service.pyrequirements.txt 内容如下。为了减少版本适配问题这里不锁死版本安装时以当前稳定版为准flask requests opentelemetry-api opentelemetry-sdk opentelemetry-instrumentation-flask opentelemetry-instrumentation-requests opentelemetry-exporter-otlp-proto-grpc安装命令pip install -r requirements.txt3.2 初始化 OpenTelemetry 并接入 Flask创建otel.py封装初始化逻辑。所有服务共用这个模块保证service.name不同但导出端一致from opentelemetry import trace from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter from opentelemetry.instrumentation.flask import FlaskInstrumentor from opentelemetry.instrumentation.requests import RequestsInstrumentor def init_telemetry(app, service_name): resource Resource.create({service.name: service_name}) provider TracerProvider(resourceresource) exporter OTLPSpanExporter(endpointhttp://localhost:4317, insecureTrue) provider.add_span_processor(BatchSpanProcessor(exporter)) trace.set_tracer_provider(provider) FlaskInstrumentor().instrument_app(app) RequestsInstrumentor().instrument() return trace.get_tracer(service_name)这里要注意三件事。第一Resource.create里的service.name是追踪系统识别服务身份的关键也是之后在 Jaeger 里按服务过滤的依据。第二OTLPSpanExporter的 endpoint 指向 Jaeger 的 OTLP gRPC 端口默认是 4317如果使用 HTTP 协议则是 4318。第三BatchSpanProcessor会攒一批 Span 再异步导出能有效减少 IO但也意味着刚产生的 Span 不会立刻出现在 UI 里查看时要稍等几秒。3.3 三个服务代码order_service.pyfrom flask import Flask import requests from otel import init_telemetry app Flask(__name__) tracer init_telemetry(app, order-service) app.route(/order) def order(): resp requests.get(http://localhost:5001/pay, timeout5) return {order: ok, pay_status: resp.status_code} if __name__ __main__: app.run(port5000)pay_service.pyimport time from flask import Flask import requests from otel import init_telemetry app Flask(__name__) tracer init_telemetry(app, pay-service) app.route(/pay) def pay(): # 模拟支付服务本地处理耗时比如加密计算、慢 SQL 或锁等待 time.sleep(1) resp requests.get(http://localhost:5002/inventory, timeout3) return {pay: ok, inventory_status: resp.status_code} if __name__ __main__: app.run(port5001)inventory_service.pyfrom flask import Flask from otel import init_telemetry app Flask(__name__) tracer init_telemetry(app, inventory-service) app.route(/inventory) def inventory(): return {inventory: ok} if __name__ __main__: app.run(port5002)运行顺序上先启动被依赖方再启动上游会更容易观察。实际项目里服务注册和发现不会这么简单但这个最小模型已经能完整展现 Trace 的传播过程。3.4 启动 Jaeger 和三个服务先用 Docker 启动 Jaegerdocker run -d --name jaeger \ -p 16686:16686 \ -p 4317:4317 \ -p 4318:4318 \ jaegertracing/jaeger:latest启动之后用三个终端分别执行python order_service.pypython pay_service.pypython inventory_service.py然后调用接口time curl http://localhost:5000/order正常情况下响应会接近 1 秒或略多一点因为 pay-service 里 sleep 了 1 秒。多次执行可以留下多条 tracefor i in $(seq 1 10); do curl -s http://localhost:5000/order /dev/null; done到这里本地 Demo 的学习闭环已经能跑通。生产环境不能照搬这套单机方案Jaeger 需要独立部署 collector 和存储OTLP 上报需要考虑鉴权和 TLS生产请求量下还要结合采样、多环境隔离和长期数据保留策略一起设计。4. 用追踪数据回答“到底谁承担了所有”4.1 打开 Jaeger 并定位一条 Trace浏览器访问http://localhost:16686在 Jaeger UI 左侧 Service 下拉框里选择order-serviceOperation 可以留空点击 Find Traces。刚才循环请求产生的 Trace 会按时间倒序出现。如果没有看到数据按下面顺序检查三个服务是否都启动成功Jaeger 的 4317 端口是否被占用服务启动时有没有因为端口冲突立刻退出请求是否真的发到了 order-service从调用到打开 UI 之间有没有等几秒。点击任意一条 Trace 后左侧会展示调用瀑布图。预期看到三层结构order-service /order └── pay-service /pay └── inventory-service /inventory4.2 从瀑布图定位真正的耗时节点在瀑布图中order-service 的总耗时接近 1 秒pay-service 的 Span 也接近 1 秒而 inventory-service 的 Span 只有几十毫秒。如果此时把“订单服务总耗时高、请求慢”误判成“订单服务被库存服务拖垮”就完全走偏了。正确读法是看每个 Span 的Duration和Self Duration。Jaeger 的 Span 详情里会显示这一项它表示当前 Span 自身做了多久不包括子 Span 的时间。点击 pay-service 的 Span能看到它自身耗时接近 1 秒原因是代码里 sleep 的 1 秒被算进了 pay-service 的 Span。inventory-service 虽然“垫在”链路最后但它只占了很小一段根因并不是它。这个例子说明追踪系统给出的不是一句“某个服务慢”而是一条可展开的时间线。真正能回答“谁承担了所有”的是时间线里耗时最大且先出现异常的那一段。4.3 用 trace_id 把调用链和日志串起来演示环境里可以直接看瀑布图但生产环境通常还需要回到日志中看具体异常。养成好习惯日志里一定要输出 trace_id。在 pay_service.py 里可以加一段获取当前 trace_id 的代码from opentelemetry import trace span trace.get_current_span() ctx span.get_span_context() if ctx.is_valid: print(ftrace_id{format(ctx.trace_id, 032x)} span_id{format(ctx.span_id, 016x)})把输出格式统一成 JSON 后日志系统可以按 trace_id 检索。出现告警时研发拿到一个 trace_id就能在日志平台和各服务日志里查出同一条请求的所有上下文。这个操作比一个个服务搜索日志、再打电话对时间点要准确得多。5. 追踪接入后可复用的排查链路5.1 一次告警后的标准排查顺序接入链路追踪后不要立刻回归旧的排查习惯。推荐按下面的顺序处理线上问题先拿到入口信息告警里通常会包含接口、时间、业务侧 trace_id如果没有 trace_id按服务和最近时间段搜索。在追踪系统找到对应 Trace先看整体调用链是否完整。从入口 Span 逐步向下看每个子 Span 的状态和耗时优先找状态为 ERROR 的 Span。对耗时异常的 Span查看 Self Duration确认耗时是发生在该服务自身还是等待子 Span。打开 Span 的 Tags、Logs、Events记录是否有错误堆栈、超时信息、依赖地址。用 trace_id 回到日志平台查该服务内部具体代码路径。结合基础设施指标判断是否伴随 CPU 飙高、连接池耗尽、磁盘延迟等资源问题。修复后对照历史 Trace确认同一条链路的耗时是否恢复。这套顺序的核心是不猜。每一步都基于数据缩小范围而不是先选一个“看起来像根因”的服务再去找证据。5.2 排错清单从现象到处理实际排查时可以直接对照下表问题现象可能原因看哪里处理建议接口整体变慢上游服务本地处理慢入口 Span 下每个子 Span 的耗时定位 Self Duration 最大的 Span调用链断裂只有入口一个 Span跨服务传播未生效或未接入 instrumentation请求头是否有 traceparent检查 HTTP client 是否接入 OTel并配置 PropagatorTrace 中有 ERROR 但日志无异常HTTP 状态码非 2xx 被标记为错误Span Status 和 Events在业务边界明确哪些状态码该作为错误服务在 UI 里找不到service.name 配置错误或 exporter 不通服务启动日志是否报导出错误检查 Resource 和 endpoint某些请求没有 Trace采样率过低或头部采样被拒绝查看 Sampler 配置错误全采、慢请求提高采样率生产环境里这条链路还需要和告警平台打通。理想状态下告警消息里直接附带 trace_id打开就能跳转到对应 Trace能省掉大量人工找数据的时间。6. 常见坑为什么你的追踪图经常断在半路6.1 跨服务传播失效调用链断裂现象是 Jaeger 里只出现单个服务的 Span下游请求没有挂在同一个 Trace 下。常见原因是请求穿过了一个没有接入 instrumentation 的组件比如自定义的 HTTP 客户端、RPC 框架、消息队列。另一个常见场景是网关层自行创建了新 Trace没有透传上游traceparent。检查时在服务入口打印请求头确认是否携带traceparent如果携带了但还是新建 Trace就要看服务端是否启用了 extract 逻辑。解决方式比较简单统一使用官方提供的 instrumentation 库如果用的是框架自带的 Client需要手动调用trace.set_span_in_context并注入 Context。对于消息队列场景需要把 Trace Context 放入消息 headers 中。6.2 服务名不统一Trace 全成了乱码现象是同一个服务在 UI 中出现pay、pay-service、pay_service等多种名字导致无法按服务聚合。服务名看似小事实际会影响后续所有按服务维度做的统计和告警。建议团队维护一份服务名注册表统一使用“业务名-服务名”的小写中划线格式通过部署平台环境变量统一注入service.name而不是每个开发自己在代码里写死。6.3 采样率设置不合理现场丢了很多团队为了控制存储成本把采样率设得很低比如千分之一。这样平时没问题但故障发生时的 Trace 可能因为没被采样而完全看不到。推荐做法是错误 Trace 全采慢请求提高采样率普通流量使用低比例采样。如果使用 OpenTelemetry 的尾部采样处理器可以根据错误状态、耗时阈值、业务属性决定是否保留 Trace把成本花在真正有排查价值的数据上。6.4 只看 Span 总耗时忽略 Self Duration这个坑与本文开头的“垫脚石承担所有”高度相关。看到某个下游 Span 耗时很高就直接判定是该服务的问题是典型的误读。Span 的总耗时包含所有子 Span真正要关注的是当前 Span 自身耗时。比如 pay-service 的 Span 耗时高很可能是因为它在等待 inventory-service但如果没有展开看就会把库存服务当成根因。排查时一定要点开 Span对比 Duration 和 Self Duration并看子 Span 的分布。7. 让数据说话故障复盘里如何不冤枉“垫脚石”服务7.1 复盘时用追踪证据回答四个问题故障复盘最容易出现的不是“没有结论”而是“结论靠投票”。引入链路追踪后可以让复盘围绕四个问题展开。第一个问题哪一个请求失败了不能只写“订单接口 14:30 开始失败”要给出具体的 trace_id、入口服务、用户维度或商户维度。第二个问题这个请求经过了哪些服务用调用瀑布图回答而不是靠每个人回忆。第三个问题时间到底花在哪里要用每个 Span 的 Duration 和 Self Duration 说话区分“哪个服务最慢”和“哪个服务是根因”。第四个问题哪里先出现错误按 Span 执行顺序看第一个 ERROR 状态而不是只看最后表现明显的服务。这四个问题回答完毕根因范围通常能收敛到某个具体代码路径或资源瓶颈而不是“某个服务能力不足”。7.2 从“谁承担所有”到“根因共担”的复盘模板复盘文档可以按下面这张表组织复盘项需要回答的问题所需追踪证据现象用户或监控看到了什么入口 Span 的状态、耗时、错误信息影响范围哪些服务和请求受影响同 trace 下涉及的服务名、Span 数量调用路径请求实际经过哪些节点Trace 瀑布图耗时分布时间消耗在哪一段每个 Span 的 Duration 与 Self Duration异常起点第一个失败的节点Span 状态、Events、日志异常堆栈根因判断是逻辑、依赖、资源还是配置结合 Trace、日志、指标交叉确认改进项如何避免再次发生新增监控、调整超时重试、加缓存、优化代码这个模板的价值在于它把“责任”从个人或单个服务转移到了“根因”和“改进项”上。即使某个公共服务确实被大量依赖也必须有 trace 证明它才是延迟瓶颈反过来如果 trace 显示上游重试风暴导致公共服务过载那根因就在上游的调用策略。7.3 团队接入全链路追踪时的最佳实践清单最后给一份可以直接拿去落地的清单服务名统一维护服务名注册表禁止在代码里散落多种写法。入口出口全覆盖网关、HTTP、RPC、消息队列、数据库访问都要接入 instrumentation。日志关联所有服务日志输出统一格式包含 trace_id 和 span_id。错误状态明确业务失败和框架异常都要显式标记 Span Status。依赖信息补全数据库 Span 记录表名、SQL 摘要、Redis 命令和 key 前缀。采样策略分级错误全采、慢请求高比例采样、普通请求低比例采样。复盘模板固定每次故障文档都带 trace_id 和瀑布图截图结论必须引用证据。这套清单不需要一次全部完成。可以先从核心链路开始接入一个公共网关和两个核心服务跑通 trace_id 与日志关联再逐步扩展到消息队列、数据库和更多服务。链路追踪只是可观测体系的一部分最终还要和指标、日志、告警联动才能形成完整的故障定位闭环。综艺里那个总是被拉来当垫脚石的角色最后承担了所有是因为剧本需要戏剧冲突。但真实的分布式系统里没有剧本。一次故障该由谁承担责任不该取决于谁被依赖得多也不该取决于谁打电话声音大而应该由一条条真实的调用数据来决定。从最小 Demo 开始把 trace_id 打印进日志把调用链展示给团队以后再遇到“荡秋千”式的线上抖动就能少一点人肉八卦多一点证据链。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻