ElasticSearch 近實時搜索的實現

  1. 近實時搜索

1.1 實時與近實時

實時搜索(Real-time Search)很好理解,對於一個數據庫系統,執行插入以後立刻就能搜索到剛剛插入到數據。而近實時(Near Real-time),所謂 “近” 也就是說比實時要慢一點點。

1.2 近實時的挑戰

對於一個單機系統來說,這也並不容易實現,因爲還要保證數據的持久化,還要利用緩存等技術加快數據的訪問(注:這裏不討論內存計算系統)。對於 ElasticSearch 這樣一個分佈式系統,保證持久化的同時,還要初始化好用於全文檢索的內部數據結構,做到近實時的難度可想而知。而這就是 ElasticSearch 大獲成功的地方,也正是本文所要學習的主題:ElasticSearch 是如何解決這些實現近實時搜索的難題的。

2.ElasticSearch 的實現

2.1 不可變的數據結構

有經驗的程序員一定知道,在做併發編程時,控制可變數據的併發訪問是個難題。古往今來,各種粗細粒度的鎖,信號量,Actor 模型等概念層出不窮。而另一流派函數式編程更爲徹底,尤其是純函數式比如 Haskell,用不可變數據來徹底解決這個問題。

在 ElasticSearch 這樣主要服務全文檢索的系統中,Inverted Index 是核心數據結構。這裏簡單說一句,Inverted Index 本質上一組 document 中 term 的各種統計信息,比如最重要的詞頻,以及其他許多統計信息,比如文檔長度,詞序等等。要做到近實時搜索,就要保證新數據能快速構建,已有數據能被高速訪問。解決問題的關鍵就在於 Inverted Index 的不可變性,這也是 ElasticSearch 底層依賴的高性能 Lucene 的根本奧祕。

2.2 從不可變到可變

所以當用戶向 ElasticSearch 中的數據庫插入一組 document 後,底層 Lucene 構建出一個不可變的 Inverted Index。可我們知道,一個數據庫不可能是靜態的,當用戶再次插入新數據時,Lucene 該怎樣處理呢?答案就是增量保存和邏輯標記。

所謂增量保存就是爲新數據構建一個新的不可變的 Inverted Index,當執行搜索時,要合併每個 Inverted Index 中的統計信息得到最終結果。保存新數據的問題解決了,而邏輯標記就是解決更新和刪除的。Lucene 爲每個 Inverted Index 都額外維護一個 del 數據結構,當執行刪除時,只需在 del 中標記,這樣最終結果就會排出掉刪除掉 document。同理,更新時也是給老數據做標記,新 document 會保存在新的 Inverted Index 中,最終結果會使用最新版本數據的統計信息。在 Lucene 中,每個 Inverted Index 叫做 Segment,而管理這些 Segment 的叫做 Index。

ElasticSearch 中一個數據庫被稱爲 Index,每個 Index 可以在創建時指定要劃分爲幾份,每一份叫做 Shard。Shard 會被 ElasticSearch 分配到不同結點,運行中還會根據壓力做 Rebalance。這個 Shard 其實就是 Lucene 中的 Index。由於不同層級上名字的重複,初學時很容易混淆。

這種思想其實並非獨創,在其他一些高級數據結構中也能找到它的影子。如果沒記錯的話,一個經典的例子就是 LSM 樹:https://en.m.wikipedia.org/wiki/Log-structured_merge-tree。

2.3 分佈式數據存儲

對於分佈式的數據存儲,ElasticSearch 採取了經典的做法,對數據進行分片和路由,這裏每個分片 Shard 就是一個 Lucene 數據庫 Index。對於有副本 replica 的 Shard,ElasticSearch 操作完 primary 後,再去同步到 replica。

2.4 挑戰磁盤 I/O

現在我們已經可以高效地維護全文檢索的數據結構,也遵循經典做法解決了分佈式數據存儲。可就像前面提到的,還有個挑戰就是磁盤讀寫的巨大開銷。Lucene 的做法是,每個 Segment 在文件系統 Cache 中構建起來就可以被訪問,同步到磁盤的 fsync 之後纔會執行。Lucene 的 Index 內部的 Commit Point 會記住哪些 Segment 還未同步。ElasticSearch 默認每隔 1 秒會用 Buffer 中的 document 新建一個 Segment,這個操作叫做 refresh。正因爲這 1 秒鐘的間隔,ElasticSearch 支持的是近實時而非實時。

一個很自然的問題就是每秒鐘都會新建一個 Segment,那 Lucene Index 中的 Segment 個數豈不是很容易就爆炸了。每個 Segment 都是一個物理文件,操作系統中打開文件的句柄個數是有限的,而且即便不考慮上限,過多 Segment 也會拖慢搜索,因爲前面講過一次搜索的最終結果是要合併所有 Segment 中的統計信息的。

ElasticSearch 的做法是維護一個後臺線程去做 Merge,Merge 的過程中不僅將多個小 Segment 合併成大的,同時還會排除掉刪除或修改的文件的老版本,最終修改 Commit Point 排除掉老的 Segment,這樣那些 “垃圾”document 就徹底被刪除了。得益於 Segment 的不可變性,後臺進程 Merge 時並不會影響數據插入和搜索的性能。

2.5 保證數據不丟失

一個可以預料到的問題就是,如果當前結點上的 ElasticSearch 進程意外中止,那 Buffer 中等待處理的 document 和未同步到磁盤的 Segment 中的數據都會丟失。爲了避免這一點,ElasticSearch 引入了傳統數據庫中所謂的 Write-Ahead Log(WAL)日誌,ElasticSearch 爲其起名爲 translog。每次插入 Buffer 時,都會同時寫入 translog。下面的圖示清晰地展示 ElasticSearch 是如何與 Lucene 配合的。

當創建新 Segment 時,Buffer 清空,但 translog 會一直保留到 Segment 同步到磁盤纔會清空。** 所以當 ElasticSearch 重啓時,先根據 Commit Point 將所有之前已經 commit 到磁盤的 Segment 恢復到 Cache,然後再重放(replay)translog 中的所有操作。默認每 30 分鐘或者 translog 很大時,ElasticSearch 做一次 full commit,即 flush 操作。

繼續刨根問底,translog 保證了 Buffer 和 Segment 的安全,誰來保證它的安全呢?默認情況下,translog 每 5 秒鐘會同步到磁盤,也就是說我們至多會丟失 5 秒到數據。因爲 translog 只是原始的請求 document,所以這裏的寫磁盤開銷是遠小於 Segment 的一次 commit 的。

  1. 題外話:如何深入學習 ElasticSearch

以本文爲例,談一談如何學習 ElasticSearch。在有了一些分佈式系統和開發經驗後,像本文 2.3 和 2.5 節是完全可以跳過的。前者是分佈式系統的通用做法,而後者則早已存在於傳統數據庫中。要掌握 ElasticSearch,基本用法和系統命令是一方面,而設計中的精華往往在前文 2.1 和 2.2 中。光理解了設計還不行,就像前面說過的,思想可能流傳已久,但做出來東西的質量則可能千差萬別。“天下大事,必做於細”,實現中的精髓只能在源代碼中體會。

其實這種方法在另一篇文章裏也提到過,就是學一門編程語言時也是要抓住它的精髓,而不是每門語言都花很多時間去學基本語法,而沒有精力去掌握精華,最終迷失了。在此再次強調一下,自己也引以爲戒。

如喜歡本文,請點擊右上角,把文章分享到朋友圈
如有想了解學習的技術點,請留言給若飛安排分享

作者:cdai

來源:blog.csdn.net/dc_726/article/details/94252850

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/SrK2gZn4qKMA0xUyaXZwTA