尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

从零搭建全链路追踪系统:用OpenTelemetry+Jaeger定位慢请求根因

从零搭建全链路追踪系统:用OpenTelemetry+Jaeger定位慢请求根因 多服务系统上线后最常遇到的一个问题不是功能不会写而是请求报错时根本不知道去哪里查。日志散落在十几个服务节点里数据库慢 SQL 有一句提示但一次请求到底经过了哪些服务、在哪一层耗时最高、是哪个环节抛了异常往往只能靠经验猜。如果把一次请求看成一条从入口流向出口的线索它在服务之间跨越一个又一个环节时链路追踪系统就是那位鉴定师它不眨眼也不遗忘会一直看着这条线索留下的全部痕迹。这就是 OpenTelemetry、Jaeger、Prometheus、Grafana 这类可观测性组件存在的意义。这篇文章会从零搭建一套支持 Trace、指标和日志关联的追踪体系并用一条慢请求演示如何从故障的“残虹”里定位根因。1. 先理解日志、指标和链路追踪为什么缺一不可1.1 三种数据信号决定了排障模式在可观测性领域日志、指标和链路追踪不是三个互相替代的方案而是三种粒度不同的证据来源。日志面向“事件”记录的是某个时间点发生的具体事情例如“数据库连接池获取连接超时”“用户 ID 不存在”。日志能回答“发生了什么”却不携带全局调用结构。日志量越大靠关键词人肉拼接不同服务日志的成本越高。指标面向“聚合”把一段时间内的状态压缩成数值例如 QPS、P95 延迟、5xx 错误率。指标适合做趋势判断和告警通常用 Prometheus 一类的时序数据库存储。但指标只能回答“系统整体是否健康”无法回答“这一条具体请求经过了哪些服务”。链路追踪面向“请求”它把一次请求从入口开始经过的每个组件都记录成一个 Span再按照调用关系组成一棵树。链路追踪能回答“这条请求去了哪里、每段耗时多少、哪个 Span 出错”。它需要额外的上下文传递机制和存储所以实现成本最高但单请求排障时价值也最大。维度日志指标链路追踪记录单位事件聚合数值请求或事务典型问题具体异常堆栈是什么系统整体健康度如何某次请求的完整路径和耗时查询方式关键词、时间范围PromQL 查询trace_id 搜索存储成本高低高主要场景错误详情回溯趋势、告警单请求根因定位1.2 Trace 和 Span 的最小模型理解链路追踪先要理解两个核心概念Trace 和 Span。一个 Trace 代表一次完整请求从客户端进入第一个服务开始到最后一个服务返回结束。一个 Trace 由多个 Span 组成每个 Span 代表一个具体操作例如“接收 HTTP 请求”“查询数据库”“调用下游接口”。Span 与 Span 之间通过parent_span_id形成父子关系。入口服务创建的 Span 是根 Span下游服务创建的 Span 挂在根 Span 下面最终形成一棵调用树。下面是一个最小 Span 结构用于理解字段含义{ trace_id: 4bf92f3577b34da6a3ce929d0e0e4736, span_id: 00f067aa0ba902b7, parent_span_id: d2c3f8f7a2f1e9b4, name: HTTP GET /api/order, start_time: 1710000000000000000, end_time: 1710000000000800000, status: OK }其中trace_id在整个链路中保持不变span_id是当前操作的唯一标识parent_span_id指向调用方。只要这些 ID 能顺着调用链传递下去不同服务的 Span 就能在链路追踪平台里被拼成完整调用图。1.3 为什么仅靠日志不够很多团队在业务早期只用日志系统觉得加一个requestId就能关联所有节点。日志分散在多个服务的文件或索引里按时间混排后顺序并不可靠各节点时钟存在偏差时前后关系甚至会被颠倒没有父子关系时阻塞点也很难判断。更关键的是日志缺少“上下文自动传递”机制。服务 A 调用服务 B需要在代码里手动把requestId放入 HTTP HeaderB 服务再手动取值塞进日志。只要有一个服务忘记传链路就断了。链路追踪框架则在协议层解决了这个问题让 Trace 上下文在 HTTP、RPC、消息队列之间自动传播。这也是它比“人肉传递 requestId”更可靠的原因。2. 环境准备先把可观测性底座跑起来2.1 组件清单与分工本文使用 4 个核心组件搭建本地可观测性环境。它们各自承担不同职责组件作用选择理由OpenTelemetry Collector接收客户端上报的 Trace 和指标数据做批量、过滤、脱敏后转发统一数据入口避免每个服务直连存储Jaeger链路数据存储和查询 UI开发环境用 all-in-one 模式即可快速启动Prometheus指标抓取和告警计算社区标准指标存储和 Grafana 配合成熟Grafana指标面板、告警展示统一可视化入口支持 Prometheus 数据源这里先明确一个原则服务端 SDK 不直接写数据库而是把数据上报给 Collector。生产环境下Collector 承担采样、脱敏、批量发送并隔离业务网络和存储网络。学习环境为了简单也可以让 SDK 直接上报 Jaeger但建议一开始就按 Collector 模式搭后续迁移成本低。2.2 docker-compose 启动链路存储和指标组件在本地新建一个目录例如observability-demo然后创建docker-compose.ymlversion: 3.8 services: otel-collector: image: otel/opentelemetry-collector-contrib:0.80.0 command: [--config/etc/otelcol-contrib/config.yaml] volumes: - ./otel-collector.yaml:/etc/otelcol-contrib/config.yaml ports: - 4317:4317 - 4318:4318 depends_on: - jaeger jaeger: image: jaegertracing/all-in-one:1.48 ports: - 16686:16686 - 14250:14250 prometheus: image: prom/prometheus:v2.47.0 volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml - ./alert.rules.yml:/etc/prometheus/alert.rules.yml ports: - 9090:9090 grafana: image: grafana/grafana:10.1.0 ports: - 3000:3000 environment: - GF_SECURITY_ADMIN_PASSWORDadmin镜像版本会随时间更新落地前建议先确认当前稳定版本。不要直接照搬一个旧版本用于生产版本落后可能导致 OTLP 协议字段不兼容。2.3 OpenTelemetry Collector 的配置要点在同一个目录创建otel-collector.yamlreceivers: otlp: protocols: grpc: endpoint: 0.0.0.0:4317 http: endpoint: 0.0.0.0:4318 processors: batch: timeout: 5s send_batch_size: 1024 exporters: otlp/jaeger: endpoint: jaeger:4317 tls: insecure: true service: pipelines: traces: receivers: [otlp] processors: [batch] exporters: [otlp/jaeger]这份配置只处理 Trace 数据核心是 OTLP Receiver。业务服务的 OpenTelemetry SDK 通过 gRPC 或 HTTP 协议把 Span 数据发给 CollectorCollector 经过批处理后转发给 Jaeger。batchprocessor 很重要。它把多条 Span 合并成一个大请求再发送能显著降低下游存储的压力。生产环境还需要在这里加入资源属性、脱敏规则和采样逻辑。2.4 验证基础设施是否就绪启动全部组件docker compose up -d docker compose ps确认服务状态为 running 后做三个健康检查curl http://localhost:9090/-/healthy curl http://localhost:3000/api/health浏览器访问以下地址Jaeger UIhttp://localhost:16686Grafanahttp://localhost:3000 默认账号admin密码adminPrometheushttp://localhost:9090注意Docker Compose 中各组件镜像版本要与后续使用的 OpenTelemetry SDK 兼容。版本相差过大时OTLP 数据可能无法正常解析问题会表现为 Jaeger 搜索不到任何 Trace。3. 给服务埋点让请求留下完整足迹3.1 自动埋点与手动埋点的选择OpenTelemetry 提供两种埋点方式适合不同场景。自动埋点通过 Java Agent、Node.js SDK 等机制拦截主流 HTTP 框架、数据库客户端、消息队列客户端自动为每次外部调用生成 Span。它对业务代码侵入极小适合快速接入。手动埋点则由开发者在代码里显式创建 Span记录具有业务语义的操作例如“校验库存”“计算优惠金额”。生产环境通常两者结合自动埋点保证主流程不漏手动埋点补充业务关键步骤。3.2 最小 Spring Boot 服务下面创建一个最小订单服务模拟一次跨服务调用。这个服务会分别调用用户服务和库存服务。Maven 依赖只需要 Spring Web 和 Actuator 相关基础依赖parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.1.2/version /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-actuator/artifactId /dependency dependency groupIdio.micrometer/groupId artifactIdmicrometer-registry-prometheus/artifactId /dependency /dependencies控制器代码RestController RequestMapping(/api) public class OrderController { private final RestTemplate restTemplate; public OrderController(RestTemplateBuilder builder) { this.restTemplate builder.build(); } GetMapping(/order) public MapString, Object order(RequestParam String userId, RequestParam String productId) { MapString, Object user restTemplate.getForObject( http://localhost:8082/api/user?userId userId, Map.class); MapString, Object stock restTemplate.getForObject( http://localhost:8083/api/stock?productId productId, Map.class); MapString, Object result new HashMap(); result.put(user, user); result.put(stock, stock); return result; } }启动时通过 Java Agent 自动埋点这是最省事的接入方式java -javaagent:opentelemetry-javaagent.jar \ -Dotel.service.nameorder-service \ -Dotel.traces.exporterotlp \ -Dotel.exporter.otlp.endpointhttp://localhost:4317 \ -jar order-service.jar关键参数说明参数含义注意事项otel.service.name服务名称Jaeger 中按服务检索时依赖它不要在不同环境使用相同名称otel.traces.exporterTrace 导出方式这里使用otlpotel.exporter.otlp.endpointCollector 地址本地是 4317 端口容器内使用服务名otel.traces.sampler采样器开发环境可设为always_on3.3 手动创建 Span 捕获业务步骤自动埋点能覆盖 HTTP 调用但业务方法内部的耗时和异常并不会自动生成 Span。对于需要重点关注的步骤应该手动埋点。注入 OpenTelemetry 的TracerAutowired private Tracer tracer; public void checkStock(String productId) { Span span tracer.spanBuilder(checkStock) .setAttribute(product.id, productId) .startSpan(); try (Scope scope span.makeCurrent()) { // 模拟库存查询 Thread.sleep(300); } catch (Exception e) { span.recordException(e); span.setStatus(StatusCode.ERROR); throw new RuntimeException(e); } finally { span.end(); } }这段代码的关键点有两个。makeCurrent()的作用是把当前 Span 放入上下文让后续自动创建的 Span 自动挂到它下面finally中调用end()则保证异常时 Span 也能正常关闭。忘记调用end()Span 不会上报耗时统计会失真。注意不要在高频业务方法里为每一行代码都创建 Span粒度控制在“一个外部调用”或“一次关键业务操作”即可否则 Span 数量会迅速膨胀存储和排查成本都会上升。4. 数据是怎么串起来的上下文传递机制4.1 W3C Trace Context 协议要让不同服务生成的 Span 拼成一条链路关键是上下文在服务间传递。OpenTelemetry 默认遵循 W3C Trace Context 标准通过 HTTP Header 传递 Trace 信息。一次标准请求头如下traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01字段拆解字段段示例值含义版本号00协议版本trace-id4bf92f3577b34da6a3ce929d0e0e473632 位十六进制全局唯一parent-id00f067aa0ba902b716 位十六进制调用方 Span IDflags0101表示采样00表示不采样服务 A 发起 HTTP 调用前SDK 会把当前上下文写入traceparent服务 B 收到请求后读取该 Header并把它作为自己新 Span 的父级。这样一条 Trace 的 ID 就能跨越多个服务保持不变。4.2 自动传播与断链场景OpenTelemetry 的 Java Agent 会自动为 RestTemplate、Apache HttpClient、OkHttp 等常见客户端注入 Trace Header所以上面的订单服务只要能访问用户服务和库存服务链路通常会自动串起来。最容易断链的场景包括使用了框架本身不支持的 HTTP 客户端。代码中使用了自定义线程池子线程没有继承上下文。通过消息队列发送事件消费方无法直接继承生产者上下文。使用了异步非阻塞框架但没有把 Context 传递给回调。4.3 异步线程和消息队列如何传递上下文在线程池中父线程的Context.current()不会自动传给子线程。需要手动把 Context 捕获后放到子线程中。示例ExecutorService executor Executors.newFixedThreadPool(4); public void asyncProcess(Order order) { Context context Context.current(); executor.submit(() - { try (Scope scope context.makeCurrent()) { sendOrderMessage(order); } }); }这里在提交任务前先拿到Context.current()然后在子线程中通过context.makeCurrent()恢复上下文。这样子线程内创建的 Span 才能挂到父链路上。对于消息队列需要在消息头中透传 Trace 上下文。Kafka 场景下OpenTelemetry 提供了对应的拦截器也可以手动在 Producer 和 Consumer 中注入、解析traceparent。5. 指标与告警让监控不只停留在“看链路”5.1 用 Prometheus 收集服务指标链路追踪解决“单请求怎么看”的问题指标解决“整体是否健康”的问题。为了让这两个维度能配合需要让 Prometheus 抓取服务的指标数据。在 Spring Boot 服务中启用 Actuator 和 Prometheus 注册表后服务会暴露/actuator/prometheus端点。Prometheus 配置如下global: scrape_interval: 15s evaluation_interval: 15s scrape_configs: - job_name: demo-services metrics_path: /actuator/prometheus static_configs: - targets: - host.docker.internal:8081 - host.docker.internal:8082 - host.docker.internal:8083这里的端口对应订单、用户、库存三个服务的端口。Docker 容器内访问宿主机服务需要使用host.docker.internal如果服务本身部署在宿主机进程中也可以直接写localhost:8081。验证是否抓取成功可以在 Prometheus 页面执行查询up结果中对应 target 的up应为1。5.2 Grafana 面板查询示例在 Grafana 中添加 Prometheus 数据源然后建立 Dashboard。常用的三个指标查询如下。接口 QPSsum(rate(http_server_requests_seconds_count[1m])) by (uri)5xx 错误率sum(rate(http_server_requests_seconds_count{status~5..}[5m])) by (uri) / sum(rate(http_server_requests_seconds_count[5m])) by (uri)P95 延迟histogram_quantile(0.95, sum(rate(http_server_requests_seconds_bucket[5m])) by (le, uri))这三个查询分别覆盖流量、错误和延迟是接口监控的基本盘。实际面板中建议按服务、实例、接口维度分组避免指标叠加后无法区分来源。5.3 告警规则配置在alert.rules.yml中定义一条基础告警当接口 5xx 错误率超过 5% 并持续 5 分钟时触发groups: - name: demo-api-alerts rules: - alert: ApiErrorRateHigh expr: | sum(rate(http_server_requests_seconds_count{status~5..}[5m])) / sum(rate(http_server_requests_seconds_count[5m])) 0.05 for: 5m labels: severity: warning annotations: summary: API 5xx 错误率超过5%持续5分钟告警规则的核心是表达式的准确性而不是把阈值拍脑袋写死。建议先观察两周基线数据再根据业务容忍度调整阈值避免告警频繁误报导致团队麻木。6. 实战排查从“残虹”定位慢请求根因6.1 构造一个可复现的慢请求为了演示完整排查链路在库存服务中制造一个慢操作GetMapping(/api/stock) public MapString, Object checkStock(RequestParam String productId) throws InterruptedException { if (p-001.equals(productId)) { Thread.sleep(800); } MapString, Object result new HashMap(); result.put(productId, productId); result.put(stock, 10); return result; }启动订单、用户、库存三个服务后访问curl http://localhost:8081/api/order?userIdu-100productIdp-001预期订单接口耗时在 800 毫秒以上但具体慢在哪层需要看链路数据。6.2 在 Jaeger 中按请求路径检索链路打开 Jaeger UI按照以下路径搜索Service 选择order-service。Operation 选择GET /api/order。点击 Find Traces。搜索到 Trace 后点击进入链路详情。瀑布视图会显示每个 Span 的耗时和父子关系。正常情况下的结果类似HTTP GET /api/order约 800msHTTP GET /api/stock约 800msHTTP GET /api/user约 5ms从瀑布图可以快速判断瓶颈在库存服务的下游调用。再点开库存服务的 Span查看属性里记录的http.target、http.status_code等数据确认是接口逻辑慢还是数据库访问慢。6.3 结合日志中的 trace_id 定位异常点如果链路中存在异常Jaeger 只能显示哪个 Span 报错具体堆栈还要看日志。要让日志和 Trace 关联起来需要在日志格式中输出 trace_id。在logback-spring.xml中配置pattern%d{yyyy-MM-dd HH:mm:ss.SSS} %5p [%t] [%X{trace_id:-}] [%X{span_id:-}] %logger{36} - %msg%n/pattern使用 OpenTelemetry Java Agent 启动服务时它会自动把trace_id和span_id写入日志 MDC。业务代码不需要额外处理日志行中就会出现可检索的 Trace ID。排障顺序在某条日志中复制trace_id。在日志平台搜索该trace_id得到这次请求在所有服务中打印的日志。在 Jaeger 搜索框粘贴同一个trace_id查看链路结构。结合错误日志和 Span 耗时区分是业务异常、依赖慢还是数据结构问题。注意在分析 Span 时间差之前先确认所有节点时间一致。节点时钟不同步会导致 Span 排序错乱耗时统计也会失真。6.4 一套完整的排障顺序实际生产中建议按以下顺序处理一次跨服务故障先看入口服务有没有收到请求确认请求是否到达系统。再看入口服务有没有完整 Trace确认采集链路是否正常。沿着链路瀑布图逐层查看 Span 耗时和状态码。筛选错误 Span查看其中的异常属性和日志关联。返回指标系统查看该接口的错误率和延迟趋势判断是偶发还是持续恶化。最后回到代码修复根因并部署验证。这套顺序比直接翻日志更高效因为链路结构先给了你一张“地图”日志只是定位到节点后的细节补充。7. 常见问题排查清单7.1 高频问题的现象、原因和处理方式以下表格整理了全链路追踪体系搭建过程中最常见的几类问题。问题现象可能原因检查方式处理建议Jaeger 搜索不到 Trace启动参数没配置或 SDK 版本与 Collector 不兼容查看 Collector 日志检查启动参数确认otel.exporter.otlp.endpoint地址正确统一升级 SDK链路在服务边界断开HTTP 客户端不被 SDK 支持或异步线程未传上下文检查调用方日志是否出现 trace_id升级 SDK或手动传播 ContextSpan 时间乱序各节点时钟不一致执行date对比容器时间为所有节点配置 NTP 时间同步Trace 里有 Span 但缺业务细节只用了自动埋点查看 Span 属性在关键业务方法手动创建 Span日志中无 trace_idLogback pattern 没配置 MDC或服务没用 Agent 启动查看日志输出格式修改 pattern确认带-javaagent启动开发环境链路不完整采样率太低检查 Trace 数量与请求量比例开发环境设置OTEL_TRACES_SAMPLERalways_onCollector 长时间内存增长batch 配置不合理查看容器内存监控调整 batch timeout 和 send_batch_size或扩容7.2 通用排查步骤遇到链路数据异常时优先按这个顺序排查确认服务是否使用带埋点的 Agent 或 SDK 启动。确认otel.service.name是否设置且是否与 Jaeger 中看到的服务名一致。确认客户端能访问 Collector 的 4317 或 4318 端口。确认 Collector 的 pipeline 是否同时配置了 receiver、processor、exporter。确认采样器配置没有把请求全部丢弃。确认下游服务和上游服务版本一致避免 OTLP 字段解析失败。这条链路每一步都能验证问题通常能在一两轮检查内定位。8. 生产环境落地采样、安全和扩展8.1 采样策略怎么选链路数据量远大于日志量生产环境不可能也不应该全量保存所有请求的 Trace。采样是必须做的。头部采样在入口服务根据 trace_id 哈希决定是否
返回列表