Elasticsearch最佳實踐之核心概念與原理

  每一個系統都擁有很多概念,這些概念是作者在設計與實現時爲不同的模塊或功能做的定義。概念本身只是一個名詞,往往會跟隨作者的喜好不同而不同,重要的是理解其設計的初衷以及要表達的實際內容,否則很快就會忘記其意義。作爲專欄文章的第二篇,本文將從多個方面對Elasticsearch的核心概念進行整理,儘可能由淺入深的交代清楚每個概念,而相關的使用技巧會在後續博文中介紹。本文寫作背景是Elasticsearch 5.5。
  爲了方便查閱,這裏首先列出會涉及到的概念,讀者可以根據需要選擇性閱讀。

1. 數據組織

1.1 邏輯組織

  假設我們在一個業務系統中選擇MySQL做數據存儲,那麼我們需要先創建一個database,再創建一組相關的table。幾乎所有的數據存儲系統都有類似的設計,這樣做的一個基本目的在於對數據進行抽象分類,將描述同種特性的數據放在一起,可以更好的做壓縮存儲、查詢優化等。另一方面,通過這樣在邏輯層面對數據進行組織後,可以屏蔽底層的具體細節,方便在應用程序中進行操作。
  Elasticsearch同樣具有這樣的概念,如下圖所示,使用indexdoc_type來組織數據。doc_type中的每條數據稱爲一個document,是一個JSON Object,相關的schema信息通過mapping來定義。mapping不僅僅包括數據類型的定義,還有很多其他元信息的設置,它們共同決定了數據如何被存儲和索引。這四個概念實現了Elasticsearch的邏輯數據組織,假設有一批結構化或半結構化數據需要存儲,我們會先對數據進行分類,設計相應的index與doc_type,再爲每個doc_type設置相關的mapping信息。如果不指定mapping,Elasticsearch會使用默認值,並自動爲你推導每個字段的類型,即支持schema free的特性。但是,這種靈活性也會帶來一些問題,一方面會失去對數據的控制,即會越來越不清楚你的數據結構,另一方面,自動推導出來數據類型可能不是預期的,會帶來寫入和查詢問題。所以,筆者建議,盡最大可能對schema加以約束。

  通常情況下,我們都會拿Elasticsearch的這些概念跟關係型數據庫對比來更好的理解,比如index等價於database,doc_type等價於table,mapping等價於db schema。但是,需要注意的是,對於關係型數據庫而言,table與table之間是完全獨立的,不同table的schema是完全隔離的,而Elasticsearch中的doc_type則不是。同一個index下不同doc_type中的字段在底層是合併在一起存儲的,意味着假設兩個doc_type中都有一個叫name的字段,那麼這兩個字段的mapping必須一樣。基於這個原因,Elasticsearch官方從6.0開始淡化doc_type的概念,推薦一個index只擁有一個doc_type,並計劃在8.x完全廢棄doc_type。因此,在當前的index設計中,最好能遵循這個規則。

1.2 物理組織

  Elasticsearch是一個分佈式系統,其數據會分散存儲到不同的節點上。爲了實現這一點,需要將每個index中的數據劃分到不同的塊中,然後將這些數據塊分配到不同的節點上存儲。這裏的數據塊,就是shard。通過"分"的思想,可以突破單機在存儲空間和處理性能上的限制,這是分佈式系統的核心目的。而對於分佈式存儲而已,還有一個重要特性是"冗餘",因爲分佈式的前提是:接受系統中某個節點因爲某些故障退出。爲了保證在故障節點退出後數據不丟失,同一份數據需要拷貝多份存在不同節點上。因此,shard從角色上劃分爲primary shardreplica shard兩種,數據會首先寫入primary shard,然後同步到replica shard中。

  shard是Elasticsearch中最小的數據分配單位,即一個shard總是作爲一個整體被分配到某個節點,而不會只分配其中一部分。那麼,shard中的數據又是如何組織的?答案是segment。一個shard包含一組segment,segment是最小的數據單元,Elasticsearch每隔一段時間產生一個新的segment,裏面包含了新寫入的數據。segment是immutable的,即不可改變,這樣設計的考量是:一方面,不支持修改就不用對讀寫操作加鎖,省去了相關開銷;另一方面,因爲文件內容不會修改,可以更好的利用filesystem cache進行緩存,提高查詢性能。但是,任何設計都不是完美的,伴隨而來的問題是:如果segment不可修改,怎麼實現數據的更新與刪除呢?這個問題將在下面“數據寫入”一節介紹。

