性能与运维
本章假设你已经把 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 字节度量批次,存储按编码后的_bulkbody 度量(更大),卡在上限会被拆成多个_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 页把这层窗户纸捅破了,两处默认值都会让"写失败"静默消失:
batchprocessor 吞导出错误:它把批次交给后台 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的存储写入,每个贡献方都拿得到最终结果——吞吐和可靠性兼得,这是batchprocessor 给不了的。- 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-collectorchart 的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 根本不上来
按顺序做,每一步都能二分:
- 确认采样。调试期把 SDK 配成全采样——OTel SDK 默认
parentbased_always_on本身就是 100%,如果你们用远端采样,见第 4 步。OTEL_TRACES_SAMPLER可以环境变量覆盖。 - 验证 span 真的产生了:给 SDK 临时挂 stdout exporter,能打出 span 说明业务埋点活着,问题在导出链路(见
05-Go客户端埋点.md)。 - 绕过中间层直连:把 SDK 的 endpoint 直接指到 Jaeger collector 的 4317/4318,跳过宿主机 agent / 网关 collector,缩小链路。
- 远端采样策略下发验证(怀疑策略没生效时):
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 协议口)。
- 容器网络三查(官方点名的典型错误):端口有没有从容器网络命名空间暴露出去;监听地址是不是
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)。 - 看计数器定性:
curl -s http://jaeger-collector:8888/metrics,otelcol_receiver_accepted_spans不涨 = 数据没到(回到第 3/5 步);accepted 涨但otelcol_exporter_sent_spans不涨 = 到了没写进去(查存储与症状四)。 - 还不行就
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 未设置
SAMPLEDflag,或远端概率采样只抽中部分节点,链路天然断;调试期全采样复现。 - 管道丢弃:比对症状一第 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/OSwrite_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) |