一、前言
數(shù)據(jù)平臺(tái)已迭代三個(gè)版本,從頭開始遇到很多常見的難題,終于有片段時(shí)間整理一些已完善的文檔,在此分享以供所需朋友的實(shí)現(xiàn)參考,少走些彎路,在此篇幅中偏重于ES的優(yōu)化,關(guān)于HBase,Hadoop的設(shè)計(jì)優(yōu)化估計(jì)有很多文章可以參考,不再贅述。
二、需求說(shuō)明
項(xiàng)目背景:
在一業(yè)務(wù)系統(tǒng)中,部分表每天的數(shù)據(jù)量過(guò)億,已按天分表,但業(yè)務(wù)上受限于按天查詢,并且DB中只能保留3個(gè)月的數(shù)據(jù)(硬件高配),分庫(kù)代價(jià)較高。
改進(jìn)版本目標(biāo):
數(shù)據(jù)能跨月查詢,并且支持1年以上的歷史數(shù)據(jù)查詢與導(dǎo)出。
按條件的數(shù)據(jù)查詢秒級(jí)返回。
三、Elasticsearch檢索原理
3.1 關(guān)于ES和Lucene基礎(chǔ)結(jié)構(gòu)
談到優(yōu)化必須能了解組件的基本原理,才容易找到瓶頸所在,以免走多種彎路,先從ES的基礎(chǔ)結(jié)構(gòu)說(shuō)起(如下圖):
一些基本概念:
Cluster: 包含多個(gè)Node的集群
Node: 集群服務(wù)單元
Index: 一個(gè)ES索引包含一個(gè)或多個(gè)物理分片,它只是這些分片的邏輯命名空間
Type: 一個(gè)index的不同分類,6.x后只能配置一個(gè)type,以后將移除
Document: 最基礎(chǔ)的可被索引的數(shù)據(jù)單元,如一個(gè)JSON串
Shards : 一個(gè)分片是一個(gè)底層的工作單元,它僅保存全部數(shù)據(jù)中的一部分,它是一個(gè)Lucence實(shí)例 (一個(gè)Lucene: 索引最大包含2,147,483,519 (= Integer.MAX_VALUE - 128)個(gè)文檔數(shù)量)
Replicas: 分片備份,用于保障數(shù)據(jù)安全與分擔(dān)檢索壓力
ES依賴一個(gè)重要的組件Lucene,關(guān)于數(shù)據(jù)結(jié)構(gòu)的優(yōu)化通常來(lái)說(shuō)是對(duì)Lucene的優(yōu)化,它是集群的一個(gè)存儲(chǔ)于檢索工作單元,結(jié)構(gòu)如下圖:
在Lucene中,分為索引(錄入)與檢索(查詢)兩部分,索引部分包含分詞器、過(guò)濾器、字符映射器等,檢索部分包含查詢解析器等。
一個(gè)Lucene索引包含多個(gè)segments,一個(gè)segment包含多個(gè)文檔,每個(gè)文檔包含多個(gè)字段,每個(gè)字段經(jīng)過(guò)分詞后形成一個(gè)或多個(gè)term。
通過(guò)Luke工具查看ES的lucene文件如下,主要增加了_id和_source字段:
3.2 Lucene索引實(shí)現(xiàn)
Lucene 索引文件結(jié)構(gòu)主要的分為:詞典、倒排表、正向文件、DocValues等,如下圖:
Lucene隨機(jī)三次磁盤讀取比較耗時(shí)。其中.fdt文件保存數(shù)據(jù)值損耗空間大,.tim和.doc則需要SSD存儲(chǔ)提高隨機(jī)讀寫性能。另外一個(gè)比較消耗性能的是打分流程,不需要?jiǎng)t可屏蔽。
關(guān)于DocValues
倒排索引解決從詞快速檢索到相應(yīng)文檔ID, 但如果需要對(duì)結(jié)果進(jìn)行排序、分組、聚合等操作的時(shí)候則需要根據(jù)文檔ID快速找到對(duì)應(yīng)的值。
通過(guò)倒排索引代價(jià)缺很高:需迭代索引里的每個(gè)詞項(xiàng)并收集文檔的列里面 token。這很慢而且難以擴(kuò)展:隨著詞項(xiàng)和文檔的數(shù)量增加,執(zhí)行時(shí)間也會(huì)增加。Solr docs對(duì)此的解釋如下:
在lucene 4.0版本前通過(guò)FieldCache,原理是通過(guò)按列逆轉(zhuǎn)倒排表將(field value ->doc)映射變成(doc -> field value)映射,問(wèn)題為逐步構(gòu)建時(shí)間長(zhǎng)并且消耗大量?jī)?nèi)存,容易造成OOM。
DocValues是一種列存儲(chǔ)結(jié)構(gòu),能快速通過(guò)文檔ID找到相關(guān)需要排序的字段。在ES中,默認(rèn)開啟所有(除了標(biāo)記需analyzed的字符串字段)字段的doc values,如果不需要對(duì)此字段做任何排序等工作,則可關(guān)閉以減少資源消耗。
3.3 關(guān)于ES索引與檢索分片
ES中一個(gè)索引由一個(gè)或多個(gè)lucene索引構(gòu)成,一個(gè)lucene索引由一個(gè)或多個(gè)segment構(gòu)成,其中segment是最小的檢索域。
數(shù)據(jù)具體被存儲(chǔ)到哪個(gè)分片上:shard = hash(routing) % number_of_primary_shards
默認(rèn)情況下 routing參數(shù)是文檔ID (murmurhash3),可通過(guò) URL中的 _routing 參數(shù)指定數(shù)據(jù)分布在同一個(gè)分片中,index和search的時(shí)候都需要一致才能找到數(shù)據(jù),如果能明確根據(jù)_routing進(jìn)行數(shù)據(jù)分區(qū),則可減少分片的檢索工作,以提高性能。
四、優(yōu)化案例
在我們的案例中,查詢字段都是固定的,不提供全文檢索功能,這也是幾十億數(shù)據(jù)能秒級(jí)返回的一個(gè)大前提:
1、ES僅提供字段的檢索,僅存儲(chǔ)HBase的Rowkey不存儲(chǔ)實(shí)際數(shù)據(jù)。
2、實(shí)際數(shù)據(jù)存儲(chǔ)在HBase中,通過(guò)Rowkey查詢,如下圖。
3、提高索引與檢索的性能建議,可參考官方文檔(如 https://www.elastic.co/guide/en/elasticsearch/reference/current/tune-for-indexing-speed.html)。
一些細(xì)節(jié)優(yōu)化項(xiàng)官方與其他的一些文章都有描述,在此文章中僅提出一些本案例的重點(diǎn)優(yōu)化項(xiàng)。
4.1 優(yōu)化索引性能
1、批量寫入,看每條數(shù)據(jù)量的大小,一般都是幾百到幾千。
2、多線程寫入,寫入線程數(shù)一般和機(jī)器數(shù)相當(dāng),可以配多種情況,在測(cè)試環(huán)境通過(guò)Kibana觀察性能曲線。
3、增加segments的刷新時(shí)間,通過(guò)上面的原理知道,segment作為一個(gè)最小的檢索單元,比如segment有50個(gè),目的需要查10條數(shù)據(jù),但需要從50個(gè)segment分別查詢10條,共500條記錄,再進(jìn)行排序或者分?jǐn)?shù)比較后,截取最前面的10條,丟棄490條。在我們的案例中將此 "refresh_interval": "-1" ,程序批量寫入完成后進(jìn)行手工刷新(調(diào)用相應(yīng)的API即可)。
4、內(nèi)存分配方面,很多文章已經(jīng)提到,給系統(tǒng)50%的內(nèi)存給Lucene做文件緩存,它任務(wù)很繁重,所以ES節(jié)點(diǎn)的內(nèi)存需要比較多(比如每個(gè)節(jié)點(diǎn)能配置64G以上最好)。
5、磁盤方面配置SSD,機(jī)械盤做陣列RAID5 RAID10雖然看上去很快,但是隨機(jī)IO還是SSD好。
6、使用自動(dòng)生成的ID,在我們的案例中使用自定義的KEY,也就是與HBase的ROW KEY,是為了能根據(jù)rowkey刪除和更新數(shù)據(jù),性能下降不是很明顯。
7、關(guān)于段合并,合并在后臺(tái)定期執(zhí)行,比較大的segment需要很長(zhǎng)時(shí)間才能完成,為了減少對(duì)其他操作的影響(如檢索),elasticsearch進(jìn)行閾值限制,默認(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)),如果是機(jī)械磁盤,可以考慮設(shè)置為1:index.merge.scheduler.max_thread_count: 1,在我們的案例中使用SSD,配置了6個(gè)合并線程。
4.2 優(yōu)化檢索性能
1、關(guān)閉不需要字段的doc values。
2、盡量使用keyword替代一些long或者int之類,term查詢總比range查詢好 (參考lucene說(shuō)明 http://lucene.apache.org/core/7_4_0/core/org/apache/lucene/index/PointValues.html)。
3、關(guān)閉不需要查詢字段的_source功能,不將此存儲(chǔ)僅ES中,以節(jié)省磁盤空間。
4、評(píng)分消耗資源,如果不需要可使用filter過(guò)濾來(lái)達(dá)到關(guān)閉評(píng)分功能,score則為0,如果使用constantScoreQuery則score為1。
5、關(guān)于分頁(yè):
①from + size: 每分片檢索結(jié)果數(shù)最大為 from + size,假設(shè)from = 20, size = 20,則每個(gè)分片需要獲取20 * 20 = 400條數(shù)據(jù),多個(gè)分片的結(jié)果在協(xié)調(diào)節(jié)點(diǎn)合并(假設(shè)請(qǐng)求的分配數(shù)為5,則結(jié)果數(shù)最大為 400*5 = 2000條) 再在內(nèi)存中排序后然后20條給用戶。這種機(jī)制導(dǎo)致越往后分頁(yè)獲取的代價(jià)越高,達(dá)到50000條將面臨沉重的代價(jià),默認(rèn)from + size默認(rèn)如下:index.max_result_window :10000
②search_after: 使用前一個(gè)分頁(yè)記錄的最后一條來(lái)檢索下一個(gè)分頁(yè)記錄,在我們的案例中,首先使用from+size,檢索出結(jié)果后再使用search_after,在頁(yè)面上我們限制了用戶只能跳5頁(yè),不能跳到最后一頁(yè)。
③scroll 用于大結(jié)果集查詢,缺陷是需要維護(hù)scroll_id
6、關(guān)于排序:我們?cè)黾右粋€(gè)long字段,它用于存儲(chǔ)時(shí)間和ID的組合(通過(guò)移位即可),正排與倒排性能相差不明顯。
7、關(guān)于CPU消耗,檢索時(shí)如果需要做排序則需要字段對(duì)比,消耗CPU比較大,如果有可能盡量分配16cores以上的CPU,具體看業(yè)務(wù)壓力。
8、關(guān)于合并被標(biāo)記刪除的記錄,我們?cè)O(shè)置為0表示在合并的時(shí)候一定刪除被標(biāo)記的記錄,默認(rèn)應(yīng)該是大于10%才刪除:"merge.policy.expunge_deletes_allowed": "0"。
五、性能測(cè)試
優(yōu)化效果評(píng)估基于基準(zhǔn)測(cè)試,如果沒有基準(zhǔn)測(cè)試無(wú)法了解是否有性能提升,在這所有的變動(dòng)前做一次測(cè)試會(huì)比較好。在我們的案例中:
1、單節(jié)點(diǎn)5千萬(wàn)到一億的數(shù)據(jù)量測(cè)試,檢查單點(diǎn)承受能力。
2、集群測(cè)試1億-30億的數(shù)量,磁盤IO/內(nèi)存/CPU/網(wǎng)絡(luò)IO消耗如何。
3、隨機(jī)不同組合條件的檢索,在各個(gè)數(shù)據(jù)量情況下表現(xiàn)如何。
4、另外SSD與機(jī)械盤在測(cè)試中性能差距如何。
性能的測(cè)試組合有很多,通常也很花時(shí)間,不過(guò)作為評(píng)測(cè)標(biāo)準(zhǔn)時(shí)間上的投入有必要,否則生產(chǎn)出現(xiàn)性能問(wèn)題很難定位或不好改善。對(duì)于ES的性能研究花了不少時(shí)間,最多的關(guān)注點(diǎn)就是lucene的優(yōu)化,能深入了解lucene原理對(duì)優(yōu)化有很大的幫助。
六、生產(chǎn)效果
目前平臺(tái)穩(wěn)定運(yùn)行,幾十億的數(shù)據(jù)查詢100條都在3秒內(nèi)返回,前后翻頁(yè)很快,如果后續(xù)有性能瓶頸,可通過(guò)擴(kuò)展節(jié)點(diǎn)分擔(dān)數(shù)據(jù)壓力。