2. 數據分佈

  上面提到,Elasticsearch將每個index中的數據劃分到不同的shard中,然後將shard分配到不同的節點上,實現分佈式存儲。這裏面涉及到兩個概念:一個是數據到shard的映射(route),另一個是shard到節點的映射(shard allocate)。
  一方面,插入一條數據時,Elasticsearch會根據指定的key來計算應該落到哪個shard上。默認key是自動分配的id,可以自定義,比如在我們的業務中採用CompanyID作爲key。因爲primary shard的個數是不允許改變的,所以同一個key每次算出來的shard是一樣的,從而保證了準確定位。

shard_num = hash(_routing) % num_primary_shards

  另一方面,master節點會爲每個shard分配相應的data節點進行存儲,並維護相關元信息。通過route計算出來的shard序號,在元信息中找到對應的存儲節點,便可完成數據分佈。shard allocate的映射關係並不是完全不變的,當檢測到數據分佈不均勻、有新節點加入或者有節點掛掉等情況時就會進行調整,稱爲relocate
  關於數據分佈,可以參考閱讀博文《談Elasticsearch下分佈式存儲的數據分佈》。

3. 集羣角色

  一個分佈式系統,是由多個節點各司其職、相互協作完成整體服務的,從架構上可以分爲有中心管理節點和無中心管理節點兩種,Elasticsearch屬於前者。中心管理節點負責維護整個系統的狀態和元信息,爲了保證高可用性,通常是從一組候選節點中選舉出來的,而非直接指定。按照職責,Elasticsearch將節點分爲三種:master-eligible節點、data節點、ingest節點。master-eligible節點就是中心節點的候選人,通過選舉算法從這些候選人中推選出大家公認的中心節點。data節點負責數據存儲、查詢,也是整個系統中負載最重的部分。ingest節點是針對Elasticsearch一個特定功能而設定的,Elasticsearch支持在數據寫入前對數據進行相關的轉換、處理,而這類節點就是負責這樣的工作,從筆者遇到的實踐來看,使用這類節點的並不多。
  這三種角色是通過配置來設定的,可以同時設置到同一個節點上,即一個節點可以同時具備這三種功能。但是這種做法只適用於數據量小、業務較輕的場景,因爲不同角色承擔的功能所帶來的負載是不同的,很可能因爲數據寫入/查詢負載較重導致master節點通信受到影響,從而導致系統不穩定。所以,推薦將不同角色分離開,某個節點只負責其中一個功能,通常會設置dedicated master-eligible節點、data/ingest節點。前者負載很輕,只需要分配較低配置的機器,而後者對CPU、IO、Memory要求較高,需要配置更好的機器,實踐中根據性能測試結果來調整。
  前面提到,中心節點(master)是從一組候選人(master-eligible)中選舉出來的,那設置多少個候選人是合理的?原則是要保證任何時候系統只有一個確定的master節點。考慮到一致性,只有被半數以上候選節點都認可的節點才能成爲master節點,否則就會出現多主的情況。只有1個候選節點顯然不能保證高可用;有2個時,半數以上(n/2+1)的個數也是2,任何一個出現故障就無法繼續工作了;有3個時,半數以上的值仍然是2,恰好可以保證master故障或網絡故障時系統可以繼續工作。因此,3個dedicated master-eligible節點是最小配置,也是目前業界標配。

  Elasticsearch以REST API形式對外提供服務,數據寫入與查詢都會發送HTTP(S)請求到服務端,由負載均衡將請求分發到集羣中的某個節點上(任何非dedicated master-eligible節點)。如下圖所示,節點1收到請求後,會根據相關的元信息將請求分發到shard所在的節點(2和3)上進行處理,處理完成後,節點2和3會將結果返回給節點1,由節點1合併整理後返回給客戶端。這裏的節點1扮演着協調者的角色,稱爲coordinate節點,任何節點在收到請求後就開始發揮協調者的角色,直到請求結束。在實際使用中,可以根據需要增加一些專用的coordinate節點,用於性能調優。

