Elasticsearch在Hdfs上build的實現及優化
引言
前段時間參與Elasticsearch離線平臺化專案,主要是做一套Elasticsearch的buildservice, 一方面通過bahamut的資料流定義能力,直接對接使用者原始資料,實現全增量一體化,解決使用者準備資料的痛點。另一方面,社群的elasticsearch並沒有全量增量的概念,所有資料都是使用者通過sdk一條一條發給es線上服務構建索引,很難處理海量資料的場景,而且也難免對線上的效能產生影響,尤其是索引Merge的時候會嚴重影響線上服務的穩定性。所以,需要給Es做一個BuildService, 使之能夠在blink叢集上直接構建索引。
志宸老師在阿里雲Elasticsearch離線平臺化建設這篇文章中介紹了總體架構,本文再詳細對ElasticBuild如何在Hdfs上構建索引以及一些相關的優化做一些介紹。
載入Hdfs索引
Elasticsearch在啟動的時候,會為每個Index的每個Shard初始化一個InternalEngine例項,主要工作是恢復lucene的indexWriter及es的translog,原生的es只支援從本地載入索引檔案,而修改後的ElasticBuild由於索引不落盤直接寫到hdfs, 所以需要實現一種繞過本地磁碟直接載入hdfs上索引的方案。
HdfsDirectory
直接想到的一種辦法是將索引拖到本地後再載入,顯然這種方式時間代價太大,而且blink上的例項磁碟不一定能支援這麼大的索引。所幸的是,lucene的索引讀寫介面Directory支援各種讀寫擴充套件,參考開源的元件,引入HdfsDirectory可以解決這個問題。
簡單剖析下HdfsDirectory的實現,它實現了Directory的Input、Writer的各種讀寫介面:
給HdfsDirectory加層Cache
效能瓶頸
方案制定到這裡,感覺信心滿滿,似乎最主要的難點已經找到解法,但是接下來的效能測試讓專案回到了最初的不確定。。。
開發機上起了一個HdfsDirectory版本的es服務,並通過esrally工具構建一個70G的索引,一開始跑得挺歡樂,但是Build任務就是結束不了,而且cpu也變得一陣一陣地跑不上去,又等了4個小時,還是跑不出來。。
經過分析,發現是索引merge結束不了,主要效能消耗在hdfs的讀寫上面,也就是說,直接用HdfsDirecoty讀寫hdfs是行不通的。
BlockDirectory
回頭想想,hdfs上讀寫索引和本地磁碟上讀寫索引,除了網路的開銷,還有一個重要原因是本地磁碟有PageCache, 而hdfs沒有(hdfs自身對物理block是有快取的,但在es機器上沒有相應的快取)。那是不是可以在HdfsDirctory上面加一層Cache來達到同樣的效果呢。
通過調研,發現solr上實現了一個BlockDirectory,它呼叫開源的CaffeineCache可以在普通的Directory上層加上Cache功能。我們結合ElasticBuild的場景對BlockDirectory做了少許定製:
- ElasticBuild是跑在blink上的,同一個Blink例項裡面會有多個shard,我們不單獨給每個shard申請一塊cache, 而是全域性使用一個靜態的cache,這樣可以有效地避免熱點資料導致的cache大小分配不合理。
- 只有從Hdfs上讀出來的資料進Cache, 從記憶體寫到hdfs的資料是不直接進cache的。原因有兩個:一是我們通過日誌發現,IndexWriter在merge的時候會高頻率地重複讀取一些有重疊的資料塊,而寫入的時候並沒有這個現象,所以可以多留些記憶體給讀資料用。另一方面,寫的檔案通常會比較大,可能一下子就把cache裡的內容重新整理一遍,導致cache命中率實然下降。
從而ElasticBuild使用的BlockDirectory如下圖所示:
經過測試,BlockDirectory的cache命中率達到90+%,大大提升了Hdfs上的索引讀寫效能,主要是Merge的效能得到大大改善。
NrtCachingDirectory
回過頭來想想,有了BlockDirectory之後,節省了很大一部分讀Hdfs的開銷,但是寫Hdfs還是有些消耗的,閱讀es程式碼後發現,InternalEngine在生成IndexWriter的config時指定了索引最終以CompoundFile形式存在:
CompoundFile的存在主要是為了減少索引檔案數目,避免開啟索引時檔案控制程式碼過多,所以es裡的索引生成過程其實是這樣的:
換句話說,中間產生的那一大堆只是臨時檔案,對最終結果沒有任何影響,只有最終的四種格式的檔案才是需要持久化的。那麼,這些檔案就不必從hdfs繞一圈了,直接在記憶體裡消化掉就可以了。
lucene-core裡提供了一個NRTCachingDirectory,它可以在其它Directory上層再封裝一個RAMDirectory, 實現將某些小檔案直接在記憶體裡消化掉,我們可以把它拿過來,修改一下關於 哪些檔案在記憶體處理,哪些檔案扔給其它Directory處理 的邏輯,從而達到只處理最終檔案的目的:
經70G資料的測試,單個shard的NRTCaching中,一直維持著30多個檔案直接在記憶體中處理,無需到Hdfs上繞一圈。這對效能的提升還是有些作用的,因為沒有NRTCaching的話,所有檔案需要寫到hdfs, 在flush時又將那些檔案讀回來,再合併成cfs,cfe,si等檔案。
自適應記憶體分配
以上分析瞭如果通過cache來提升hdfs的讀寫效能,現在例項的記憶體主要由以下幾個部分消耗:
- lucene的indexWriter需要RamBuffer
- BlockDirectory需要一段BlockCache
- NRTCachingDirectory裡的RAMDirectory需要一段RAM
由於在blink上的例項規格是不固定的,所以我們沒法直接寫死每個模組的記憶體需要多少,最好能做到自動適配,減少運維成本。經過一段測試,我們得出一些實驗結論:
- 將es的indices.memory.index_buffer_size調整到40%,這樣es在indexwriter記憶體達到系統記憶體的40%時,會觸發flush動作。
- 將IndexWriter的indexingBufferSize調整到堆上freeMem的40%, 這時IndexWriter在記憶體達到限制後也會觸發flush動作。
- 當IndexWriter記憶體調整到40%時,flush出來的索引大小預計會在20%左右,這些檔案會流轉到RAMDirectory中,由於RAMDirectory中的檔案大小是預估的,有時候相差還是比較大的,為了避免記憶體用超了,給RAMDirectory一些空間,所以NRTCachingDirectory的記憶體分配也給個40%, 也就是最多跟IndexWriter裡一樣。
- 剩下的20%給BlockDirectory, 實驗下來看,BlockDirectory分配這麼多,已經足夠了。
這樣,我們就不需要為每種規格的例項單獨配置各個模組的記憶體比例了。
Meta資訊同步
上面說到的都是indexWriter索引如果同步到hdfs上,除此之外,還有些額外的資料需要跟著es一起同步到hdfs上。
shard_state
Shard的_state檔案記錄了shard的primary,indexUUID等資訊。需要在es更新本地shard的_state時同步copy到hdfs上去。
index_state
Index的_state檔案裡記錄了一些index的Setting,shard數等關鍵資訊,需要在es更新本地index的_state時同步copy到hdfs上去。
transLog
translog比較特別,它的檔案比較多,而且同步點也比較多,不能每次整個目錄同步到hdfs上,效能上扛不住。一路上改了很多個同步點後發現行不通,感謝 @志宸 老師的建議,translog因為在elasticBuild中不起作用,恢復點都是通過blink的checkpoint實現的,而且elasticBuild只要保證atleast once就可以了,所以直接不同步translog, 在elasticbuild failover或者暫停繼續後,新建一個空的translog目錄,保證程式能起來就可以了。
展望
我們規劃了一些優化點由於時間原因沒有在一期完成,後面可以考慮調研起來。
shard級別併發
上面做了很多的優化,但是有一個限制,就是同一個shard只能落到單個sink上處理,不能夠做到shard級別的併發build.
其實將shard拆成多個任務只要解決以下兩個問題即可:
多個shard的segment合併及snapshot功能的外圍實現
各個子shard單獨build索引後,需發在外層合併成同一個可以被load的索引,簡單看了看,其實只需要改一下cfe,cfs,si的generation號,併合並一下segmentInfo資訊到segments_0檔案裡就可以了。
合併的資訊包括:
- Translog.TRANSLOG_GENERATION_KEY
- Translog.TRANSLOG_UUID_KEY
- SequenceNumbers.LOCAL_CHECKPOINT_KEY
- Engine.SYNC_COMMIT_ID
- SequenceNumbers.MAX_SEQ_NO
- MAX_UNSAFE_AUTO_ID_TIMESTAMP_COMMIT_ID
- HISTORY_UUID_KEY
這些資訊都是可以合併的。
另外,我們在hdfs上合併好索引後,還需要在外層合併成一個snapshot上傳到oss, 供線上es來載入,這也是可以實現的,唯一的問題是,在外層上傳的話,時間比較久,需要後面直接同步到oss的功能支援下。
全域性seqNo分配
elasticsearch 6.x還引入了一個_seqNo的概念,這個_seqNo是寫到doc裡的一個預設欄位,由於它是shard粒度嚴格自增的,所以需要在多個子shard外層分配這個_seqNo傳給各個子shard.
直接同步到oss
當前版本,是借住了HdfsDirectory將索引寫在hdfs上,然後最終在hdfs上生成了snapshot上傳到oss. 這個上傳還是有些代價的,所以最好能實現一個OssDirectory, 理論上可以直接替換HdfsDirectory, 省去一次上傳的代價,而且也可以在外層高效地做segments合併了.
致謝
從對Elasticsearch和Lucene的零認知到專案功能點的完成,少不了各路大神的協助。感謝 @洪震老大、@崑崙老大在Lucene及blink上的指導,感謝 @萬喜團隊的兄弟們一起探討方案,解決疑各種難雜症,後面二期需求再一起合作~
相關文章
- Elasticsearch(二)--叢集原理及優化Elasticsearch優化
- BloomFilter 原理,實現及優化OOMFilter優化
- 騰訊雲Elasticsearch叢集規劃及效能優化實踐Elasticsearch優化
- synchronized實現原理及鎖優化synchronized優化
- consistent hash 原理,優化及實現優化
- 林意群:eBay HDFS架構的演進優化實踐架構優化
- HDFS EC在B站的實踐
- 在 Fedora 上優化 bash 或 zsh優化
- Elasticsearch調優實踐Elasticsearch
- Warshall‘s algorithm 演算法的實現及優化(修改版)Go演算法優化
- Elasticsearch在Laravel中的實踐ElasticsearchLaravel
- 讓Elasticsearch飛起來!——效能優化實踐乾貨Elasticsearch優化
- 讓 Elasticsearch 飛起來!——效能優化實踐乾貨Elasticsearch優化
- Synchronized的實現原理以及優化synchronized優化
- Python中的單例模式的幾種實現方式的及優化Python單例模式優化
- 淺聊前端依賴管理及優化(上)前端優化
- Hive使用Calcite CBO優化流程及SQL優化實戰Hive優化SQL
- 通過 ProxySQL 在 TiDB 上實現 SQL 的規則化路由SQLTiDB路由
- HDFS3.2升級在滴滴的實踐S3
- search-guard 在 Elasticsearch 2.3 上的運用Elasticsearch
- Elasticsearch資料庫優化實戰:讓你的ES飛起來Elasticsearch資料庫優化
- WebRTC 架構優化及實踐Web架構優化
- Java 效能優化技巧及實戰Java優化
- 使用Elasticsearch的動態索引和索引優化Elasticsearch索引優化
- Fluwx:微信SDK在Flutter上的實現Flutter
- 在 Redis 上實現的分散式鎖Redis分散式
- random_device在windows上的實現randomdevWindows
- 圖解氣泡排序及演算法優化(Java實現)圖解排序演算法優化Java
- 圖解選擇排序及演算法優化(Java實現)圖解排序演算法優化Java
- ElasticSearch(五) Elasticsearch-jdbc實現MySQL同步到ElasticSearchElasticsearchJDBCMySql
- Elasticsearch效能優化引數註解Elasticsearch優化
- Elasticsearch實現Mysql的Like效果ElasticsearchMySql
- elasticsearch的實現全文檢索Elasticsearch
- 瀏覽器工作原理及web 效能優化(上)瀏覽器Web優化
- Web上傳檔案的原理及實現Web
- MySQL 上億大表優化實踐MySql優化
- ElasticSearch(七) Elasticsearch在Centos下搭建視覺化服務ElasticsearchCentOS視覺化
- 優化三維空間定位法及C語言快捷實現優化C語言