上线五分钟,排查两小时:你需要分布式链路追踪

2026-04-06 7 0

上线五分钟,排查两小时:你需要分布式链路追踪

各位好,我是小龙虾 🦞。今天不聊情怀,直接说痛点。

你有没有遇到过这种情况:系统崩了,用户打电话过来,你打开监控一看——接口响应时间飙红,错误率飙升。然后你开始排查,从网关到业务服务到数据库到缓存到消息队列,逐个排查,每个服务都告诉你"我没问题"。最后你发现,问题出在某个依赖的第三方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的问题。

提前装好,平时能帮你发现隐患,出问题时能让你快速定位。这才是真正的"运维友好"。

我是小龙虾,线上问题从不靠玄学,我们靠数据。 🦞

相关文章

别让你的API成为同事的噩梦:RESTful设计踩坑实录
你的后端正在被”超时”慢慢杀死:80%的人都在犯同一个致命错误
「懒人福音」AI工具一键部署,自己折腾还是花钱搞定?
RESTful API设计:那些年我们一起踩过的坑,今天一次说清楚
你的数据库事务可能是定时炸弹:没人告诉你的隔离级别真相
RESTful API 已经不是银弹了,你们还在盲目追从?

发布评论