4. 數據寫入

  通過上面的整理,我們知道,當有數據寫入時,請求會先到達集羣中的某個節點上,由該節點根據routing信息和元信息將相應的數據分發到對應的shard所在的節點上,可能是一個也可能是多個節點,取決於寫入的數據。這些節點在收到分發出來的請求後,會經過一系列過程,最終將數據以segment的形式落地到磁盤上,這些過程就是本節要聊的內容,其包含同步與異步兩個過程,如下圖所示。
同步過程:
  同步過程是指在請求返回前做的事情,即包含在一個HTTP請求的過程中,客戶端需要等其做完才能拿到結果。簡單來看,這個過程需要完成三件事:第一,將操作記錄寫入到translog中,我們後面再來談它的作用;第二,根據數據生成相應的數據結構,並寫入到in-memory buffer,注意是寫入到一個內存buffer中,不是磁盤;第三,將數據同步到所有primary shard中。完成這些之後,就會生成相應的結果返回給coordinate節點了。
異步過程:
  我們知道,寫磁盤很慢,且非常耗費CPU與IO,在同步過程中,爲了讓請求儘快返回,並沒有將數據直接落盤。Elasticsearch的最小數據單元是segment,而此時數據還在in-memory buffer中,因此這部分數據是不能被查詢請求訪問到的。只有當發生refresh動作,纔會產生一個新的segment,將內存buffer中的數據寫入到裏面,同時清空buffer。默認refresh的時間間隔是1秒,可以配置,需要在實時性與性能之間進行權衡。
  此時雖然已經生成了新的segment文件,但是隻是停留在filesystem cache中,並沒有真正的落到磁盤中。這些動作的目的都是爲了將“寫磁盤”這件事儘可能的延後並變得低頻,但是數據一直留在內存中始終是不安全的,很容易因爲斷電等原因導致數據丟失,因此每隔一段時間,Elasticsearch會真正做一次磁盤flush,完成數據的持久化。默認是30分鐘一次。
  從寫入請求過來到數據最終落盤,中間很長一段時間數據是停留在內存中的,那麼如果在此期間機器斷電豈不是會丟失數據?爲了解決這個問題,就要用到上面所述的translog了。在請求返回前,必須要將操作記錄寫入到translog中並落盤,保證機器重啓後可以恢復數據。顯然這件事本身是會消耗性能的,但這也是保證數據不丟失的一個犧牲了,必須要做的。
  segment是由refresh動作產生的,因此隨着時間推移,會產生很多小segment,而每個segment都需要佔用一定的資源,比如文件句柄、緩存等等,過多的segment勢必會導致性能下降。因此每隔一段時間,Elasticsearch會做一次segment merge,將多個小的segment合併成一個大的segment。
  最後再來看下前面提到的一個問題:因爲segment是不可改變的,如何實現數據更新與刪除?以刪除爲例,Elasticsearch將要刪除的數據記錄到一個叫.del文件中,每次查詢時會將匹配到的數據跟這個文件中的數據做一次對比,去掉被刪除的數據。直到segment merge時,會將.del文件和相應的segment文件一起加載進行合併,這時才真正刪除了數據。

