客户端、Bulk 与重试:批量写入怎样处理局部失败
为什么 Bulk 返回 HTTP 200,业务仍然可能少写数据? 这不是某个 DSL 参数没调好,而是数据身份、索引结构、分布式执行与资源治理没有被放进同一张设计图。Bulk 是传输批处理而非批次事务。可靠写入必须逐 item 解释结果,把成功、可重试失败和永久失败分别落账,并让重放受业务版本约束。
先守住搜索系统的三个事实
第二,可搜索不等于已提交。客户端把 action/source 组成 NDJSON,经连接池发送;协调节点按目标 shard 拆分子请求,各 shard 独立执行,因此响应同时包含成功与失败项。429、超时和节点切换通常可退避重试,mapping 冲突与非法字段必须隔离修复。 因而需要分别观测“事实源已提交”“投影事件已消费”“索引已接收”“refresh 后可见”四个水位。只报一个写入成功率,不能证明用户已经能搜到新状态。
一条记录怎样跨过事实、投影和查询边界
当更新经过消息系统时,至少存在四种正常扰动:重复投递、乱序到达、消费者重启和批次局部失败。正确性不能依赖它们“不发生”。索引端或投影器必须比较 sourceVersion:大于当前版本才更新,等于当前版本视为幂等重放,小于当前版本则拒绝并计数。这样才能把至少一次投递转化为最终收敛,而不是最后到达者获胜。
批次大小以字节、item 数和单批耗时共同限制;每个 item 携带 businessId、sourceVersion、targetGeneration 与 traceId。重试队列要有次数、指数退避、抖动、截止时间和死信出口。
只看 HTTP 状态会吞掉局部失败;整批无脑重试会重复已成功 item;无限队列会把集群背压转成应用 OOM;按到达顺序覆盖会在重试乱序时回滚业务状态。
这类错误的危险之处在于,正常样本往往完全正确。只有在动态字段持续增长、事件乱序、深页访问、某个 shard 变慢或迁移追平时,隐藏的复杂度才突然显现。架构验证因此必须制造反向实验,而不是只跑 happy path。
运行链不是黑盒:从入口到 Segment
客户端把 action/source 组成 NDJSON,经连接池发送;协调节点按目标 shard 拆分子请求,各 shard 独立执行,因此响应同时包含成功与失败项。429、超时和节点切换通常可退避重试,mapping 冲突与非法字段必须隔离修复。
[ Disk_{peak} ge Source imes IndexRatio imes (Primary+Replica) imes GenerationOverlap + MergeReserve ]
查询放大要在请求进入集群之前计算
[ CandidateAmplification = rac{Shards imes (from + size)}{size} ]
两层失败必须分开处理
传输层失败回答“请求是否抵达并得到响应”,item 或 shard 层失败回答“哪些操作真正执行”,业务层失败回答“索引状态是否等于目标业务版本”。三者不能由一个 HTTP 状态代替。响应成功但 item 失败、客户端超时但服务端已完成、查询只成功部分 shard,都会让“成功/失败”二元判断失真。
可运行模型一:把放大或局部失败变成数字
运行 BulkPartialFailureDemo.java:
javac --release 17 -Xlint:all -Werror BulkPartialFailureDemo.java
java BulkPartialFailureDemo预期输出:
items=8 httpStatus=200 success=5 retryable=2 permanent=1 wholeBatchSuccess=false itemLedgerRequired=true这个模型不是模拟某个产品实现,而是稳定复现设计中的数量关系。验收重点不是输出一行 true,而是能解释每个输入如何改变候选量、字段量、失败分类或迁移条件。把规模扩大十倍后,如果队列、heap、磁盘或人工补偿量也按不可接受的比例增长,说明边界仍需前移。
可运行模型二:证明乱序、重放或切换仍收敛
javac --release 17 -Xlint:all -Werror RetryVersionDemo.java
java RetryVersionDemo预期输出:
events=[4, 6, 5, 6] writes=2 staleRejected=1 replayed=1 finalVersion=6 converged=true第二个模型故意制造反向条件:重复、乱序、并发插入、漏投影或引擎误路由。只要 invariant 依赖“事件总会顺序到达”或“迁移期间不会写入”,它就不是工程约束。可交付的机制必须在反向输入下拒绝旧版本、保持稳定视图、补齐差异或明确降级。
新鲜度不是一个平均延迟
观测必须能回答“哪个边界坏了”
安全、删除与审计不能交给查询约定
脚本查询、正则、通配符、runtime field 和高亮都属于受控能力。它们应按接口白名单开放,设置超时、复杂度和并发预算。让外部调用者提交任意 Query DSL,相当于把集群 CPU、heap 与数据访问边界一起交给客户端。
Schema 演进必须以 generation 为单位
回滚也不能只把 alias 指回旧索引。如果切换后新写入只进入新 generation,旧索引已经落后。可回滚方案要么保持旧代增量同步,要么能根据切换水位重放缺口。所谓“旧索引还在”不等于“旧索引可安全接流”。
架构决策记录应该留下什么
一份可复审的记录至少包含:主查询形状、数据事实源、文档身份、版本规则、字段职责矩阵、分片与 routing 依据、峰值容量测量、查询预算、失败分类、降级语义、迁移状态机、回滚窗口和数据删除路径。每个数字标明样本、压测方法或监控来源。
Bulk 是传输批处理而非批次事务。可靠写入必须逐 item 解释结果,把成功、可重试失败和永久失败分别落账,并让重放受业务版本约束。 对这个主题,最终必须守住的条件是:每个 item 都有最终归宿,重试、乱序和重复投递不能回滚业务版本。如果团队无法通过运行模型、指标和核对账本证明它,就还没有完成架构设计。
能从事实源按 generation 完整重建,不把索引当唯一数据副本。businessId、sourceVersion、eventId 和目标 generation 在写入链可追踪。正常、重复、乱序、局部失败和 poison event 均有稳定结果。
查询限制覆盖 shard fan-out、深分页、bucket、脚本、超时、并发和响应体。所有查询、聚合、suggest、导出和缓存路径执行同一权限约束。指标能区分事实水位、消费水位、索引接收与 refresh 可见水位。
新旧索引以版本、缺失主键和摘要核对,不只比较文档数量。降级响应标明来源与新鲜度,不把派生结果冒充权威事实。旧 generation 保留到回滚窗口结束,删除与隐私擦除覆盖全部副本。
两个 Java 17 模型通过严格编译并产生与文中一致的输出。
以下资料用于核对 API 语义和引擎数据结构;架构结论仍需结合实际版本、数据分布与压测结果验证:
