作者:京东科技 王长春

业务问题

小编工作中负责业务的一个服务端系统,使用了 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);
}

最后欢迎大家点赞、收藏、评论,转发!️️️

【问题排查篇】一次业务问题对 ES 的 cardinality 原理探究的更多相关文章

  1. 走向DBA[MSSQL篇] 针对大表 设计高效的存储过程【原理篇】 附最差性能sql语句进化过程客串

    原文:走向DBA[MSSQL篇] 针对大表 设计高效的存储过程[原理篇] 附最差性能sql语句进化过程客串 测试的结果在此处 本篇详解一下原理 设计背景 由于历史原因,线上库环境数据量及其庞大,很多千 ...

  2. JS基础篇--sort()方法的用法,参数以及排序原理

    JS基础篇--sort()方法的用法,参数以及排序原理   sort() 方法用于对数组的元素进行排序,并返回数组.默认排序顺序是根据字符串Unicode码点.语法:arrayObject.sort( ...

  3. 日志查看技巧之筛选&去重[排查篇]

    引语:相信大家都会偶尔遇到要排查问题发生的原因的情况,那这种时候,我们最有力后盾就是日志文件了,所以谨记日志记录真的很重要.但是日志文件往往是很大的文件,而且里面有太多的东西可能不是我们需要的,如无数 ...

  4. 【整合篇】Activiti业务与流程的整合

    对于不管是Activtit还是jbpm来说,业务与流程的整合均类似.启动流程是绑定业务.流程与业务的整合放到动态代理中 [java] view plain copy print" style ...

  5. CRM【第三篇】: crm业务

    1. 项目背景 crm系统是某某教育公司正在使用的项目,系统主要为 销售部.运营部.教质部门提供管理平台,随着公司规模的扩展,对公司员工的业务信息量化以及信息化建设越来越重要. crm系统为不同角色的 ...

  6. 运维排查篇 | Linux 连接跟踪表满了怎么处理

    nf_conntrack (在老版本的 Linux 内核中叫 ip_conntrack )是一个内核模块,用于跟踪一个网络连接的状态 一旦内核 netfilter 模块 conntrack 相关参数配 ...

  7. 报警系统:php输出头信息以方便脚本抓取信息[排查篇]

    做监控系统时,需要对某个页面进行监控,可以通过很多方式进行报警,如:正常则输出一个规定的变量,错误时则不输出.但是还有一个更为方便的做法,就是当前错误时,直接使用header抛出信息,如: heade ...

  8. Tars | 第8篇 TarsJava Subset最终代码的执行流程与原理分析

    目录 前言 1. SubsetConf配置项的结构 1.1 SubsetConf 1.2 RatioConfig 1.3 KeyConfig 1.4 KeyRoute 1.5 SubsetConf的结 ...

  9. 整理分布式锁:业务场景&分布式锁家族&实现原理

    1.引入业务场景 业务场景一出现: 因为小T刚接手项目,正在吭哧吭哧对熟悉着代码.部署架构.在看代码过程中发现,下单这块代码可能会出现问题,这可是分布式部署的,如果多个用户同时购买同一个商品,就可能导 ...

  10. Python面向对象篇之元类,附Django Model核心原理

    关于元类,我写过一篇,如果你只是了解元类,看下面这一篇就足够了. Python面向对象之类的方法和属性 本篇是深度解剖,如果你觉得元类用不到,呵呵,那是因为你不了解Django. 在Python中有一 ...

随机推荐

  1. vue打包记录

    这里的确是css以及js文件的路径问题,但解决时并不需要手动改路径或者加一段判断去修改,最方便的办法时在项目打包前的vue.config.js里面将publicPath属性添加或者修改为 public ...

  2. Apache Ranger系列七:Hive 和 Spark 执行过程中的文件路径配置

    背景:在使用Ranger鉴权的过程中,要求必须开启impersonation功能(即执行用户与提交用户保持一致,而不是统一代理的hive/spark).但是在执行的过程中,会需要在hdfs存储临时的文 ...

  3. Tcp网络模型

    要摸清网络,那么第一步肯定是要清楚网络协议的分层结构,用上帝视角来看网络. 对于同一台设备上的进程间通信,有很多种方式,比如有管道.消息队列.共享内存.信号等方式,而对于不同设备上的进程间通信,就需要 ...

  4. 在昇腾平台上对TensorFlow网络进行性能调优

    摘要:本文就带大家了解在昇腾平台上对TensorFlow训练网络进行性能调优的常用手段. 本文分享自华为云社区<在昇腾平台上对TensorFlow网络进行性能调优>,作者:昇腾CANN . ...

  5. .NET Task 揭秘(3)async 与 AsyncMethodBuilder

    目录 前言 AsyncMethodBuilder 介绍 AsyncMethodBuilder 是状态机的重要组成部分 AsyncMethodBuilder 的结构 AsyncMethodBuilder ...

  6. MyBatis 重点知识归纳

    一.MyBatis 简介 [1]MyBatis 是支持定制化 SQL,存储过程以及高级映射的优秀持久化框架.[2]MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取查询结果集.[3 ...

  7. redis.clients.jedis.exceptions.JedisConnectionException: Failed connecting to "xxxxx"

    Java 连接 Redis所遇问题 1. 检查Linux是否关闭防火墙,或对外开放redis默认端口6379 关闭防火墙. systemctl stop firewalld 对外开放端口.firewa ...

  8. flutter issue---->Scaffold.of(context)

    当我们想showSnackBar的时候,需要通过Scaffold.of(context)得到Scaffold.但是如果这个context用错的话,flutter就会抛出错误.下面我们通过代码仔细看一下 ...

  9. MySQL事务还没提交,Canal就能读到消息了?

    [问题描述] 开发有天碰到一个很奇怪的问题,他的场景是这样子的: 通过Canal来订阅MySQL的binlog, 当捕获到有数据变化时,回到数据库,反查该数据的明细,然后做进一步处理. 有一次,他碰到 ...

  10. Seata锁等待超时问题排查

    问题描述 生产环境,一个简单的事务方法,提交失败,报 Global lock wait timeout 伪代码如下: @GlobalTransactional(rollbackFor = Except ...