Elasticsearch是最近兩年異軍突起的一個兼有搜索引擎和NoSQL數據庫功能的開源系統,基於Java/Lucene構建。最近研究了一下,感受 Elasticsearch 的架構以及其開源的生態構建都有許多可借鑑之處,因此整理成文章分享下。本文的代碼以及架構分析主要基於 Elasticsearch 2.X 最新穩定版。html
Elasticsearch 看名字就能大概瞭解下它是一個彈性的搜索引擎。首先彈性隱含的意思是分佈式,單機系統是無法彈起來的,而後加上靈活的伸縮機制,就是這裏的 Elastic 包含的意思。它的搜索存儲功能主要是 Lucene 提供的,Lucene 至關於其存儲引擎,它在之上封裝了索引,查詢,以及分佈式相關的接口。java
分佈式系統要解決的第一個問題就是節點之間互相發現以及選主的機制。若是使用了 Zookeeper/Etcd 這樣的成熟的服務發現工具,這兩個問題都一併解決了。但 Elasticsearch 並無依賴這樣的工具,帶來的好處是部署服務的成本和複雜度下降了,不用預先依賴一個服務發現的集羣,缺點固然是將複雜度帶入了 Elasticsearch 內部。node
Elasticsearch 將以上服務發現以及選主的流程叫作 ZenDiscovery 。因爲它支持任意數目的集羣(1-N),因此不能像 Zookeeper/Etcd 那樣限制節點必須是奇數,也就沒法用投票的機制來選主,而是經過一個規則,只要全部的節點都遵循一樣的規則,獲得的信息都是對等的,選出來的主節點確定是一致的。但分佈式系統的問題就出在信息不對等的狀況,這時候很容易出現腦裂(Split-Brain)的問題,大多數解決方案就是設置一個quorum值,要求可用節點必須大於quorum(通常是超過半數節點),才能對外提供服務。而 Elasticsearch 中,這個quorum的配置就是 discovery.zen.minimum_master_nodes 。 說到這裏要吐槽下 Elasticsearch 的方法和變量命名,它的方法和配置中的master指的是master的候選節點,也就是說可能成爲master的節點,並非表示當前的master,筆者就被它的一個 isMasterNode 方法坑了,開始一直沒能理解它的選舉規則。程序員
Elasticsearch 的彈性體如今兩個方面: 1. 服務發現機制讓節點很容易加入和退出。 2. 豐富的設置以及allocation API。web
Elasticsearch 節點啓動的時候只須要配置discovery.zen.ping.unicast.hosts,這裏不須要列舉集羣中全部的節點,只要知道其中一個便可。固然爲了不重啓集羣時正好配置的節點掛掉,最好多配置幾個節點。節點退出時只須要調用 API 將該節點從集羣中排除 ( Shard Allocation Filtering ),系統會自動遷移該節點上的數據,而後關閉該節點便可。固然最好也將不可用的已知節點從其餘節點的配置中去除,避免下次啓動時出錯。算法
分片(Shard)以及副本(Replica) 分佈式存儲系統爲了解決單機容量以及容災的問題,都須要有分片以及副本機制。Elasticsearch 沒有采用節點級別的主從複製,而是基於分片。它當前還未提供分片切分(shard-splitting)的機制,只能建立索引的時候靜態設置。spring
好比上圖所示,開始設置爲5個分片,在單個節點上,後來擴容到5個節點,每一個節點有一個分片。若是繼續擴容,是不能自動切分進行數據遷移的。官方文檔的說法是分片切分紅本和從新索引的成本差很少,因此建議乾脆經過接口 從新索引 。sql
Elasticsearch 的分片默認是基於id 哈希的,id能夠用戶指定,也能夠自動生成。但這個能夠經過參數(routing)或者在mapping配置中修改。當前版本默認的哈希算法是 MurmurHash3 。數據庫
Elasticsearch 禁止同一個分片的主分片和副本分片在同一個節點上,因此若是是一個節點的集羣是不能有副本的。express
分佈式系統的一個要求就是要保證高可用。前面描述的退出流程是節點主動退出的場景,但若是是故障致使節點掛掉,Elasticsearch 就會主動allocation。但若是節點丟失後馬上allocation,稍後節點恢復又馬上加入,會形成浪費。Elasticsearch的恢復流程大體以下:
但若是該節點上的分片沒有副本,則沒法恢復,集羣狀態會變爲red,表示可能要丟失該分片的數據了。
分佈式集羣的另一個問題就是集羣整個重啓後可能致使不預期的分片從新分配(部分節點沒有啓動完成的時候,集羣覺得節點丟失),浪費帶寬。因此 Elasticsearch 經過如下靜態配置(不能經過API修改)控制整個流程,以10個節點的集羣爲例:
好比10個節點的集羣,按照上面的規則配置,當集羣重啓後,首先系統等待 minimum_master_nodes(6)個節點加入纔會選出master, recovery操做是在 master節點上進行的,因爲咱們設置了 recover_after_nodes(8),系統會繼續等待到8個節點加入, 纔開始進行recovery。當開始recovery的時候,若是發現集羣中的節點數小於expected_nodes,也就是還有部分節點未加入,因而開始recover_after_time 倒計時(若是節點數達到expected_nodes則馬上進行 recovery),5分鐘後,若是剩餘的節點依然沒有加入,則會進行數據recovery。
Elasticsearch 除了支持 Lucene 自己的檢索功能外,在之上作了一些擴展。 1. 腳本支持
Elasticsearch 默認支持groovy腳本,擴展了 Lucene 的評分機制,能夠很容易的支持複雜的自定義評分算法。它默認只支持經過sandbox方式實現的腳本語言(如lucene expression,mustache),groovy必須明確設置後才能開啓。Groovy的安全機制是經過java.security.AccessControlContext設置了一個class白名單來控制權限的,1.x版本的時候是本身作的一個白名單過濾器,但限制策略有漏洞,致使一個 遠程代碼執行漏洞 。 2. 默認會生成一個 _all 字段,將全部其餘字段的值拼接在一塊兒。這樣搜索時能夠不指定字段,而且方便實現跨字段的檢索。 3. Suggester Elasticsearch 經過擴展的索引機制,能夠實現像google那樣的自動完成suggestion以及搜索詞語錯誤糾正的suggestion。
Elasticsearch 能夠做爲數據庫使用,主要依賴於它的如下特性:
Elasticsearch 的依賴注入用的是guice,網絡使用netty,提供http rest和RPC兩種協議。
Elasticsearch 之因此用guice,而不是用spring作依賴注入,關鍵的一個緣由是guice能夠幫它很容易的實現模塊化,經過代碼進行模塊組裝,能夠很精確的控制依賴注入的管理範圍。好比 Elasticsearch 給每一個shard單獨生成一個injector,能夠將該shard相關的配置以及組件注入進去,下降編碼和狀態管理的複雜度,同時刪除shard的時候也方便回收相關對象。這方面有興趣使用guice的能夠借鑑。
ClusterState
前面咱們分析了 Elasticsearch 的服務發現以及選舉機制,它是內部本身實現的。服務發現工具作的事情其實就是跨服務器的狀態同步,多個節點修改同一個數據對象,須要有一種機制將這個數據對象同步到全部的節點。Elasticsearch 的ClusterState 就是這樣一個數據對象,保存了集羣的狀態,索引/分片的路由表,節點列表,元數據等,還包含一個ClusterBlocks,至關於分佈式鎖,用於實現分佈式的任務同步。
主節點上有個單獨的進程處理 ClusterState 的變動操做,每次變動會更新版本號。變動後會經過PRC接口同步到其餘節點。主節知道其餘節點的ClusterState 的當前版本,發送變動的時候會作diff,實現增量更新。
Rest 和 RPC
Elasticsearch 的rest請求的傳遞流程如上圖(這裏對實際流程作了簡化): 1. 用戶發起http請求,Elasticsearch 的9200端口接受請求後,傳遞給對應的RestAction。 2. RestAction作的事情很簡單,將rest請求轉換爲RPC的TransportRequest,而後調用NodeClient,至關於用客戶端的方式請求RPC服務,只不過transport層會對本節點的請求特殊處理。
這樣作的好處是將http和RPC兩層隔離,增長部署的靈活性。部署的時候既能夠同時開啓RPC和http服務,也能夠用client模式部署一組服務專門提供http rest服務,另一組只開啓RPC服務,專門作data節點,便於分擔壓力。
Elasticsearch 的RPC的序列化機制使用了 Lucene 的壓縮數據類型,支持vint這樣的變長數字類型,省略了字段名,用流式方式按順序寫入字段的值。每一個須要傳輸的對象都須要實現:
void writeTo(StreamOutput out)
T readFrom(StreamInput in)
兩個方法。雖然這樣實現開發成本略高,增刪字段也不太靈活,但對 Elasticsearch 這樣的數據庫系統來講,不用考慮跨語言,增刪字段確定要考慮兼容性,這樣作效率最高。因此 Elasticsearch 的RPC接口只有java client能夠直接請求,其餘語言的客戶端都走的是rest接口。
網絡層Elasticsearch 的網絡層抽象很值得借鑑。它抽象出一個 Transport 層,同時兼有client和server功能,server端接收其餘節點的鏈接,client維持和其餘節點的鏈接,承擔了節點之間請求轉發的功能。Elasticsearch 爲了不傳輸流量比較大的操做堵塞鏈接,因此會按照優先級建立多個鏈接,稱爲channel。
每一個節點默認都會建立13個到其餘節點的鏈接,而且節點之間是互相鏈接的,每增長一個節點,該節點會到每一個節點建立13個鏈接,而其餘每一個節點也會建立13個連回來的鏈接。
線程池因爲java不支持綠色線程(fiber/coroutine),我前面的《併發之痛》那篇文章也分析了線程池的問題,線程池裏保留多少線程合適?如何避免慢的任務佔用線程池,致使其餘比較快的任務也得不到執行?不少應用系統裏,爲了不這種狀況,會隨手建立線程池,最後致使系統裏充塞了大的量的線程池,浪費資源。而 Elasticsearch 的解決方案是分優先級的線程池。它默認建立了10多個線程池,按照不一樣的優先級以及不一樣的操做進行劃分。而後提供了4種類型的線程池,不一樣的線程池使用不一樣的類型:
這種解決方案雖然要求每一個用到線程池的地方都須要評估下執行成本以及應該用什麼樣的線程池,但好處是限制了線程池的泛濫,也緩解了不一樣類型的任務互相之間的影響。
之後每篇分析架構的文章,我都最後會提幾個和該系統相關的改進或者擴展的想法,稱爲腦洞時間,做爲一種鍛鍊。不過只提供想法,不深刻分析可行性以及實現。
還記得10年前在大學時候搗鼓 Lucene,弄校園內搜索,還弄了個基於詞典的分詞工具。畢業後第一份工做也是用 Lucene 作站內搜索。當時搭建的服務和 Elasticsearch 相似,提供更新和管理索引的api給業務程序,固然沒有 Elasticsearch 這麼強大。當時是有想過作相似的一個開源產品的,後來發現apache已經出了 Solr(2004年的時候就建立了,2008年1.3發佈,已經相對成熟),感受應該沒啥機會了。但 Elasticsearch 硬是在這種狀況下成長起來了(10年建立,14年才發佈1.0)。 兩者的功能以及性能幾乎都不相上下(開始性能上有些差距,但 Solr 有改進,差很少追上了),參看文末比較連接。
我以爲一方面是 Elasticsearch 的簡單友好的分佈式機制佔了先機,也正好遇上了移動互聯網爆發移動應用站內搜索需求高漲的時代。第一波站內搜索是web時代,也是 Lucene 誕生的時代,但web的站內搜索能夠簡單的利用搜索引擎服務的自定義站點實現,而應用的站內搜索就只能靠本身搭了。另一方面是 Elasticsearch 的周邊生態以及目標市場看把握的很是精準。Elasticsearch 如今的主要目標市場已經從站內搜索轉移到了監控與日誌數據的收集存儲和分析,也就是你們常談論的ELK。
Elasticsearch 如今主要的應用場景有三塊。站內搜索,主要和 Solr 競爭,屬於後起之秀。NoSQL json文檔數據庫,主要搶佔 Mongo 的市場,它在讀寫性能上優於 Mongo(見文末比較連接),同時也支持地理位置查詢,還方便地理位置和文本混合查詢,屬於歪打正着。監控,統計以及日誌類時間序的數據的存儲和分析以及可視化,這方面是引領者。
聽說 Elasticsearch 的創始人當初建立 Elasticsearch 的時候是爲了給喜歡作菜的媳婦搭建個菜譜的搜索網站,雖然菜譜搜索網站最後一直沒作出來,但誕生了 Elasticsearch。因此程序員堅持一個業餘項目也是很重要的,萬一無意插柳就成蔭了呢?