常见陷阱与最佳实践
本章把前九章散落各处的坑收拢成一张网:70 条陷阱按六个领域汇总,事故率最高的十组问题单独深讲,再给一份"旧教程 → v2 现状"的纠偏对照(Jaeger v2 与 OTel Go 近两年变化密集,网上教程和 AI 生成代码的半数错误都落在这张表里)、分主题最佳实践、上线自检清单和一张按症状索引的调试速查表。基线 Jaeger v2.22.0、OpenTelemetry Go v1.47.0 / contrib v0.72.0,各条目的验证出处见对应章节正文。
陷阱汇总表
领域一:版本与部署(见 00-概述与安装.md、02-v2架构与组件.md、03-部署与Kubernetes.md)
| 陷阱 | 为什么会踩、后果 | 正确姿势 |
|---|---|---|
| 照 v1 三件套教程部署 v2 | jaeger-agent/jaeger-collector/jaeger-query 拆分镜像与 COLLECTOR_*、SPAN_STORAGE_TYPE 环境变量 v2 一概不读;进程照常启动,配置静默失效 |
单一 jaeger 二进制,角色由 --config YAML 里的组件开关决定 |
| 以为 SIGHUP 能热加载 | v2 没有 SIGHUP 通道,信号被忽略 | 唯一热更通道是 remote_sampling file 策略 JSON 的 reload_interval;其余改动必须重启进程 |
镜像钉 latest |
v2 小版本间行为变化频繁(feature gate 转正、配置校验收紧),随时被升级 | 固定 2.22.0(本手册基线),升级前过一遍 09 的迁移流程 |
| all-in-one 内存存储原样上生产 | memory backend 出厂 max_traces: 100000,重启即全失 |
单机持久用 badger + 卷,多实例生产用 ES/OS/Cassandra(04-存储后端.md) |
| 只映射了 TCP 端口、漏了 UDP 6831/6832 | UDP sendto 永远成功,老客户端零报错、数据蒸发 |
nc -zuv 验证 UDP 通路;没有历史包袱就直接改走 OTLP 4317/4318 |
| 容器里 endpoint 写 localhost | 出厂模板是 ${env:JAEGER_LISTEN_HOST:-localhost}:...,端口映射"看着在监听"实则只绑 127.0.0.1,外部拒连 |
容器/跨主机部署显式绑 0.0.0.0(JAEGER_LISTEN_HOST=0.0.0.0) |
| K8s 继续找 jaeger-operator | operator 已归档停维 | 两条正路:Helm chart jaeger 4.16.1(NOTES 明标 EXPERIMENTAL,做好跟版本的心智)或 OpenTelemetry Operator |
以为 Helm storage.type 会让 Jaeger 连上 ES |
v2 里 storage.* 只注入 ES 维护 CronJob 的 env,主容器 env 仅来自 jaeger.extraEnv——v1 chart 经验迁移的头号坑 |
存储后端写进 config.yaml(--set-file userconfig),extensions.jaeger_storage 段自己配 |
compose 挂卷 + BADGER_* 环境变量 |
这些环境变量 v2 不识别,数据仍写容器可写层,删容器即失 | badger 参数进 config.yaml,directory 指向挂载卷 |
| 把"query→ingester→collector 升级顺序"当官方要求 | 2.22 任何文档都找不到这条成文规定 | 它是本手册惯例;真正要紧的是先灰度、验证配置校验通过再全量(09) |
| 引用"v2.15 起服务端采样已删除" | 旧教程/AI 转述编造的谣言,FAQ 原文并无此话 | remote_sampling 与 adaptive sampling 在 2.22 都健在;一切以 docs.jaeger.io sampling 页为准 |
领域二:埋点与 SDK(见 01-核心概念.md、05-Go客户端埋点.md)
| 陷阱 | 为什么会踩、后果 | 正确姿势 |
|---|---|---|
不调 SetTextMapPropagator |
OTel Go 全局 propagator 默认 no-op(其他语言 SDK 多有默认值),跨进程静默断链,本服务有 trace、下游全孤立 | 初始化时显式设 propagation.TraceContext{}(或 composite),这是 Go 埋点最需要记住的一行 |
本地连 all-in-one 不带 WithInsecure |
otlptracegrpc 默认开 TLS,对明文 4317 握手失败,SDK 侧只有重试日志 |
本地调试显式 WithInsecure;生产配真 TLS |
各服务裸设 TraceIDRatioBased |
parent-based 缺失时同一条 trace 各服务各自掷骰子,出来一堆碎渣 trace | SDK 默认本就是 ParentBased(AlwaysOn);要降采样就包一层 ParentBased(ratio) |
span 忘 End() / 进程退出忘 Shutdown() |
BSP 只导出"已结束"的 span;缓冲区里没刷出去的尾巴直接消失 | defer span.End();退出路径 Shutdown 配独立的短超时 ctx(推荐 1s) |
不设 service.name |
服务列表冒 unknown_service:<可执行名>,被当成"丢数据"排查半天 |
Resource 里设 service.name,全链路命名口径一致 |
| 服务名带大写(ES 后端) | ES 后端写入/查询大小写行为不一致,大写服务名在 UI 静默搜不到 | 服务名统一小写,从命名规范层面掐掉 |
| 属性/事件条数超 128 | SpanLimits 默认全 128,超出静默丢弃、不报错(值长度不限) | 控制单 span 属性规模;大内容走事件或外部关联(日志/对象存储链接) |
| tag 值超 256 字符 | ES 的 tag.* 索引 ignore_above: 256,超长值不进索引,UI 按 tag 过滤查不到 |
tag 短小精确;长文本放 span event 的 attributes 或日志 |
以为 RecordError 会标红 |
它只加 exception 事件,不改 status;SPM 错误率只认 STATUS_CODE_ERROR |
RecordError + SetStatus(codes.Error, ...) 成对出现 |
| operationName 写真实 URL/订单 ID | 高基数炸搜索索引和 SPM 序列数 | 用路由模板:otelgin 默认 METHOD + FullPath 就是正确示范 |
MQ 异步链路硬用 child_of |
消费者几小时后才处理,时间轴上父子倒挂、荒谬 | 用 link 关联;要表达因果用 follows_from 语义 |
goroutine 里 context.Background() 发起出站调用 |
新 ctx 不带 span 上下文,服务常年孤立根 | ctx 源头传递,出站复用同一 ctx |
| 照抄旧教程的 otelgrpc 四 interceptor | v0.63.0 已删除 interceptor API,升级直接编译失败 | 用 stats.Handler:NewServerHandler/NewClientHandler |
| 以为 baggage 会自动进 span attributes | Baggage 是传播容器,不是 attributes,不会自动注入 | 关键 span 手动 baggage.FromContext 读出再 SetAttributes |
| 指望消息队列客户端自动带 traceparent | franz-go 对 OTel 零依赖 | propagation.MapCarrier 手动 inject 到 kgo.Record.Headers,消费端 extract 续链 |
| SDK 升级后 panic 双重记录 | v1.47.0 起 End() 遇 panic 自动 recover→记 exception→re-panic,旧的手动 recover+记错写法会叠加 |
需要旧行为用 WithoutPanicRecording();存量代码检查重复记录 |
领域三:采样(见 06-采样.md)
| 陷阱 | 为什么会踩、后果 | 正确姿势 |
|---|---|---|
| 误读"不配 remote_sampling 会丢数据" | 没配置时策略服务对来拉策略的 SDK 宣告 probabilistic 0.001;已经到达 collector 的 span 一条不弃 | 先分清"没采上来"还是"丢了":查 SDK 侧策略来源,再看 collector 日志采样段 |
| 把 all-in-one 的全采心智带进生产 | all-in-one 出厂 file 策略是 default_sampling_probability: 1;独立 collector 未配置时下发 0.001——环境切换直接 1000 倍落差 |
换环境时显式写出采样策略,别信"默认值会跟着你走" |
| 指望 ratelimiting 按接口限流 | ratelimiting 不支持 per-operation 覆盖,只有 probabilistic/adaptive 有 operation 表 | 按操作限速改 adaptive,或拆到服务级策略 |
| tail sampling 想救 head 丢的 span | SDK 侧未采样的 span 根本没发上来,tail 只能在已到达的 span 里二次挑选 | head 保底采样 + 关键路径单独提额,tail 做"精挑"不做"打捞" |
decision_wait 白拿 |
默认 5s 既是决策窗也是内存驻留期,高流量下 collector RSS 直接抬升 | 按业务 trace 时长分布设置,并和内存上限、09 容量规划联动 |
| 盲目信任跨语言 remote sampling | 官方 sampling 页原文:“Some, but not all, OpenTelemetry SDKs support remote sampling” | 依赖策略下发前先验证自家语言 SDK 支不支持,否则远端配置等于没配 |
| all-in-one 的 5778/5779 跨容器拉策略 | 这两个采样端口在 all-in-one 出厂绑 localhost,同机通、跨容器不通 | 与 endpoint 同类病:JAEGER_LISTEN_HOST=0.0.0.0,或改走容器网络内的正式端口 |
领域四:存储后端(见 04-存储后端.md)
| 陷阱 | 为什么会踩、后果 | 正确姿势 |
|---|---|---|
照旧文档写 ES 索引管理 use_ilm/use_aliases/create_mappings |
v2.21 起 Beta feature gate jaeger.es.config.rejectLegacyRotationFlags 默认拒绝这些键,配置直接启动失败;docs 部分页面还在写旧字段(页面自称以 config.go 权威) |
新语法 `indices. |
顶层 tags_as_fields |
v2.22 起被 rejectLegacyTagsAsFields gate 拒绝 |
挪到 indices.spans.tags_as_fields |
把 date_layout/rollover_frequency 留在顶层又开 rotation |
rotation 块与 legacy 周期字段混写校验报错 | 周期参数移进 rotation.periodic;官方 config-elasticsearch.yaml 仍用顶层旧写法(v2.22 接受但 Deprecated),新配置别跟 |
配 data_stream 上线 |
语法和 e2e 示例都在,但 v2.22 校验返回 “data_stream is not yet implemented”(RFC 0004 Phase 2) | 等实现落地;现在在另外三种里选 |
| 迷信"ILM 策略必须叫 jaeger-ilm-policy" | 这个名字只在 jaeger-es-rollover init 流程里写死 |
新语法 auto_rollover.policy_name 可自定义,但策略本体必须预先在集群创建 |
async write_mode 下配 poison_pill_handling |
毒丸处理只在 write_mode: sync 下生效;async 下被拒文档只记日志、不重试 |
有损写路径别指望毒丸兜底;要兜底就 sync + drop/dead-letter |
开 numeric_attributes 期待存量生效 |
只对开启后新建索引生效(RFC 0015),老索引里数值 tag 照样查不到 | 配合索引滚动节奏开启,规划迁移窗口 |
写 disable_health_check/send_get_body_as/max_actions/sniffing.use_https |
v2.20 起无效且被校验拒绝,启动失败 | 删掉这四个键 |
| 把 ES 写入 ACK 当"立刻可查" | refresh 默认约 1s,刚写入的 trace 瞬间搜不到是正常现象 | 写入后 1 秒内搜不到别按丢数据排查 |
| ES 版本按 docs 页"7.x/8.x"一刀切理解 | 文档口径滞后,代码校验实际允许 7/8/9(OpenSearch 101/102/103) | 新版本引入前用真实例跑一次启动校验 |
| badger 想横向扩容或多进程共享目录 | badger 是单进程内嵌存储;docs 明确 collector 启用则 query 不能同进程;卷不能挂 NFS 多写 | 单机形态专用;多实例换集中式后端 |
| 上 ClickHouse 当稳定方案 | 仍是实验特性,靠 feature gate storage.clickhouse 开门 |
评估升级破坏性后再用,备好回滚 |
领域五:可靠性与管道(见 09-性能与运维.md)
| 陷阱 | 为什么会踩、后果 | 正确姿势 |
|---|---|---|
| 把官方示例管道当 lossless | 默认示例含 batch processor,它吞掉导出错误只记一条 “Sender failed” 日志——官方原话这类配置 “not lossless” |
无损配方三件套:去 batch + queue.wait_for_result: true + ES/OS write_mode: sync |
queue.batch.min_size 留零值 |
零值 = 每个请求单独成批不合并,批量写入白做 | 按存储写吞吐显式设 min/max/flush_timeout |
collector 直连 OTLP 还开 retry_on_failure |
SDK 侧本来就会重试,双层重试放大下游压力;毒丸数据还会无限重试堵死队列 | 直连模式交给客户端重试;要开必须配 poison_pill_handling: drop 或 dead-letter |
以为 jaeger_storage_exporter 天然不丢 |
默认入队即 ACK;只有 wait_for_result: true 才等真实写入结果,且这种模式下管道不能再叠 batch |
按无损配方显式声明,别靠组件名脑补语义 |
靠调大 queue.batch.max_size 提吞吐 |
wait_for_result 下批次受 Kafka 分区数约束(每分区同时只有一个批在合并);加 ingester 副本反而每实例分到更少分区、批次更小 |
提高写吞吐要加分区,不是加副本或改 max_size |
| 高估直连模式的容灾窗口 | receiver 把存储失败转成 gRPC Unavailable / HTTP 503 回给客户端;Go SDK 一次导出 30s 超时 + 60s 重试窗口——collector 挂超一分钟,SDK 缓冲区就开始丢 |
要扛大故障在中间垫 Kafka(collector → ingester → 存储) |
| 丢数据无感 | otelcol_receiver_refused_spans 出现即真实丢失,exporter 失败只进日志 |
告警覆盖 refused/send_failed 增速(09 有 PromQL) |
| 存储持续失败把管道堵死 | 毒丸/满盘/权限故障下整条管道停摆 | 预留死信通道:connectors + traces/dead_letter 管道,死信 sink 关自身 sending_queue、整收整拒(04) |
领域六:查询、UI 与 SPM 联动(见 07-查询UI与API.md、08-可观测性联动.md)
| 陷阱 | 为什么会踩、后果 | 正确姿势 |
|---|---|---|
| Monitor 空白直接怀疑"没采到数据" | 四类原因:指标命名错位 / exclude_dimensions 破坏错误判定 / span 未设 kind(默认只查 server)/ 真的没数据 |
按 08 排障顺序:先在 Prometheus 直接查序列增速,再开 telemetry.logs.level: debug 看 Jaeger 实际发的 PromQL |
| 看板照抄 v1 指标名 | v2 是 Collector 指标体系:jaeger_collector_spans_rejected_total → otelcol_receiver_refused_spans,一片改名 |
用仓库内置对照表 cmd/jaeger/docs/migration/{5 后端}-metrics.md 逐项迁,别猜名字 |
找 v1 的 jaeger_collector_save_latency_bucket 判断存储健康度 |
v2.22 不提供该指标(官方 performance-tuning 页挂红框声明) | 用 ES 侧 jaeger_bulk_index_latency_ok/err 两个直方图替代 |
| 拿 mixin 的告警规则直接用 | monitoring/jaeger-mixin 已不再内置 prometheus_alerts.yml(README 原文;docs 监控页滞后) |
用 OTel Collector 生态的告警规则(如 opentelemetry-helm-charts 的 prometheusrule.yaml) |
| 把 SPM 指标名吵成"到底有没有 traces_span_metrics_calls_total" | 两头都偏:connector 基名是 calls_total/duration_[units],但 Prometheus 侧默认全名经 namespace 清洗后就是 traces_span_metrics_calls_total |
基名进 collector 配置,全名进 PromQL/告警;最终以实际 scrape 到的名字为准 |
| 直方图桶数、维度随手加 | 序列数公式 status_code 数 × span_kind 数 × (1 + 桶数) × 操作数,默认典型 2×2×18 = 72 条/操作;30 服务×20 接口约 4.3 万条 |
基数预算先行;且 SPM 查询层不支持自定义维度,多余的会被聚合掉 |
| 错误率恒 0 | 两个成因:只 RecordError 不 SetStatus;或 exclude_dimensions: ['status.code'] 把判定依据裁掉 |
埋点成对 + 维度保留 status.code + span 设 kind |
| Trace to logs 按钮不出现 | 派生字段生效前提:tag 真实存在于 span 属性、且自定义查询变量全部非空;点号标签名需在 Grafana 侧重映射 | provisioning 里 $ 要写 $$,datasourceUid 必须等于 Jaeger 数据源真实 uid |
| 点开派生字段是空 trace | 内部链接只携带值;那条 trace 若 head 采样没入库,必跳空 | 关键操作路径在采样策略里提额,或对空跳转有预期 |
| exemplars 看不到 | 三处开关一个都不能少:connector exemplars.enabled 默认 false;exemplar 只活一个 flush 周期(默认 60s、每序列最多 5 点);Prometheus 要 --enable-feature=exemplar-storage |
三处一起开,且知道 60 秒后旧 exemplar 就没了 |
| duration 单位切到秒后告警差千倍 | feature gate useSecondAsDefaultMetricsUnit 计划把毫秒改秒 |
告警阈值参数化,别把 1000 写死进表达式 |
把 UI 的 /api/* 当稳定接口集成 |
官方定位是内部 API,无兼容承诺 | 程序化查询走 16685 的 gRPC QueryService(v3 稳定);UI 嵌入用 /search?...&uiEmbed=v0 链接参数 |
| 把归档当可搜索的第二存储 | 官方原文:“Archived traces are only accessible directly by ID, they are not searchable” | 归档只用于"案例保藏":按 traceID 精确取回 |
| 依赖图靠在线 UI 聚合 | 生产规模下在线聚合不可靠,官方姿势是离线批任务回填 | spark-dependencies 或 Flink 作业定时跑(Helm 里 spark-dependencies CronJob 默认 disabled,要显式开) |
| gRPC reflection 想发现全部服务 | 16685 反射只列 query 相关服务 | 按文档的 proto 契约写客户端,别靠反射动态适配 |
| 用 UI 搜索回放历史当报表用 | 每次搜索都打到存储后端:深时间窗 + 高基数 tag + 大 limit 最容易打穿 ES | 趋势分析查指标后端;生产收紧搜索护栏 search.maxLookback/search.maxLimit(默认值偏开发友好) |
高危陷阱深讲
1. “UI 没数据”:一条五层排查链
这是 Jaeger 排障的第一高频场景,而答案几乎总是多个默认值叠加,不是单一故障。按命中概率排序过一遍:
- SDK 根本没导出来:本地连 all-in-one 没带
WithInsecure(TLS 打明文端口)、忘Shutdown()(短生命周期服务尾部全丢)、propagatorno-op 导致只有孤立根。先开 SDK 侧 debug 日志或在 collector 上查otelcol_receiver_accepted_spans增速——增速为 0 是上游问题,增速正常再往下查。 - 采样把数据掐了:独立 collector 未配 remote_sampling 时对来拉策略的 SDK 宣告 0.001;SDK 默认虽是 ParentBased(AlwaysOn),一旦有人显式配了裸 ratio 就更糟。
- 网络与绑定:容器里 endpoint 绑了 localhost(出厂模板默认值),端口"可见但拒连";UDP 6831 没映射(老客户端还零报错)。
- 落库了但查不到:ES 后端大写服务名静默失踪;写入后 1 秒内 refresh 未完成;tag 值超 256 字符不进索引。
- UI 姿势问题:服务列表看
unknown_service:;搜索时间窗没覆盖;trace 被归档后不可搜索。
2. batch processor 吞错:不报错的丢数据
Collector 生态里 batch 是最顺手加的 processor,但对 Jaeger 管道它有个致命细节:batch 异步导出时对上游返回成功、吞掉下游错误,失败只体现在 “Sender failed” 日志里。官方 delivery-guarantees 页因此明说示例配置 “not lossless”。无损骨架:
service:
pipelines:
traces:
receivers: [otlp]
processors: [] # 关键一:不放 batch
exporters: [jaeger_storage_exporter]
exporters:
jaeger_storage_exporter:
queue:
wait_for_result: true # 关键二:拿到真实写入结果才确认
storage:
backends:
es_main:
write_mode: sync # 关键三:ES 同步刷盘语义批处理需求改用 exporter 自带的 queue.batch.{min_size,max_size,flush_timeout}(注意 min_size 零值 = 不合并)。这条链打通后,存储失败会一路回传到 receiver,以 gRPC Unavailable / HTTP 503 告知客户端——错误可见,才有重试和背压可言。完整配方见 09-性能与运维.md。
3. ES 索引管理:v2.21+ 起旧字段是"启动失败"级错误
docs 部分页面还在展示 use_ilm、use_aliases、create_mappings 这些 v1 时代的键,但 v2.22 源码里 jaeger.es.config.rejectLegacyRotationFlags(Beta,自 v2.21.0)默认拒绝它们,rejectLegacyTagsAsFields(自 v2.22.0)拒绝顶层 tags_as_fields。新语法是 indices.<type>.rotation 下 periodic / manual_rollover / auto_rollover / data_stream 四选一——其中 data_stream 语法存在但校验返回 “not yet implemented”。两个容易误判的细节:官方 config-elasticsearch.yaml 示例仍用顶层 date_layout/rollover_frequency(属于"legacy 周期字段未被列进拒绝名单",接受但 Deprecated,别当新配置范本);jaeger-ilm-policy 这个策略名只在 jaeger-es-rollover init 流程里写死,新语法 auto_rollover.policy_name 可自定义,只是策略本体要预先建好。被 gate 拒绝时进程直接起不来,报错会点名违规键——这是少数"配置写错反而好排查"的场景。
4. 采样的三层误解:0.001、all-in-one 和 tail
- 0.001 到底坑了谁:不配 remote_sampling 时,策略服务对来拉策略的 SDK 宣告 probabilistic 0.001;已到达 collector 的 span 一条不丢。所以"collector 不配置会丢数据"是误读,真正受伤的是信任了远端策略的 SDK 端。
- all-in-one 是反直觉的:它出厂 file 策略
default_sampling_probability: 1全采。本地开发"怎么都有数据",上生产换独立 collector 忘了带策略,就是 1 → 0.001 的千倍落差。 - tail 不是打捞船:tail_sampling(decision_wait 默认 5s)只能对已经上报到 collector 的 span 做去留决策;head 处被 SDK 丢掉的 span 物理上到不了眼前。想"错误全保、正常降采",正确组合是 head 保底比例 + 关键路径提额,tail 只做精挑。decision_wait 本身还有内存驻留成本,和 collector RSS 联动规划。
- 另外记住 ratelimiting 不支持 per-operation;官方 FAQ 原话 “Some, but not all, OpenTelemetry SDKs support remote sampling”——跨语言团队先验证自家 SDK 能不能拉策略,再决定这套架构。
5. localhost 绑定:容器化的头号网络坑
v2 出厂配置里的监听地址模板是 ${env:JAEGER_LISTEN_HOST:-localhost}——宿主机直跑一切正常,进容器就变成"端口映射在、连接被拒"。三类症状同源:receiver/扩展 endpoint 绑 127.0.0.1;all-in-one 的采样端口 5778/5779 同样只绑 localhost,跨容器拉策略不通;docker run 忘 -p 且走 UDP 时连报错都没有。统一解法:容器环境设 JAEGER_LISTEN_HOST=0.0.0.0(或逐 endpoint 覆盖),并用 Pod IP 实测一次 curl/nc,别信"端口映射存在"。
6. 全局 propagator 默认 no-op:Go 埋点最阴的静默坑
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
propagation.TraceContext{},
propagation.Baggage{},
))OTel Go 的全局 propagator 出厂是 no-op(internal/global/propagator.go 实测),这四行缺失的后果非常迷惑:单服务里 span 齐全、trace 完整,一到跨服务/跨进程边界就断,且没有任何错误日志——因为没注入就等于没携带。Java/Python 等 SDK 有默认 propagator,教程互抄时这行经常被当冗余删掉。相关地:baggage 不会自动变成 attributes;消息队列(franz-go 对 OTel 零依赖)要用 propagation.MapCarrier 手动把 traceparent 塞进消息 header,消费端再 extract 续链,跨异步边界用 link 而不是硬挂 child_of。
7. 错误率恒 0:SPM 只认 status
Monitor 页签和 SPM 指标的错误判定只数 STATUS_CODE_ERROR 的 calls_total。三个独立成因都会让曲线趴平:只 span.RecordError(err) 不 span.SetStatus(codes.Error, ...)(RecordError 仅加 exception 事件,不改 status);exclude_dimensions: ['status.code'] 裁掉了判定依据;span 没设 kind(SPM 默认只聚合 server 侧)。埋点层成对写、配置层保维度、中间件层设 kind,三层各查一遍。
8. 毒丸与死信:存储侧的两种烂数据
被 ES 校验拒绝的文档(毒丸)在不同模式下落点完全不同:write_mode: sync 下才谈得上 poison_pill_handling(drop 或进死信),async 下只记日志、不重试;直连 OTLP 又开了 exporter retry_on_failure 的话,毒丸会无限重试堵死整条队列。系统性的兜底是 04 章给出的第三条路径:jaeger_storage_exporter 换成 connectors + traces/dead_letter 管道,存储保持 fail,死信 sink 必须关掉自身 sending_queue、整收整拒,否则死信通道自己先丢。
9. 指标体系换代:看板、告警、单位三处旧账
v2 的 collector 指标全面换到 OTel Collector 命名(otelcol_receiver_accepted_spans、receiver_refused_spans…),迁移不许猜名字:仓库内置 cmd/jaeger/docs/migration/{all-in-one,badger,cassandra,elasticsearch,opensearch}-metrics.md 五张对照表。两处特例:ES 写入指标 jaeger_bulk_index_* 同名保留;而 v1 调优页赖以判断存储健康的 jaeger_collector_save_latency_bucket 在 v2.22 尚未提供(官方页红框声明),过渡期盯 jaeger_bulk_index_latency_ok/err。告警侧,monitoring/jaeger-mixin 已删除 prometheus_alerts.yml(依赖 v1 指标、无法平移),改用 OTel 生态规则。未来还有一颗雷:feature gate useSecondAsDefaultMetricsUnit 计划把 duration 指标单位从毫秒切到秒——告警阈值写成变量。
10. 基数三连击:操作名、tag 长度、属性条数
同一类事故——“数据进不去/查不到,但不报错”——有三个独立闸门:operationName 写真实 URL/ID 使序列数按公式 status_code × span_kind × (1 + 桶数) × 操作数 线性爆炸(默认典型 72 条/操作);tag 值超 256 字符不进 ES tag.* 索引(ignore_above);单 span 属性/事件超 128 条被 SpanLimits 静默截断。治理动作都在埋点层:路由模板命名、tag 短小、大内容出事件、属性做减法。
旧教程纠偏清单
网上存量教程、博客与 AI 生成代码多写于 v1 时代或 Jaeger v2 早期,以下 23 条"旧 → 新"对照均可直接用于排错:
| 旧写法/旧说法 | v2.22 现状 |
|---|---|
agent/collector/query 三镜像 + --span-storage.type |
单一二进制 + config.yaml,角色由组件段决定;无参启动 = 内嵌 all-in-one + memory |
COLLECTOR_*、SPAN_STORAGE_TYPE、BADGER_* 环境变量 |
不识别;一切进 config.yaml,--set key=value 可覆盖单键 |
--collector.num-workers / --queue-size 调导出队列 |
对应物是 jaeger_storage_exporter 的 queue.{num_consumers,queue_size,wait_for_result,block_on_overflow} |
| 必装 jaeger-agent sidecar 收发 UDP 6831 | 6831/6832/14250/14268 仍在(Stable、Deprecated),主线 OTLP 4317/4318 |
| “Zipkin 9411 已 Deprecated” | Zipkin receiver 状态是 Stable;Deprecated 的是 legacy thrift 那组 |
| “v2.15 起服务端采样已删除” | 谣言:remote_sampling(file/adaptive)在 2.22 文档与出厂配置里都在 |
| 采样下发端口只认 5778 或只认 14268 | APIs 页列 5778;troubleshooting 页示例用 14268——两处都真,出处不同 |
| jaeger-operator 管理 Jaeger CRD | operator 已归档;改 Helm chart 4.16.1(EXPERIMENTAL)或 OpenTelemetry Operator |
Helm storage.type: elasticsearch = 自动接后端 |
只注入 ES 维护 CronJob;主容器配置在 userconfig(config.yaml) |
| “官方升级顺序 query→ingester→collector” | 文档未成文;本手册惯例,核心动作是先灰度 |
jaeger-client-go / OpenTracing API 埋点 |
客户端已归档(最后一版 v2.30.0,2021-12-07);用 OTel SDK |
| otelgrpc 四件套 interceptor | v0.63.0 删除,改 stats.Handler(NewServerHandler/NewClientHandler) |
| spanmetrics connector 旧组件名 | 组件更名 span_metrics(旧名 deprecated 可用;Jaeger SPM 文档示例仍写旧名) |
ES use_ilm/use_aliases/create_mappings |
v2.21+ 被 gate 拒绝;用 indices.<type>.rotation 新语法 |
mixin 自带 prometheus_alerts.yml |
已移除(依赖 v1 指标);用 OTel Collector 生态告警规则 |
jaeger_collector_save_latency_bucket 看存储健康 |
v2.22 不提供;用 jaeger_bulk_index_latency_ok/err |
“v2 文档里没有 traces_span_metrics_calls_total” |
SPM 排障节 PromQL 原文即此名(namespace traces.span.metrics 清洗而来);基名才是 calls_total |
| SIGHUP 热加载 collector 配置 | 无 SIGHUP;只有 remote_sampling file JSON 按 reload_interval 自动重载 |
| “v2 接受/拒绝 64-bit traceID"的绝对说法 | 官方无承诺;按 128-bit 主线理解,16 字符 legacy 短 ID 能查即懂 |
HotROD 用单镜像 docker run 起 |
官方姿势 git clone jaegertracing/examples/hotrod && docker compose up,前端 8080 |
独立 jaeger_mcp extension |
已退役,MCP 内嵌进 UI(/api/ai/mcp/) |
| zpages 用 OTel 默认端口 55679 | all-in-one 出厂显式覆盖为 27778(因官方默认口 ephemeral 才改) |
| 回传状态码写成 “HTTP 53” | delivery-guarantees 原文是 HTTP 503 |
最佳实践清单
埋点(Go)
- initTracer 三件套缺一不可:
SetTextMapPropagator、带 service.name 的 Resource(全小写)、deferShutdown配 1s 超时 ctx。 - 采样统一走 ParentBased;服务自己永远不做"裸 ratio"的决定。
RecordError与SetStatus成对;HTTP/gRPC 中间件设好 span kind(SPM 与错误率判定都靠它)。- 操作名用路由模板,tag 短小(<256 字符)、属性条数克制(128 内),大内容进事件或外部关联。
- 异步/MQ 边界显式 inject-extract +
link;goroutine 传递 ctx,不重新起 Background。
部署与配置
- 镜像、chart、手册版本三者对齐钉死(本基线 2.22.0 / chart 4.16.1);升级前跑一遍出厂配置做校验预演。
- 容器化第一天就设
JAEGER_LISTEN_HOST=0.0.0.0,并把"端口可达"纳入部署验收(curl/nc 实测,不靠映射声明)。 - 配置改动默认按"要重启"设计运维流程;只有采样策略文件享受热更。
- K8s 存储配置写进 userconfig;ES 维护 CronJob(index-cleaner/rollover/lookback/spark-dependencies)默认全 disabled,按需显式开启并监控其执行结果。
采样与存储
- 生产采样策略显式成文(file 或 adaptive),环境间迁移时对照检查;all-in-one 的全采默认值绝不允许外溢到生产。
- 新配置一律用 rotation 新语法;
jaeger-ilm-policy、策略预建、numeric_attributes只作用新索引这三个约束写进 runbook。 - 留存期闭环:TTL/清理 CronJob 与
max_span_age/trace_ttl对齐,磁盘水位有告警。 - badger 认清单机定位;ClickHouse 只在能承受实验性破坏变更时入场。
可靠性与可观测联动
- 对每条管道书面声明"是否要求 lossless”;要求的按三件套配方配置,并把
refused/send_failed增速做进告警。 - 大流量、强可靠走 Kafka 缓冲层;分区数、ingester 副本、批大小按"批次受分区数约束"的模型规划,用 tracegen 压拐点(
09)。 - SPM 序列数做预算公式核算;Monitor 排障固定走"Prometheus 查序列 → debug 看 PromQL → 核对 normalize 开关"三步。
- Grafana 集成(exemplars、derived fields、Trace to logs)把三处开关/转义/uid 写进 provisioning 模板,不手点。
生产上线自检清单
- SDK:propagator 已显式设置;service.name 全小写统一;
Shutdown挂进退出流程 - SDK:错误路径
RecordError+SetStatus成对;中间件设 span kind - SDK:属性/事件 ≤128、tag 值 ≤256、操作名用路由模板
- 采样:生产采样策略成文并版本化;不依赖任何"出厂默认"
- 部署:镜像/Chart 版本钉死;
JAEGER_LISTEN_HOST=0.0.0.0;UDP/HTTP 端口逐个实测连通 - 部署:v1 环境变量与旧 flag 全部清零;配置进 config.yaml(K8s 走 userconfig)
- 存储:ES 配置用 rotation 新语法且启动校验通过;清理/滚动 CronJob 已开且最近一次执行成功
- 存储:磁盘水位、TTL、
max_span_age三者对齐留存方案 - 可靠性:管道 lossless 声明明确;batch 不在无损链路上;毒丸有 drop 或死信兜底
- 可靠性:直连模式知道 30s+60s 容灾窗口;超窗故障有 Kafka 缓冲层承接
- 监控:看板按 migration 对照表迁移;告警覆盖
refused/send_failed;单位 gate 变更阈值可参数化 - 查询:程序化查询走 16685 gRPC v3;UI
/api/*无集成依赖;搜索护栏已收紧 - SPM:序列基数在预算内;Monitor 三步排障路径演练过至少一次
- 文档:本手册
09的故障演练(杀 collector / 打满磁盘 / 灌毒丸)做过一轮
调试速查表
| 症状 | 先查什么 | 典型根因 |
|---|---|---|
| UI 完全没数据 | collector otelcol_receiver_accepted_spans 增速 |
SDK 没导出(TLS/Shutdown/端口)→ 采样 0.001 → localhost 绑定 → ES 大写名/refresh 延迟 |
| 链路在跨服务处断开 | 两个服务各自有没有独立 trace | propagator no-op;出站没带 ctx;MQ 没 inject/extract |
| 某服务从不出现在下拉列表 | 服务列表有没有 unknown_service: |
service.name 未设;ES 后端大写名失踪 |
| trace 碎成多段 | 每段起点是不是各服务入口 | 裸 ratio 采样没包 ParentBased |
| 搜索写不到、按 ID 能查到 | 写入时间与 ES refresh | 刚写 1s 内不可见;lookback 时间窗太窄;已归档(不可搜索) |
| Monitor 页签空白 | Prometheus 里 traces_span_metrics_calls_total 有没有序列 |
normalize 命名错位;exclude_dimensions 裁了 status.code;span 无 kind;真没数据 |
| 错误率恒 0 | 出错 span 的 status 字段 | 只 RecordError;维度裁除;kind 不匹配 |
| 点开派生字段跳空 trace | 那条 trace 在不在主存储 | head 采样未入库;$$/uid 配置错 |
| 改配置不生效 | 进程重启时间戳 | 除采样 file JSON 外没有热加载;v1 环境变量根本不被读 |
| 容器间连致拒 | endpoint 实际绑定地址 | localhost 出厂模板;5778/5779 跨容器 |
| UDP 客户端无错但没数据 | nc -zuv 实测 |
6831/6832 未映射 |
| collector RSS 持续上涨 | tail decision_wait 驻留、队列积压 | tail 缓冲、batch 未生效(min_size 零值)、分区数卡批次 |
| ES 配置启动即失败 | 报错点名的键名 | legacy rotation/tags_as_fields 被 gate 拒;v2.20 起无效的四个旧键;data_stream 未实现 |
| 数据丢但不报错 | exporter 日志有没有 “Sender failed” | batch 吞错;async write_mode;毒丸堵重试 |
| Kafka 模式吞吐上不去 | 分区数 vs ingester 副本数 | wait_for_result 下批次受分区数约束;加分区才有用 |
| UI 查询把 ES 打挂 | 慢查询日志与搜索参数组合 | 深时间窗 + 高基数 tag + 大 limit;护栏未收紧 |