存储后端
Jaeger 的后端选型是整个链路里最"一次决定、长期锁定"的部分:埋点可以改、采样可以调、UI 无所谓,但 traces 写进哪种存储,决定了你能不能按 tag 搜 trace、数据能留多久、运维要养几套集群,也决定了 v1→v2 迁移时旧数据还查不查得到。本章基线 Jaeger v2.22.0,所有 YAML 骨架均按官方仓库 cmd/jaeger/ 下对应版本的示例配置改写,参数语义以官方源码 internal/storage/elasticsearch/config/config.go 等为权威(docs 页面自己也写明 “please refer to config.go as the authoritative source”)。
官方对后端的定性(2.22 storage 页原文):Cassandra、Elasticsearch、OpenSearch 是三个主力分布式后端;ClickHouse 是实验性,需要 feature gate 才允许启用;memory/badger 内置于二进制、无外部依赖;大规模生产部署,Jaeger 团队推荐 OpenSearch 而非 Cassandra(原文 “For large scale production deployment the Jaeger team recommends OpenSearch backend over Cassandra”)。另外两条容易被误读的定性:Kafka 不是存储,只是 collector→ingester 之间的中间缓冲;Remote Storage(gRPC)让你把任何实现了 Jaeger 存储 API 的外部服务当后端挂进来。
选型地图:先看规模,再看运维熟手程度
| 你的处境 | 推荐后端 | 一句话理由 |
|---|---|---|
| 本地开发、跑 demo、CI 冒烟 | memory | 零依赖零配置,进程退出数据即消失,所以绝不上生产 |
| 单机小规模、想让 trace 活过重启 | all-in-one + badger | 官方定位:可用于生产,但受限于单实例、无法横向扩展,只适合 modest data volumes |
| 中等规模、团队已有/愿意建 ES 或 OpenSearch 集群 | Elasticsearch / OpenSearch | 搜索能力最强,查询直落倒排索引,官方与社区工具链(rollover、ILM、Kibana)最全 |
| 大规模生产、要长期扛写吞吐 | OpenSearch(官方优先)/ Elasticsearch | storage 页明确 OpenSearch 优于 Cassandra;写入单份、索引在节点内完成 |
| 已有重度 Cassandra 运维经验、TTL 原生到期是刚需 | Cassandra | 原生 TTL 省心;代价是搜索由 Jaeger 客户端在 KV 模型上拼装,能力受限 |
| 流量尖峰大、已有 Kafka 平台、要重放/旁路分析 | 任意主力后端 + Kafka 缓冲层 | 削峰、解耦扩容、支持后处理管道;Kafka 本身不存 trace |
| 想要列存分析式查询、能接受实验性 | ClickHouse(feature gate) | 列式压缩+向量化执行适合 trace 分析,schema 尚无稳定性承诺 |
| 上述都不合适(如要落 PostgreSQL) | remote storage(grpc) |
社区已有 PostgreSQL 实现(robbert229),自定义后端按 Remote Storage API v2 开发 |
为什么 Jaeger 需要专用存储 schema
trace 的两种访问模式天然分裂:
- 点查:按 traceID 拉出一条完整 trace 的所有 span——要求按 traceID 快速定位;
- 搜索:按 service + operation + tags + duration 区间 + 时间窗过滤,再聚合出 trace 列表——这是倒排索引加数值 range 查询的主场。
Cassandra 是 key-value 分区表模型,点查极快,但搜索只能在 Jaeger 客户端侧基于分区扫描拼出来——FAQ 原文直言这种实现 “is limited and may produce inconsistent results”,且为了弥补搜索能力,写一条 span 要同时写 span 主记录、service/operation 索引和每个 tag 的索引,存在写放大;而写入 OpenSearch/Elasticsearch 是单份文档、索引动作发生在节点内部。这也解释了官方"推荐 OpenSearch 胜过 Cassandra"的完整逻辑:Cassandra 单条写更快,但索引写放大把总体吞吐拉回到与 OpenSearch 相当,而搜索体验明显更好。反面代价是 Cassandra 原生 TTL 到期自动清理,ES/OS 的数据过期必须靠索引轮换/ILM 额外搭建。
无论选哪个主力后端,Jaeger 写入的都是自己的 schema(如 ES 的 jaeger-span-*),query 服务也只认这套 schema——把第三方 exporter 写进同一个 ES 的自定义索引,Jaeger UI 是查不到的(FAQ 明确)。
v2 统一配置结构:命名后端与接线图
v2 把 v1 的 SPAN_STORAGE_TYPE 环境变量体系整个废掉,改为在 collector/query 合一的单一二进制里用 YAML 声明"命名后端",写侧和读侧分别引用同一个后端名字:
jaeger_storage.backends.<名字>.<类型>: { ... } ← 后端在这里定义(可多个)
exporters.jaeger_storage_exporter.trace_storage: <名字> ← 写入侧引用
jaeger_query.storage.traces: <名字> ← 查询侧引用
jaeger_query.storage.traces_archive: <另一个名字> ← 归档侧引用
jaeger_query.storage.metrics: <名字> ← SPM 指标查询引用
jaeger_storage.metric_backends.<名字>.<类型>: { ... } ← SPM 指标后端文字接线图(直写模式):
应用 OTLP ──> receivers.otlp ──> processors.batch ──> exporters.jaeger_storage_exporter
│ trace_storage: some_storage
▼
extensions.jaeger_storage.backends.some_storage.(memory|badger|elasticsearch|opensearch|cassandra|clickhouse|grpc)
▲
浏览器/ API ──> extensions.jaeger_query.storage.traces: some_storage这个"一个后端名字被写侧和读侧分别引用"的设计是 v2 的核心心智:同一进程里 collector 与 query 通过同一个 jaeger_storage extension 共享后端实例;拆成独立部署时,两边配置文件里的同名 backend 指向同一套外部集群即可。官方示例大量使用 YAML 锚点(如 &elasticsearch_config)让 traces 后端与 metric_backends 复用同一段连接配置,生产里也可照抄。
注意:Kafka 不在这张图的 backends 里。它出现在管道组件层——collector 侧是
exporters.kafka,ingester 侧是receivers.kafka(来自 opentelemetry-collector-contrib),trace 的终点存储仍然是某个命名 backend。详见后文 Kafka 专节。
memory:只用于开发
官方 storage/memory 页原文定性:“The in-memory storage is not intended for production workloads”,数据随进程退出丢失。默认对留存 trace 数不设上限,靠 max_traces 封顶防 OOM:
service:
extensions: [jaeger_storage, jaeger_query]
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [jaeger_storage_exporter]
extensions:
jaeger_query:
storage:
traces: some_store
jaeger_storage:
backends:
some_store:
memory:
max_traces: 100000 # 超出后按数量淘汰最老的 trace
receivers:
otlp:
protocols:
grpc:
processors:
batch:
exporters:
jaeger_storage_exporter:
trace_storage: some_store容量直觉:一条 span 文档在内存里通常是 KB 级放大后的对象,10 万 trace(每 trace 几十 span)轻松吃掉几个 GB。memory 没有任何持久化、没有任何搜索语法可配——它的全部意义是让 jaeger all-in-one 开箱即用。
badger:单机持久化的天花板
badger 页原文:badger 是嵌入式持久化 KV(类似 RocksDB),Jaeger 二进制内嵌它,无外部依赖。官方给的 Pros/Cons 值得逐句读:
- Pros:数据落本地文件系统,进程重启不丢;单节点吞吐可观;
- Cons:只适合单节点部署,无法横向扩展;如果 collector 启用了 badger backend,query 不能在同进程里访问它(反之亦然)——所以 badger 只有两条活路:all-in-one 一体化进程,或者配
jaeger-remote-storage把 badger 包成 gRPC 服务供远端 query 读。
architecture 页的补充定性:“all-in-one with the Badger backend can be used in production, but only for modest data volumes”。
官方 config-badger.yaml 改写的最小骨架(含归档双后端):
extensions:
jaeger_query:
storage:
traces: some_store
traces_archive: another_store
jaeger_storage:
backends:
some_store:
badger:
directories:
keys: "/data/jaeger/"
values: "/data/jaeger/"
ephemeral: false # true 则用内存盘,与 memory 无异
ttl:
spans: 48h # 主存短 TTL
metrics_update_interval: 10s
another_store:
badger:
directories:
keys: "/data/jaeger-archive/"
values: "/data/jaeger-archive/"
ephemeral: false
ttl:
spans: 720h # 归档放 30 天
receivers:
otlp:
protocols:
grpc:
processors:
batch:
exporters:
jaeger_storage_exporter:
trace_storage: some_store容量直觉:磁盘占用约等于 span 总量(压缩 LSM),受单机磁盘和单节点吞吐约束;badger 没有分布式副本,磁盘一坏数据即丢,生产使用务必搭配备份卷或干脆升级到主力后端。管道接线与上一节 memory 示例相同(pipeline + jaeger_query extension 同进程)。
Elasticsearch:中长期主力与本章主菜
docs 标注支持 ES 7.x / 8.x,版本在启动时通过 root/ping 端点自动探测,并按探测到的版本选择兼容的 mapping 与 REST API;可用 version: 显式钉死(源码校验允许 7/8/9,置 0 表示自动探测)。ES 无需任何"初始化":集群起来后,把正确的连接参数交给 Jaeger 即可。
连接与鉴权参数
jaeger_storage:
backends:
some_storage:
elasticsearch:
server_urls:
- https://es-node-1:9200
- https://es-node-2:9200
auth:
basic:
username: jaeger
password: "${env:ES_PASSWORD}"
# password_file: /run/secrets/es_password # 支持从文件读,可配 reload_interval 周期性重读
tls:
ca_file: /etc/jaeger/es-ca.pem
query_timeout: 30s # 查询超时,0 表示不限
http_compression: true # 默认开启 gzip,置 false 可关闭
# custom_headers: # AWS OpenSearch + SigV4 代理等场景
# Host: "my-domain.us-east-1.es.amazonaws.com"
sniffing:
enabled: false # 默认关:集群暴露客户端不可达地址时会连不上要点:auth 三选一(basic / bearer_token / api_key),源码同时配置多种会直接校验失败;custom_headers 是官方示例里给 AWS 托管 OpenSearch/ES 留的通道;sniffing.enabled 打开后客户端用种子节点探出全集群地址——官方注释特别提醒"只有每个节点都直连可达时才开"。
注意:v1 时代的
disable_health_check、send_get_body_as、bulk_processing.max_actions、sniffing.use_https在 v2 自 v2.20.0 起已无任何效果,且现在会被配置校验直接拒绝(源码标注 Deprecated 并 “rejected by config validation”)。老配置文件里带着这几个键升级上来的,启动会报错,删掉即可。
一条 span 在 ES 里长什么样
Jaeger 写入的是自有 schema,索引模板内置于源码(internal/storage/elasticsearch/esclient/index_templates/jaeger-span.json)。核心字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| traceID / spanID / parentSpanID | keyword(ignore_above 256) | 点查入口,按 traceID term 匹配 |
| operationName / process.serviceName | keyword | UI 的 Service/Operation 下拉来源 |
| startTime | long(微秒) | 时间过滤主力字段 |
| startTimeMillis | date(epoch_millis) | 便于与 ES date math/Kibana 对齐 |
| duration | long(微秒) | UI 的 Min/Max Duration 就是它的 range 查询 |
| flags | integer | 采样/调试标记位 |
| tags | nested 数组,元素 {key, type, value} | tag 搜索走 nested query |
| logs | nested,内含 nested fields{key,value,type} | span 事件日志 |
| references | nested {refType, traceID, spanID, …} | FOLLOWS_FROM/CHILD_OF 引用 |
| process.tags / scopeTags | nested | 进程级(资源)属性与 instrumentation scope 属性 |
| tag.* / process.tag.* / scopeTag.* | keyword,由 dynamic_templates 生成 | 仅当 tags_as_fields 开启才有独立字段 |
两点与 UI 行为直接相关:tags 之所以用 nested 而不是平铺 keyword 数组,是为了让"key=http.status AND value=200"必须命中同一个元素——平铺数组会把跨 tag 的错误组合也匹配上。UI 搜 duration 实际落在 duration long 字段的 range;搜服务/操作/时间分别落到 process.serviceName、operationName、startTime 的 term/range——这就是"Jaeger 需要倒排类引擎"的具体原因:这套查询在 Cassandra 上只能靠客户端拼装。
索引生命周期:三种策略与 v2.22 的新语法
官方三种索引管理策略,ES 页称 ILM 为推荐项:
| time-based(默认) | manual rollover | ILM/ISM 自动 rollover(推荐) | |
|---|---|---|---|
| 索引怎么产生 | Jaeger 按天/小时建 jaeger-span-2024-06-18 |
jaeger-es-rollover init 建首个 jaeger-span-000001 |
同左,一次性 init |
| 轮换触发 | 自动(新日期) | jaeger-es-rollover rollover cron |
ES 的 ILM policy 自动 |
| 过期清理 | jaeger-es-index-cleaner cron |
rollover lookback(可选)+ index-cleaner cron | ILM policy 包办 |
| 外部依赖 | 无 | 两个 cron 任务 | init 一次性 + policy 预建 |
v2.22 的策略语法收敛到每个索引类型的 rotation: 块(源码 config_rotation.go),四选一且只能选一:periodic / manual_rollover / auto_rollover / data_stream。
# 策略一:time-based(等价于官方 config-elasticsearch.yaml,用新语法写)
elasticsearch:
server_urls: ["http://localhost:9200"]
write_mode: async # 默认值,可省略
indices:
index_prefix: "jaeger-main"
spans:
shards: 5
replicas: 1
rotation:
periodic:
date_layout: "2006-01-02" # 改 "2006-01-02-15" 即按小时建索引
rollover_frequency: "day" # 读侧扫索引的粒度,须与 date_layout 匹配
tags_as_fields: # 把指定 tag 平铺成独立字段(旧顶层 tags_as_fields 的替代位)
include: "http.status_code,tenant_id"
dot_replacement: "_"
services:
shards: 5
replicas: 1
dependencies:
shards: 5
replicas: 1
sampling:
shards: 5
replicas: 1# 策略二:manual rollover —— 别名交给外部工具管
spans:
shards: 5
replicas: 1
rotation:
manual_rollover: {} # 默认读/写别名 <prefix>-jaeger-span-read/-write,可显式 read_alias/write_alias
# 上线流程:jaeger-es-rollover init 建模板+000001+别名 → 部署 Jaeger →
# cron: jaeger-es-rollover rollover(max_age 1d 等条件) + jaeger-es-rollover lookback + jaeger-es-index-cleaner# 策略三:ILM 自动 rollover —— ES 页标注的推荐方案
spans:
shards: 5
replicas: 1
rotation:
auto_rollover:
policy_name: "jaeger-ilm-policy" # 嵌入 index template,新索引自动挂 policy注意(两条硬约束):
- 老的顶层开关
use_aliases/use_ilm/span_read_alias/span_write_alias等已被rotation块取代,并被源码标记 Deprecated;自 v2.21.0 起 feature gatejaeger.es.config.rejectLegacyRotationFlags默认开启(Beta),配置里再写这些旧字段会直接校验报错,临时逃生阀是启动参数--feature-gates=-jaeger.es.config.rejectLegacyRotationFlags。同理,顶层tags_as_fields被拒绝(v2.22.0 起,逃生阀-jaeger.es.config.rejectLegacyTagsAsFields),改用indices.spans.tags_as_fields;rotation与旧字段(date_layout/rollover_frequency直接放在索引层、别名开关)互斥,混用报错。官方 docs 页仍在教旧字段——以源码为准。- 走
jaeger-es-rollover init初始化 ILM 时,集群里必须预先存在名为jaeger-ilm-policy的 ILM 策略,否则 init 直接报 “ILM policy jaeger-ilm-policy doesn’t exist in Elasticsearch”。用新语法auto_rollover.policy_name可自定义名字,但 policy 本体仍须提前建好(Jaeger 只把它嵌进模板,不负责创建)。
顺带一提 data_stream:配置语法已在(rotation.data_stream,见官方 config-elasticsearch-data-stream.yaml),但 v2.22.0 的校验对显式设置 data_stream 返回 “not yet implemented”——那是 RFC 0004 Phase 2 的前瞻配置,现阶段别在生产尝试。
ILM 模式下还有个读侧参数要配套:max_span_age 应设成与 ILM/ISM 保留期一致,否则按 traceID 直查老 trace 时读不到(源码注释原文)。
数值属性与范围查询:numeric_attributes(RFC 0015)
schema 里所有 tag 值都存成 keyword,字符串无法做"按属性排序/数值范围"。开启 indices.spans.numeric_attributes: true(默认关)后,模板为 tag 值追加 number 子字段(double,coerce:false, ignore_malformed:true),数值属性双写 number 与 keyword。两个必须知道的边界:mapping 只对开启之后新建的索引生效——老索引里没有 number 字段,排序谓词在数据随保留期轮转干净之前查不到东西;且每个属性 key 多占一份 mapped field,吃 total_fields_limit 预算(可用 indices.spans.total_fields_limit 设上限)。
写路径:write_mode、bulk 与毒丸
写入走 _bulk API,write_mode 二选一(官方源码注释与 docs 语义一致):
async(默认):span 先进客户端缓冲,后台 worker 攒批 flush。WriteTraces 在数据持久化之前就返回成功——flush 失败只有日志和计数,上游 pipeline 毫无感知。官方措辞很直白:async 是默认"因为它代价最低,不是因为它更安全"。sync:pipeline 交给存储的每一批,变成一次阻塞的_bulk请求,任一 span 未落盘就向 pipeline 返回真实错误(RFC 0007)。代价是每批多一次 _bulk 往返延迟。
bulk_processing 只在 async 下全部生效,sync 模式只有 max_bytes 起作用(超过就拆成多个 _bulk 请求):
elasticsearch:
write_mode: async
bulk_processing:
max_bytes: 5000000 # 单次 _bulk 请求体积上限,默认 5,000,000
flush_interval: 200ms # 仅 async:缓冲到时间就 flush
workers: 1 # 仅 async:并发 flush worker 数毒丸(poison pill)指"每次重试都会同样失败"的文档——典型如属性类型与 mapping 冲突。sync 写会逐条读 _bulk 响应里的 per-item 状态,区分瞬时失败(429/5xx/网络,整批报错交给 pipeline 重试)与终态拒绝,后者由 poison_pill_handling 决定去留:
poison_pill_handling: fail # fail(默认)| drop;仅 sync 模式有意义fail:整批失败、无限重试,毒丸会阻塞它后面的所有数据(Kafka 场景即分区停摆),直到有人修文档或改 mapping——“一条 span 都不许丢且有人盯管道"时选它。drop:丢弃被终态拒绝的文档(记日志、计数),整批完成、offset 前进——“宁可丢极少数畸形 span 也不能让管道卡死"时选它。async 模式下此设置无作用(被拒文档只记日志)。第三条路不改这个参数而是改拓扑:把 jaeger_storage_exporter 声明为 connector 而非 exporter,终态被拒的 span 会转投 dead-letter 管道——见 Kafka 专节。
两个写语义细节(docs ES 页原文,选型和排查都常用):
- 幂等写:每个 span 文档的
_id由内容确定性派生,重试只会覆盖不会产生重复;但_id唯一性只在单个索引内强制——rollover/别名切换后的重试会落到新索引留下第二份。 - 持久 ≠ 可搜:_bulk 返回成功意味着已落 translog(默认
request级),但可搜性由 refresh(默认 1 秒)决定,Jaeger 写侧不强制 refresh。sync 模式下 span 被确认最长约 1 个 refresh 周期后才出现在搜索结果里。
ES 运维工具
jaeger-es-rollover(init / rollover / lookback 三个子命令)与jaeger-es-index-cleaner:2.22.0均有二进制与容器镜像;Helm chart 里 esIndexCleaner(默认55 23 * * *,numberOfDays 7)、esRollover(10 0 * * *,max_age 1d)、esLookback、spark-dependencies 四个 CronJob 开关默认 disabled,部署侧见03-部署与Kubernetes.md。- mapping 生成已并入主二进制
jaeger mappings子命令(源码internal/storage/elasticsearch/mappings),独立esmapping-generator工具仍在仓库。 jaeger-anonymizer:把 UI 里的 trace 脱敏导出,便于向社区复现问题。- 容量直觉:日索引体积 = 日均 span 数 × 单 span 文档大小(数 KB 量级)×(1+副本数);shards/replicas 在建索引时定死,索引建好再改配置不影响存量,只能 reindex——docs 特意提醒这是要提前想清楚的参数。
OpenSearch:与 ES 同一实现,大规模优先项
docs 原文:“Overall Jaeger uses the same implementation for OpenSearch as for Elasticsearch”——opensearch backend 类型与 elasticsearch 参数完全同构(官方两份示例 config 除类型名外逐行相同),支持 1.x / 2.x / 3.x,自动从 root/ping 探测版本(显式指定时 OpenSearch 用 101/102/103 表示 1/2/3)。差异只在生命周期工具生态:ES 用 ILM,OpenSearch 用 ISM 插件,模板里对应字段是 plugins.index_state_management.rollover_alias(源码模板可见)。storage 页那句"大规模生产推荐 OpenSearch 优于 Cassandra"使它在自建集群里成为默认答案;version: 钉版本、sniffing、custom_headers(AWS 托管域 Host 头)同样适用。
jaeger_storage:
backends:
some_storage:
opensearch:
server_urls: ["https://os-node-1:9200"]
indices:
index_prefix: "jaeger-main"
spans:
shards: 5
replicas: 1
numeric_attributes: true
rotation:
auto_rollover:
policy_name: "jaeger-spans-policy" # ISM policy,需预先在集群创建Cassandra:TTL 原生,但搜索受限
支持 Cassandra 4.x / 5.x;ScyllaDB 因数据模型与 CQL 相同,官方称可 drop-in 替换。v2 的 backend 块分三节:schema(键空间与表)、connection(节点/认证/TLS)、query(超时)。自 v2.1.0 起支持自动建 schema(schema.create: true),免去手工跑初始化脚本;手工路径也存在:仓库脚本 internal/storage/v1/cassandra/schema/create.sh 输出 CQL 交 cqlsh 执行,或跑 jaegertracing/jaeger-cassandra-schema:2.22.0 容器(MODE=prod DATACENTER={dc})。
按官方 config-cassandra.yaml 扩展的完整骨架:
jaeger_storage:
backends:
some_storage:
cassandra:
schema:
keyspace: "jaeger_v1_dc1"
create: true # v2.1.0+ 自动建表;生产已有集群可置 false
datacenter: dc1
trace_ttl: 48h # span 原生 TTL(docs 默认示例值)
dependencies_ttl: 48d # 依赖图 TTL
compaction_window: 2h # time-window compaction,控制写放大
replication_factor: 1 # 多节点集群务必调大
connection:
servers: ["cassandra-0:9042", "cassandra-1:9042"]
auth:
basic:
username: cassandra
password: "${env:CASSANDRA_PASSWORD}"
tls:
insecure: true # 生产改为提供证书路径(client-to-node TLS)
query:
timeout: 30sCassandra 后端的运维红利是不用装清理任务:trace 数据按列 TTL 自动到期。代价则在前文 FAQ 已展开的搜索模型——以及依赖图数据同样靠 dependencies_ttl 留存、service 索引由额外写维护。如果你的团队本来就在运维 Cassandra 集群,且查询以"按 traceID 取回"为主,它是好选择;否则官方态度很明确。
ClickHouse:实验性,feature gate 解锁
clickhouse 页原文:“ClickHouse storage is experimental and must be explicitly enabled via the storage.clickhouse feature gate. The schema is subject to breaking changes in future releases.” 支持 ClickHouse 26.x(注意:官方 release 版本号形如 26.x,以 docs 为准)。列式存储、高压缩比、向量化执行,对 trace 这类宽表聚合分析有天然优势;schema 全部 SQL 在仓库 sql/ 目录,create_schema: true 时启动自动建表,服务/操作等辅助表靠物化视图随新 span 填充。
启动需显式开闸:jaeger --feature-gates=storage.clickhouse ...(与其他 gate 逗号分隔)。官方 config-clickhouse.yaml 骨架(顺带演示 ClickHouse 兼作 SPM 指标后端):
jaeger_storage:
backends:
some-storage:
clickhouse:
addresses: ["localhost:9000"]
database: jaeger
auth:
basic:
username: default
password: "${env:CH_PASSWORD}"
create_schema: true
metric_backends:
some-metrics:
clickhouse:
addresses: ["localhost:9000"]
database: jaeger
auth:
basic:
username: default
password: "${env:CH_PASSWORD}"判断:除非你有专人盯 schema 变更、能接受升级时重建表,否则把 ClickHouse 留在观察名单——官方自己不给稳定性承诺,本章其余生产建议均按主力三后端展开。
Kafka:缓冲层,不是存储
为什么中间垫一层 Kafka
kafka 页定位(原文措辞):Kafka 用作 collector 与真实存储之间的 intermediary buffer,支持 3.x。三个真实动因:削峰(trace 洪峰打进队列,ingester 按自己的节奏写 ES,collector 不再被后端拖垮);重放(按 offset/时间重新消费,做后处理管道或故障回灌——官方文档的 post-processing pipeline 图即为此意);解耦扩容(加 ingester 副本即扩写力,不动 collector)。另外 OTel collector 的 kafka exporter/receiver 支持 OTLP/Jaeger/Zipkin 多种编码,trace 队列可与日志等其他信号混用同一平台。
写入侧(collector 出口,官方 config-kafka-collector.yaml)与消费侧(ingester,config-kafka-ingester.yaml):
# collector 侧:收 OTLP/Jaeger,转投 Kafka
exporters:
kafka:
brokers: ["kafka:9092"]
traces:
topic: jaeger-spans
encoding: otlp_proto # 官方示例默认编码# ingester 侧:消费 Kafka,写后端
receivers:
kafka:
brokers: ["kafka:9092"]
traces:
topics: ["jaeger-spans"]
encoding: otlp_proto # 收发两侧编码必须一致
initial_offset: earliest # 全新消费组从头开始
exporters:
jaeger_storage_exporter:
trace_storage: some_storage注意:编码是收发两侧的一致性约束,
config-kafka-ingester.yaml用同一个环境变量同时配置两端,就是在防"collector 用 otlp_proto、ingester 还按老 jaeger_proto 解"这种错配——错配的表现是 ingester 反序列化失败、数据进不了存储。
at-least-once 全链配置清单
kafka 页警示:按默认配置,receiver 在 pipeline 接受记录时就提交 offset,而 pipeline 在存储落盘前就接受——后端一故障,Kafka 认为已交付的 span 就丢了。要做到"落盘后才提交”,官方 config-kafka-ingester-sync.yaml 给出的组合是(逐条对照 RFC 0007):
receivers:
kafka:
message_marking:
after: true # 记录处理完(写落盘)才提交 offset
on_error: false # 失败记录不跳过,offset 不动
service:
pipelines:
traces:
receivers: [kafka]
processors: [] # 关键:去掉 batch processor!
exporters: [jaeger_storage_exporter]
exporters:
jaeger_storage_exporter:
trace_storage: some_storage
retry_on_failure:
enabled: true
max_elapsed_time: 0 # 永不放弃重试;放弃会把错误抛回 receiver 导致分区暂停
queue:
wait_for_result: true # 阻塞等待写结果,offset 与持久化耦合
block_on_overflow: true # 队列满时等待而不是失败(失败同样会暂停分区)
sizer: bytes
num_consumers: 1
queue_size: 104857600 # 100 MiB 缓冲
batch: # 批在 exporter 内部攒,取代 pipeline batch
sizer: bytes
flush_timeout: 200ms
min_size: 1048576 # 必须为正,0 = 每条记录单独 flush
max_size: 4194304 # 4 MiB,须远小于 ES bulk_processing.max_bytes存储侧配套:Cassandra/ClickHouse 的写失败天然返回错误;ES/OpenSearch 必须 write_mode: sync,并配 poison_pill_handling: drop 或死信管道,否则一个终态被拒的毒丸文档能把 offset 永久钉死、分区停摆。
注意:at-least-once 三件套最容易漏的是去掉 pipeline 的
batchprocessor——批一旦由 pipeline 攒,记录被 pipeline 接受时就会提交 offset,写结果与提交彻底脱钩;批量语义必须留在 exporter 的 sending queue(queue.batch)里。第二条:单副本 ingester 一旦写失败把错误抛到 receiver,分区会暂停直到 rebalance——而只有一个消费者的消费组永远等不来 rebalance,官方注释明说这种情况分区"stalls for good”,靠max_elapsed_time: 0重试兜底。
不想丢毒丸也不想要 drop,还有死信模式(官方 config-kafka-ingester-dead-letter.yaml):把 jaeger_storage_exporter 挪到 connectors: 下声明,存储保持在 poison_pill_handling: fail;终态被拒的 span 转投 traces/dead_letter 管道(出口可以是 otlphttp、或写死信 topic 的 kafka exporter),批照常完成、offset 照常前进。两个细节:死信出口自己的 sending_queue 必须关(否则入队即算成功,connector 会在 sink 未确认时就推进 offset);死信 sink 必须"整单接受或整单拒绝"(otlphttp 把 OTLP partial_success 当成功,靠它丢的 span 无声无息)。
容量与分区
kafka 页 Sizing 原文可操作结论:wait_for_result 下,批大小受分区数约束——receiver 串行处理每个分区、并行跨分区,同一时刻每分区最多一条记录在 exporter 攒批器里等待,分区少的 topic 永远喂不出大批写;加吞吐靠加分区,只调大 queue.batch.max_size 没用。加 ingester 副本确实加处理力,但副本数不能超过分区数(多余的副本分不到分区),且副本间分摊分区反而使各自批变小。体积链必须满足:queue.batch.max_size(OTLP protobuf 计)× 膨胀系数 ≤ bulk_processing.max_bytes(NDJSON 计,官方经验系数 1.3~3.6 倍)≤ ES http.max_content_length(默认 100 MB)。分区路由用 traceID 作 key,同一条 trace 的 span 进同一分区(performance-tuning 页)——这让按 trace 的写入局部性更好,也和存储幂等 _id 共同把 at-least-once 的重复代价压到最低。
archive storage 与多后端组合
storage 页对归档的定义很清晰:主存储(short TTL)存所有进来的 trace;archive 只接收用户在 UI 里手工点"保存"的 trace,不自动晋升,因此可以配极长甚至无限的保留期,适合事故关联、性能基线这类要长期留档的样本。配置动作只有两步:定义第二个 backend,在 jaeger_query.storage.traces_archive 引用它——官方示例里两个 backend 用同一个 ES 集群、靠 index_prefix 分开(jaeger-main / jaeger-archive)。归档后端不必与主存同类型(原文),badger 归档 ES 主存、Cassandra 归档 ES 主存都合法。SPM 场景再加一类 metric_backends:ES/OpenSearch 兼做指标存储时,示例直接用 YAML 锚点复用同一段 backend 配置,jaeger_query.storage.metrics 指向它。
Remote Storage:接入自定义后端
需要把 trace 存进 Jaeger 不原生支持的库时,走 Remote Storage API v2:后端类型写 grpc,远端服务实现 TraceReader / DependencyReader / TraceService 三个 gRPC service。官方已提供 jaeger-remote-storage 独立二进制(本质是一个跑着 remote_storage extension 的 Jaeger 进程,把任何内置 backend 包成 gRPC 服务),社区实现列表中 PostgreSQL(robbert229)被 docs 点名。客户端侧最小接线:
jaeger_storage:
backends:
some-storage:
grpc:
endpoint: "remote-storage:17271" # gRPC 读服务地址
tls:
insecure: true
writer:
endpoint: "remote-storage:4316" # 官方示例:写路径走远端 OTLP 入口多进程拓扑(collector/query/remote-storage 怎么摆、端口怎么对)见 03-部署与Kubernetes.md;badger 想给远端 query 读,也必须经这一层(前文 badger 限制)。
v1 → v2 迁移视角
必须建立的两个事实:
- flag 全失效。v1 的
SPAN_STORAGE_TYPE、COLLECTOR_*环境变量体系在 v2 不存在,v2 只有 YAML(OCA 语法)。仓库cmd/jaeger/docs/migration/下有各后端的 metrics 迁移说明可作参照,但存储接线请按本章结构重写。 - 索引 schema 兼容无承诺。v2 写自有
jaeger-span-*等新索引(前缀由index_prefix控制),官方文档没有承诺 v2 query 直接读 v1 时代生成的历史索引的兼容性——迁移规划请把"旧数据在旧 v1 里查到过期为止、新数据从 v2 起写"当作默认假设,不要指望无缝切换。 - ES/OpenSearch 侧的坑叠加一条:v2.21+ 默认拒绝
use_aliases/use_ilm等旧顶层开关,把 v1 时代抄来的 ES 配置原样搬进 v2.22 会在启动期即报错(而不是静默降级),改写成rotation块。
实战建议
选型决策,按问句顺序走:只在本地跑?memory。单机、数据量 modest、要持久化?all-in-one + badger,超规模就换。已有 ES/OS 运维经验或愿意建集群?OpenSearch(自建大规模首选,官方推荐)或 Elasticsearch,ILM 自动轮换起步。团队只有 Cassandra 且查询以 traceID 取回为主?Cassandra,享受原生 TTL。洪峰与重放需求明确、已有 Kafka 平台?在任一主力后端前面垫 Kafka,按 at-least-once 清单逐条配。想要列存分析红利且能陪跑实验?ClickHouse,记得 feature gate 与 schema 破坏性变更风险。都不是?写个 Remote Storage gRPC。
存储侧上线检查清单:
- 磁盘/堆水位:badger 单机盘、ES 日索引体积按"span 数 × 文档大小 × (1+副本)“估,留 30% 余量;ES 集群 disk watermark 会主动拒写。
- TTL/保留期闭环:ES/OS 确认 ILM policy 或 index-cleaner CronJob 真的在跑(见
03-部署与Kubernetes.md),max_span_age与保留期一致;Cassandra 确认trace_ttl与 keyspace 实际一致。 - TLS 与凭据:后端连接上生产一律 TLS + 认证;ES 密码用
password_file+ 定期轮转更稳。 - 写入确认语义与丢失容忍匹配:直写用默认 async 就要接受"flush 失败无人知晓”;要不丢上 sync(直连配 exporter 重试,配 Kafka 则完整走 at-least-once 清单);毒丸策略 drop/fail/死信三选一,想清楚再选。
- 体积链:Kafka 攒批 max_size < ES bulk max_bytes < http.max_content_length。
- 配置面自查:没有残留
use_aliases/顶层tags_as_fields/max_actions/send_get_body_as等已被拒绝的旧字段。 - shards/replicas 建索引时定死,上线前定稿;事后只能 reindex。
跨章衔接:部署形态与 Helm 开关见 03-部署与Kubernetes.md,查询 API 与 UI 行为见 07-查询UI与API.md,链路指标(SPM)见 08-可观测性联动.md,容量与调优全景见 09-性能与运维.md,全部陷阱清单见 10-常见陷阱与最佳实践.md,阅读顺序与资源索引见 README.md。