Skip to content
常见陷阱与最佳实践

常见陷阱与最佳实践

本章把前九章散落各处的坑收拢成一张网: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..rotation.{periodic
顶层 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 排障的第一高频场景,而答案几乎总是多个默认值叠加,不是单一故障。按命中概率排序过一遍:

  1. SDK 根本没导出来:本地连 all-in-one 没带 WithInsecure(TLS 打明文端口)、忘 Shutdown()(短生命周期服务尾部全丢)、propagator no-op 导致只有孤立根。先开 SDK 侧 debug 日志或在 collector 上查 otelcol_receiver_accepted_spans 增速——增速为 0 是上游问题,增速正常再往下查。
  2. 采样把数据掐了:独立 collector 未配 remote_sampling 时对来拉策略的 SDK 宣告 0.001;SDK 默认虽是 ParentBased(AlwaysOn),一旦有人显式配了裸 ratio 就更糟。
  3. 网络与绑定:容器里 endpoint 绑了 localhost(出厂模板默认值),端口"可见但拒连";UDP 6831 没映射(老客户端还零报错)。
  4. 落库了但查不到:ES 后端大写服务名静默失踪;写入后 1 秒内 refresh 未完成;tag 值超 256 字符不进索引。
  5. 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(全小写)、defer Shutdown 配 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;护栏未收紧