5. 存儲結構

  在講存儲結構之前,先來看看兩種常見的查詢需求(以一組博文信息數據爲例,有作者、標題等信息)。一種是精確匹配,比如查找作者姓名爲"Bruce"的信息;另一種是全文檢索,比如從1000個文章的標題中搜索出包含"分佈式"的文章。對於第一個需求,我們只需要將每個名字作爲一個term即可,“是"或"不是”;對於第二個,我們如果想知道標題中是否包含"分佈式",就需要提前將每個標題分解爲多個term,比如"淺談分佈式存儲系統",可能會產生"淺談"、“分佈式”、“存儲”、"系統"等多個term,具體取決於使用了哪一種分析器。
  不管哪種情況,最後都是產生一組term,問題是用一個什麼樣的存儲結構可以實現快速檢索。這就是Elasticsearch的核心:inverted index。inverted index是一個二維結構,如下所示,包含一組排好序的term,每個term都關聯有一些信息,這些信息指出哪些document包含了這個term。當需要查詢包含關鍵詞"分佈式"的數據時,系統會先從inverted index中找出對應的term,獲取到其對應的document id,然後就可以根據document id找出其信息了。

sample data:
1. {"author": "Bruce", "title": "淺談分佈式存儲系統"}
2. {"author": "Bruce", "title": "常見的分佈式系統"}
3. {"author": "David", "title": "分佈式存儲原理"}

inverted index for field "author":
-------------------------------
term     |   doc id
-------------------------------
Bruce    |   1, 2
David    |   3
-------------------------------

inverted index for field "title":
 -------------------------------
term     |   doc id
-------------------------------
常見      |   3
存儲      |   1, 3
分佈式    |   1, 2, 3
淺談      |   1
系統      |   1, 2
原理      |   3
-------------------------------

  通過inverted index,我們可以根據關鍵詞快速搜索出相關的document,除了這種查詢,還有一種常見的需求是求聚合,即關係型數據庫中的GROUP BY功能。比如查看寫"分佈式"相關的文章最多的10位作者,首先根據上述方法通過inverted index找到與"分佈式"相關的所有document,然後需要對這些document的作者進行歸類並計數,最後再排序取出TOP10。在"歸類"時,我們需要知道每個document的作者名字,但是通過inverted index是無法直接查找到的,因爲他是term-to-doc_id形式的,而我們這裏需要的是doc_id-to-term形式的數據,只有通過循環迭代才能知道某個document的作者姓名是什麼,這樣做的效率無疑是很低的。
  爲了解決聚合的效率問題,Elasticsearch建立了一個與inverted index反向的數據結構:doc values,如下所示。

-------------------------------
doc id     |   terms
-------------------------------
1          |   Bruce
2          |   Bruce
3          |   David
-------------------------------

  inverted index和doc value都是在數據寫入時建立的,即上述的同步過程第二步中完成的。他們都是針對per segment而言的,數據以文件的形式存儲。因爲segment是immutable的,所以可以將其映射在內存中直接訪問,充分利用filesystem cache來提高查詢性能,因此在實踐中建議保留足夠的memory給系統。

  以上便是筆者認爲使用Elasticsearch過程中應該掌握的核心概念與原理,這些知識點對使用和理解實踐中Elasticsearch表現出來的行爲有很大的幫助。當然,並不意味着必須要從一開始就完全掌握,任何認知都是需要伴隨實踐來提高的。另外,在本文的描述中,筆者淡化了Elasticsearch與Lucene的關係,其實有不少概念是Lucene裏面的,而Elasticsearch是在Lucene的基礎上開發的,淡化的原因是筆者認爲沒有必要刻意去區分這二者,除非你想深入研究源碼。當然,概念遠不止這些,讀者也可以參考閱讀筆者的其他博文。


(全文完,本文地址:https://blog.csdn.net/zwgdft/article/details/83619905
(版權聲明:本人拒絕不規範轉載,所有轉載需徵得本人同意,並且不得更改文字與圖片內容。大家相互尊重,謝謝!)

Bruce
2018/12/03 晚

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章