天天看點

ElasticSearch架構反向思路

我曾經在多個場合說過,我分析一個系統的設計思路,往往不是一開始就去看看這個系統的設計文檔或者源代碼,而是去看系統的基本介紹,特别是架構類的功能詳細介紹,然後根據介紹可以大概了解這樣一個系統用來解決什麼問題,有哪些特色,然後基于自己對這些問題的想法,根據自己的經驗來同樣設計一個系統,看包含哪些内容,使用哪些架構模式和思路,然後帶着自己設計的東西再去看另一個系統的設計思路,可能再更加清楚,也會反思自己的設計是否哪些地方存在問題,可以加以改進。

最近正好準備玩ElasticSearch,本來在2013年就想玩這個,但由于工作原因耽誤了,現在又翻出來看看有什麼好玩的,下面就詳細地記錄了我對ElasticSearch的反向架構思考。順便補充一句,目前用來研究的ElasticSearch的版本号是6.3

先來看看一份對ElasticSearch比較典型的介紹:

Elasticsearch是一個基于Apache Lucene(TM)的開源搜尋引擎。無論在開源還是專有領域,Lucene可以被認為是迄今為止最先進、性能最好的、功能最全的搜尋引擎庫。
 
但是,Lucene隻是一個庫。想要使用它,你必須使用Java來作為開發語言并将其直接內建到你的應用中,更糟糕的是,Lucene非常複雜,你需要深入了解檢索的相關知識來了解它是如何工作的。
 
Elasticsearch也使用Java開發并使用Lucene作為其核心來實作所有索引和搜尋的功能,但是它的目的是通過簡單的RESTful API來隐藏Lucene的複雜性,進而讓全文搜尋變得簡單。
 
不過,Elasticsearch不僅僅是Lucene和全文搜尋,我們還能這樣去描述它:
 
分布式的實時檔案存儲,每個字段都被索引并可被搜尋
分布式的實時分析搜尋引擎
可以擴充到上百台伺服器,處理PB級結構化或非結構化資料
而且,所有的這些功能被內建到一個服務裡面,你的應用可以通過簡單的RESTful API、各種語言的用戶端甚至指令行與之互動。
 
上手Elasticsearch非常容易。它提供了許多合理的預設值,并對初學者隐藏了複雜的搜尋引擎理論。它開箱即用(安裝即可使用),隻需很少的學習既可在生産環境中使用。
 
随着你對Elasticsearch的了解加深,你可以根據不同的問題領域定制Elasticsearch的進階特性,這一切都是可配置的,并且配置非常靈活。
           

幸虧以前使用過Lucene做IDE底層項目模型關系的管理,對Lucene還算比較熟悉,否則還得先去看看Lucene的功能和用法。

從上面的介紹可以看出幾個關鍵内容:

  1. Lucene在做索引的時候本身就有存儲功能,是以存儲這個東西是天然就有的,反而不用花時間考慮。
  2. 性能是一個比較關鍵的東西,特别是要做實時引擎,怎麼保證高性能。
  3. ElasticSearch是一個分布式的系統,那麼必然存在多結點通訊,協作等問題,比如使用ZooKeeper之類的系統進行注冊和協同,當然也保不齊他自己玩一套。
  4. 既然是分布式系統,那麼資料存儲就不可能完全單機化,也就是存在Sharding的情況,如何Sharding,如何同步,在查找結果的時候,如何聚合。
  5. 分布式系統,隻要涉及到資料更新,必然存在資料不一緻問題,怎麼解決。
  6. 由于索引本身原因,一旦出現Sharding,就很難做聯合的查詢,這個應該不能實作的,至少說不可能很簡單得實作。
  7. 有一個網絡層或者說對外服務接口層,用來進行互動,看介紹,支援多種協定,比如Client直接調用,或者是Restful風格。
  8. 參考服務接口層,還允許很多地方進行配置,那麼很顯然,應該是使用了類似于插件的技術來支援很多功能。

我的習慣是從使用者角度來倒推系統架構

  1. 對外服務,稱為Interface,這個其實還相對簡單,應該提供兩個基本功能,即BuildIndex(不一定要區分Create和Update,但Delete肯定要有)和Query(應該基于主Key和Condition兩種查詢),把這兩個基本接口設計好,然後在上面加不同的封裝或者通過Netty之類網絡架構提供Rest服務,也可能基于Stub類似的機制提供RPC調用。
  2. 查詢功能,是采用SQL還是Query模型的方式,我更傾向于後者,因為關聯查詢等很多功能是無法提供的,SQL校驗會是比較麻煩的事情。
  3. 不管是BuildIndex還是Query,肯定要找到一台機器或者多台機器進行處理,由于這是一個分布式系統,而且還支援Sharding,那麼可以肯定,需要分組,即Group,一個Group中包括若幹個Node,用來支援服務。
  4. 怎麼分組,正常可能是分兩級,一種是基于模型定義的,比如對于某一些資料,象商品,使用者這些資料可能分成一類資料對應一個Group來處理,這種處理比較直覺,也簡單。也就是說每一類模型會對應一個Group,而一個Group可能對着多個模型,特别是資料相對較少的時候。還有一種就是Sharding,通常來說,是對一類資料,根據某一個或者幾個字段(Field),進行條件分組,也就說在這種分組情況下,每個Node的資料都是不全的,需要将多個Node合并在一起,才會形成完整的資料集。這兩種分組都需要支援的。
  5. 對于BuildIndex和Query,當系統分成多個Group的時候,肯定要有一個Router的概念,即一個BuildIndex或者Query服務來的時候,得找到相應的Group(應該是Group下的Node),因為Lucene中的Document和Term特性,應該需要設計一個類似于資料庫中的Table模型,一個Group負責處理多個Table。在BuildIndex和Query請求裡,1. 必須帶有Table的準确定義,比如User,Item等。

    按照前面的思考,Group是肯定應該存在的,但是每個Group否需要一個MasterNode呢?

  6. 當一個Query請求定義清楚後,會以路由的方式找到一個Group,如果資料量不大的話,一個Group中的Node應該是資料對等的,那麼請求落到任何一個Node上都可以得到相應的結果。如果資料量很大,出現Sharding,就分兩種情況,一種是Query中的條件,能夠符合Sharding的定義條件,那麼落到任何一個Node上以後,通過轉發的方式,總是可以拿到請求,應該有兩種實作方式,一是請求發到某個Node上以後,由Node分析後,将可以導向的Node傳回,由請求方再次将指定的Node發送請求,二是任意Node直接向可以導向的Node轉發請求,并拿到結果後傳回給請求方,第二種對用戶端友好,但如果資料量大的話,可能不太合适。還有一種情況就是,如果Query中的條件不能夠符合Sharding定義,那麼就出現類似于資料庫查詢的FullScan,由收到的Node将請求轉發給相應的Node,構成全量搜尋,然後由該Node合并後,傳回。如果這樣看,最好的方式還是Node統一處理,對請求方更友好一些,也更一緻。
  7. 當BuildIndex的時候,必然是發給一個Node,由其完成Index後,再同步給其它Node,此時同步,是有一個MasterNode還是沒有好呢?感覺設計一個MasterNode可能使得邏輯更簡單。即大的Group裡,MasterNode主要負責協作和BuildIndex同步,而Query則可以盡可能地落到DataNode側。
  8. 雖然有了MasterNode,但仍然是可以将BuildIndex請求發給DataNode,由DataNode轉發給MasterNode,這樣會更加簡單和友好。
  9. 考慮到BuildIndex和Query會有不同步的情況,那麼怎麼減少這種不一緻性呢?如果由MasterNode或者指定的一個DataNode進行BuildIndex的時候,對其它Node的Query都會産生資料不一緻性問題。假設由MasterNode給其它DataNode全部上鎖,此時查詢性能急速下降,這種方法不是非常建議,容易形成堵塞,不過如果資料很少更新,而且對資料一緻性有較高要求,也可以支援,那裡可能得在這個地方允許使用者配置一緻性優先還是性能優先了。如果是後者的話,按照我對Lucene的了解,此時每個DataNode最好有一個DiskStore和一個MemoryStore,查詢時将兩者合并查詢,這樣在保證高性能的情況下可以減少不一緻性。或者更靈活一點,允許在BuildIndex的時候允許指定是否加鎖,但這樣可能會增加複雜度,需要再思考一下。
  10. 同樣是資料不一緻問題,除了上面的内容以外,還需要使用Log,這樣MasterNode先記錄Log,然後進行Index,同時分發給DataNode,DataNode也是先記錄Log,這樣一旦出現問題,可以随時在啟動時從Log處Redo。
  11. 維護和管理功能:動态擴容,Reindex(擴容時肯定要用到),啟動時先與多個DataNode同步Log,再根據Log進行Redo,保證資料的一緻性。
  12. 插件化設計沒什麼難點,不管是類似于OSGi,還是說直接寫一個Plugin的接口,然後加一個PluginManager都可以解決問題。但關鍵是Plugin需要在哪些情況下調用,以便讓開發者可以更多的加入自己的定制。我猜可能有以下幾個點:網絡請求的Before和After處理(比如支援不同的資料模型,不同的安全檢查等,記錄日志,流量控制等),啟動後的After處理(比如對Log進行Check,以便Redo),BuildIndex和Query的Before和After處理(其實就可以通過這個擴充來處理資料同步的問題)。
  13. 上面說的插件化設計并不難,但是否使用統一的Plugin接口,還是分開,需要考慮一下,畢竟可以提供擴充點的地方太多了。如果是我設計,大概是三大級繼承,最頂層的有一個Plugin或者Extension的接口,提供Name,Desription,Dependecy等内容的定義,這個和Equinox都類似,其實不帶任何業務支援的,第二層是業務級别的,比如說網絡請求的,日志處理的,第三層就是具體實作了。再多就有點複雜了,有一個最頂層接口的好處是,在Eclipse裡,查下繼承關系,就得到所有實作了,友善分析代碼,如果隻設計二和三層,哈哈,就有得找了。

基于以上分析,可以列出來幾個基本的元素和服務:

  1. Node+Group+MasterNode+DataNode
  2. Table+Field+Key+Condition
  3. BuildIndex+Query
  4. Log
  5. Plugin

下面是大緻的架構域圖:

ElasticSearch架構反向思路

還有幾個難點,需要再考慮一下:

  1. Query可能會有Paging的需要,那麼一旦出現Sharding的話,需要将多個DataNode的結果Merge後,進行Sort,再計算Paging後傳回。這個對性能的要求比較高,特别是當頁面翻到幾十頁的時候,性能損失非常大,如何處理?還是說技術層面上不做解決,直接讓業務方來自行規劃。
  2. 因為ElasticSearch是基于Lucene的,而Lucene并不提供事務操作,比如先行鎖再Update,是以一旦出現沖突時,因為網絡延時等原因,有可能後面的資料覆寫前面的資料,這種情況怎麼考慮,是加一個時間版本号還是忽略這種情況?
  3. 另外ElasticSearch對資料一緻性不可能提供太好的解決方案,是以最好還是将一些非核心業務資料進行查詢,比如日志,就不會出現修改,再比如電商中的商品表,修改相對并不頻繁,但如果商品表裡包含商品數量,那麼就挂了,所有必須減少将頻繁更新的資料放入搜尋。
  4. 有點記不清楚Lucene的存儲機制了,是否支援類似于資料庫的Update語句,隻更新部分資料。如果不支援,那麼ElasticSearch是否需要支援呢?如果是我,應該不會支援,做太多的事情更容易出錯。
  5. 當MasterNode當掉,顯然可以通過選舉或者别的方法找到一個新的MasterNode,但如果一個MasterNode或者DataNode收到一個BuildIndex請求後,再當掉,最好是通知Client失敗,由Client發起重試。由于所有BuildIndex請求都是發給MasterNode來處理的,那麼就相對簡單了,如果MasterNode失敗後重新加入Group,由于此時它不再是Master,就可以丢棄這個日志,保證資料一緻性。這塊的細節會比較多,記錄Log,然後如何Redo,如何Sync,如何抛棄,都需要深入分析。不在這裡折騰了。