ElasticSearch 億級數據檢索深度優化

-     前言    -

數據平臺已迭代三個版本,從頭開始遇到很多常見的難題,終於有片段時間整理一些已完善的文檔,在此分享以供所需朋友的實現參考,少走些彎路,在此篇幅中偏重於 ES 的優化,關於 HBase,Hadoop 的設計優化估計有很多文章可以參考,不再贅述。

-     需求說明    -

項目背景:

在一業務系統中,部分表每天的數據量過億,已按天分表,但業務上受限於按天查詢,並且 DB 中只能保留 3 個月的數據 (硬件高配),分庫代價較高。

改進版本目標:
  1. 數據能跨月查詢,並且支持 1 年以上的歷史數據查詢與導出。

  2. 按條件的數據查詢秒級返回。

-     深入原理****    -****

Elasticsearch 檢索原理

3.1 關於 ES 和 Lucene 基礎結構

談到優化必須能瞭解組件的基本原理,才容易找到瓶頸所在,以免走多種彎路,先從 ES 的基礎結構說起 (如下圖):

一些基本概念:

在 Lucene 中,分爲索引 (錄入) 與檢索 (查詢) 兩部分,索引部分包含分詞器、過濾器、字符映射器等,檢索部分包含查詢解析器等。

一個 Lucene 索引包含多個 segments,一個 segment 包含多個文檔,每個文檔包含多個字段,每個字段經過分詞後形成一個或多個 term。

通過 Luke 工具查看 ES 的 lucene 文件如下,主要增加了_id 和_source 字段:

3.2 Lucene 索引實現

Lucene 索引文件結構主要的分爲:詞典、倒排表、正向文件、DocValues 等,如下圖:

注:

整理來源於 lucene 官方:

 http://lucene.apache.org/core/7_2_1/core/org/apache/lucene/codecs/lucene70/package-summary.html#package.description

Lucene 隨機三次磁盤讀取比較耗時。其中. fdt 文件保存數據值損耗空間大,.tim 和. doc 則需要 SSD 存儲提高隨機讀寫性能。另外一個比較消耗性能的是打分流程,不需要則可屏蔽。

-     關於 DocValues    -

倒排索引解決從詞快速檢索到相應文檔 ID, 但如果需要對結果進行排序、分組、聚合等操作的時候則需要根據文檔 ID 快速找到對應的值。

通過倒排索引代價缺很高:需迭代索引裏的每個詞項並收集文檔的列裏面 token。這很慢而且難以擴展:隨着詞項和文檔的數量增加,執行時間也會增加。Solr docs 對此的解釋如下:

For other features that we now commonly associate with search, such as sorting, faceting, and highlighting, this approach is not very efficient. The faceting engine, for example, must look up each term that appears in each document that will make up the result set and pull the document IDs in order to build the facet list. In Solr, this is maintained in memory, and can be slow to load (depending on the number of documents, terms, etc.)

在 lucene 4.0 版本前通過 FieldCache,原理是通過按列逆轉倒排表將(field value ->doc)映射變成(doc -> field value)映射,問題爲逐步構建時間長並且消耗大量內存,容易造成 OOM。

DocValues 是一種列存儲結構,能快速通過文檔 ID 找到相關需要排序的字段。在 ES 中,默認開啓所有 (除了標記需 analyzed 的字符串字段) 字段的 doc values,如果不需要對此字段做任何排序等工作,則可關閉以減少資源消耗。

3.3 關於 ES 索引與檢索分片

ES 中一個索引由一個或多個 lucene 索引構成,一個 lucene 索引由一個或多個 segment 構成,其中 segment 是最小的檢索域。

數據具體被存儲到哪個分片上:

shard = hash(routing) % number_of_primary_shards

默認情況下 routing 參數是文檔 ID (murmurhash3), 可通過 URL 中的 _routing 參數指定數據分佈在同一個分片中,index 和 search 的時候都需要一致才能找到數據,如果能明確根據_routing 進行數據分區,則可減少分片的檢索工作,以提高性能。

-     優化案例    -

優化案例

