重建索引、Alias、双写与 CDC:Schema 怎样无损演进
Mapping 不能原地兼容修改时,怎样迁移而不丢写入? 这不是某个 DSL 参数没调好,而是数据身份、索引结构、分布式执行与资源治理没有被放进同一张设计图。索引演进是数据迁移,不是一次配置发布。安全路径必须同时管理历史回填、增量追平、双边核对、读写切换和可验证回滚。
先守住搜索系统的三个事实
第二,可搜索不等于已提交。为新 schema 创建新 generation,记录源库快照水位并回填历史数据;CDC 从该水位继续投影增量;按业务主键、版本、计数和摘要核对;读 alias 原子切向新 generation,写 alias 明确唯一 write index。 因而需要分别观测“事实源已提交”“投影事件已消费”“索引已接收”“refresh 后可见”四个水位。只报一个写入成功率,不能证明用户已经能搜到新状态。
一条记录怎样跨过事实、投影和查询边界
迁移状态机至少包含 PREPARE、BACKFILL、CATCH_UP、VERIFY、CANARY、CUTOVER、OBSERVE、RETIRE。每次转换都绑定可量化条件,失败则停留或回退,不靠人工感觉宣布追平。
设计评审不能停在“能搜出来”。至少要逐项回答:一个业务对象拆成几份文档;数组元素是否需要保持内部关联;哪些字段进入倒排、doc values 或仅保留在 _source;更新是整文档替换还是事件投影;租户和权限条件是否在所有查询路径强制注入;删除与隐私擦除怎样传播;索引能否从事实源完整重建。
无水位回填会在扫描窗口漏更新;应用双写没有统一事件身份会产生永久分叉;只比文档总数无法发现错字段与旧版本;过早删除旧 generation 会消灭快速回滚能力。
这类错误的危险之处在于,正常样本往往完全正确。只有在动态字段持续增长、事件乱序、深页访问、某个 shard 变慢或迁移追平时,隐藏的复杂度才突然显现。架构验证因此必须制造反向实验,而不是只跑 happy path。
运行链不是黑盒:从入口到 Segment
为新 schema 创建新 generation,记录源库快照水位并回填历史数据;CDC 从该水位继续投影增量;按业务主键、版本、计数和摘要核对;读 alias 原子切向新 generation,写 alias 明确唯一 write index。
容量估算至少拆成:源数据量、索引膨胀系数、副本数、segment 合并临时空间、translog、迁移时新旧 generation 并存空间。若只按当前主分片 store 大小规划磁盘,reindex 或节点恢复时很容易越过水位。保守表达可以写成:
[ Disk_{peak} ge Source imes IndexRatio imes (Primary+Replica) imes GenerationOverlap + MergeReserve ]
查询放大要在请求进入集群之前计算
[ CandidateAmplification = rac{Shards imes (from + size)}{size} ]
两层失败必须分开处理
可运行模型一:把放大或局部失败变成数字
javac --release 17 -Xlint:all -Werror AliasCutoverDemo.java
java AliasCutoverDemo预期输出:
oldDocs=100 newDocs=100 checksumEqual=true lag=0 aliasBefore=v1 aliasAfter=v2 atomic=true rollbackReady=true可运行模型二:证明乱序、重放或切换仍收敛
运行 DualWriteReconcileDemo.java:
javac --release 17 -Xlint:all -Werror DualWriteReconcileDemo.java
java DualWriteReconcileDemo预期输出:
events=6 oldApplied=6 newApplied=5 replayedMissing=1 divergenceBefore=1 divergenceAfter=0 converged=true第二个模型故意制造反向条件:重复、乱序、并发插入、漏投影或引擎误路由。只要 invariant 依赖“事件总会顺序到达”或“迁移期间不会写入”,它就不是工程约束。可交付的机制必须在反向输入下拒绝旧版本、保持稳定视图、补齐差异或明确降级。
新鲜度不是一个平均延迟
观测必须能回答“哪个边界坏了”
故障定位顺序可以固定为:先确认事实源是否正确,再核对投影水位与版本拒绝,再看目标 generation 与 alias,随后检查 partial shard failure,最后才调整查询或集群参数。若一开始就扩容,可能只是让错误文档更快写入更多节点。
安全、删除与审计不能交给查询约定
删除请求要传播到所有 generation、缓存、导出和分析投影。只删当前读 alias 指向的索引,旧 generation 仍可能保留敏感数据;只写 delete event 而没有水位和死信核对,也无法证明删除完成。审计记录应保留业务身份、版本、操作结果和目标代次,不保留不必要的敏感正文。
脚本查询、正则、通配符、runtime field 和高亮都属于受控能力。它们应按接口白名单开放,设置超时、复杂度和并发预算。让外部调用者提交任意 Query DSL,相当于把集群 CPU、heap 与数据访问边界一起交给客户端。
Schema 演进必须以 generation 为单位
改变 analyzer、字段类型、分片数或核心文档结构时,创建新 generation 并重建通常比原地修补更可验证。迁移需要明确 source snapshot/watermark、backfill 进度、CDC lag、差异抽样、全量摘要、canary 流量、alias 切换与 rollback window。
回滚也不能只把 alias 指回旧索引。如果切换后新写入只进入新 generation,旧索引已经落后。可回滚方案要么保持旧代增量同步,要么能根据切换水位重放缺口。所谓“旧索引还在”不等于“旧索引可安全接流”。
架构决策记录应该留下什么
一份可复审的记录至少包含:主查询形状、数据事实源、文档身份、版本规则、字段职责矩阵、分片与 routing 依据、峰值容量测量、查询预算、失败分类、降级语义、迁移状态机、回滚窗口和数据删除路径。每个数字标明样本、压测方法或监控来源。
索引演进是数据迁移,不是一次配置发布。安全路径必须同时管理历史回填、增量追平、双边核对、读写切换和可验证回滚。 对这个主题,最终必须守住的条件是:新旧 generation 在业务版本与摘要上收敛后才切流,旧索引保留到回滚窗口结束。如果团队无法通过运行模型、指标和核对账本证明它,就还没有完成架构设计。
查询限制覆盖 shard fan-out、深分页、bucket、脚本、超时、并发和响应体。所有查询、聚合、suggest、导出和缓存路径执行同一权限约束。指标能区分事实水位、消费水位、索引接收与 refresh 可见水位。
新旧索引以版本、缺失主键和摘要核对,不只比较文档数量。降级响应标明来源与新鲜度,不把派生结果冒充权威事实。旧 generation 保留到回滚窗口结束,删除与隐私擦除覆盖全部副本。
两个 Java 17 模型通过严格编译并产生与文中一致的输出。
以下资料用于核对 API 语义和引擎数据结构;架构结论仍需结合实际版本、数据分布与压测结果验证:
