Skip to content
性能与运维

性能与运维

本章假设你已经把 Jaeger v2(基线 v2.22.0,2026-10)跑在生产环境里,现在关心的是三件事:它扛不扛得住这个量、数据会不会丢、出了事怎么排查。内容以官方 2.22 文档(deployment、operations 两大板块)与仓库内实测配置为依据。

运维心智:你在运维一个 OTel Collector 发行版

Jaeger v2 是构建在 OpenTelemetry Collector 框架上的定制发行版:单一 jaeger 二进制,角色由 --config 传入的 YAML 决定(receivers / processors / connectors / exporters / extensions / service 的标准 collector 结构)。这带来一个重要的运维红利:所有面向 OTel Collector 的调优语汇——队列大小、背压、内存限制器、批处理、指标口径——全部原样适用于 Jaeger,官方 performance-tuning 页也明确说"collector 扩缩容的通用建议基本都适用"。

同时接受一个约束:v1 的 COLLECTOR_* / QUERY_* 环境变量与命令行 flag 在 v2 全部失效,改配置 = 改 YAML = 重启进程(详见下节"日常运维")。整体没有热加载,唯一例外是 remote_sampling 扩展的 file 模式会监听策略文件内容变化自动重载(官方 architecture/sampling 页)。

管理端口速查

官方 deployment 页给出的"默认对外暴露的管理端口"如下,均可在 YAML 中改:

端口 协议 端点 用途
8888 HTTP /metrics collector 角色的内部遥测(Prometheus 格式,抓它做告警)
8889 HTTP /metrics ingester 角色的指标端口(与 collector 分开的默认值)
13133 HTTP /status healthcheckv2 扩展,K8s 探针打这里
27777 HTTP /expvar Go 进程级运行时指标
1777 HTTP debug 端点 pprof 扩展;默认不开,官方 cmd/jaeger/config.yaml 示例里以 endpoint: 0.0.0.0:1777 启用

zpages 扩展同样是示例中注释掉的选项(# zpages: endpoint: 0.0.0.0:55679),需要时打开,默认端口按 OTel collector 约定是 localhost:55679;all-in-one 的内嵌配置则显式把它覆盖为 ${env:JAEGER_LISTEN_HOST:-localhost}:27778(all-in-one.yaml 源码注释:official extension listens on ephemeral port 55679, so we override it)。两种写法都对,只是语境不同,端口全表见 02-v2架构与组件.md。业务端口(4317/4318、16685/16686 等)的完整口径见 02-v2架构与组件.md 与 07-查询UI与API.md。

容量规划方法学

容量规划的目的不是算出精确数字,而是先把数量级立起来,再压测校准。

从 QPS 到存储体积

一条估算链:

入口 QPS(被调用的业务请求数/秒)
  × 每 trace 平均 span 数(典型 3~20,微服务链路越长越大)
  × 采样率(如 0.1)
  = spans/秒
spans/秒 × 86400 = 日 span 总量
日 span 总量 × 平均单 span 字节数 = 日原始体积
日原始体积 × 副本数 / 压缩比 = 磁盘占用(ES 压缩后经验值约为原始的 1/3~1/2)

其中"平均单 span 字节数"最不能拍脑袋——先测再乘是这门手艺的核心:接入真实流量跑一天,直接在 ES 侧读 jaeger-span-* 索引的 store.size_in_bytes / docs.count(_cat/indices 一把梭),拿到"一条 span 平均占多少存储字节",再乘上未来的 span 总量。拿实测值套公式,比拿别人的数字套公式可靠得多。

curl -s "http://es:9200/_cat/indices/jaeger-span-*?v&s=index"
# 用 store.size / docs.count 反推单 span 平均存储成本

一个算例(数字均为示意,务必用你们的实测值替换):

业务 QPS                5000/s
每 trace 平均 span 数    6
采样率                  0.1
→ spans/s              5000 × 6 × 0.1 = 3000
→ 日 span 量            3000 × 86400 ≈ 2.6 亿
实测单 span 存储成本     600 B(_cat/indices 反推,已含压缩)
→ 日磁盘占用            2.6 亿 × 600 B × 副本数 2 ≈ 300 GB/天
→ 保留 7 天             ≈ 2 TB 量级(还要加 log/dependency 索引与 headroom)

数字吓人是因为乘数链条里每个"大概"都会传染——这正是"先测一条 span 再乘"能救你一命的地方:600 B 实测值若是 200 B 或 2 KB,容量结论差一个数量级。同样的方法学适用于 Cassandra(nodetool cfstats 的占盘字节除以分区行数)。

ES 分片与日索引体积

分片和副本在建索引时就定死了,事后调整只能 reindex 或改模板管新索引,所以建索引前要想清楚(官方 elasticsearch 存储页专门提醒这一点)。经验做法:让单日索引体积落在几个 GB 到几十 GB 一档,主分片数按"日体积 / 单分片目标体积"取,副本至少 1 保查询高可用。Jaeger 侧的三种索引管理模式(v2.22 新语法是 indices.<type>.rotation.periodic / manual_rollover / auto_rollover;旧的顶层 use_ilm/use_aliases 开关自 v2.21 起被 feature gate 默认拒绝;若走 jaeger-es-rollover init 初始化 ILM,策略名仍固定为 jaeger-ilm-policy)与清理脚本的细节在 04-存储后端.md,这里只强调运维动作:选 ILM 就把 rollover 阈值和保留期写进策略,选 time-based 就把 jaeger-es-index-cleaner 定时任务跑起来,否则磁盘满了是 span 被丢还是索引被拒,两头的事故都会来。

Kafka 分区、ingester 副本、collector 副本的约束关系

走 Kafka 中转(collector → Kafka → ingester → 存储)时,官方 sizing 规则很清楚:

  • traceID 是 Kafka 分区的 sharding key——同一条 trace 的所有 span 落同一分区;collector 副本往哪个分区写都行,扩 collector 不影响分区数。
  • ingester 副本数不要超过 topic 分区数,多出来的副本只能空转(官方 performance-tuning 页原话)。
  • 更要命的一条在写入侧:开了 wait_for_result 之后,导出批次的大小受分区数约束——receiver 每分区串行处理,批合并器里同一时刻最多每分区一条记录;分区少,则写存储的请求就小,吞吐上不去。加分区才能提高写吞吐,单纯调大 queue.batch.max_size 没有用(官方 Kafka 页 Sizing 一节)。副本加多了反而每实例分到更少分区、批次更小。
  • 对 ES/OpenSearch:queue.batch.max_size 要明显小于存储侧 bulk_processing.max_bytes(默认 5000000)。原因是 collector 按 OTLP protobuf 字节度量批次,存储按编码后的 _bulk body 度量(更大),卡在上限会被拆成多个 _bulk 请求。两个值还都必须低于 ES 的 http.max_content_length(默认 100 MB)。
分区数 P ≥ 目标 ingester 副本数 R(留 rebalance 余量取 P ≈ 2R)
queue.batch.max_size(4 MiB 量级) << bulk_processing.max_bytes(默认 5 MB) < http.max_content_length(100 MB)

SPM 的指标成本别漏算

开了 Service Performance Monitoring(spanmetrics connector 产出的 RED 指标),Prometheus 侧新增时间序列按官方公式估算:

num_status_codes * num_span_kinds * (1 + num_latency_buckets) * num_operations
// status_codes 上限 3(典型 2),span_kinds 上限 6(典型 2),latency_buckets 默认 17
// 默认配置下:typical = 72 * num_operations,max = 324 * num_operations

自定义 duration 桶或附加维度会再乘系数。按 operations 数(服务 × 接口)估一个量级,接到你们 metrics 后端的基数预算里;看板与 SPM 使用侧见 07-查询UI与API.md,Prometheus 依赖配置见 08-可观测性联动.md。指标基名是 calls_total 与 duration_[units] 系列 histogram(默认 17 桶);connector 默认命名空间 traces.span.metrics 经 Prometheus 清洗后,scrape 到的全名就是 traces_span_metrics_calls_total(v2.22 SPM 文档排障一节的 PromQL 原文如此),一切以自家端点实际输出为准,口径详见 08-可观测性联动.md。

不丢数据的管道:lossless pipeline

默认示例配置不是 lossless 的

官方 delivery-guarantees 页把这层窗户纸捅破了,两处默认值都会让"写失败"静默消失:

  • batch processor 吞导出错误:它把批次交给后台 goroutine 后立即向上游返回成功,导出失败时只打一条 Sender failed 日志、批次直接丢弃,错误永远到不了 receiver。官方点名的配置包括仓库里的 config.yaml、config-elasticsearch.yaml 示例,“因此不是 lossless”。
  • ES/OpenSearch 默认 write_mode: async:span 先进客户端缓冲,后台 flush 失败只记日志不回传——发送方早就被告知"已接受",既不会重试也没有降速信号。

注意:v1 时代靠 --collector.num-workers / --queue-size 控制导出队列,v2 里这些 flag 不存在。对应物是 jaeger_storage_exporter 的 queue.{num_consumers, queue_size, wait_for_result, block_on_overflow} 与 queue.batch.*。丢数据往往不是容量问题,而是这三个开关的组合问题。

lossless 的定义也是官方口径:存储对每次写入返回真实结果,且 receiver 与存储之间的每个组件都在拿到结果之后才确认 span。两头缺一头都不算。

三件套配方

直连 OTLP 写入 + ES 存储,一个可抄的 lossless 骨架(字段语义均出自 delivery-guarantees 页):

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: []        # 关键一:去掉 batch
      exporters: [jaeger_storage_exporter]

exporters:
  jaeger_storage_exporter:
    trace_storage: some_store
    queue:
      wait_for_result: true # 关键二:拿到写入结果才确认
      sizer: bytes
      num_consumers: 4
      queue_size: 104857600 # 100 MiB
      batch:                # 官方推荐用队列内置 batch 代替 batch processor
        sizer: bytes
        flush_timeout: 200ms
        min_size: 1048576   # 必须为正,否则每个请求单发单写、合并不生效
        max_size: 4194304

extensions:
  jaeger_storage:
    backends:
      some_store:
        elasticsearch:
          write_mode: sync            # 关键三:_bulk 同步返回、错误如实上报
          poison_pill_handling: drop  # 直连场景见下文

三点补充:

  • 完全不配 queue 也是 lossless 的(每个上报请求同步写一次存储),客户端自身批得好、数量不多时反而简单。
  • queue + batch + wait_for_result: true 是官方推荐的批处理姿势:多个客户端/分区进来的请求合并成一次不超过 max_size 的存储写入,每个贡献方都拿得到最终结果——吞吐和可靠性兼得,这是 batch processor 给不了的。
  • Cassandra / ClickHouse 天然同步写并返回错误,不需要 write_mode;ES/OpenSearch 必须显式 sync。写失败返回的确认语义基于 ES 默认的 index.translog.durability: request——如果你们改过这个 index setting,“已确认"就不等于"已落盘”。

直连 OTLP:把错误还给 SDK

管道把错误回传到 receiver 之后,OTLP receiver 会将其转成规范允许重试的失败响应:gRPC Unavailable / HTTP 503(官方 delivery-guarantees 页口径)。SDK 侧的兜底预算是有限数的:OpenTelemetry Go SDK 的 BatchSpanProcessor 给一次导出 30 秒超时,OTLP exporter 重试窗口 60 秒——collector 挂了 1 分钟以内 SDK 能扛过去,超过就是 SDK 缓冲区丢弃。所以直连模式的保证是"存储失败一定会告知客户端",扛多大故障取决于客户端配置。

两个配套动作:

  • Jaeger 把所有存储错误都上报为可重试错误,包括每条必被 ES 拒收的毒丸文档。直连场景务必把 poison_pill_handling: drop 配起来(或走 dead-letter 管道),否则一个畸形 span 会占着它后面整队数据被无限重试。
  • 直连场景不要把 exporter 的 retry_on_failure 打开:客户端本来就在重试,collector 再重试只会把客户端的请求按在手里直到超时。

Kafka 中转:削峰与 at-least-once

为什么要队列:流量尖峰时存储短时跟不上,Kafka 做 durable buffer,ingester 按自己节奏追平——这是官方 performance-tuning 页给的标准答案;顺带还能接实时数据管道。at-least-once 的组合(receiver 侧 message_marking.after: true + on_error: false,导出侧禁 batch、retry_on_failure.max_elapsed_time: 0、queue.block_on_overflow: true)逐项细节在 04-存储后端.md,本章只讲为什么:

  • offset 必须写入后才提交,否则"Kafka 认为投递完成"是假的;
  • 导出失败必须回传给 receiver 扣住 offset,否则分区直接跳过这批;而重试不配、导出器默认不重试的组合会让分区一路暂停到下次 rebalance——单副本 ingester 永远等不到那次 rebalance,分区就此卡死;
  • 重试是安全的:ES span 文档的 _id 由 span 内容确定性生成,重复写入是覆盖不是重复入库(官方 idempotent writes 一节)。

仓库里有现成参照:cmd/jaeger/config-kafka-ingester-sync.yaml(Kafka e2e 测试同源配置)。被存储永久拒收的毒丸在 Kafka 链路会死锁 offset,所以 ingester 要么 poison_pill_handling: drop,要么上 dead-letter:把 jaeger_storage_exporter 声明到 connectors: 段,它会把被拒的 span 原样(带 jaeger.storage.rejection_reason 属性)转发到第二条 pipeline(如 kafka/dead_letter),计数在 jaeger_storage_exporter_dead_letter_spans——这个计数器缓慢爬升是个别脏数据,陡增八成是 mapping 变更把一整类 span 拒了。注意目前只有 ES/OpenSearch 的 sync 模式支持该 connector。完整配方见 delivery-guarantees 页。

内存与背压

collector 是 Go 进程,span 全在堆里过一遍:内存上限交给 memory_limiter processor(OTel 标准件,按容器 limit 的百分比配 limit_percentage/spike_limit,写法照抄 collector 社区最佳实践),队列上限交给 queue_size + sizer: bytes。两种满载语义要分清:

  • 直连模式:队列满就快速失败,给客户端回可重试错误——这是正确的背压信号,别开 block_on_overflow。
  • Kafka 消费侧:数据已经进了本进程,队列满就阻塞等待(block_on_overflow: true),让消费自然减速,而不是伪造失败再重来。

collector 与 query 都无状态,官方 deployment 页明说"两者都可以随意并行多实例",K8s 上直接 HPA:

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: jaeger-collector
spec:
  minReplicas: 3
  maxReplicas: 30
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70

比 CPU 更贴近业务饱和度的自定义指标是"导出队列占用率"与 refused_spans 增速——把 8888 的计数器接进 Prometheus adapter 再挂 pods 指标,扩容时机比 CPU 提前一个身位。

监控 Jaeger 自身

8888 上四个计数器,组合判读

curl -s http://jaeger-collector:8888/metrics | grep -E 'otelcol_(receiver|exporter)_(accepted|refused|sent|send_failed)_spans'
指标 含义
otelcol_receiver_accepted_spans receiver 收下并已入管道的 span 数
otelcol_receiver_refused_spans receiver 直接拒掉的 span 数(格式错误、管道关闭中等,出现即丢数据)
otelcol_exporter_sent_spans 成功写进存储的 span 数
otelcol_exporter_send_failed_spans 导出失败的 span 数

判读套路:正常情况下 accepted 与 sent 的增速应当接近。refused 涨——问题在入口(客户端发错格式 / 管道被 shutdown / 校验不过);sent 跟不上 accepted 且 send_failed 涨——问题在出口(存储慢或挂)。指标带 receiver=/transport=/service_instance_id= 等标签,混部多 receiver 时按标签拆开看。官方 monitoring 页还建议抓基础面:进程 CPU/RSS/fd/网络、Go runtime(GC 时长、goroutine 数——goroutine 持续爬坡通常是某处泄漏或下游卡住)。

可以直接抄进看板的起步 PromQL:

# 每秒接收 / 每秒落库,两条线拉开就是积压或丢失
sum(rate(otelcol_receiver_accepted_spans[1m])) by (receiver)
sum(rate(otelcol_exporter_sent_spans[1m])) by (exporter)

# 丢包告警用:increase 优于 rate,短窗口内出现即异常
increase(otelcol_receiver_refused_spans[5m]) > 0
increase(otelcol_exporter_send_failed_spans[5m]) > 100

# 进程重启检测(target_info 序列翻转)
changes(target_info[30m]) > 0

查询侧与 ES 写入侧指标

query 扩展在同一个 8888 上继续暴露 jaeger_query_* 系列(请求数、响应数与延迟直方图,名称沿用 v1 口径:jaeger_query_requests_total / jaeger_query_responses / jaeger_query_latency;具体以 curl -s localhost:8888/metrics | grep jaeger_query_ 实际输出为准)。ES 侧写入指标(jaeger_bulk_index_attempts_total、jaeger_bulk_index_errors_total、jaeger_bulk_index_latency_ok/err 等)在 v2 沿用同名——v1 调优页赖以判断"存储跟不上"的 jaeger_collector_save_latency_bucket 在 v2.22 尚未提供(官方 performance-tuning 页挂了红框声明),过渡期用 bulk latency 两个直方图代替盯存储写入健康度。

v1 → v2 指标对照表与官方看板

升级改看板不要猜名字:仓库内置了按存储后端分组的对照表,路径 cmd/jaeger/docs/migration/,五个文件 all-in-one / badger / cassandra / elasticsearch / opensearch 的 -metrics.md,每个分"Combined Metrics(同名保留)“和"Equivalent Metrics(改名换标签)“两张表——例如 jaeger_collector_spans_rejected_total 对应 receiver_refused_spans,jaeger_build_info 对应 target_info。

Grafana 看板:仓库 monitoring/jaeger-mixin/dashboard-for-grafana.json 可直接导入或塞进 provisioning 目录。README 提醒:该看板默认指标面向"各组件分开部署"的生产拓扑,all-in-one 拓扑的指标名有差异,要自己调。

注意:官方 docs monitoring 页仍写着"仓库提供一组生产验证过的 Prometheus 告警规则"并链接到 monitoring/jaeger-mixin——但同一目录 v2.22.0 的 README 已明说内置的 prometheus_alerts.yml 已移除(那套规则依赖 v1 指标),README 给出的替代方案是直接套用 OTel Collector 生态的告警规则,例如 opentelemetry-helm-charts 里 opentelemetry-collector chart 的 templates/prometheusrule.yaml 模板。v2 既然吐的是标准 otelcol_* 指标,任何 collector 告警规则(队列饱和度、导出失败率、重启检测)原样可用。另外 mixin README 的 PodMonitor 示例还在抓 v1 的 14269 端口,属残留,v2 请抓 8888。

抓取与自身开销控制

官方 monitoring 页给的 scrape 样例(scrape_interval: 15s,target jaeger:8888)照抄即可。三个成本直觉:

  • 自追踪:Jaeger 默认会追踪自身 query 请求(UI 刷新多了能在服务列表里看到 jaeger)。生产不需要就关:OTEL_TRACES_SAMPLER=always_off——否则你至少要为查询 QPS 付一份 tracing 管道成本,还容易在排障时混淆"业务 trace"与"查询 trace”。
  • 日志:默认 stderr 纯文本,生产 telemetry.logs.level 用 info 或 warn 即可(示例 config.yaml 里 debug 是演示值,别带上生产);service.telemetry.metrics.level: detailed 会多吐一批 collector 内部指标,基数预算紧可降。
  • expvar/zpages/pprof:按需临时开,pprof 只在排障期监听 1777,用完收回。典型动线:
# CPU / 堆快照(extensions 里启用 pprof: endpoint 0.0.0.0:1777 后)
go tool pprof -http=:8081 http://jaeger-collector:1777/debug/pprof/profile?seconds=30
go tool pprof -http=:8082 http://jaeger-collector:1777/debug/pprof/heap

# zpages:查看 receiver/exporter 实时运行状态页(需示例配置中取消注释)
curl -s http://jaeger-collector:55679/debug/tracez

# expvar:进程级 Go 指标(默认即开)
curl -s http://jaeger-collector:27777/expvar

排障决策树(文字版)

官方 troubleshooting 页的顺序是"从 SDK 到存储逐段缩小怀疑面”,下面按症状组织。前提意识:如果你链路里还有独立的 OTel Collector 网关,先按 collector 自己的排障手册过一遍,Jaeger 侧只负责 Jaeger 段。

总览一棵树(每个节点都是一个可执行动作,细节见后续小节):

trace 不上来
├─ SDK 采样率是多少?调试期先全采样
├─ stdout exporter 有 span 吗?没有 → 埋点问题(见 05-Go客户端埋点.md)
├─ 直连 collector 4317/4318 复现吗?
│   └─ 直连能好 → 中间 agent/网关问题,逐跳二分
├─ 容器三查:端口暴露 / 监听 0.0.0.0 / DNS 可见
├─ curl :14268/api/sampling?service=x → 策略下发正常吗
├─ curl :8888/metrics → accepted 涨?sent 涨?
│   └─ 到了没存下 → 存储段(症状四)
└─ debug 镜像 + delve(12345) → 深水区

trace 残缺            → 未 End 的子 span / 采样截断 / 队列丢弃(查计数器)
UI 乱序、负时长        → NTP;max_clock_skew_adjust(本章症状三)
写入被拒              → ES:毒丸 / 429+watermark / 413(症状四)

症状一:trace 根本不上来

按顺序做,每一步都能二分:

  1. 确认采样。调试期把 SDK 配成全采样——OTel SDK 默认 parentbased_always_on 本身就是 100%,如果你们用远端采样,见第 4 步。OTEL_TRACES_SAMPLER 可以环境变量覆盖。
  2. 验证 span 真的产生了:给 SDK 临时挂 stdout exporter,能打出 span 说明业务埋点活着,问题在导出链路(见 05-Go客户端埋点.md)。
  3. 绕过中间层直连:把 SDK 的 endpoint 直接指到 Jaeger collector 的 4317/4318,跳过宿主机 agent / 网关 collector,缩小链路。
  4. 远端采样策略下发验证(怀疑策略没生效时):
curl "http://jaeger-collector:14268/api/sampling?service=foobar"
# {"strategyType":"PROBABILISTIC","probabilisticSampling":{"samplingRate":0.001}}
# 返回的 rate 若是 0.001 这种小值,"不上来"可能根本不是故障而是策略

同时确认 SDK 配置的 sampling endpoint 地址对、网络可达(官方 apis 页:remote sampling 走 collector 的 14268 HTTP 或 5778/5779 的 Jaeger 协议口)。

  1. 容器网络三查(官方点名的典型错误):端口有没有从容器网络命名空间暴露出去;监听地址是不是 localhost——裸机没事,容器里必须 0.0.0.0(v2 配置如 endpoint: "${env:JAEGER_LISTEN_HOST:-localhost}:4317",K8s 部署记得把 JAEGER_LISTEN_HOST=0.0.0.0 或等价 --set 设上);应用容器与 Jaeger 主机名是否互相可见(同 network namespace,或正确的 Service DNS)。
  2. 看计数器定性:curl -s http://jaeger-collector:8888/metrics,otelcol_receiver_accepted_spans 不涨 = 数据没到(回到第 3/5 步);accepted 涨但 otelcol_exporter_sent_spans 不涨 = 到了没写进去(查存储与症状四)。
  3. 还不行就 telemetry.logs.level: debug 开日志细看;终极手段是官方 debug 镜像:内含关闭优化编译的二进制 + delve,进程一启动就 attach 并在 TCP 12345 监听远程调试,VS Code / GoLand 按标准 Go remote attach(port 12345)连上去打断点,适合"配置看着没毛病但行为诡异"的深水区。

服务网格(Istio 等)场景 span 缺失属于另一棵树,先查网格侧 tracing 配置。

症状二:trace 残缺(有头没尾、缺子 span)

  • 有 span 没结束:子 span 未调用 End() 就永远不会导出——先回 05-Go客户端埋点.md 查埋点。
  • 采样截断:父采样 + 局部手动 trace 未设置 SAMPLED flag,或远端概率采样只抽中部分节点,链路天然断;调试期全采样复现。
  • 管道丢弃:比对症状一第 6 步的计数器;refused/send_failed 有增量,对照 lossless 一节查 batch/队列/存储错误回传是否配置缺位。
  • 查询窗口太窄:时钟漂移会让 span 落在检索窗口外(见症状三)。

症状三:UI 里 span 顺序乱、时长为负

根因几乎总是机器间时钟不同步,Jaeger 的时钟偏移调整器只是缓解:jaeger_query 扩展的 max_clock_skew_adjust 控制"允许校正的最大偏移",示例 config.yaml 里默认 0s(即关闭);设成 30s 会启用因果调整并给被调整的 span 打 UI 警告标注。

extensions:
  jaeger_query:
    max_clock_skew_adjust: 30s

注意:调这个参数治标不治本,把节点 NTP/chrony 修好才是根治;调整器本身对跨数据中心大偏移无能为力,v2 默认关闭也说明官方态度是"先修时钟"。

症状四:写入被存储拒绝

ES/OS 链路(细节见 04-存储后端.md):

  • mapping 冲突型永久拒绝:_bulk 响应里逐条 status 为终态失败,sync 模式下表现为同一条 trace 反复重试或整批卡住——毒丸处理走 drop 或 dead-letter(jaeger_storage_exporter_dead_letter_spans 有数即实锤),先查是不是最近改过索引模板/mapping。
  • 429 / 磁盘 watermark:ES 在 disk watermark 触发后会把索引置 read-only,Jaeger 侧看到的就是批量写入失败、send_failed_spans 陡增。第一优先级是清磁盘/删老索引/升容量,这是 ES 运维常识层的事;长效方案 ILM/rollover 提前管住日索引体积。
  • 请求超限:批次超过 http.max_content_length(默认 100 MB)整个请求 413,会被当普通失败一直重试直到人工干预——调小 queue.batch.max_size。
  • 请求 429 与 5xx 属瞬时故障,sync 模式会整批回错让管道重试,一般不需要动作,但要确保重试预算够(直连看 SDK 的 30s/60s,Kafka 链路看 max_elapsed_time: 0 永不放弃)。

压测:tracegen

官方自带压测器是 tracegen,持续生成简单 span,镜像随发行版发版:

# 10 个 worker 各持续发 60 秒,打到本机 collector
docker run --rm \
  -e OTEL_EXPORTER_OTLP_ENDPOINT=http://collector:4318 \
  jaegertracing/jaeger-tracegen:2.22.0 \
  -duration 60s -workers 10 -pause 1ms

关键参数(-h 可查全表):-duration 运行时长、-workers 并发 worker 数、-pause 每 span 间隔;还有 -adaptive-sampling 指向 http://localhost:14268/api/sampling 走远端采样——压 adaptive sampling 行为时用这个。firehose 模式给 span 打 debug 标记跳过索引,适合"我要打满 ingest 管道但不想污染存储检索"的纯吞吐实验。

读数方法就一条主线:压测流量稳定时,比对 8888 上 otelcol_receiver_accepted_spans 的增速与 otelcol_exporter_sent_spans 的增速——两者的差值增速就是你这套拓扑的真实丢弃/积压率;同时盯 collector 的 RSS、refused/send_failed、以及 ES 侧 jaeger_bulk_index_latency_err 的 p99 是否右移。逐步加 -workers 直到 sent 增速不再上升,那个拐点就是容量水位,反过来套"容量规划方法学"一节修正你的估算参数。

没有 Prometheus 也能量增速——两次采样做差分:

snap() { curl -s http://collector:8888/metrics \
  | awk '/otelcol_receiver_accepted_spans|otelcol_exporter_sent_spans/ {s+=$2} END {print s}'; }
A=$(snap); sleep 30; B=$(snap)
echo "spans processed avg: $(( (B-A)/30 )) /s"   # 与 tracegen 理论发送速率对照

理论发送速率 = -workers × 每 worker 每秒 span 数(-pause 决定间隔);实测速率低于理论值且差值稳定,说明管道在丢,回去看四个计数器。

注意:历史仓库 jaegertracing/jaeger-performance 已归档(最后提交 2023-07),不要再当现役工具引用;除 tracegen 外的第三方压测工具官方未背书,选型自负。

安全加固清单

官方 security 页按"成对连接"组织加固面,照链路逐段落实:

传输段

  • SDK → collector:gRPC 与 HTTP 均支持 TLS + mTLS(客户端证书),覆盖 span 导出与采样策略拉取两条流。内网零信任建议 mTLS;至少 TLS。
  • 应用/运维 → query:16685 gRPC 与 16686 HTTP 同样支持 TLS/mTLS。
  • 埋点侧配置见 05-Go客户端埋点.md。

存储段(官方支持矩阵)

存储 TLS mTLS Basic Bearer API Key Kerberos
Cassandra 支持 支持 支持 — — —
Elasticsearch / OpenSearch 支持 支持 支持 支持 支持 —
Kafka 支持 支持 — — — 支持
Prometheus(SPM) 支持 支持 支持 — — —

UI 与反向代理

Jaeger UI 没有自带账号体系,认证鉴权一律交给反向代理(OAuth sidecar、OIDC 网关、Basic 都可以,官方 security 页给了两篇参考架构博文)。挂在路径前缀下时 UI 支持 base_path 配置,v2.18.0 起可自动探测外部前缀,照抄仓库 examples/reverse-proxy/ 的示例改。查询 API 若给程序调用,建议只暴露 16685 的 gRPC/Protobuf(官方口径的稳定 API),16686 上 /api/* 的 JSON 接口标注为 Internal、随时可变(见 07-查询UI与API.md)。

端口收敛

官方 apis 页把 legacy 写入 API(6831/6832 UDP thrift、14250 legacy protobuf、14268 thrift HTTP)标注为 Stable, Deprecated——仍可用但只为兼容而活。生产建议:collector 只对外暴露 4317/4318,从配置里删掉不再需要的 legacy receiver,减少攻击面;14268 若仅用于远端采样策略下发,可限制来源。注意一个易踩的口径:“v2 移除了 legacy thrift 端口"是错的——端口都还在,只是 Deprecated。

# 收敛后的最小 collector 管道:只留 OTLP
receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318
service:
  pipelines:
    traces:
      receivers: [otlp]   # 删掉示例里的 jaeger、zipkin 即停发 legacy 端口

删之前先做一次实锤检查:如果 SDK 侧靠 14268/api/sampling 拉远端策略,收掉 jaeger receiver 后重跑上文那条 curl,确认策略下发通道还在(或已改走 OTLP/5778 通道)再上线,别把采样下发一起收了。

日常运维

升级与回滚

官方 deployment 页给的顺序:组件无状态、状态全在存储,没有 breaking change 时升级顺序无所谓;出现 release notes 明确标注的协议变化时,从管道末端往前升:先 query,再 ingester(用了 Kafka 才涉及),最后 collector——保证接收端先具备理解上游新协议的能力。涉及存储 schema 变化的版本会附带升级前置步骤,滚动前把 release notes 读一遍是固定动作。回滚同理逆向:collector 先退,query 最后退。

配置变更 = 重启

改 --config 的 YAML 不会热生效,滚动重启是预期管理的一部分(all-in-one 形态则是一次短中断)。K8s 上的标准做法是用 checksum annotation 把 ConfigMap 变更"接线"到 Pod 模板:

# Helm values 或手写 manifest 的核心思路
podAnnotations:
  config checksum: '{{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}'

裸 manifest 则手动把 configmap 的 resourceVersion 或文件哈希写进 annotation。唯一热加载例外:remote_sampling 的 file 模式策略文件改内容会自动重载(官方 sampling 架构页),调采样率不用重启——这也是为什么生产常把采样策略文件单独挂一个 ConfigMap,与主配置分开。

注意:别学 v1 运维手册里"SIGHUP 热加载 Jaeger 配置"的说法——v2 没有这个机制,collector 发行版对 --config 不做文件监听。

定时与维护任务

  • ES/OS:Helm chart(4.16.1,官方横幅标注仍为实验性,默认装的是 all-in-one)里 esIndexCleaner / esRollover / esLookback / spark-dependencies 这些 CronJob 默认全部 disabled——上生产要按 03-部署与Kubernetes.md 的清单逐个打开,否则保留期形同虚设、服务地图没有数据。
  • 服务地图:dependency graph 是离线 Spark 作业(项目 spark-dependencies)聚合写回的,部署它不是 Jaeger 进程的职责,运维要单独排班。
  • badger:单机单节点形态,collector 与 query 分进程共享存储必须经 jaeger-remote-storage(gRPC 17271,admin HTTP 17270);数据在本地磁盘,目录备份策略与 TTL 清理见 04-存储后端.md,坏一盘丢一段历史是它的固有代价。memory 后端仅限开发。
  • Cassandra:大规模写入曾是默认答案,但官方 FAQ 的口径在更大规模时推荐 OpenSearch 优于 Cassandra(写放大考量),选型迁移背景见 04-存储后端.md。

告警建议(起步集)

基于 v2 现成指标的最小告警集:increase(otelcol_receiver_refused_spans[5m]) > 0(有丢包)、increase(otelcol_exporter_send_failed_spans[5m]) 超阈值(写存储失败)、go_goroutines 斜率异常、进程重启检测(target_info 序列翻转)、jaeger_storage_exporter_dead_letter_spans 陡增(mapping 事故信号)、P99 查询延迟劣化。规则模板来源见上文 mixin 注意块。

实战建议

把这一章压缩成一个 senior 的巡检动线:先看计数器再翻日志——8888 四个 span 计数器三十秒定性"没到/没存下”;先测单 span 平均字节再谈容量——所有公式的乘数都应是实测值;默认示例只为演示而非无损——上生产第一件事是核对 processors 里有没有 batch、ES 的 write_mode 是不是还在 async;直连还是中转按尖峰形态选——存储能跟平峰就跟不上尖峰才值得引入 Kafka,多两个组件多一份运维面;告警抄 collector 生态而不是考古 Jaeger v1 规则——指标已经换血,v1→v2 对照表在仓库 migration 目录里等着。调优永远遵循"测量 → 单变量改 → 再测量",一次只动一个参数。

生产检查清单

接入段:

  • SDK 侧 export 超时/重试预算与 collector SLA 对齐(知道 30s/60s 兜底在哪断)
  • 生产 collector 只暴露 OTLP 4317/4318(+ 确需的采样口),legacy 端口已收敛
  • SDK→collector、consumers→query 两段 TLS/mTLS 已配置
  • UI 挂在带认证的反向代理之后,路径前缀方案已验证

管道段:

  • 明确声明本管道是否要求 lossless;要求则 processors 无 batch、queue.wait_for_result: true、ES/OS write_mode: sync
  • queue.batch.min_size 为正且 max_size 明显小于 bulk_processing.max_bytes(<100 MB 红线)
  • 直连场景 poison_pill_handling: drop 或 dead-letter 二选一;retry_on_failure 保持关闭
  • Kafka 链路:分区数 ≥ 规划副本数 ×2、message_marking.after: true、block_on_overflow: true、max_elapsed_time: 0
  • memory_limiter 已按容器 limit 配置

存储段:

  • 索引管理模式已选定(time-based/rollover/ILM),清理 CronJob 在跑
  • ES 索引管理用 rotation.periodic/manual_rollover/auto_rollover 新语法;配置里没有 use_ilm/use_aliases/顶层 tags_as_fields 等被 v2.21+/v2.22 默认拒绝的 legacy 字段;走 ILM(auto_rollover 或 jaeger-es-rollover init)则策略已预先在集群创建(init 流程固定要求名 jaeger-ilm-policy)
  • 日索引体积 / 分片数 / 副本数在容量规划表里对得上
  • 磁盘 watermark 与保留期告警已接

查询段:

  • query 无状态多副本,HPA 指标(CPU 或自定义 QPS)已配
  • 服务地图依赖 spark-dependencies 任务已排班

监控段:

  • Prometheus 抓 8888(ingester 抓 8889),间隔与基数预算确认
  • 自追踪 OTEL_TRACES_SAMPLER=always_off 已按需关闭,日志级别 info/warn
  • 丢包四计数器进了告警,dead-letter 计数器进了告警
  • Grafana 看板基于 monitoring/jaeger-mixin/dashboard-for-grafana.json,指标名过了一遍 migration 对照表

安全段:

  • 管理端口(8888/13133/27777/1777)不暴露公网,pprof/zpages 仅排障期开启
  • 存储凭证走 secret 管理,TLS 矩阵按 security 页逐后端核对
  • 升级 runbook 写明"breaking change 时按 query → ingester → collector 顺序"

调优速查表

症状 第一怀疑 动作
refused_spans 涨 客户端格式/协议不匹配 对 SDK 版本与 receiver 协议,必要时全采样复现
sent 增速 < accepted 存储跟不上或写失败 看 ES latency 与 429/413,扩 collector 没用,扩存储或加 Kafka
直连 SDK 侧超时 collector 队列满 扩 collector 副本,确认背压语义(不阻塞直连侧)
Kafka lag 持续增长 分区少或 ingester 少 加分区提批次上限,副本 ≤ 分区数
dead-letter 计数陡增 mapping 变更 回滚模板或修 mapping,drop 模式则查日志丢弃原因
UI 乱序/负时长 时钟偏移 修 NTP;临时 max_clock_skew_adjust: 30s
collector RSS 锯齿触顶 队列积压 查 ES 慢,缩 queue_size 或降采样率(见 06-采样.md)
查询 P99 高 索引大/分片不合理 收窄默认查询窗口、核对分片与日体积(见 04-存储后端.md)