概要
本篇我們來看看shard內部的一些操作原理,了解一下人家是怎么玩的。
倒排索引
倒排索引的結構,是非常適合用來做搜索的,Elasticsearch會為索引的每個index為analyzed的字段建立倒排索引。
基本結構
倒排索引包含以下幾個部分:
- 某個關鍵詞的doc list
- 某個關鍵詞的所有doc的數量IDF(inverse document frequency)
- 某個關鍵詞在每個doc中出現的次數:TF(term frequency)
- 某個關鍵詞在這個doc中的次序
- 每個doc的長度:length norm
- 某個關鍵詞的所有doc的平均長度
記錄這些信息,就是為了方便搜索的效率和_score分值的計算。
不可變性
倒排索引寫入磁盤后就是不可變的,這樣有幾個好處:
- 不需要鎖,如果不更新索引,不用擔心鎖的問題,可以支持較高的並發能力
- 如果cache內存足夠,不更新索引的話,索引可以一直保存在os cache中,可以提升IO性能。
- 如果數據不變,filter cache會一直駐留在內存。
- 索引數據可以壓縮,節省cpu和io開銷。
doc底層原理
前面提到倒排索引是基於不可變模式設計的,但實際Elasticsearch源源不斷地有新數據進來,那光是建立、刪除倒排索引,豈不是非常忙?
如果真是不停地建立,刪除倒排索引,那ES壓力也太大了,肯定不是這么實現的。ES通過增加新的補充索引來接收新的文檔和修改的文檔,而不是直接用刪除重建的方式重寫整個索引。
doc寫入
整個寫入過程如下圖所示:

- 新文檔先寫入內存索引緩存
- 當間隔一定時間(1秒),將緩存的數據進行提交,這個過程會創建一個Commit Point,Commit Point包含index segment的信息。
- 緩存的數據寫入新的index segment。
- index segment的數據先寫入os-cache中
- 等待操作系統將os-cache的數據強制刷新到磁盤中
- 寫入磁盤完成后,新的index segment被打開,此時segment內的文檔可以被搜索到。
- 同時buffer的數據被清空,等待下一次新的文檔寫入。
index segment翻譯過來叫"段",每秒會創建一個,ES把這個1秒內收到的、需要處理的文檔都放在這個段里,可以把段認為是倒排索引的一個子集。
索引、分片、段的關系如下:
索引包含多個分片,每個分片是一個Lucene索引實例,一個分片下面有多個段。如果把分片看作是一個獨立的倒排索引結構,那么這個倒排索引是由多個段文件的集合。
三者之間是包含關系:索引包含多個分片,分片包含多個段。
doc刪除和更新
當文檔被刪除時,Commit Point會把信息記錄在.del文件中,在.del文件中會標識哪些文檔是有deleted標記的,但該文檔還是存在於原先的index segment文件里,同樣能夠被檢索到,只是在最終結果處理時,標記為deleted的文檔被會過濾掉。
更新也是類似的操作,更新會把舊版本的文檔標記為deleted,新的文檔會存儲在新的index segment中。
近實時搜索
上面的流程細節的童鞋可以會發現,每次都需要fsync磁盤,數據才是可搜索的,那IO壓力將特別大,耗費時間比較長,並且執行周期由操作系統控制,從一個新文檔寫入到可以被搜索,超過1分鍾那是常有的事。
所以Elasticsearch對此做了一個改進:
index segment信息寫入到os-cache中,即完成上面的第4步,該segment內的文檔信息就可以被搜索到了。fsync操作就不立即執行了,
os-cache的寫入代價比較低,最耗時的fsync操作交由操作系統調度執行。
上述的index segment寫入到os-cache,並打開搜索的過程,叫做refresh,默認是每隔1秒refresh一次所以,es是近實時的,數據寫入到可以被搜索,默認是1秒。
refresh的時間也可以設置,比如我們一些日志系統,數據量特別大,但實時性要求不高,我們為了優化資源分配,就可以把refresh設置得大一些:
PUT /music
{
"settings": {
"refresh_interval": "30s"
}
}
此參數需要在創建索引時使用,要注意一下的是除非有充分的依據,才會對refresh進行設置,一般使用默認的即可。
translog機制
上述的寫入流程當中,如果fsync到磁盤的操作沒執行完成,服務器斷電宕機了,可能會導致Elasticsearch數據丟失。Elasticsearch也設計了translog機制,跟關系型數據庫的事務日志機制非常像,整個寫入過程將變成這樣:

