Grafana Tempo Live-store 架构深度解析:从内存追查到本地 WAL 的近期数据读取路径
【免费下载链接】tempoGrafana Tempo is a high volume, minimal dependency distributed tracing backend.项目地址: https://gitcode.com/GitHub_Trending/tempo1/tempo
Grafana Tempo 的 live-store 是读取路径(read path)上的核心组件,专门负责服务“刚刚写入、尚未落入对象存储 block”的近期 trace 数据。本文以仓库文档 live-store.md 为主线,结合modules/livestore目录下的源码实现,系统讲解 live-store 的定位、trace 生命周期、分区所有权(partition ownership)、多可用区高可用、本地 WAL 与关键监控指标。读完本文,你将能够理解近期数据查询的完整链路,掌握live_store配置块的每个参数及其默认值,并学会通过指标与 HTTP 接口完成 live-store 的缩容与排障。
Live-store 是什么:读取路径上的“内存热点”
Live-store 是 Tempo 读取路径中负责服务近期 trace 数据的组件。它把 trace 保存在内存中,从而在“数据被写入 Kafka”与“block-builder 把数据刷入对象存储、block 可被查询”之间的窗口期内,依然能够响应查询请求。
Live-store 如何接收数据,取决于部署模式:
- 微服务模式(Microservices):live-store独立于 block-builder 消费 Kafka中的 trace 数据。此时 live-store 既是查询入口,也是 Kafka 的消费者。
- 单体模式(Monolithic):live-store 直接在进程内接收来自 distributor 的数据(通过
LiveStore.PushBytes方法),不涉及 Kafka 消费。
两种模式在代码层面对应Config.ConsumeFromKafka字段(见 modules/livestore/config.go):微服务模式下为true,单体模式下由应用装配层(app wiring)显式关闭。这一开关还决定了 complete-block 生命周期策略——Kafka 模式下完成块只保留在本地(kafkaCompleteBlockLifecycle为 no-op),单体模式下则会通过localCompleteBlockLifecycle在后台把完成的 block 刷入对象存储(见 modules/livestore/complete_block_lifecycle.go)。
为什么需要 live-store
在微服务模式下,数据链路存在一个天然的空档:
distributor ──> Kafka ──> block-builder ──> 对象存储 block(可查询) │ └──> live-store(内存中可查询)当 trace 数据写入 Kafka、但 block-builder 尚未把它刷新到对象存储时,唯一能查询这批数据的途径就是 live-store。在单体模式下,live-store 扮演同样的角色——为最近摄入的数据提供即时查询能力——只是数据来源从 Kafka 变成了进程内的 distributor。
无论哪种模式,live-store 都会:
- 在内存中按 trace ID 组织trace;
- 响应 querier 对近期数据的查询;
- 周期性把 traceflush 到本地 WAL(Parquet 格式),使数据可用于 TraceQL 搜索和指标(metrics)查询。
Trace 生命周期:active → idle → WAL → complete block
当 live-store 收到 spans 后,会在内存中把它们组装成 trace。每条 trace 会经历三个阶段的流转(对应源码instance中的liveTraces、walBlocks与completeBlocks三类数据容器,见 modules/livestore/instance.go):
- Active(活跃期):trace 正在接收 spans,驻留内存,可以被查询(trace ID 查询直接命中内存中的
LiveTraces,见 instance_search.go 中“先查 live traces”的逻辑)。 - Idle(空闲期):当超过配置的
max_trace_idle时间内没有新的 span 到达,trace 被判定为 idle,并被flush 到本地 WAL(cutIdleTraces,见 instance.go)。写入 WAL 的数据是Parquet 格式,此后数据便可用于 TraceQL 搜索,而不仅是 trace ID 查询。 - Complete block(完整块):WAL 数据最终被“切块”(cut)并完成(complete)成本地完整块。切块的触发条件在
shouldCutHead中定义(见 instance.go),包括:immediate(立即/停机)、达到max_block_duration(默认 30s)或达到max_block_bytes(默认 50MB)。WAL block 通过instance.completeBlock(见 instance.go)转换成查询性能更优的 complete block,并保留在本地磁盘上继续提供查询服务。
整个流水线由后台循环驱动:每个租户实例有独立的perTenantCutToWalLoop(按flush_check_period周期切 WAL)与perTenantCleanupLoop(清理过期 block),全局则有globalCompleteLoop消费一个按租户/block 排重的完成队列(见 live_store_background.go)。
背压(backpressure)机制
为了避免内存无界增长,instance实现了两级背压(见 instance.go):
- 当活跃 trace 占用的内存字节数达到
max_live_traces_bytes(默认 250MB)时,等待内存被 flush 腾出空间; - 当未完成的 WAL block 数量超过
walBackpressureLimit(4 个)时,等待 block 完成、减少积压。
背压时长通过tempo_live_store_back_pressure_seconds_total与tempo_live_store_back_pressure_duration_seconds指标暴露。
Trace 空闲期:max_trace_idle
max_trace_idle控制 live-store 在最后一个 span 到达后、判定 trace 空闲并 flush 到 WAL 之前需要等待多久:
live_store: max_trace_idle: 10s调大该值会让 trace 在内存中驻留更久,提高同一 trace 的全部 span 在 flush 时被聚合到一起的概率——这对长运行 trace(long-running traces)尤其有益。但代价是内存占用上升。需要特别注意两点:
- 源码默认值为
5s(见 modules/livestore/config.go),上述示例中的10s是文档给出的调优值; - 校验规则:
max_trace_idle不能大于max_trace_live(默认 30s),否则配置校验直接报错(见 config.go),因为 idle 判定本质上不能晚于 trace 的最长驻留上限。
分区所有权(Partition ownership)
在微服务模式下,live-store 是 Tempo 中分区生命周期的所有者:每个 live-store 实例消费一个或多个 Tempo 分区,而每个分区在每个可用区(availability zone)中恰好被一个 live-store 拥有。
分区环(Partition ring)
live-store 维护一个分区环,用来追踪:
- Tempo 中存在哪些分区;
- 每个分区由哪些 live-store 拥有;
- 每个分区的状态:
pending(待定)、active(活跃)或inactive(不活跃)。
该环通过memberlist gossip传播(PartitionRingConfig默认将 KVStore 设为memberlist,见 partition_ring.go),存储键为livestore-partitions(见 live_store.go)。分区状态的完整定义与转移规则可参考分区环文档。
启动流程
live-store 启动时(LiveStore.starting,见 live_store.go)会执行以下步骤:
- 检查 shutdown marker:若存在(说明上次是受控缩容),则预先进入 prepare-shutdown 模式。
- 初始化 WAL 并重放本地 block(
reloadBlocks),清理 tombstone 遗留。 - 启动分区 lifecycler:查询分区环中自己负责的分区——
- 分区已存在:以 owner 身份加入;
- 分区不存在:以
pending状态创建,等待足够的 owner 注册后(由min_partition_owners_count与min_partition_owners_duration控制,默认分别为 1 和 10s)自动提升为active。
- 启动读取路径:微服务模式下创建 Kafka reader,从上次提交的 Kafka offset 开始回放(
PartitionReader.fetchLastCommittedOffset,见 partition_reader.go)以重建内存状态。若本地没有数据,则强制从 lookback 周期(2 × complete_block_timeout)处开始消费;若分区已处于inactive(上一个 Pod 已排空),则跳过 lookback 回放(见 live_store.go)。 - 就绪判定:等待 Kafka 追赶到
readiness_target_lag阈值(默认0,即关闭该等待,保持向后兼容)后,将tempo_live_store_ready置为 1,CheckReady返回 nil,开始服务查询。
关闭与缩容
缩容 live-store 的正确姿势是先给分区打上 inactive 标记,让分区进入只读模式(read-only)。实现上,Tempo 提供了两个 HTTP 管理接口(见 downscale.go):
PreparePartitionDownscaleHandler(作用于分区本身):POST:把分区切换为inactive(若分区处于pending状态则返回 409,因为无法确认回退目标状态);DELETE:取消缩容准备,把分区从inactive恢复为active;GET:查询分区当前状态与切换时间戳。
PrepareDownscaleHandler(作用于 live-store 实例,基于 shutdown marker 文件):POST:设置 shutdown marker,并配置“关闭时从分区环移除 owner”且“启动时不创建分区”;DELETE:取消准备;GET:查询是否已设置。
等待足够时间让数据全部 flush 到对象存储之后,就可以安全地移除分区和 live-store 实例。
反之,直接粗暴地杀掉 live-store(不先标记 inactive),会使其分区的近期数据暂时不可查询,直到另一个 live-store 接管该分区;在多可用区部署下,其他可用区的 live-store 会继续服务这些分区。此外,remove_owner_on_shutdown(默认true)控制正常关闭时是否从分区环清理 owner 注册,避免残留陈旧条目(见 config.go)。
多可用区高可用:读法定人数为 1
生产环境中,live-stores 通常跨多个可用区(AZ)部署。每个 Tempo 分区在每个可用区各由一个 live-store 拥有:
- 当某个可用区的 live-store 不可用时,另一可用区的 live-store 继续为相同分区服务查询;
- querier 对每个分区只需要来自一个 live-store 的响应(读法定人数 read quorum = 1),因此只要至少一个可用区健康,查询就能成功;
- 这提供了高可用性,同时无需在读取路径上做数据去重。
这一设计的收益在于:读取路径可以容忍单个可用区故障,而不会引入额外的合并/去重开销。分区环通过 memberlist 在各可用区之间传播,min_partition_owners_count可配置为期望的 owner 数量(例如每个分区在 2 个可用区各有一个 owner),min_partition_owners_duration则防止 pending 分区在 owner 尚未齐备时过早提升。
本地 WAL:搜索可用性与重启恢复
当 trace 从内存中 flush 出来时,会被写入本地 WAL,格式为 Parquet。WAL 承担两个目的:
- 搜索可用性:数据进入 WAL 后,即可被TraceQL 搜索命中,而不再局限于 trace ID 查询。源码中
instance.iterateBlocks会并发遍历三类 block——head block、WAL blocks、complete blocks(受query_block_concurrency限制,默认 10)——并对其统一执行 Search / SearchTags / SearchTagValues / QueryRange(见 instance_search.go)。 - 重启恢复:微服务模式下,live-store 重启后会从 Kafka 回放,而 WAL 提供在回放期间继续服务查询的能力。
reloadBlocks(见 live_store_background.go)会重扫 WAL block 与 complete block,把未完成的 WAL block 重新入队完成,从而做到“重启不丢查询窗口”。
WAL 最终会被切成本地完整块(complete blocks),这些块同样存储在本地,并且一直保持可查询,直到数据超出 live-store 的保留窗口。保留由complete_block_timeout(默认 20 分钟)驱动:deleteOldBlocks会删除结束时间早于now - complete_block_timeout的 block(见 instance.go)。另外block_reclaim_grace(默认 2 分钟)会延迟删除已从快照移除 block 的磁盘文件,避免在途 reader 遇到ENOENT——配置约束是不得小于 querier 的search.query_timeout(默认 30s)。
摄入路径上的数据质量保障
Kafka 消费回调LiveStore.consume(见 live_store.go)会:
- 丢弃时间戳早于
now - complete_block_timeout的过期记录(避免回放过老数据,reason=too_old); - 丢弃解码失败的记录(reason=
decoding_failed); - 丢弃租户实例无法创建的记录(reason=
instance_not_found); - 成功处理的记录按租户累加
tempo_live_store_records_processed_total,并在每批消费后推进待提交 offset;提交周期由commit_interval(默认 5s)控制,设为0时改为同步提交。
关键指标
live-store 暴露的指标以tempo_live_store_为前缀,核心监控项如下:
| 指标 | 说明 |
|---|---|
tempo_live_store_traces_created_total | live-store 中创建的 trace 总数(按租户) |
tempo_live_store_lagged_requests_total | 因 Kafka 延迟而无法保证结果完整性的请求数,按route标签区分 |
tempo_live_store_query_inspected_bytes_total | live-store 查询检查(inspected)的总字节数,按tenant和op标签区分 |
tempo_warnings_total | trace 处理期间的告警,按reason标签区分 |
tempo_ingest_group_partition_lag{group="live-store"} | 每个分区的消费延迟(consumer lag) |
其中,tempo_live_store_query_inspected_bytes_total的op标签标识 live-store 的查询操作类型,可能取值包括:search、search_tags、search_tag_values、trace_by_id、query_range(对应源码中的常量定义,见 instance.go)。该指标用于在 query frontend 聚合整个查询路径的 inspected bytes之前,先行评估近期数据查询的成本。
tempo_live_store_lagged_requests_total与fail_on_high_lag(默认true)配合使用:当 Kafka 延迟导致无法保证结果完整性时,搜索与指标查询会直接失败(返回cannot guarantee complete results),避免返回不完整结果误导用户(见 live_store.go)。
其他可用于运维监控的补充指标(同样来自源码):
tempo_live_store_ready:1 表示就绪,0 表示启动中/停止中;tempo_live_store_live_traces/tempo_live_store_live_trace_bytes:当前活跃 trace 数量与占用内存;tempo_live_store_blocks_cut_total(按reason:immediate/max_block_duration/max_block_bytes)、tempo_live_store_blocks_completed_total、tempo_live_store_complete_queue_length;tempo_live_store_completion_duration_seconds、tempo_live_store_completion_size_bytes;tempo_live_store_partition_owned(分区归属)、tempo_ingest_storage_reader_receive_delay_seconds(Kafka 接收延迟)。
配置参考:完整 live_store 配置块
结合文档与 modules/livestore/config.go 中的默认值,live_store的完整配置项如下:
live_store: # 定时类参数 flush_check_period: 5s # 周期性切 WAL 的检查间隔 flush_op_timeout: 5m # 清理周期(清理过期 block) max_trace_live: 30s # trace 在内存中的最长驻留时间 max_trace_idle: 5s # 判定 trace 空闲的等待时间(不得大于 max_trace_live) max_live_traces_bytes: 250000000 # 活跃 trace 占用内存上限(250MB),触发背压 max_block_duration: 30s # head block 按时间切块阈值 max_block_bytes: 52428800 # head block 按大小切块阈值(50MB) commit_interval: 5s # Kafka offset 提交周期,0 表示同步提交 # 块完成与查询 complete_block_timeout: 20m # 完成块在 live-store 中的保留时长(默认 20 分钟) complete_block_concurrency: 2 # 并行完成 block 的协程数 query_block_concurrency: 10 # 查询时并发扫描的 block 数 block_reclaim_grace: 2m # block 文件删除延迟,须 >= querier search.query_timeout # 就绪与延迟控制 readiness_target_lag: 0 # 就绪前的目标 Kafka 延迟,0 表示禁用等待(默认) readiness_max_wait: 30m # 追赶超时上限,仅 readiness_target_lag > 0 时生效 fail_on_high_lag: true # 高延迟时是否让搜索/指标请求失败 remove_owner_on_shutdown: true # 正常关闭时是否从分区环移除 owner # 存储路径 shutdown_marker_dir: /var/tempo/live-store/shutdown-marker wal: path: /var/tempo/live-store/traces # 本地 WAL 存储路径 # 分区环 partition_ring: kvstore: store: memberlist # 分区环通过 memberlist gossip 传播 min_partition_owners_count: 1 # pending 提升为 active 所需最少 owner 数 min_partition_owners_duration: 10s # 最少 owner 数的持续验证时长 delete_inactive_partition_after: 13h # inactive 分区可被删除的等待时长,0 禁用删除 metrics: time_overlap_cutoff: 0.2 # 指标查询是否加载 trace 级时间戳列的重叠比例阈值(0.0~1.0)配置校验规则(见 config.go)要求上述定时类参数均大于 0,且max_trace_idle <= max_trace_live。commit_interval为0时 partition reader 采用同步提交(见 partition_reader.go)。
相关资源
- 完整配置项与部署参数:以 modules/livestore/config.go 为准,结合 config.go 中存储相关设置(如 block 版本、dedicated columns);
- 分区状态机与转移规则:partition-ring.md;
- 部署模式差异:deployment-modes.md;
- 查询 I/O 与 span 时间戳距离的监控:参见仓库
docs/sources/tempo/operations/monitor/目录下的监控文档,以及上述tempo_live_store_query_inspected_bytes_total指标的使用方式; - 测试参考:
modules/livestore/下的live_store_test.go、instance_test.go、instance_search_test.go、config_test.go等测试文件覆盖了分区接管、trace 切块、查询与配置校验等关键路径,是理解 live-store 行为的绝佳补充材料。
【免费下载链接】tempoGrafana Tempo is a high volume, minimal dependency distributed tracing backend.项目地址: https://gitcode.com/GitHub_Trending/tempo1/tempo
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考