21 June 2014

最近專案有個搜尋的需求,以往我做過的案例,大多數用 RDBMS 就能解決。如果要再更進階的操作,大多直接使用 Lucene 進行客製,一般常見的搜尋產品我反而沒有機會使用。這一次的需求也是很小,但是卻沒有 RDBMS 可用 (DB 是 Cassandra),所以反到有機會嘗試別的做法。目前 Java 界兩個比較大的搜尋產品是 SolrElasticsearch 。這兩個產品已經推出很久了,也有一個有趣的 比較網站 詳細列出所有功能一一比較。

Solr 與 DataStax Enterprise (Cassandra 開發團隊的公司)產品有整合,理論上對我們來說是很自然的選擇。不過要花錢升成企業版功夫太大了,對一個小需求太過。我一開始就先試 Elasticsearch,結果一整天下來,我需求寫完了,Unit Test 也 ok,也上線部署好了 cluster,再加上簡單的 health check。一整套就這樣叭啦叭啦搞定,收工下班,我連試 Solr 的動機都沒了。

名詞釋疑

一開始試 Elasticsearch ,最先撞牆的就是它的專有名詞,我卡了一段時間才了解那些詞的意思,用 RDBMS 來類比就很好理解了:

  • Node: 相對於 RDBMS 中的伺服器
  • Index: 相對於 RDBMS 中的 database (or schema)
  • Type: 相對於 RDBMS 中的 Table
  • Document: 相對於 RDBMS 中的 row
  • Field: 相對於 RDBMS 中的 column
  • Mapping: 可搜尋的範圍,建立 Type與Field 時就會自動產生 Mapping,所以 Type/Field 預設就可搜尋。

Document 是巢狀結構,不像 RDBMS row 只是個二維表。

API

Elasticsearch 提供相當優秀的 RESTful API,使用上相當的簡單直覺,從資料操作、使用統計、系統健康狀態、直到叢集管理。通通可透過 RESTful。這真是令人讚賞的設計,我想這除了代表 Elasticsearch 很好用之外,它的 RESTful API 設計也是我們可以模仿學習對象。

除了 RESTful 之外,也有各個語言的 driver。Elasticsearch 是 Java 寫的,所以 Java 的 driver 效能最好,也走特別的 binary protocol。不過他們設計的 Java API 就沒有 RESTful 簡單,文件也不週全。一開始我就用 Java API 來寫,結果卡了快一小時。平平一樣建立一筆資料進 Index,為什麼 RESTful 可以,Java API 卻不行?撞牆很久後才發現答案:RESTful API 在新增文件時預設會自動建立 Index,而 Java API 則不是,要自己先建立。文件沒寫然後錯誤訊息也不清不楚的,真不爽。

好在過了這無聊的牆之後就順了,Java API 本質上就是 Elasticsearch 伺服器本身,所以可以利用它來做 Unit Test,以及開啟 embeded 伺服器方便開發。這一點就很不錯,不用為了開發/測試還要多管理一台開發用伺服器。

Cluster

選擇伺服器產品時,我很在意產品的叢集能力。我個人偏好非集權式的架構,也就是說不需要額外安裝一台 master/coordinator 去管理。非集權可以讓管理的難度大大的降低,很適合人力不足的創業公司 (一般來說大多是開發者來兼任,就是 devops)。這也是為什麼我偏好P2P式,每台都是對等的 Cassandra、偏好不需安裝 Master Agent 的 Ansible、而我們公司的產品 Cubie 的訊息伺服器本身也是非集權式的。

在 Elasticsearch cluster 裡,會有節點扮演 Master 的角色 (管理節點增減,Index 變更等等),不過在部署時不需要特別去額外安裝一台,每個節點都有機會遴選為 Master。因此從管理的角度來看也是相當的簡單,只要設好節點間 discovery 的機制後就好了,節點主機會自行管理。

Elasticsearch 採用常見的 Shard 與 Replica 的架構應付 scalibility 的需求。Shard 將 Index 分割存放在不同節點,提升寫入的效能以及應付大量的文件。Replica 將同樣的資料複製到不同節點,提升搜尋效能,也應付 fail-over 的需求。

Elasticsearch 在寫入資料時,預設的條件是 Quorum ,簡單講就是一半以上的複本數寫入成功,這筆才算寫入:

R = number of replicas
quorum = floor( (primary + R) / 2 ) + 1  if R > 1        (式1)
quorum = 1                               if R = 1 or 0   (式2)

例如如果設定成資料要複製二份到別的節點 (所以總共是三份),那麼每次寫入都要滿足二台寫入成功才算數 (1+2)/2 + 1 = 2,到這裡為止跟一般 quorum 的定義相同,但它比較特別的是當設定 number_of_replicas 為 0 或 1 時,變成只要一台寫入成功就結束 (式2)。這是為了簡化單機測試才有的特別規則。

一開始我裝起單台 Elasticsearch Cluster 節點,預設就是 replica = 1,照 quorum 的公式 (式1) 來看應該是不能寫入才對,想半天想不懂為啥,查了文件才發現有 (式2) 這個特規。

我們的需求簡單,所以我只部署了兩台,其中一台當 fail-over 兼備份。按它的 Quorum 特規,如果其中一台掛掉時,還是可以繼續寫入與讀取的服務。

Eventually Consistent

在預設的設定下,Elasticsearch 每一秒會進行一次 Index refresh,也就是說你寫入了一筆文件,要一秒後才會出現在搜尋結果,以搜尋的應用來說一秒真的是夠快了。大概只有在測試時才會遇到寫入後查不到的案例 (會一直鬼打牆啊)。雖然這種案例很少,不過設計的服務還是要避免寫入後馬上查詢的情境。這一秒是可以調整的參數,所以可以因應不同需求來調校。

Elasticsearch 資料會有複本,因此會有 consistency 的問題。基本上它的設計是有節點掛掉,再重開,它內部會自動自我修復,讓資料最終會一致。這樣的設計當然讓管理簡單很多,但是回復的資料穩不穩又是另一回事了。我這禮拜剛好在試 Elasticsearch,知名分散式專家 Kyle Kingsbury 剛好也出了一篇 Call Me Maybe - Elasticsearch,該文詳細探討 Elasticsearch 在遭遇 network partition 後,資料回復的正確性。結果你猜作者的結論是什麼?

Elasticsearch 的用戶,請自求多福。

看到這結論我冷掉了,作者測試 Elasticsearch 叢集在經過 partition 後,當時寫入的資料有些會不見,救不回來。他建議 Elasticsearch 的用戶不要把它當做資料庫來使用,正確的做法是服務有自己的資料庫存放資料,然後 Elasticsearch 只擔任 index/查詢的工作。

所幸,搜尋的功能通常不是很在意 100% 正確性,一點小落差還可以接受,Elasticsearch 團隊被鞭了之後,下一版 1.3 就會修正該文發現的一些嚴重 bug。(嚇,我才剛部署完 1.2 版又要馬上升級啦)

小結

本文只探討了 Elasticsearch 外圍 的功能,像是 API 的使用與 Cluster 特性,真正的核心 搜尋 反而沒有討論到,現在我用的功能都太淺了,我想之後隨著我們的需求演進才會深入到它的進階功能與效能調校。屆時才會有比較實務的經驗可以討論。


回響

可以用 Tag <I>、<B>,程式碼請用 <PRE>