- 新文檔寫入內存buffer的同時,也寫一份到translog當中。
- 內存buffer的數據每隔1秒寫入到index segment,並寫入os-cache,完成refresh操作。
- 內存buffer被清空,但translog一直累加。
- 每隔5秒translog信息fsync到磁盤上。
- 默認每30分鍾或translog累積到512MB時,執行全量commit操作,os-cache中的segment信息和translog信息fsync到磁盤中,持久化完成。
- 生成新的translog,舊的translog歸檔(6.x版本translog做歸檔操作,不刪除)。
flush API
這個執行一個提交並且歸檔translog的行為稱作一次flush。分片每30分鍾被自動刷新(flush),或者在 translog 太大的時候(默認512MB)也會刷新,當然也可以手動觸發flush的執行,如下請求:
POST /music/_flush
但任其自動flush就夠了。如果重啟節點前擔心會對索引造成影響,可以手動flush一下。畢竟節點重啟后需要從translog里恢復數據,translog越小,恢復就越快。
durability同步和異步
translog寫磁盤行為主要有兩種,是由index.translog.durability配置項決定的:
- request:同步寫磁盤,每次寫請求完成之后立即執行(新增、刪除、更新文檔),以及primary shard和replica shard同步都會觸發,數據安全有保障,不丟失,但會帶來一些性能損失。如果是bulk數據導入,每個文檔平攤下來的損失是比較小的。
- async:異步寫磁盤,默認5秒fsync一次,如果有宕機事件,可能會丟失幾秒的數據,適用於允許偶爾有數據丟失的場景,如日志系統。
如果系統不接受數據丟失,用translog同步方式,示例設置:
# 異步方式
PUT /music_new
{
"settings": {
"index.translog.durability": "async",
"index.translog.sync_interval": "5s"
}
}
# 同步方式
PUT /music_new
{
"settings": {
"index.translog.durability": "request"
}
}
segment合並
Elasticsearch針對活躍的索引,每秒都會生成一個新的index segment,這些segment最終會以文件的形式存儲在磁盤里,如果不對其進行處理,那么索引運用一段時間后,會有特別多的文件,零碎的文件太多了,也不是什么好事情,更耗費更多的文件資源,句柄等,搜索過程也會變慢。
合並過程
Elasticsearch會在后台對segment進行合並,減少文件的數量,同時,標記為deleted的文檔在合並時會被丟棄(delete請求只是將文檔標記為deleted狀態,真正的物理刪除是在段合並的過程中),合並過程不需要人工干預,讓Elasticsearch自行完成即可。

兩個已經提交的段和一個未提交的段合並成為一個大的段文件
合並時會挑一些大小接近的段,合並到更大的段中,段合並過程不阻塞索引和搜索。

合並完成后,新的更大的段flush到磁盤中,並完成refresh操作,老的段被刪除掉。
optimize API
optimize命令可以強制合並API,並指定最終段的數量,如下命令:
POST /music_new/optimize
{
"max_num_segments": 1
}
指定segment最大數量為1,表示該索引最終只有一個segment文件。
適用場景
- 正常活躍的、經常有更新的索引不建議使用
- 日志類的索引,對老數據進行優化時,可以將每個分片的段進行合並
使用建議
- 一般不需要人工干預合並過程
- optimize操作會消耗大量的IO資源,使用要慎重考慮
小結
本篇主要介紹shard內部的原理,包含寫入、更新刪除,translog機制,segment合並等,了解數據庫的童鞋對translog機制應該非常熟悉,原理上大同小異,僅作拋磚引玉,謝謝。
