作者:京东科技 王长春

业务问题

小编工作中负责业务的一个服务端零碎,应用了 Elasticsearch 服务做数据存储,业务经营人员反馈,用户在应用该产品时发现,用户后盾统计的订单笔数和导出的订单笔数不统一

交易订单笔数不对,呈现过错订单了?这一听极为震撼!呈现这样的问题,在金融科技公司外面是相对不容许产生的,得马上定位问题并解决!

小编马上联系业务和相干人员,通过梳理上游零碎的调用关系,发现业务零碎应用到的是我这边的 ES 的存储服务,而后对线上状况进行复现,根本理解问题的景象:

  1. 用户操作后盾里的订单总笔数:商户页面的"订单总笔数","订单总笔数"应用的是小编 ES 存储服务中 ES 的统计聚合性能,其中订单总笔数是应用了 cardinality 操作,并且应用的是 orderId(订单编号)进行统计去重。
  2. 导出性能里的订单总笔数:导出性能应用的是 ES 存储服务中的 ES 条件查问性能,导出性能是进行分页查问的。

问题定位

这两个查问数量不统一,首先看查问条件是否统一呢?

通过一番排查,业务零碎在调用查问订单总数和导出订单总数的这两个查问条件是统一的,也就是申请到我这边 ES 服务时,统计聚合的查问和分页导出的查问条件是统一的,然而为什么会在 ES 外面查问的后果是不统一的呢?难道 ES 外面的数据不全?统计聚合或分页导出的其中有一个不准了?

为了具体排查哪个操作可能存在问题,于是通过雷同条件下查询数据库的总数和 ES 外面的数据进行比照。发现雷同条件下,数据库外面的数据和 ES 条件查问的总数是统一的, 同时业务的 orerId 字段是没有反复,所以能够确定的是:通过 orderId 进行统计聚合去重的操作是有问题的。

数据库查问:数据库是做分库分表,此处数据库查问应用的是公司内的数据部河汉大表——公司数据部会 T+1日从业务从库数据库中抽取 T 日的增量数据放在建设的"大表"中, 不便各业务进行数据应用。

经营后盾查问:经营后盾查问是间接查问 ES 存储服务。

数据部大表数量 = MySQL 数据库分库分表表里数量 = 经营控制台查问数量 = ES 存储文档数量

问题定位:
ES 存储服务对外给业务提供的: 通过 orderId 进行统计聚合去重(cardinality)的性能应该是有问题的。

ES 的 cardinality 原理探索

下面说过,小编负责的 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"      }    }  }}

为什么 cardinality 操作会呈现这样的后果呢?

小编开始陷入了想当然的陷阱—— 认为这就是一个简简单单的统计去重的性能,ES 做的多好,帮你去重并统计数量了。而后事实并不是,通过 Elasticsearch 对 cardinality 官网文档解释,终于找到了起因。

能够参考Elasticsearch 2.x 版本官网文档对 cardinality的解释:cardinality

其中对 cardinality 算法外围解释是:

能够总结如下:

  1. cardinality 并不是像关系型数据库 MySQL 一样准确去重的,cardinality做的是一个近似值,是 ES 帮你"估算"出的,这个估算应用的HyperLogLog++(HLL)算法,在速度上十分快,遍历一次即可统计去重,具体可看文档中举荐的论文。
  2. ES 做cardinality估算,是能够设置估算精确度,即设置参数  precision_threshold 参数,然而这个参数在 0-40000, 这个值越大意味着精度越高,同时意味着损失更多的内存,是以内存空间换精度。
  3. 在小数据量下,ES 的这个"估算"精度是十分高的,简直能够说是等于理论数量。

ES 中 cardinality 参数验证

上面对 ES 的 cardinality 的precision_threshold参数进行验证:

1、大数据量下,设置最高精度及其以上,依然会存在误差:

2、小数据量下,设置最高精度,能够和理论数量保持一致:

那么线上的为什么聚合统计的是 21514,条件查问的是 21427?

线上代码运行和ES集群设置都没有被动设置过 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参数的钻研文档:precision_threshold

总结与计划

通过对 cardinality 的原理探索, 须要明确的是 : 咱们应用 cardinality 是须要辨别应用场景的。

  1. 对于准确统计的业务场景,是不倡议应用的。例如:订单数的统计(统计后果会引起歧义)的场景下,不倡议应用。
  2. 对于非准确统计的业务场景,那么能够说是很有用了,尤其是在大数据量的场景下,在放弃肯定的准确性下,同时能提供高性能。例如:监控指标数据,大盘比例计算等场景,在非准确统计下,是有很大用途。

基于小编的这个业务场景,对商户订单进行统计,是属于准确统计场景,那 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);}

最初欢送大家点赞、珍藏、评论,转发!❤️❤️❤️