一、前言
數據平臺已迭代三個版本,從頭開始遇到很多常見的難題,終于有片段時間整理一些已完善的文檔,在此分享以供所需朋友的實現參考,少走些彎路,在此篇幅中偏重于ES的優化,關于HBase,Hadoop的設計優化估計有很多文章可以參考,不再贅述。
二、需求說明
項目背景:
在一業務系統中,部分表每天的數據量過億,已按天分表,但業務上受限于按天查詢,并且DB中只能保留3個月的數據(硬件高配),分庫代價較高。
改進版本目標:
數據能跨月查詢,并且支持1年以上的歷史數據查詢與導出。
按條件的數據查詢秒級返回。
三、Elasticsearch檢索原理
3.1 關于ES和Lucene基礎結構
談到優化必須能了解組件的基本原理,才容易找到瓶頸所在,以免走多種彎路,先從ES的基礎結構說起(如下圖):

一些基本概念:
Cluster: 包含多個Node的集群
Node: 集群服務單元
Index: 一個ES索引包含一個或多個物理分片,它只是這些分片的邏輯命名空間
Type: 一個index的不同分類,6.x后只能配置一個type,以后將移除
Document: 最基礎的可被索引的數據單元,如一個JSON串
Shards : 一個分片是一個底層的工作單元,它僅保存全部數據中的一部分,它是一個Lucence實例 (一個Lucene: 索引最大包含2,147,483,519 (= Integer.MAX_VALUE - 128)個文檔數量)
Replicas: 分片備份,用于保障數據安全與分擔檢索壓力
ES依賴一個重要的組件Lucene,關于數據結構的優化通常來說是對Lucene的優化,它是集群的一個存儲于檢索工作單元,結構如下圖:

在Lucene中,分為索引(錄入)與檢索(查詢)兩部分,索引部分包含分詞器、過濾器、字符映射器等,檢索部分包含查詢解析器等。
一個Lucene索引包含多個segments,一個segment包含多個文檔,每個文檔包含多個字段,每個字段經過分詞后形成一個或多個term。
通過Luke工具查看ES的lucene文件如下,主要增加了_id和_source字段:

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


Lucene隨機三次磁盤讀取比較耗時。其中.fdt文件保存數據值損耗空間大,.tim和.doc則需要SSD存儲提高隨機讀寫性能。另外一個比較消耗性能的是打分流程,不需要則可屏蔽。
關于DocValues
倒排索引解決從詞快速檢索到相應文檔ID, 但如果需要對結果進行排序、分組、聚合等操作的時候則需要根據文檔ID快速找到對應的值。
通過倒排索引代價缺很高:需迭代索引里的每個詞項并收集文檔的列里面 token。這很慢而且難以擴展:隨著詞項和文檔的數量增加,執行時間也會增加。Solr docs對此的解釋如下:
在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、關于分頁:
①from + size: 每分片檢索結果數最大為 from + size,假設from = 20, size = 20,則每個分片需要獲取20 * 20 = 400條數據,多個分片的結果在協調節點合并(假設請求的分配數為5,則結果數最大為 400*5 = 2000條) 再在內存中排序后然后20條給用戶。這種機制導致越往后分頁獲取的代價越高,達到50000條將面臨沉重的代價,默認from + size默認如下:index.max_result_window :10000
②search_after: 使用前一個分頁記錄的最后一條來檢索下一個分頁記錄,在我們的案例中,首先使用from+size,檢索出結果后再使用search_after,在頁面上我們限制了用戶只能跳5頁,不能跳到最后一頁。
③scroll 用于大結果集查詢,缺陷是需要維護scroll_id
6、關于排序:我們增加一個long字段,它用于存儲時間和ID的組合(通過移位即可),正排與倒排性能相差不明顯。
7、關于CPU消耗,檢索時如果需要做排序則需要字段對比,消耗CPU比較大,如果有可能盡量分配16cores以上的CPU,具體看業務壓力。
8、關于合并被標記刪除的記錄,我們設置為0表示在合并的時候一定刪除被標記的記錄,默認應該是大于10%才刪除:"merge.policy.expunge_deletes_allowed": "0"。
五、性能測試
優化效果評估基于基準測試,如果沒有基準測試無法了解是否有性能提升,在這所有的變動前做一次測試會比較好。在我們的案例中:
1、單節點5千萬到一億的數據量測試,檢查單點承受能力。
2、集群測試1億-30億的數量,磁盤IO/內存/CPU/網絡IO消耗如何。
3、隨機不同組合條件的檢索,在各個數據量情況下表現如何。
4、另外SSD與機械盤在測試中性能差距如何。
性能的測試組合有很多,通常也很花時間,不過作為評測標準時間上的投入有必要,否則生產出現性能問題很難定位或不好改善。對于ES的性能研究花了不少時間,最多的關注點就是lucene的優化,能深入了解lucene原理對優化有很大的幫助。
六、生產效果
目前平臺穩定運行,幾十億的數據查詢100條都在3秒內返回,前后翻頁很快,如果后續有性能瓶頸,可通過擴展節點分擔數據壓力。


