跨集群Elasticsearch算分不一致?从BM25原理到排序一致性修复指南
做搜索或推荐服务的同学多半遇到过这种诡异情况两套集群明明是同一套模板部署的同一句 query 打过去返回的结果排序却肉眼可见地不一样。追到分数看同一个 doc 在 A 集群和 B 集群的_score差了十万八千里。尤其在跨集群环境下大家嘴上说的“算分一致”和“相关性一致”其实是两条容易被混在一起的线——前者指的是数字层面完全一致后者指的是排序结果不被破坏。搞混这两件事后面排查起来很容易走进死胡同。我在这块踩过不少坑前后花了小两周才把两个集群的排序差异从“随缘”收敛到“可用”。这篇文章就把整个过程、涉及的原理、排查链路和最终落地方案拆开来讲希望对正在被跨集群算分问题折磨的同行有参考价值。1. 跨集群“同查不同分”的问题到底长什么样先说一个我实际遇到的场景。业务从单机房集群迁移到双机房双活两个集群的数据用消息队列近实时同步索引模板是同一份分词器、字段映射、副本数全部一致。上线第一天就收到反馈用户在页面刷到的结果从主力机房切到容灾机房后前几条的顺序变了。当时第一反应是数据没同步全立刻去查两个集群的 doc 总数和最新一条数据的延迟发现都在预期范围内。于是分发层把同样的 query 分别打到两个集群拿回每条结果的_id和_score做对比问题马上就清楚了文档集合几乎一样但同一个 doc 在两个集群的_score明显不同——有的差距在 1% 以内有的能差到 20% 以上。这种差异带来的直接后果是双活流量切来切去时排序结果像布朗运动一样抖来抖去。更麻烦的是AB 系统在比较两个版本的排序模型效果时如果流量基底本身就带着集群偏差评测结论完全不可信。1.1 一次典型的 AB 集群分数对不上做个简化实验一个索引两个集群各 3 个主分片 1 个副本索引 10 万条新闻类文档。query 用“北京 疫情 通报 最新”对比结果里 Top 5 文档的分数。A 集群第一位文档得分 8.72B 集群同文档得分 7.13A 集群第 5 名的文档在 B 集群直接掉到 20 名开外。第一眼会觉得是不是数据有差异因为文档同步有延迟或者某些文档在同步过程中被过滤掉。但把两边的文档 ID 全集拉出来做集合对比发现完全一致。这时才开始怀疑问题出在“算分”环节而不是“数据”环节。也是从这次实验开始我把目标从“找出谁的数据有问题”调整为“找出哪一层的统计或配置在集群间不一致”。1.2 相关性一致性与算分一致性是两条线在这之前我一直把“相关性一致”等同于“算分一致”后来发现这个认知害死人。算分一致性是指同一个 query 对同一条文档在两套环境里计算出来的_score数值完全相同。这是个严格的数字等价关系要求所有参与计算的统计量都一样。排序一致性则是同一个 query 返回的 Top N 结果集合以及它们的相对顺序基本一致。即使_score有差异只要排序序没有明显变化用户感知不到问题AB 评测也能正常进行。工程上“分数完全一致”是一个非常苛刻的目标尤其在跨集群、跨机房场景下几乎不可能长期维持。但“排序一致”是可以做到的。首先要对齐的就是算分的每个输入环节其次再考虑在算分无法完全对齐时如何用外层机制保证排序不漂移。这两条线分开之后排查方向就清晰了很多。2. 算分公式拆解哪些变量会让集群之间产生偏差在 Elasticsearch 这类基于 Lucene 的引擎里默认相关度打分用的是 BM25 算法。虽然大家天天见到_score但真要一字不差说出公式里哪些是全局统计量、哪些是文档局部量很多人会卡壳。这个公式对跨集群一致性来说太关键了必须拆开看。2.1 BM25 里的三个敏感变量BM25 的核心公式是这样Lucene 实现score boost * IDF * ((k1 1) * tf) / (tf k1 * (1 - b b * dl / avgdl))其中boost是查询权重tf是词频k1和b是 BM25 的参数dl是文档长度avgdl是字段的平均长度IDF是逆文档频率。跨集群场景下最容易被忽略也最容易出问题的就是IDF。Lucene 里它的实际计算方式是IDF ln(1 (N - n 0.5) / (n 0.5))N是包含该字段的文档总数n是包含这个 term 的文档数。请特别注意N是整个索引分片集合层面统计出来的值不是单条文档的属性。也就是说只要两个集群中这个字段的文档总数不同或者某个词命中的文档数不同同一个 term 的 IDF 就会不同后续所有涉及这个词的分数都会偏移。第二个敏感变量是avgdl字段平均长度。这个也是全局统计量。两个集群即使文档全集几乎一致只要有一批长文没有同步到另一个集群avgdl 就会发生漂移进而通过dl / avgdl影响所有文档的长度归一化结果。第三个敏感变量是norms。Lucene 里 norm 本质上是文档长度对算分的影响因子如果某个字段的 norms 被禁用了就等于放弃了dl / avgdl这部分信息。两个集群对同一个字段的 norms 配置不一致时算分结果自然南辕北辙。2.2 文档增删滚动的累积效应更隐蔽的一点即使两个集群的初始配置完全相同、初始数据完全相同随着业务运行A 集群比 B 集群多建了一天索引或者 B 集群因为清理任务删掉了一部分过期文档两边对应的分片段(segment)统计立刻产生分歧。BM25 里的N、n、avgdl都是从分片段层面读取统计信息的。每次文档新增或删除最早是通过 segment 级别的统计变化体现出来的。如果两边增删节奏不一致那么每一个接受查询的分片返回的_score天然就有偏差。这种偏差不会立刻让所有结果全部错乱但会导致部分长尾文档的分数悄悄变化进而在边界情况下改变排序。我遇到过一种非常难查的情况A 集群比 B 集群多跑了一个重灌历史数据的任务把 3 万条旧新闻重新写入了索引。因为新写入的文档触发了新的 segment 生成旧 segment 里的统计被合并改写了导致它算出来的 IDF 从那一刻起就和 B 集群对不上了。所以跨集群算分一致性不是一个“配好一次就行了”的事它是一个随数据变化持续漂移的指标。3. 三类不一致源头的排查链路排查跨集群算分不一致建议按“配置层 → 统计层 → 查询机制层”的顺序走。不要一上来就怀疑 BM25 参数大概率是更基础的配置差异第一步就把你带偏了。3.1 第一类模板配置与分词层的差异先把两个集群的索引模板拉出来做 diff。重点看三块字段 mapping、分词器配置、similarity 参数。如果两个集群使用的 IK 分词器版本不同同一个词可能被切成不同 term。比如一个版本的词典里加了“新冠疫苗”另一个版本没有那么 query “新冠疫苗接种率” 在 A 集群会命中专用词B 集群会切分成“新冠”“疫苗”“接种”“率”几个词。这直接导致同一个 doc 在两边被命中的 term 组合不同评分自然没法一致。第二梯队看 similarity 参数。BM25 的k1默认 1.2、b默认 0.75一般不会有人改但模板刷写时如果带了别的值并且只在其中一个集群生效差距会立刻显现。这种情况在配置审计不严格的团队里很常见。再往下看 norms。有人为了节省存储空间在某个索引上关掉了特定字段的 norms另一个集群没关。由于 norms 会被存进倒排索引的元数据里在线动态调整还影响很大同一个 doc 的dl / avgdl完全无法被还原。检查这些项可以把两个集群的 mapping 以 JSON 格式导出写个脚本逐字段比对。不要只看字段类型analyzer、similarity、norms、index_options这几个子项都要细抠。3.2 第二类索引统计信息的漂移配置层完全一致后下一步就是统计层。核心问题是两个集群各自维护了一份独立的N、n、avgdl。验证方法有两种。第一种是直接看索引的统计信息通过_statsAPI 或者更深层的 segment 统计接口查看文档总数、分片数、segment 数量。如果两边 doc 总数有差异要立刻定位是什么原因。第二种更精确的做法是直接验证某个 term 在两个集群里的文档频率是否一致。Elasticsearch 里可以用 termvectors API 查看特定 term 的 doc_freq 和 term_freqGET /your_index/_termvectors/doc_id { fields: [title], term_statistics: true }对比同一 doc 在两个集群的 termvector 结果如果doc_freq不一致说明包含该 term 的文档集合在两个集群里已经分叉了。还有一类隐蔽问题来自副本分片不一致。主分片和副本分片的统计存在轻微差异跨集群查询如果 A 集群命中主分片、B 集群命中副本分片也可能在统计层面有细微偏移。这种情况比较少见但若查询负载导致两个集群分别路由到不同分片你会在日志中发现分数呈现出间歇性不对齐的状态。3.3 第三类查询执行机制本身的差异跨集群搜索最天然的坑在于Elasticsearch 的跨集群搜索CCSCross Cluster Search在搜索阶段会让每个远程集群各自算分协调节点拿到结果后不会重算。也就是说如果你的业务有一个“总集群”把 query 转发给 A 集群和 B 集群那么 A 集群算出来的 Top N 是站在 A 集群自身统计信息基础上算的B 集群同理。即使两边数据一模一样只要统计信息的更新节奏没对齐排序就可能不一致。再一个问题是search_type。默认的query_then_fetch是先到分片上查到 top N再回协调节点汇总重排。这种方式下每个分片独立使用自己局部统计值做算分全局一致性完全得不到保证。而dfs_query_then_fetch会先向全部分片收集 term 统计信息再统一用全局统计做算分。后者的分数一致性会好很多但性能开销也成倍增长。所以在这个阶段你要确认业务实际使用的查询方式判定“不一致到底是统计漂移造成的还是查询执行机制就没有全局视野造成的”。4. 两种修正路线用“词典与模板统一”治本用 DFS 治标把问题源头理清楚后就到了执行层次。我自己的经验是不要妄图用一两个参数调整就解决所有问题跨集群算分一致性的恢复需要分两类手段并行一类治本一类治标。4.1 做一次完整的一致性盘点和治理治本路线的重点在于让两个集群的配置、词典、数据同步节奏和索引生命周期完全一致。首先是版本级统一。两个集群的 Elasticsearch 版本、分词器插件版本、同义词词典文件都必须统一。我建议把同义词词典放到独立配置中心管理版本变更时自动同步到所有集群而不是手动拷文件。其次是索引模板统一。把模板从 A 集群导出在 B 集群上用同样的名字和优先级重新创建然后强制做一次 reindex。注意“强制 reindex”很重要因为 mapping 一旦已经生效部分属性改不了。想让两个集群从底层配置保持一致最稳妥的做法是重建索引。再次是数据量对齐。全量数据用离线管道周期比对比如每小时比对一次两个集群的 doc 总数、最近 10 分钟的写入量、segment 数量。如果发现 A 集群数据量比 B 集群多出明显阈值立刻触发告警。近实时同步场景下这种差异经常是同步任务积压、消费 lag 不一致引起的不能等到用户反馈才去查。最后是分片规划统一。两个集群的同一个索引主分片数要一致副本数要一致段合并策略也要一致。因为分片层面的统计值是算分的基准分片划分不一样即使总文档数相同单个分片内的 term 频率分布也可能不一样最终分数就是会对不上。这一套做完两个集群的算分差异能收敛到很小。但注意几乎不可能完全归零——主分片内 doc 的物理分布不同avgdl仍然会有细微差异。4.2 DFS 模式能解决什么解决不了什么dfs_query_then_fetch是治标手段里的典型代表。它先做一次全局的 term 统计收集把各分片的docFreq汇总到协调节点再用全局统计值去各分片执行查询。从原理上讲它能解决“各分片拿局部 IDF 当全局 IDF”的问题算分一致性比默认的query_then_fetch强很多。实际测试中两个数据完全一致的集群用dfs_query_then_fetch查询Top 20 结果的_score能对齐到小数点后 4 位以上。这是默认搜索方式做不到的。但 DFS 的代价也很直接。它在每个分片上多执行一轮“统计收集”对 CPU、内存和网络都有额外损耗。查询 QPS 高的情况下用 DFS 会显著拖慢响应时间。我遇到过的实践是把 DFS 用在两类场景一是低 QPS、高一致性要求的后台评测任务二是跨集群对比验证时的临时手段。另外要注意DFS 并不能消除文档增删漂移产生的差异。它解决的是查询执行机制层面的“全局视野”问题如果 A 集群和 B 集群本身的数据统计就不同DFS 再全局也没用。所以 DFS 只能作为验证和低流量场景的辅助手段不能反过来替代数据层的对齐。那如果既不想用 DFS 拖慢线上速度又希望排序不要漂移怎么办我的方案是对在线查询保持query_then_fetch但在返回结果后增加一层业务排序规则。反正线上主排序更多依赖业务权重、时间衰减因子、个性化向量纯相关性分数只是其中一环把业务排序权重拉大纯 BM25 分数差异对最终排序的影响就被稀释掉了。5. 从“分数一致”走向“排序一致”如何验收和维护折腾一大圈最后要回答的问题只有一个修复之后怎么证明它修好了我不建议只看某一两个 case 的分数是否一致太容易被偶然因素迷惑。5.1 定义一个可量化的不一致指标我的做法是把一致性拆成三个指标第一是“分数偏移率”对同一 query 的 top K 文档计算每个 doc 在两个集群的_score相对误差取平均值或 P95。偏移率越小说明算分越接近。第二是“排序翻转率”对同一 query 的 top K 结果统计在两个集群的排序序差异。如果 docA 在 A 集群排第 3、在 B 集群排第 7这就是一次翻转。翻转率 翻转文档对数 / 总可对比文档对数。第三是“命中重合率”也就是两个集群返回的 Top K 结果集中重合的 doc 比例。这个指标不看顺序只看集合是否接近。这三个指标各有侧重。分数偏移率用于底层的算分对齐度验证排序翻转率和命中重合率用于线上效果的可接受度评估。经过治理后我的目标基线基本是命中重合率不低于 98%排序翻转率不超过 5%分数偏移率 P95 不超过 10%。达到这个程度用户侧基本无感知AB 评测也能正常跑。5.2 持续监控的报警阈值设计一致性治理不是一次性项目数据层的任何改动都可能让指标重新劣化。维护阶段要接持续监控。我建议在离线管道里抽一批固定 query 集每小时跑一次双集群对比生成上面三个指标落到监控系统。报警阈值可以分层设置命中重合率低于 95%P0 告警数据同步大概率出问题。翻转率超过 10%P1 告警可能某个集群的词典或配置被改动。分数偏移率 P95 超过 15%P2 告警先做观察同时检查文档增删节奏是否严重失衡。这个监控对跨集群迁移、索引重建、data stream 滚动这类常规操作都有效。有一次我就是在做索引滚动升级时发现其中一个集群的新索引没有继承同义词词典半小时内排序翻转率从 3% 一路飙到 20%监控直接定位到模板配置问题这要是靠用户反馈去发现后果就是整批线上流量全跑在错误排序上。最后分享一点个人体会跨集群算分一致性精确定位到“一个分片的 docFreq 差异导致某个词对分数造成整体偏移”往往没有想象中那么难难的是在过程中保持耐心不被“分数差不多”“偶尔才差一点”这类感觉带偏。如果你能将差异量化成指标把排查链路按配置层、统计层、执行机制层逐级拆开大部分问题都能在半天内找到根源。最忌讳的是上来就把 BM25 参数改成自定义值那样只会让集群间差异更不可控。先把基础配置和词典统一再谈其他这条顺序千万不要走反。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →