Quickwit Ingest V2 深度解析:基于 Shard 的动态分布式写入架构、配置实战与 V1 迁移指南
【免费下载链接】quickwitCloud-native OSS search engine for observability项目地址: https://gitcode.com/GitHub_Trending/qu/quickwit
Ingest V2 是 Quickwit 自 0.9 起默认启用的新一代写入链路,它以“可动态分布的 WAL 单元(shard)”取代了 V1 中“节点本地队列 + 索引级 checkpoint”的模型,让索引写入可以跨越集群中任意节点进行负载均衡。本文以 docs/internals/ingest-v2.md 为主线,结合控制平面、ingester、router 与配置解析源码,完整拆解 Ingest V2 的架构、一次写入请求的端到端旅程、V1/V2 差异以及全部相关配置项,帮助你在大规模多索引集群中正确启用、调优并平滑迁移到 Ingest V2。
为什么需要 Ingest V2:V1 的局限与 V2 的设计目标
在 Ingest V1 中,接收写入请求的节点必须把文档持久化到自己本地的 WAL(write-ahead log)中,队列位置与索引进度被记录为索引元数据中的 checkpoint。这种“写哪存哪”的模型有两个明显约束:
- 写入压力高度依赖接收节点的本地磁盘与内存容量,节点间负载天然不均;
- 每个索引的 checkpoint 都要在 metastore 中维护,当集群承载上千个索引时,元数据交互与状态追踪的开销会显著放大。
Ingest V2 的设计目标正是解决这两点:面向数千个索引规模,让写入可以被动态分发到集群中的任意 indexer 节点,并由控制平面统一调度,从而把索引工作尽可能均衡地铺开到所有 indexer 上(见 ingest-v2.md 开篇)。
Ingest V2 架构剖析
仍然以 mrecordlog 为持久化基石
无论是 V1 还是 V2,待索引文档的持久化都依赖mrecordlog——一个高性能的多队列记录日志库。差异在于组织方式:
- V1 使用
queues/目录存放本地队列; - V2 使用
wal/目录存放 WAL(ingest-v2.md 中的差异清单明确指出这一点)。
在源码层面,V2 的 ingester 通过MultiRecordLogAsync包装 mrecordlog 异步访问,并用WalCapacityTracker同时跟踪磁盘与内存两类容量预算(见 ingest_v2/state.rs)。
Shard:可动态分布的 WAL 单元
V2 的核心抽象是shard:一段被独立管理进度的 WAL 单元。一条写入请求中的文档会被划分成若干 shard,每个 shard 由控制平面动态指派给一个 ingester 节点。这个节点既可以是接收请求的本地节点,也可以是集群中的其他 indexer——这正是 V1 做不到的跨节点写入分发。
从 proto 定义可以看到 shard 的完整生命周期状态机(quickwit_proto::ingest::ShardState),ingester 在启动时会加载本地的 shard 状态、向控制平面注册本地 shard,并周期性广播自身的容量评分(见 ingest_v2/ingester.rs 中BroadcastLocalShardsTask与BroadcastIngesterCapacityScoreTask两个后台任务)。
控制平面:shard 分配与再平衡
控制平面(control plane)是 Ingest V2 的“调度中枢”。当 router 需要为某个 source 打开 shard 时,会向控制平面发起GetOrCreateOpenShardsRequest;控制平面则基于各 ingester 当前持有的 shard 数量与 WAL 容量评分决定分配目标。
分配策略在 ingest_controller.rs 中实现得非常直观:pick_least_loaded_ingester优先选择当前 shard 数最少的 ingester,并用随机化打破平局;allocate_shards会先按“已持有 shard 数量”将 ingester 组织成有序结构,再逐 shard 挑选负载最低者(见该文件pick_least_loaded_ingester与allocate_shards两个函数)。此外控制平面还内置了ScalingArbiter与周期性的 shard 再平衡操作(源码中有REBALANCE_SHARDS指标与CLOSE_SHARDS_UPON_REBALANCE_DELAY延迟参数),保证运行期间 shard 分布随负载变化持续收敛到均衡状态。
进度跟踪:从索引级 checkpoint 到 metastoreshards表
V1 把写入进度作为索引元数据 checkpoint 维护;V2 则不再这样做——每个 shard 的消费进度被记录在 metastore 中一张专用的shards表里(ingest-v2.md 架构小节原文)。控制平面通过 metastore 客户端执行OpenShardsRequest/OpenShardsResponse等操作来登记和推进 shard 状态(见 ingest_controller.rs 中对quickwit_proto::metastore的引用),从而把“索引元数据”与“写入队列进度”两类状态彻底解耦。
副本写入与持久性模型(规划中)
需要澄清的是,shard 副本写入目前仍处于规划阶段。按 ingest-v2.md 的说明,未来基于 shard 的写入将为每个 shard 维护一份副本,从而显著提升“等待索引文档”的持久性;而已索引文档的持久性本身由对象存储(如 S3)保证,与写入路径无关。配置项ingest_api.replication_factor在当前版本中尚未生效——详见下文配置章节。
一次写入请求的完整旅程
1. REST 入口的路由选择
POST /api/v1/{index_id}/ingest请求到达后,REST 处理器在 rest_handler.rs 的ingest函数中做版本裁决:当 V2 服务启用且请求未显式要求旧版写入时,走ingest_v2路径;否则回落到ingest_v1,此时若 V1 被禁用则直接返回错误(QW_DISABLE_INGEST_V1生效时)。这一分支逻辑同时作用于普通 ingest 端点与 bulk 批量写入端点(见 ingest-api.md 的说明)。
在ingest_v2实现中,文档会先被构造成带DocUid的DocBatchV2,随后包装成IngestSubrequest发送给IngestRouter。值得注意的是,V2 支持detailed_response选项以返回逐文档的解析结果,而 V1 对detailed_response会直接返回BadRequest(rest_handler.rs)。
2. IngestRouter:去抖、路由表与转发
IngestRouter 是 V2 写入路径上的转发层,其内部状态(见 ingest_v2/router.rs)包含:
RoutingTable:维护各节点、各 WAL 容量以及每个 source 当前打开的 shard 数,是“往哪转发”的依据;- 去抖器(debouncer):将高频的
GetOrCreateOpenShardsRequest合并后批量发给控制平面,降低控制面压力; - 信号量:以字节为粒度限制在途 ingest 请求量,配合速率限制做写入背压。
3. 同步解析与校验:错误直接返回客户端
与 V1 不同,V2 会在写入路径上同步地解析并校验输入文档:JSON 格式错误与 schema(doc mapping)不匹配会在 ingest 响应中直接返回,而 V1 中这类错误只能从服务器日志中看到(ingest-v2.md 差异清单)。
底层实现在 ingest_v2/doc_mapper.rs:validate_doc_batch将 CPU 密集的校验任务放入run_cpu_intensive线程池执行,逐文档解析 JSON 并用 doc mapper 校验字段类型,失败项被记录为带ParseFailureReason(InvalidJson或InvalidSchema)的ParseFailure,随后连同合法的DocBatchV2一起随响应返回,实现“部分成功、逐条报告”。
4. Ingester:落盘、持久化与推进
目标节点上的Ingester收到转发来的批量后,将文档追加到对应 shard 的 WAL(mrecordlog),期间通过WalCapacityTracker检查磁盘/内存容量预算,超限时按速率限制策略拒绝请求(对应 router 侧的INGEST_RESULT_WAL_FULL、INGEST_RESULT_RATE_LIMITED等指标,见 ingest_v2/router.rs 的指标枚举)。随后持久化过程(persist)会向控制平面与索引管道推进 shard 位置,索引管道消费 WAL 生成 split 并上传对象存储。
V1 与 V2 对比速查
| 维度 | Ingest V1 | Ingest V2 |
|---|---|---|
| 持久化目录 | queues/ | wal/ |
| WAL 位置 | 始终写入接收请求的本地节点 | 可由控制平面转发到任意 indexer(本地或远端) |
| 写入负载均衡 | 无(取决于请求落在哪个节点) | 控制平面按 shard 数与容量评分动态分配、再平衡 |
| 进度追踪 | 索引元数据 checkpoint | metastore 专用shards表 |
| 文档解析校验 | 异步,错误仅出现在服务器日志 | 同步解析校验,schema/JSON 错误随 ingest 响应返回 |
| 副本写入 | 不支持 | 规划中(replication_factor尚未生效) |
| 公共配置 | max_queue_memory_usage、max_queue_disk_usage | 相同参数 +replication_factor(暂未生效) |
以上差异均来自 ingest-v2.md 的差异清单小节。
配置指南
切换 Ingest 版本的环境变量
两个环境变量共同决定写入服务如何被启用(默认值来自 quickwit-config/src/lib.rs,与 ingest-api.md 文档一致):
| 变量 | 说明 | 默认值 |
|---|---|---|
QW_ENABLE_INGEST_V2 | 启动 V2 ingest 服务并默认使用 | true |
QW_DISABLE_INGEST_V1 | 仅当 V2 被禁用时,API 才会使用 V1;保留 V1 是为了在不丢失存量未索引日志的前提下迁移到 V2 | false |
也就是说:默认情况下 V2 与 V1 两个服务同时启动、API 走 V2;当你需要回退到 V1 时,同时设QW_ENABLE_INGEST_V2=false并保持QW_DISABLE_INGEST_V1=false即可。REST 处理器正是据此在 rest_handler.rs 中决定调用ingest_v2还是ingest_v1。
ingest_api 配置项
ingest 容量相关配置位于节点配置的ingest_api段(解析与校验见 node_config/mod.rs):
version: 0.8 ingest_api: # 队列在内存中的最大占用,默认 2GiB。 max_queue_memory_usage: 2GiB # 队列(WAL)在磁盘上的最大占用,默认 4GiB。 max_queue_disk_usage: 4GiB # 单次写入请求体大小上限,默认 10MiB。 content_length_limit: 10MiB # 节点退役时等待在途写入完成/转移的超时,默认 300s。 decommission_timeout: 300s注意事项(均为源码中可验证的硬性约束):
max_queue_disk_usage至少为 256 MiB,且必须不小于max_queue_memory_usage,否则配置校验直接失败并给出对应错误信息(node_config/mod.rs);ingest_api.replication_factor当前会被解析器忽略并输出警告日志(源码将其反序列化为IgnoredAny并在warn_if_replication_factor_is_set中提示“尚未生效”),请勿依赖它——副本写入属于规划中的能力(node_config/mod.rs)。
启用协作式索引(enable_cooperative_indexing)
当集群中活跃写入的 indexer 数量很大(几十个量级)时,可以为 indexer 打开协作式索引选项,通过全局信号量协调各索引管道的提交节奏,从而限制 indexing workbench 的内存消耗。该选项默认关闭,在节点配置中开启:
version: 0.8 # [...] indexer: enable_cooperative_indexing: true源码中,IndexerConfig.enable_cooperative_indexing默认值为false(node_config/mod.rs),开启后索引服务会创建一个共享的cooperative_indexing_permits信号量并传入各索引管道(见 indexing_service.rs 与 cooperative_indexing.rs 中基于CooperativeIndexingCycle的周期协同算法)。完整节点配置模板可参考 config/quickwit.yaml。
写入路径上的超时与批大小参数
V2 写入路径内部使用一组层级化超时参数,约束关系记录在 ingest_v2/ingest.md:
| 参数 | 默认值 | 含义 |
|---|---|---|
Itimeout(ingest 请求超时) | 35s | 一次 ingest 请求的总体超时 |
Ptimeout(persist 请求超时) | 6s | 一次持久化请求的超时 |
Rtimeout(replicate 请求超时) | 3s | 一次副本请求的超时(为未来副本能力预留) |
k(persist 尝试次数) | 5 | 持久化失败后的最大重试次数 |
由于 persist 请求内部会发起 replicate 请求、ingest 请求内部会发起 persist 请求,三者的取值必须满足约Ptimeout >= 2 * Rtimeout且Itimeout >= k * Ptimeout(当前 35s >= 5 × 6s = 30s,满足约束)。
此外还有两个可用环境变量调节的运行时参数:
QW_INGEST_BATCH_NUM_BYTES:单个 mrecordlog 批次的字节阈值,默认1 MiB(见 ingest_v2/ingester.rs);QW_INGEST_REQUEST_TIMEOUT_MS:自定义 ingest 请求超时毫秒数,若设置的数值小于PERSIST_REQUEST_TIMEOUT × MAX_PERSIST_ATTEMPTS + 5s的下限,会被自动抬升到该下限(见 ingest_v2/router.rs)。
迁移与回退注意事项
- 两版本并存是刻意的:默认配置下 V1 服务仍然运行,目的是让存量 V1 队列中尚未索引的文档可以继续被消费,避免切换瞬间丢数据。迁移完成后若确认无存量 V1 队列,再考虑设
QW_DISABLE_INGEST_V1=true收紧(ingest-api.md)。 - 错误可见性变化:迁移到 V2 后,文档级的 JSON/schema 错误会出现在 ingest 响应(配合
detailed_response可拿到逐条解析结果),监控与告警策略应相应从“扫日志”调整为“解析响应”。 - 容量语义:
max_queue_memory_usage/max_queue_disk_usage在 V1、V2 下语义一致;V2 下 WAL 分布在多个节点上,评估磁盘水位时应按节点聚合查看,而不是只盯接收节点。 - 客户端重试:队列容量打满时服务端会返回
429,Quickwit CLI 的./quickwit index ingest会自动重试;自定义客户端接入时同样应处理429(ingest-api.md 中已明确说明)。
参考阅读
- 本文主文档:docs/internals/ingest-v2.md
- Ingest API 使用教程与版本开关:docs/ingest-data/ingest-api.md
- 节点配置总览(含 ingest_api 段):docs/configuration/node-config.md
- 节点配置示例:config/quickwit.yaml
- Ingest 路由与转发实现:quickwit-ingest/src/ingest_v2/router.rs
- Ingester(WAL 落盘)实现:quickwit-ingest/src/ingest_v2/ingester.rs
- 控制平面 shard 分配实现:quickwit-control-plane/src/ingest/ingest_controller.rs
- REST 层版本路由:quickwit-serve/src/ingest_api/rest_handler.rs
- 配置解析与校验:quickwit-config/src/node_config/mod.rs
【免费下载链接】quickwitCloud-native OSS search engine for observability项目地址: https://gitcode.com/GitHub_Trending/qu/quickwit
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考