在我們的案例中,查詢字段都是固定的,不提供全文檢索功能,這也是幾十億數據能秒級返回的一個大前提:

  1. ES 僅提供字段的檢索,僅存儲 HBase 的 Rowkey 不存儲實際數據。

  2. 實際數據存儲在 HBase 中,通過 Rowkey 查詢,如下圖。

  3. 提高索引與檢索的性能建議,可參考官方文檔(如 https://www.elastic.co/guide/en/elasticsearch/reference/current/tune-for-indexing-speed.html)。

一些細節優化項官方與其他的一些文章都有描述,在此文章中僅提出一些本案例的重點優化項。

4.1  優化索引性能

  1. 批量寫入,看每條數據量的大小,一般都是幾百到幾千。

  2. 多線程寫入,寫入線程數一般和機器數相當,可以配多種情況,在測試環境通過 Kibana 觀察性能曲線。

  3. 增加 segments 的刷新時間,通過上面的原理知道,segment 作爲一個最小的檢索單元,比如 segment 有 50 個,目的需要查 10 條數據,但需要從 50 個 segment 分別查詢 10 條,共 500 條記錄,再進行排序或者分數比較後,截取最前面的 10 條,丟棄 490 條。在我們的案例中將此 "refresh_interval": "-1" ,程序批量寫入完成後進行手工刷新 (調用相應的 API 即可)。

  4. 內存分配方面,很多文章已經提到,給系統 50% 的內存給 Lucene 做文件緩存,它任務很繁重,所以 ES 節點的內存需要比較多 (比如每個節點能配置 64G 以上最好)。

  5. 磁盤方面配置 SSD,機械盤做陣列 RAID5 RAID10 雖然看上去很快,但是隨機 IO 還是 SSD 好。

  6. 使用自動生成的 ID,在我們的案例中使用自定義的 KEY,也就是與 HBase 的 ROW KEY,是爲了能根據 rowkey 刪除和更新數據,性能下降不是很明顯。

  7. 關於段合併,合併在後臺定期執行,比較大的 segment 需要很長時間才能完成,爲了減少對其他操作的影響 (如檢索),elasticsearch 進行閾值限制,默認是 20MB/s,可配置的參數:"indices.store.throttle.max_bytes_per_sec" : "200mb"  (根據磁盤性能調整)合併線程數默認是:Math.max(1, Math.min(4, Runtime.getRuntime().availableProcessors() / 2)),如果是機械磁盤,可以考慮設置爲 1:index.merge.scheduler.max_thread_count: 1,在我們的案例中使用 SSD,配置了 6 個合併線程。

4.2 優化檢索性能

  1. 關閉不需要字段的 doc values。

  2. 儘量使用 keyword 替代一些 long 或者 int 之類,term 查詢總比 range 查詢好 (參考 lucene 說明 http://lucene.apache.org/core/7_4_0/core/org/apache/lucene/index/PointValues.html)。

  3. 關閉不需要查詢字段的_source 功能,不將此存儲僅 ES 中,以節省磁盤空間。

  4. 評分消耗資源,如果不需要可使用 filter 過濾來達到關閉評分功能,score 則爲 0,如果使用 constantScoreQuery 則 score 爲 1。

  5. 關於分頁:

  1. 關於排序:我們增加一個 long 字段,它用於存儲時間和 ID 的組合 (通過移位即可),正排與倒排性能相差不明顯。

  2. 關於 CPU 消耗,檢索時如果需要做排序則需要字段對比,消耗 CPU 比較大,如果有可能儘量分配 16cores 以上的 CPU,具體看業務壓力。

  3. 關於合併被標記刪除的記錄,我們設置爲 0 表示在合併的時候一定刪除被標記的記錄,默認應該是大於 10% 才刪除:"merge.policy.expunge_deletes_allowed": "0"。

{
    "mappings"{
        "data"{
            "dynamic""false",
            "_source"{
                "includes"["XXX"]  -- 僅將查詢結果所需的數據存儲僅_source中
            },
            "properties"{
                "state"{
                    "type""keyword",   -- 雖然state爲int值,但如果不需要做範圍查詢,儘量使用keyword,因爲int需要比keyword增加額外的消耗。
                    "doc_values"false  -- 關閉不需要字段的doc values功能,僅對需要排序,匯聚功能的字段開啓。
                },
                "b"{
                    "type""long"    -- 使用了範圍查詢字段,則需要用long或者int之類 (構建類似KD-trees結構)
                }
            }
        }
    },
   "settings"{......}
}

-     性能測試    -

優化效果評估基於基準測試,如果沒有基準測試無法瞭解是否有性能提升,在這所有的變動前做一次測試會比較好。在我們的案例中:

  1. 單節點 5 千萬到一億的數據量測試,檢查單點承受能力。

  2. 集羣測試 1 億 - 30 億的數量,磁盤 IO / 內存 / CPU / 網絡 IO 消耗如何。

  3. 隨機不同組合條件的檢索,在各個數據量情況下表現如何。

  4. 另外 SSD 與機械盤在測試中性能差距如何。

性能的測試組合有很多,通常也很花時間,不過作爲評測標準時間上的投入有必要,否則生產出現性能問題很難定位或不好改善。對於 ES 的性能研究花了不少時間,最多的關注點就是 lucene 的優化,能深入瞭解 lucene 原理對優化有很大的幫助。

-     生產效果    -

目前平臺穩定運行,幾十億的數據查詢 100 條都在 3 秒內返回,前後翻頁很快,如果後續有性能瓶頸,可通過擴展節點分擔數據壓力。

作者:fredalxin

https://fredal.xin/graceful-soa-updown

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/G3_I0NQhgWpeXeca1SPpnA