天天看點

【問題排查篇】一次業務問題對 ES 的 cardinality 原理探究

作者:京東雲開發者

作者:京東科技 王長春

業務問題

小編工作中負責業務的一個服務端系統,使用了 Elasticsearch 服務做資料存儲,業務營運人員回報,使用者在使用該産品時發現,使用者背景統計的訂單筆數和導出的訂單筆數不一緻!

交易訂單筆數不對,出現差錯訂單了?這一聽極為震撼!出現這樣的問題,在金融科技公司裡面是絕對不允許發生的,得馬上定位問題并解決!

【問題排查篇】一次業務問題對 ES 的 cardinality 原理探究

小編馬上聯系業務和相關人員,通過梳理上遊系統的調用關系,發現業務系統使用到的是我這邊的 ES 的存儲服務,然後對線上情況進行複現,基本了解問題的現象:

1.使用者操作背景裡的訂單總筆數:商戶頁面的"訂單總筆數","訂單總筆數"使用的是小編 ES 存儲服務中 ES 的統計聚合功能,其中訂單總筆數是使用了 cardinality 操作,并且使用的是 orderId(訂單編号)進行統計去重。

2.導出功能裡的訂單總筆數:導出功能使用的是 ES 存儲服務中的 ES 條件查詢功能,導出功能是進行分頁查詢的。

問題定位

這兩個查詢數量不一緻,首先看查詢條件是否一緻呢?

經過一番排查,業務系統在調用查詢訂單總數和導出訂單總數的這兩個查詢條件是一緻的,也就是請求到我這邊 ES 服務時,統計聚合的查詢和分頁導出的查詢條件是一緻的,但是為什麼會在 ES 裡面查詢的結果是不一緻的呢?難道 ES 裡面的資料不全?統計聚合或分頁導出的其中有一個不準了?

為了具體排查哪個操作可能存在問題,于是通過相同條件下查詢資料庫的總數和 ES 裡面的資料進行對比。發現相同條件下,資料庫裡面的資料和 ES 條件查詢的總數是一緻的, 同時業務的 orerId 字段是沒有重複,是以可以确定的是:通過 orderId 進行統計聚合去重的操作是有問題的。

【問題排查篇】一次業務問題對 ES 的 cardinality 原理探究
【問題排查篇】一次業務問題對 ES 的 cardinality 原理探究
資料庫查詢:資料庫是做分庫分表,此處資料庫查詢使用的是公司内的資料部銀河大表——公司資料部會 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"
      }
    }
  }
}
           
【問題排查篇】一次業務問題對 ES 的 cardinality 原理探究

為什麼 cardinality 操作會出現這樣的結果呢?

小編開始陷入了想當然的陷阱—— 以為這就是一個簡簡單單的統計去重的功能,ES 做的多好,幫你去重并統計數量了。然後事實并不是,通過 Elasticsearch 對 cardinality 官方文檔解釋,終于找到了原因。

可以參考Elasticsearch 2.x 版本官方文檔對 cardinality的解釋:cardinality

其中對 cardinality 算法核心解釋是:

【問題排查篇】一次業務問題對 ES 的 cardinality 原理探究

可以總結如下:

1. cardinality 并不是像關系型資料庫 MySQL 一樣精确去重的,cardinality做的是一個近似值,是 ES 幫你"估算"出的,這個估算使用的 HyperLogLog++ (HLL)算法,在速度上非常快,周遊一次即可統計去重,具體可看文檔中推薦的論文。

2. ES 做cardinality估算,是可以設定估算精确度,即設定參數 precision_threshold 參數,但是這個參數在 0-40000, 這個值越大意味着精度越高,同時意味着損失更多的記憶體,是以記憶體空間換精度。

3.在小資料量下,ES 的這個"估算"精度是非常高的,幾乎可以說是等于實際數量。

ES 中 cardinality 參數驗證

下面對 ES 的 cardinality 的 precision_threshold 參數進行驗證:

1、大資料量下,設定最高精度及其以上,仍然會存在誤差:

【問題排查篇】一次業務問題對 ES 的 cardinality 原理探究

2、小資料量下,設定最高精度,可以和實際數量保持一緻:

【問題排查篇】一次業務問題對 ES 的 cardinality 原理探究

那麼線上的為什麼聚合統計的是 21514,條件查詢的是 21427?

線上代碼運作和ES叢集設定都沒有主動設定過 precision_threshold 參數,那麼可以知道,這個應該是 ES 叢集設定的預設值。線上 ES 叢集版本為 5.4x 是以找到 5.4 版本的官方文檔,發現 5.4 版本中設定的是預設值 precision_threshold=3000, 在此條件下查詢的統計聚合出來的值是 21514。

另外 ES 官方對 cardinality 操作中的 precision_threshold 參數也做了研究,研究了官方文檔中precision_threshold設定和cardinality查詢失敗率、查詢資料量級的關系,可作為我們在業務開發中進行參考,如下圖所示:

【問題排查篇】一次業務問題對 ES 的 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);
}
           

最後歡迎大家點贊、收藏、評論,轉發!❤️❤️❤️

繼續閱讀