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

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

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

3.2 Lucene索引實現(xiàn)
Lucene 索引文件結(jié)構(gòu)主要的分為:詞典、倒排表、正向文件、DocValues等,如下圖:


Lucene隨機三次磁盤讀取比較耗時。其中.fdt文件保存數(shù)據(jù)值損耗空間大,.tim和.doc則需要SSD存儲提高隨機讀寫性能。另外一個比較消耗性能的是打分流程,不需要則可屏蔽。
關(guān)于DocValues
倒排索引解決從詞快速檢索到相應(yīng)文檔ID, 但如果需要對結(jié)果進行排序、分組、聚合等操作的時候則需要根據(jù)文檔ID快速找到對應(yīng)的值。
通過倒排索引代價缺很高:需迭代索引里的每個詞項并收集文檔的列里面 token。這很慢而且難以擴展:隨著詞項和文檔的數(shù)量增加,執(zhí)行時間也會增加。Solr docs對此的解釋如下:
在lucene 4.0版本前通過FieldCache,原理是通過按列逆轉(zhuǎn)倒排表將(field value ->doc)映射變成(doc -> field value)映射,問題為逐步構(gòu)建時間長并且消耗大量內(nèi)存,容易造成OOM。
DocValues是一種列存儲結(jié)構(gòu),能快速通過文檔ID找到相關(guān)需要排序的字段。在ES中,默認(rèn)開啟所有(除了標(biāo)記需analyzed的字符串字段)字段的doc values,如果不需要對此字段做任何排序等工作,則可關(guān)閉以減少資源消耗。
3.3 關(guān)于ES索引與檢索分片
ES中一個索引由一個或多個lucene索引構(gòu)成,一個lucene索引由一個或多個segment構(gòu)成,其中segment是最小的檢索域。
數(shù)據(jù)具體被存儲到哪個分片上:shard = hash(routing) % number_of_primary_shards
默認(rèn)情況下 routing參數(shù)是文檔ID (murmurhash3),可通過 URL中的 _routing 參數(shù)指定數(shù)據(jù)分布在同一個分片中,index和search的時候都需要一致才能找到數(shù)據(jù),如果能明確根據(jù)_routing進行數(shù)據(jù)分區(qū),則可減少分片的檢索工作,以提高性能。
四、優(yōu)化案例
在我們的案例中,查詢字段都是固定的,不提供全文檢索功能,這也是幾十億數(shù)據(jù)能秒級返回的一個大前提:
1、ES僅提供字段的檢索,僅存儲HBase的Rowkey不存儲實際數(shù)據(jù)。
2、實際數(shù)據(jù)存儲在HBase中,通過Rowkey查詢,如下圖。
3、提高索引與檢索的性能建議,可參考官方文檔(如 https://www.elastic.co/guide/en/elasticsearch/reference/current/tune-for-indexing-speed.html)。
一些細節(jié)優(yōu)化項官方與其他的一些文章都有描述,在此文章中僅提出一些本案例的重點優(yōu)化項。

4.1 優(yōu)化索引性能
1、批量寫入,看每條數(shù)據(jù)量的大小,一般都是幾百到幾千。
2、多線程寫入,寫入線程數(shù)一般和機器數(shù)相當(dāng),可以配多種情況,在測試環(huán)境通過Kibana觀察性能曲線。
3、增加segments的刷新時間,通過上面的原理知道,segment作為一個最小的檢索單元,比如segment有50個,目的需要查10條數(shù)據(jù),但需要從50個segment分別查詢10條,共500條記錄,再進行排序或者分?jǐn)?shù)比較后,截取最前面的10條,丟棄490條。在我們的案例中將此 "refresh_interval": "-1" ,程序批量寫入完成后進行手工刷新(調(diào)用相應(yīng)的API即可)。
4、內(nèi)存分配方面,很多文章已經(jīng)提到,給系統(tǒng)50%的內(nèi)存給Lucene做文件緩存,它任務(wù)很繁重,所以ES節(jié)點的內(nèi)存需要比較多(比如每個節(jié)點能配置64G以上最好)。
5、磁盤方面配置SSD,機械盤做陣列RAID5 RAID10雖然看上去很快,但是隨機IO還是SSD好。
6、使用自動生成的ID,在我們的案例中使用自定義的KEY,也就是與HBase的ROW KEY,是為了能根據(jù)rowkey刪除和更新數(shù)據(jù),性能下降不是很明顯。
7、關(guān)于段合并,合并在后臺定期執(zhí)行,比較大的segment需要很長時間才能完成,為了減少對其他操作的影響(如檢索),elasticsearch進行閾值限制,默認(rèn)是20MB/s,可配置的參數(shù):"indices.store.throttle.max_bytes_per_sec" : "200mb" (根據(jù)磁盤性能調(diào)整)合并線程數(shù)默認(rèn)是:Math.max(1, Math.min(4, Runtime.getRuntime().availableProcessors() / 2)),如果是機械磁盤,可以考慮設(shè)置為1:index.merge.scheduler.max_thread_count: 1,在我們的案例中使用SSD,配置了6個合并線程。
4.2 優(yōu)化檢索性能
1、關(guān)閉不需要字段的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、關(guān)閉不需要查詢字段的_source功能,不將此存儲僅ES中,以節(jié)省磁盤空間。
4、評分消耗資源,如果不需要可使用filter過濾來達到關(guān)閉評分功能,score則為0,如果使用constantScoreQuery則score為1。
5、關(guān)于分頁:
①from + size: 每分片檢索結(jié)果數(shù)最大為 from + size,假設(shè)from = 20, size = 20,則每個分片需要獲取20 * 20 = 400條數(shù)據(jù),多個分片的結(jié)果在協(xié)調(diào)節(jié)點合并(假設(shè)請求的分配數(shù)為5,則結(jié)果數(shù)最大為 400*5 = 2000條) 再在內(nèi)存中排序后然后20條給用戶。這種機制導(dǎo)致越往后分頁獲取的代價越高,達到50000條將面臨沉重的代價,默認(rèn)from + size默認(rèn)如下:index.max_result_window :10000
②search_after: 使用前一個分頁記錄的最后一條來檢索下一個分頁記錄,在我們的案例中,首先使用from+size,檢索出結(jié)果后再使用search_after,在頁面上我們限制了用戶只能跳5頁,不能跳到最后一頁。
③scroll 用于大結(jié)果集查詢,缺陷是需要維護scroll_id
6、關(guān)于排序:我們增加一個long字段,它用于存儲時間和ID的組合(通過移位即可),正排與倒排性能相差不明顯。
7、關(guān)于CPU消耗,檢索時如果需要做排序則需要字段對比,消耗CPU比較大,如果有可能盡量分配16cores以上的CPU,具體看業(yè)務(wù)壓力。
8、關(guān)于合并被標(biāo)記刪除的記錄,我們設(shè)置為0表示在合并的時候一定刪除被標(biāo)記的記錄,默認(rèn)應(yīng)該是大于10%才刪除:"merge.policy.expunge_deletes_allowed": "0"。
五、性能測試
優(yōu)化效果評估基于基準(zhǔn)測試,如果沒有基準(zhǔn)測試無法了解是否有性能提升,在這所有的變動前做一次測試會比較好。在我們的案例中:
1、單節(jié)點5千萬到一億的數(shù)據(jù)量測試,檢查單點承受能力。
2、集群測試1億-30億的數(shù)量,磁盤IO/內(nèi)存/CPU/網(wǎng)絡(luò)IO消耗如何。
3、隨機不同組合條件的檢索,在各個數(shù)據(jù)量情況下表現(xiàn)如何。
4、另外SSD與機械盤在測試中性能差距如何。
性能的測試組合有很多,通常也很花時間,不過作為評測標(biāo)準(zhǔn)時間上的投入有必要,否則生產(chǎn)出現(xiàn)性能問題很難定位或不好改善。對于ES的性能研究花了不少時間,最多的關(guān)注點就是lucene的優(yōu)化,能深入了解lucene原理對優(yōu)化有很大的幫助。
六、生產(chǎn)效果
目前平臺穩(wěn)定運行,幾十億的數(shù)據(jù)查詢100條都在3秒內(nèi)返回,前后翻頁很快,如果后續(xù)有性能瓶頸,可通過擴展節(jié)點分擔(dān)數(shù)據(jù)壓力。


