ElasticSearch的基本概念和集群分布式底層實(shí)現(xiàn)
深度分頁(yè)引發(fā)的機(jī)器性能問(wèn)題
最近碰到一個(gè)ElasticSearch深度分頁(yè)搜索,導(dǎo)致cpu占用過(guò)高問(wèn)題,通過(guò)查閱ElasticSearch: 權(quán)威指南,了解到了深度分頁(yè)為何會(huì)引起機(jī)器資源占用:
在集群系統(tǒng)中深度分頁(yè)為了理解為什么深度分頁(yè)是有問(wèn)題的,讓我們假設(shè)在一個(gè)有5個(gè)主分片的索引中搜索。當(dāng)我們請(qǐng)求結(jié)果的第一頁(yè)(結(jié)果1到10)時(shí),每個(gè)分片產(chǎn)生自己最頂端10個(gè)結(jié)果然后返回它們給請(qǐng)求節(jié)點(diǎn)(requesting node),它再排序這所有的50個(gè)結(jié)果以選出頂端的10個(gè)結(jié)果。現(xiàn)在假設(shè)我們請(qǐng)求第1000頁(yè)——結(jié)果10001到10010。工作方式都相同,不同的是每個(gè)分片都必須產(chǎn)生頂端的10010個(gè)結(jié)果。然后請(qǐng)求節(jié)點(diǎn)排序這50050個(gè)結(jié)果并丟棄50040個(gè)!你可以看到在分布式系統(tǒng)中,排序結(jié)果的資源和時(shí)間花費(fèi)隨著分頁(yè)的深入而成倍增長(zhǎng)。這也是為什么網(wǎng)絡(luò)搜索引擎中任何語(yǔ)句不能返回多于1000個(gè)結(jié)果的原因。
理解以上那段文字,有必要了解ElasticSearch集群以及在集群中是查詢的底層原理,本文試圖通過(guò)總結(jié)ElasticSearch基本概念和底層原理,加深自身理解,同時(shí)希望對(duì)使用者有所幫助,避免不必要的踩坑。
基本概念
索引(index)
“索引” 這個(gè)詞在 ElasticSearch 語(yǔ)境中包含多重意思: 索引(名詞): 類比傳統(tǒng)的關(guān)系型數(shù)據(jù)庫(kù)領(lǐng)域來(lái)說(shuō),索引相當(dāng)于SQL中的一個(gè)數(shù)據(jù)庫(kù)。索引由其名稱(必須為全小寫(xiě)字符)進(jìn)行標(biāo)識(shí),并通過(guò)引用此名稱完成文檔的創(chuàng)建、搜索、更新及刪除操作。
索引(動(dòng)詞): 索引一個(gè)文檔就是存儲(chǔ)一個(gè)文檔到一個(gè)索引(名詞)中以便它可以被檢索和查詢到。這非常類似于SQL語(yǔ)句中的 INSERT關(guān)鍵詞,除了文檔已存在時(shí)新文檔會(huì)替換舊文檔情況之外。
倒排索引: 關(guān)系型數(shù)據(jù)庫(kù)通過(guò)增加一個(gè)“索引”比如一個(gè)B樹(shù)(B-tree)索引到指定的列上,以便提升數(shù)據(jù)檢索速度。ElasticSearch 和 Lucene 使用了一個(gè)叫做 “倒排索引” 的結(jié)構(gòu)來(lái)達(dá)到相同的目的。
舉個(gè)例子,文檔和詞條之間的關(guān)系如下圖:
圖1:文檔和詞條的關(guān)系
字段值被分析之后,存儲(chǔ)在倒排索引中,倒排索引存儲(chǔ)的是分詞(Term)和文檔(Doc)之間的關(guān)系,簡(jiǎn)化版的倒排索引如下圖:
圖2:倒排索引
類型(Type)
類型是索引內(nèi)部的邏輯分區(qū)(category/partition),然而其意義完全取決于用戶需求。因此,一個(gè)索引內(nèi)部可定義一個(gè)或多個(gè)類型(type)。一般來(lái)說(shuō),類型就是為那些擁有相同的域的文檔做的預(yù)定義。類比傳統(tǒng)的關(guān)系型數(shù)據(jù)庫(kù)領(lǐng)域來(lái)說(shuō),類型相當(dāng)于“表”。
文檔(Document)
文檔類似于一行完整的數(shù)據(jù),在ElasticSearch里面文檔是基于JSON格式進(jìn)行表示的,文檔是索引和搜索的原子單位,它是包含了一個(gè)或多個(gè)域(Field)的容器。每個(gè)文檔可以存儲(chǔ)不同的域集,但同一類型(Type)下的文檔至少應(yīng)該有某種程度上的相似之處。
節(jié)點(diǎn)(Node)
一個(gè)運(yùn)行中的 ElasticSearch實(shí)例稱為一個(gè)節(jié)點(diǎn),而集群是由一個(gè)或者多個(gè)擁有相同cluster.name配置的節(jié)點(diǎn)組成, 它們共同承擔(dān)數(shù)據(jù)和負(fù)載的壓力。
ES集群中的節(jié)點(diǎn)有三種不同的類型:
- 主節(jié)點(diǎn):負(fù)責(zé)管理集群范圍內(nèi)的所有變更,例如增加、刪除索引,或者增加、刪除節(jié)點(diǎn)等。 主節(jié)點(diǎn)并不需要涉及到文檔級(jí)別的變更和搜索等操作。可以通過(guò)屬性node.master進(jìn)行設(shè)置。
- 數(shù)據(jù)節(jié)點(diǎn):存儲(chǔ)數(shù)據(jù)和其對(duì)應(yīng)的倒排索引。默認(rèn)每一個(gè)節(jié)點(diǎn)都是數(shù)據(jù)節(jié)點(diǎn)(包括主節(jié)點(diǎn)),可以通過(guò)node.data屬性進(jìn)行設(shè)置。
- 協(xié)調(diào)節(jié)點(diǎn):如果node.master和node.data屬性均為false,則此節(jié)點(diǎn)稱為協(xié)調(diào)節(jié)點(diǎn),用來(lái)響應(yīng)客戶請(qǐng)求,均衡每個(gè)節(jié)點(diǎn)的負(fù)載。
分片(Shard)
一個(gè)索引中的數(shù)據(jù)保存在多個(gè)分片中,相當(dāng)于水平分表。一個(gè)分片便是一個(gè)Lucene 的實(shí)例,它本身就是一個(gè)完整的搜索引擎。我們的文檔被存儲(chǔ)和索引到分片內(nèi),但是應(yīng)用程序是直接與索引而不是與分片進(jìn)行交互。
一個(gè)分片可以是主分片或者副本分片。 索引內(nèi)任意一個(gè)文檔都?xì)w屬于一個(gè)主分片,所以主分片的數(shù)目決定著索引能夠保存的最大數(shù)據(jù)量。一個(gè)副本分片只是一個(gè)主分片的拷貝。 副本分片作為硬件故障時(shí)保護(hù)數(shù)據(jù)不丟失的冗余備份,并為搜索和返回文檔等讀操作提供服務(wù)。
集群分布式底層實(shí)現(xiàn)
以上我們對(duì)ElasticSearch的基本概念有了一個(gè)初步認(rèn)識(shí),接下來(lái)我們深入這些內(nèi)部細(xì)節(jié)來(lái)幫助你更好的理解數(shù)據(jù)是如何在分布式系統(tǒng)中存儲(chǔ)和查詢的。
ES實(shí)際上就是利用分片來(lái)實(shí)現(xiàn)分布式。分片是數(shù)據(jù)的容器,文檔保存在分片內(nèi),分片又被分配到集群內(nèi)的各個(gè)節(jié)點(diǎn)里。 當(dāng)你的集群規(guī)模擴(kuò)大或者縮小時(shí), ES會(huì)自動(dòng)的在各節(jié)點(diǎn)中遷移分片,使得數(shù)據(jù)仍然均勻分布在集群里。
在索引建立的時(shí)候就已經(jīng)確定了主分片數(shù),但是副本分片數(shù)可以隨時(shí)修改。默認(rèn)情況下,一個(gè)索引會(huì)有5個(gè)主分片,而其副本可以有任意數(shù)量。
主分片和副本分片的狀態(tài)決定了集群的健康狀態(tài)。每一個(gè)節(jié)點(diǎn)上都只會(huì)保存主分片或者其對(duì)應(yīng)的一個(gè)副本分片,相同的副本分片不會(huì)存在于同一個(gè)節(jié)點(diǎn)中。如果集群中只有一個(gè)節(jié)點(diǎn),則副本分片將不會(huì)被分配,此時(shí)集群健康狀態(tài)為yellow,存在丟失數(shù)據(jù)的風(fēng)險(xiǎn)。
分布式文檔CRUD
索引新文檔(Create)
當(dāng)用戶向一個(gè)節(jié)點(diǎn)提交了一個(gè)索引新文檔的請(qǐng)求,節(jié)點(diǎn)會(huì)計(jì)算新文檔應(yīng)該加入到哪個(gè)分片(shard)中。每個(gè)節(jié)點(diǎn)都存儲(chǔ)有每個(gè)分片存儲(chǔ)在哪個(gè)節(jié)點(diǎn)的信息,因此協(xié)調(diào)節(jié)點(diǎn)會(huì)將請(qǐng)求發(fā)送給對(duì)應(yīng)的節(jié)點(diǎn)。注意這個(gè)請(qǐng)求會(huì)發(fā)送給主分片,等主分片完成索引,會(huì)并行將請(qǐng)求發(fā)送到其所有副本分片,保證每個(gè)分片都持有最新數(shù)據(jù)。
每次寫(xiě)入新文檔時(shí),都會(huì)先寫(xiě)入內(nèi)存中,并將這一操作寫(xiě)入一個(gè)translog文件(transaction log)中,此時(shí)如果執(zhí)行搜索操作,這個(gè)新文檔還不能被索引到。
圖3:新文檔被寫(xiě)入內(nèi)存,操作被寫(xiě)入translog
ES會(huì)每隔1秒時(shí)間(這個(gè)時(shí)間可以修改)進(jìn)行一次刷新操作(refresh),此時(shí)在這1秒時(shí)間內(nèi)寫(xiě)入內(nèi)存的新文檔都會(huì)被寫(xiě)入一個(gè)文件系統(tǒng)緩存(filesystem cache)中,并構(gòu)成一個(gè)分段(segment)。此時(shí)這個(gè)segment里的文檔可以被搜索到,但是尚未寫(xiě)入硬盤(pán),即如果此時(shí)發(fā)生斷電,則這些文檔可能會(huì)丟失。
圖4:在執(zhí)行刷新后清空內(nèi)存,新文檔寫(xiě)入文件系統(tǒng)緩存
不斷有新的文檔寫(xiě)入,則這一過(guò)程將不斷重復(fù)執(zhí)行。每隔一秒將生成一個(gè)新的segment,而translog文件將越來(lái)越大。
圖5:translog不斷加入新文檔記錄
每隔30分鐘或者translog文件變得很大,則執(zhí)行一次fsync操作。此時(shí)所有在文件系統(tǒng)緩存中的segment將被寫(xiě)入磁盤(pán),而translog將被刪除(此后會(huì)生成新的translog)。
圖6:執(zhí)行fsync后segment寫(xiě)入磁盤(pán),清空內(nèi)存和translog
由上面的流程可以看出,在兩次fsync操作之間,存儲(chǔ)在內(nèi)存和文件系統(tǒng)緩存中的文檔是不安全的,一旦出現(xiàn)斷電這些文檔就會(huì)丟失。所以ES引入了translog來(lái)記錄兩次fsync之間所有的操作,這樣機(jī)器從故障中恢復(fù)或者重新啟動(dòng),ES便可以根據(jù)translog進(jìn)行還原。
當(dāng)然,translog本身也是文件,存在于內(nèi)存當(dāng)中,如果發(fā)生斷電一樣會(huì)丟失。因此,ES會(huì)在每隔5秒時(shí)間或是一次寫(xiě)入請(qǐng)求完成后將translog寫(xiě)入磁盤(pán)??梢哉J(rèn)為一個(gè)對(duì)文檔的操作一旦寫(xiě)入磁盤(pán)便是安全的可以復(fù)原的,因此只有在當(dāng)前操作記錄被寫(xiě)入磁盤(pán),ES才會(huì)將操作成功的結(jié)果返回發(fā)送此操作請(qǐng)求的客戶端。
此外,由于每一秒就會(huì)生成一個(gè)新的segment,很快將會(huì)有大量的segment。對(duì)于一個(gè)分片進(jìn)行查詢請(qǐng)求,將會(huì)輪流查詢分片中的所有segment,這將降低搜索的效率。因此ES會(huì)自動(dòng)啟動(dòng)合并segment的工作,將一部分相似大小的segment合并成一個(gè)新的大segment。合并的過(guò)程實(shí)際上是創(chuàng)建了一個(gè)新的segment,當(dāng)新segment被寫(xiě)入磁盤(pán),所有被合并的舊segment被清除。
圖7:合并segment
圖8:合并完成后刪除舊segment,新segment可供搜索
更新(Update)和刪除(Delete)文檔
ES的索引是不能修改的,因此更新和刪除操作并不是直接在原索引上直接執(zhí)行。
每一個(gè)磁盤(pán)上的segment都會(huì)維護(hù)一個(gè)del文件,用來(lái)記錄被刪除的文件。每當(dāng)用戶提出一個(gè)刪除請(qǐng)求,文檔并沒(méi)有被真正刪除,索引也沒(méi)有發(fā)生改變,而是在del文件中標(biāo)記該文檔已被刪除。因此,被刪除的文檔依然可以被檢索到,只是在返回檢索結(jié)果時(shí)被過(guò)濾掉了。每次在啟動(dòng)segment合并工作時(shí),那些被標(biāo)記為刪除的文檔才會(huì)被真正刪除。
更新文檔會(huì)首先查找原文檔,得到該文檔的版本號(hào)。然后將修改后的文檔寫(xiě)入內(nèi)存,此過(guò)程與寫(xiě)入一個(gè)新文檔相同。同時(shí),舊版本文檔被標(biāo)記為刪除,同理,該文檔可以被搜索到,只是最終被過(guò)濾掉。
讀操作(Read):查詢過(guò)程
查詢的過(guò)程大體上分為查詢(query)和取回(fetch)兩個(gè)階段。這個(gè)節(jié)點(diǎn)的任務(wù)是廣播查詢請(qǐng)求到所有相關(guān)分片,并將它們的響應(yīng)整合成全局排序后的結(jié)果集合,這個(gè)結(jié)果集合會(huì)返回給客戶端。
查詢階段
當(dāng)一個(gè)節(jié)點(diǎn)接收到一個(gè)搜索請(qǐng)求,則這個(gè)節(jié)點(diǎn)就變成了協(xié)調(diào)節(jié)點(diǎn)。
查詢過(guò)程分布式搜索
圖9:查詢過(guò)程分布式搜索
第一步是廣播請(qǐng)求到索引中每一個(gè)節(jié)點(diǎn)的分片拷貝。 查詢請(qǐng)求可以被某個(gè)主分片或某個(gè)副本分片處理,協(xié)調(diào)節(jié)點(diǎn)將在之后的請(qǐng)求中輪詢所有的分片拷貝來(lái)分?jǐn)傌?fù)載。
每個(gè)分片將會(huì)在本地構(gòu)建一個(gè)優(yōu)先級(jí)隊(duì)列。如果客戶端要求返回結(jié)果排序中從第from名開(kāi)始的數(shù)量為size的結(jié)果集,則每個(gè)節(jié)點(diǎn)都需要生成一個(gè)from+size大小的結(jié)果集,因此優(yōu)先級(jí)隊(duì)列的大小也是from+size。分片僅會(huì)返回一個(gè)輕量級(jí)的結(jié)果給協(xié)調(diào)節(jié)點(diǎn),包含結(jié)果集中的每一個(gè)文檔的ID和進(jìn)行排序所需要的信息。
協(xié)調(diào)節(jié)點(diǎn)會(huì)將所有分片的結(jié)果匯總,并進(jìn)行全局排序,得到最終的查詢排序結(jié)果。此時(shí)查詢階段結(jié)束。
取回階段
查詢過(guò)程得到的是一個(gè)排序結(jié)果,標(biāo)記出哪些文檔是符合搜索要求的,此時(shí)仍然需要獲取這些文檔返回客戶端。
協(xié)調(diào)節(jié)點(diǎn)會(huì)確定實(shí)際需要返回的文檔,并向含有該文檔的分片發(fā)送get請(qǐng)求;分片獲取文檔返回給協(xié)調(diào)節(jié)點(diǎn);協(xié)調(diào)節(jié)點(diǎn)將結(jié)果返回給客戶端
作者:張勇
http://tech.dianwoda.com/