✏️写作:个人博客,InfoQ,掘金,CSDN 📧公众号:进击的Matrix 🚫特别声明:原创不易,未经授权不得转载或抄袭,如需转载可联系小编授权。
🚫本文已做公司信息脱敏处理
小编工作中负责业务的一个服务端系统,使用了 Elasticsearch 服务做数据存储,业务运营人员反馈,用户在使用该产品时发现,用户后台统计的订单笔数和导出的订单笔数不一致!
交易订单笔数不对,出现差错订单了?这一听极为震撼!出现这样的问题,在金融科技公司里面是绝对不允许发生的,得马上定位问题并解决!
线上反馈业务数据查询和导出数据不一致
小编马上联系业务和相关人员,通过梳理上游系统的调用关系,发现业务系统使用到的是我这边的 ES 的存储服务,然后对线上情况进行复现,基本了解问题的现象:
这两个查询数量不一致,首先看查询条件是否一致呢?
经过一番排查,业务系统在调用查询订单总数和导出订单总数的这两个查询条件是一致的,也就是请求到我这边 ES 服务时,统计聚合的查询和分页导出的查询条件是一致的。但是为什么会在 ES 里面查询的结果是不一致的呢?难道 ES 里面的数据不全?统计聚合或分页导出的其中有一个不准了?
为了具体排查哪个操作可能存在问题,于是通过相同条件下查询数据库的总数和 ES 里面的数据进行对比。发现相同条件下,数据库里面的数据和 ES 条件查询的总数是一致的, 同时业务的 orerId 字段是没有重复,所以可以确定的是:通过 orderId 进行统计聚合去重的操作是有问题的。
数据库查询数量
运营后台查询数量
数据库查询:数据库是做分库分表,此处数据库查询使用的是公司内的"数据库大表"——公司数据部会 T+1 日从业务从库数据库中抽取 T 日的增量数据放在建立的"大表"中, 方便各业务进行数据使用。
运营后台查询:运营后台查询是直接查询 ES 存储服务。
数据部大表数量 = MySQL 数据库分库分表表里数量 = 运营控制台查询数量 = ES 存储文档数量
问题定位:
ES 存储服务对外给业务提供的服务中,通过 orderId 进行统计聚合去重的功能是有问题的。
上面说过,小编负责的 ES 存储服务对外给业务提供了通过指定字段进行统计聚合去重的功能,统计聚合去重使用的是 ES 的 cardinality 功能。通过业务的查询的条件,使用 ES 的聚合功能 cardinality 操作,映射到 ES 层的操作命令如下代码所示,执行业务的查询条件操作, 从 ES 的管理端后台里面查询竟然复现了和线上生产一样的结果,聚合统计的是 21514,条件查询的是 21427!!!
可以确定的就是这个 cardinality 操作,导致了两个查询的数据不一致,如下图所示:
GET datastore_big_es_1_index/datastore_big_es_1_type/_search
{
"size": 3,
"query": {
"bool": {
"must": [
{
"match": {
"v021.raw": "selfhelp"
}
},
{
"match": {
"v012.raw": "1001"
}
},
{
"match": {
"typeId": "00029"
}
},
{
"range": {
"createdDate": {
"gte": "2021-02-01",
"lt": "2021-03-01"
}
}
},
{
"bool": {
"should": [
{
"match": {
"v031.raw": "113692300"
}
}
]
}
}
]
}
},
"aggs": {
"distinct_orderId": {
"cardinality": {
"field": "v033.raw"
}
}
}
}
ES集群控制台cardinality操作
为什么 cardinality 操作会出现这样的结果呢?小编开始陷入了想当然的陷阱 —— 以为这就是一个简简单单的统计去重的功能,ES 做的多好,帮你去重并统计数量了。然后事实并不是,通过 Elasticsearch 对 cardinality 官方文档解释,终于找到了原因。
可以参考 Elasticsearch 2.x 版本官方文档对 cardinality 的解释 :https://www.elastic.co/guide/cn/elasticsearch/guide/current/cardinality.html
其中对 cardinality 统计去重的算法核心是:
ES文档中对cardinality算法特点介绍
可以总结如下:
HLL 算法文档:http://static.googleusercontent.com/media/research.google.com/en/pubs/archive/40671.pdf
下面对 ES 的 cardinality 的 precision_threshold 参数进行了验证:
1、大数据量下,设置最高精度及其以上,仍然会存在误差:
2、小数据量下,设置最高精度,可以和实际数量保持一致:
那么线上的为什么聚合统计的是 21514,条件查询的是 21427?
从不加 precision_threshold 参数可以知道,这个应该是 ES 设置的默认值。线上 ES 集群版本为 5.4x 因此找到 5.4 版本的官方文档,发现 5.4 版本中设置的是默认值 precision_threshold=3000, 在此条件下查询的统计聚合出来的值是 21514。
另外 ES 官方对 cardinality 操作中的 precision_threshold 参数也做了研究,可作为我们在业务开发中进行参考,如下图所示:
官方文档中precision_threshold设置和cardinality查询失败率的关系研究
Elasticsearch 5.4 版本官方文档对 cardinality 的文档及其 precision_threshold 参数的研究请看:https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-cardinality-aggregation.html
通过对 cardinality 的原理, 需要明白的是 : 我们使用 cardinality 的是需要区分使用场景的。
基于小编的这个业务场景,对商户订单进行统计,是属于精确统计场景,那 cardinality 操作就不适合了。又因为业务的 orderId 是不会重复的,理论上在我们 ES 集群中每个记录的 orderId 都是唯一的,因此可以不用进行去重,而可以直接使用 ES 的 count 操作,将订单数统计汇总出,对应 Elasticsearch 开发包中 COUNT API 如下:
org.springframework.data.elasticsearch.core.ElasticsearchTemplate
#count(org.springframework.data.elasticsearch.core.query.SearchQuery, java.lang.Class<T>)
public <T> long count(SearchQuery searchQuery, Class<T> clazz) {
QueryBuilder elasticsearchQuery = searchQuery.getQuery();
QueryBuilder elasticsearchFilter = searchQuery.getFilter();
return elasticsearchFilter == null ? this.doCount(this.prepareCount(searchQuery, clazz), elasticsearchQuery) : this.doCount(this.prepareSearch(searchQuery, clazz), elasticsearchQuery, elasticsearchFilter);
}
领取专属 10元无门槛券
私享最新 技术干货