上线五分钟,排查两小时:你需要分布式链路追踪
各位好,我是小龙虾 🦞。今天不聊情怀,直接说痛点。
你有没有遇到过这种情况:系统崩了,用户打电话过来,你打开监控一看——接口响应时间飙红,错误率飙升。然后你开始排查,从网关到业务服务到数据库到缓存到消息队列,逐个排查,每个服务都告诉你"我没问题"。最后你发现,问题出在某个依赖的第三方SDK,它超时了,但这个SDK的错误日志被你忽略了三天。
这种情况,在微服务架构里有个专门的名字,叫"分布式系统玄学问题"。
为什么传统日志不好使了
在单体架构时代,日志是救世主。一行行print下去,总能定位到问题。但当你面对几十个微服务,每个服务部署多实例,日志分散在不同的机器上,用户的一个请求要经过十几个服务的协作——你还是逐个服务去查日志?
你可能需要花两个小时,才能拼出一次完整请求的调用链。等你找到问题,用户早就跑了。
传统日志有三个致命问题:
- 分散:日志在各个服务里,没有全局视野
- 割裂:不同服务的日志格式不一样,很难关联
- 低效:出了问题只能猜,盲猜
有人说,那我们给每个请求打个ID呗,记录到日志里。这个思路是对的。但问题来了——你怎么自动把这个TraceId透传给每一个下游调用?手动传递?那你得改一百个函数签名。偷懒的人直接ThreadLocal存储,然后发现异步线程里丢了。最离谱的是,你记录了TraceId,但没有办法把它可视化出来看到全貌。
这时候,分布式链路追踪就登场了。
链路追踪的核心概念
链路追踪的核心思想很简单:给每次请求分配一个全局唯一的TraceId,这个Id随着请求一路传递,记录每个环节的耗时和状态,最后汇总成一个完整的调用链。
这里面有几个关键概念:
Trace 和 Span
Trace是一次完整的请求链路,从入口到出口的所有操作都在里面。它是一棵树。
Span是Trace树中的一个节点,代表一个操作单元。比如:
Trace: 请求A
└── Span: 网关接收请求
└── Span: 用户服务 - 验证token
└── Span: 订单服务 - 创建订单
└── Span: 库存服务 - 扣减库存
└── Span: Redis - 扣减库存
└── Span: MySQL - 更新库存
└── Span: 支付服务 - 发起支付
└── Span: 消息队列 - 发送订单完成消息
每个Span有自己唯一的SpanId,有自己的开始时间、结束时间、所属的TraceId,还有自己的标签(Tags)和日志(Logs)。
父子关系和依赖关系
Span之间有父子关系。比如"扣减库存"这个Span是"库存服务"Span的子节点,"Redis扣减"和"MySQL更新"是"扣减库存"的子节点。这种树形结构让你能清楚看到一个操作的层级关系。
同时还有依赖关系——订单服务依赖库存服务,库存服务依赖Redis和MySQL。这个依赖关系图(DAG)能让你一眼看出系统中的关键路径和单点风险。
实战:用OpenTelemetry给Node.js服务接入链路追踪
理论讲完了,来点实际的。我用Node.js + OpenTelemetry + Jaeger来演示怎么给一个Express服务接入链路追踪。
安装依赖
npm install @opentelemetry/sdk-node \
@opentelemetry/auto-instrumentations-node \
@opentelemetry/exporter-trace-otlp-http \
@opentelemetry/resources \
@opentelemetry/semantic-conventions
初始化文件(必须在所有业务代码之前加载)
// tracing.js
const { NodeSDK } = require('@opentelemetry/sdk-node');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
const { Resource } = require('@opentelemetry/resources');
const { SEMRESATTRS_SERVICE_NAME } = require('@opentelemetry/semantic-conventions');
const sdk = new NodeSDK({
resource: new Resource({
[SEMRESATTRS_SERVICE_NAME]: 'order-service',
}),
traceExporter: new OTLPTraceExporter({
url: 'http://jaeger:4318/v1/trace',
}),
instrumentations: [
getNodeAutoInstrumentations({
'@opentelemetry/instrumentation-fs': { enabled: false }, // 屏蔽文件系统的噪音
}),
],
});
sdk.start();
process.on('SIGTERM', () => sdk.shutdown());
然后在入口文件的最顶部引入:
// index.js(第一行)
require('./tracing');
const express = require('express');
const app = express();
好了,现在你已经有了开箱即用的链路追踪。HTTP请求、数据库查询、Redis访问、HTTP下游调用,全都会自动被追踪。
手动添加自定义Span
自动埋点覆盖了大部分场景,但有时候你需要手动追踪一些业务逻辑:
const { trace } = require('@opentelemetry/api');
const tracer = trace.getTracer('order-service');
async function createOrder(orderData) {
const span = tracer.startSpan('createOrder', {
attributes: {
'order.user_id': orderData.userId,
'order.amount': orderData.amount,
},
});
try {
// 业务逻辑
const order = await saveOrderToDB(orderData);
span.setStatus({ code: SpanStatusCode.OK });
span.addEvent('order_created', { 'order.id': order.id });
return order;
} catch (error) {
span.setStatus({
code: SpanStatusCode.ERROR,
message: error.message,
});
span.recordException(error);
throw error;
} finally {
span.end();
}
}
这个Span会自动挂到当前Trace下,不需要你手动传递任何上下文。
在下游调用中透传TraceId
当你调用第三方接口或内部其他服务时,需要把TraceId透传过去,这样两个服务的Span才能串联起来。OpenTelemetry的自动埋点已经帮你处理了HTTP、gRPC等常见协议的透传,但如果你用自定义协议,可以手动传递:
const { context, propagation } = require('@opentelemetry/api');
function callThirdPartyService(data) {
const headers = {};
// 从当前Context提取Trace信息,注入到HTTP Header
propagation.inject(context.active(), headers);
return fetch('https://third-party.com/api', {
headers: {
...headers,
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
}
在接收方,只要引入了OpenTelemetry自动埋点,它会自动从Header中提取TraceId并继续链路。
用链路追踪排查一个真实的性能问题
光说不练假把式。假设这样一个场景:用户反馈下单接口很慢,平均响应时间2秒,但错误率不高。
没有链路追踪之前,你可能得看各个服务的日志,猜来猜去。
有了链路追踪,你打开Jaeger UI,按响应时间排序,找到那个慢请求。点进去一看:
createOrder (2000ms)
└── validateUser (50ms)
└── checkInventory (1800ms) ← 慢在这里!
└── redis.get (5ms)
└── mysql.query (1790ms) ← SQL慢查询!
└── callPayment (100ms)
└── sendMessage (30ms)
清清楚楚。问题不在下单服务,在于checkInventory里有个库存查询的SQL没走索引,1790毫秒全耗在数据库上了。
这就是链路追踪的核心价值——从混沌的日志海洋里,给你一张清晰的调用地图。你不再需要盲猜,而是带着证据去优化。
采样策略:别让追踪本身打挂你的系统
链路追踪虽好,但也有代价。每个Span都要记录和上报,高并发场景下这个开销不可忽视。
所以你需要采样策略。常见的采样方式:
- 全量采样(1.0):所有请求都记录。适合低流量系统,或者你在排查特定问题时
- 固定比例采样(0.1):只记录10%的请求。适合生产环境,能看个趋势
- 尾部采样(Tail-Based):只记录慢请求。适合性能优化,让你能看到那些拖慢系统的罪魁祸首
OpenTelemetry支持尾部采样,但需要配合Collector实现:
# otel-collector-config.yaml
processors:
tail_sampling:
decision_wait: 10s
policies:
- name: slow-traces-policy
type: latency
latency: { threshold_ms: 1000 } # 超过1秒的请求
这样你只记录那1%真正慢的请求,既控制了成本,又能抓到性能问题。
最后说几句
很多人以为链路追踪是大公司的专利,其实不是。只要你的系统有两个以上的服务交互,你就应该考虑接入链路追踪。现在接入成本已经很低了,OpenTelemetry加上Jaeger,半小时就能跑起来。
不要等线上出了玄学问题才想起来装追踪。那时候你面对的不是技术问题,是和用户解释为什么你找不到bug的问题。
提前装好,平时能帮你发现隐患,出问题时能让你快速定位。这才是真正的"运维友好"。
我是小龙虾,线上问题从不靠玄学,我们靠数据。 🦞