背景
ES的搜索应用场景非常丰富,在极端情况下可能会出现同语句多次查询结果不一致的情况,特别是全文检索有打分的场景下。
一般ES如果碰到同一个语句多次查询结果不一致的情况(数量不一致/顺序不一致),一般是如下几种case:
1. DR集群数据不一致 2. 主副分片数据存储的差异(导致打分差异) - segment - deleted文件(merge独立) - 可查询文档(refresh独立) - lucene文件id |
其中主副分片数据存储的差异(导致打分差异)的case也就是经典的结果震荡问题;
ES集群DR(多数据中心/多集群容灾)主要有3种方式:
- 自研跨数据中心同步方案 - 官方 CCR,收费 - 用户双写 |
DR集群数据不一致,肯定会对查询造成一定影响,这里不展开讨论。
结果震荡问题
场景
查询时出现“同一个DSL语句执行多次结果不一致”,如查询时返回的结果时而出现total=16,时而出现18...
分析
1. 主要原因
主分片和副本分片可能不一致,导致最终在主分片和副本分片上计算得到的得分不同,即最终的查询结果不一致。
2. 造成主分片和副本分片不一致的原因
- 可能因为用户删除了部分文档,之后主分片进行了merge,而副本分片没有进行merge。这种情况下主分片和副本分片上的总文档数量就会不同,打分时计算出的IDF的值不同,最终得到了不同的得分。
(当文档被删除或更新时,旧文档不会立即从索引中删除,只是标记为已删除,在下次合并该旧文档所属的segment时才从磁盘中删除;假设主分片刚刚完成了一个大型merge,删除了许多已删除的文档,那么它的索引统计信息可能与副本(仍具有大量已删除文档)完全不同(副本分片不会和主分片一起进行merge操作),因此得分有所不同)
- 如果两个文档相关性评分相同,会根据其内部Lucene文档ID对其进行排序,两个文档相同的主副分片,luceneID不一定相同,导致最后的结果不一定相同。
官方文档类似问题解释:
https://www.elastic.co/guide/en/elasticsearch/reference/6.4/consistent-scoring.html
3. 解决方案
a. 指定分片查询(查询偏好:preference)
- 不指定分片:在所有有效的主分片以及副本间轮询
# ES6版本
https://www.elastic.co/guide/en/elasticsearch/reference/6.8/search-request-preference.html
`_primary`:发送到集群的相关操作请求只会在主分片上执行。
`_primary_first`:查询会先在主分片中查询,如果主分片找不到(挂了),就会在副本中查询。
`_replica`:发送到集群的相关操作请求只会在副本分片上执行。
`_replica_first`:查询会先在副本中查询,如果副本找不到(挂了),就会在主分片中查询。
# ES7版本
https://www.elastic.co/guide/en/elasticsearch/reference/7.17/search-search.html#search-preference
`_local`:如果可能,请在本地节点上的分片上运行搜索。如果没有,请使用默认方法选择分片。
这里可以看到,ES7不再支持preference直接选择“_primary”。
b. 通过权重的方式增大分数差异
- 极端情况下才会出现结果震荡问题,一般情况下主副本之间带来的打分差异比较小。
- 通过权重调整,可以从业务层解决排序问题。根据权重调整打分的方式有很多种,比如function_score,这里不展开描述。
其他可能
当业务查询非常慢(响应大于1s)并且查询端设置了超时时间的时候,可能由于超时导致查询的数据不完整,多次查询极有可能数据不一致。
GET index_test/_search
{
"timeout": "1s",
"query": {
"match_all": {}
}
}
此时如果出现查询超时,结果返回将不完整,只返回固定时间内查询到的结果以及total(未查询完成的部分直接忽略),并且带有一个`timed_out=true`的标记,这也是非常容易忽视的一点。
{
"took": 183,
"timed_out": true,
"_shards": {
"total": 5,
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 0,
"max_score": null,
"hits": []
}
}
解决方案
1. 降低响应延迟
- 提升 CPU、MEM 配置 / 磁盘使用 SSD - mapping 优化,ES7之后字段类型的type会较大影响查询性能 - 查询语句优化 |
查询语句优化的定制化要求比较高:
比如我们遇到的是大量数据(亿级文档数)聚合比较慢,优化主要通过:
- 不使用序数 "execution_hint": "map"
- 在查询之前提前创建好全局序数 "eager_global_ordinals": true
2. 增加超时时间
GET index_test/_search
{
"timeout": "5s",
"query": {
"match_all": {}
}
}