有效提昇Elasticsearch整座集群效能的三個小技巧

這篇文章會從Elasticsearch的底層實作解釋應用程式要怎麼做會更好,但我不打算使用很晦澀的方式來描述太深入的細節,因此我會盡量使用簡化過的流程來說明。

在開始之前讓我們先來了解Elasticsearch幾個重要的術語。

  • Index:這就像是RDBMS的table或是MongoDB的collection,不要和RDBMS的index混淆了。
  • Shard(或稱Primary Shard):資料要寫入index的存取點,當然也能夠讀取資料。
  • Replica:讀取資料的存取點,無法用來寫入資料。

這幾個術語跟今天的三個技巧息息相關。

那麼,這些元件在Elasticsearch內扮演什麼角色?

讓我用一個例子來說明。假設我們有一個雙節點的Elasticsearch集群,並且擁有兩個index,分別是AB,他們的設定如下。

  • A index
    • number_of_shards = 2
    • number_of_replicas = 1
  • B index
    • number_of_shards = 1
    • number_of_replicas = 2

這些是index設定,很明確的指出要多少個shard和replica。在Elasticsearch內部則會是下面這張圖。

flowchart TD
    u((User)) --> n1 & n2
    classDef colored fill:#f96
    subgraph n1 [Node 1]
       a1:::colored
       ar1[a1]
       b1:::colored
       br1[b1]
    end
    subgraph n2 [Node 2]
        a2:::colored
        ar2[a2]
        br2[b1]
    end

紅色的block代表shard,而一般的block則是replica。使用者或說應用程式,會存取兩個節點來操作對應的資料。

Tip #1:Shard的數量應該要和節點數相同

這個技巧限定於那些很大型的index。因為我們知道,當shard的數量越多,index內的資料會越平均分散,若是每一個shard內的資料都只有一點點而且分散在許多節點,反而會造成更多搜尋負擔。

根據官方建議,一個shard的資料數量應為50GB以內。

為什麼會有這樣的建議?讓我們用一個反例來觀察。假設我們有很多節點,但每個index都只有一個shard。

  • A index
    • number_of_shards = 1
    • number_of_replicas = 3

那麼使用者的讀操作會均勻分散在每個節點上,但寫操作會集中在某一個節點。

flowchart TD
    u((User))--read--> n1 & n2
    u --write--> n1
    classDef colored fill:#f96
    subgraph n1 [Node 1]
       a1:::colored
       ar1[a1]
    end
    subgraph n2 [Node 2]
        ar12[a1]
        ar13[a1]
    end

在大量寫入的情境下,這樣的設定就會造成單一節點的效能瓶頸,同時也會影響在這個節點上的每個操作。

在Elasticsearch 5.x版本之後開始支援熱溫架構,若是在有開啟熱溫架構的集群,那麼shard的數量應該要與熱節點的數量一致。

Tip #2:應該要對文件設定路由

讓我們繼續最上面的例子,假設是一個雙節點的集群,並且index A有兩個shard以及各1個replica。

flowchart TD
    u((User)) --??--> n1 & n2
    classDef colored fill:#f96
    subgraph n1 [Node 1]
       a1:::colored
       ar1[a1]
    end
    subgraph n2 [Node 2]
        a2:::colored
        ar2[a2]
    end

當使用者要寫入一個文件時,他會寫到哪個節點?

這會根據Elasticsearch的內建路由規則來決定,簡單的說,公式如下。

shard = hash(_id) % number_of_shards

_id是Elasticsearch自動派發的,當然也可以由使用者自行指定。至於hash的演算法則是murmur3,不是一致性雜湊,所以會算出一個特定的shard。

所以,最糟糕的情況會像是下面這張圖。

flowchart TD
    u1((User 1)) --bulk--> n1 & n2
    u2((User 2)) --search--> n1 & n2
    classDef colored fill:#f96
    subgraph n1 [Node 1]
       a1:::colored
       ar1[a1]
    end
    subgraph n2 [Node 2]
        a2:::colored
        ar2[a2]
    end

當有一個使用者開始進行批量操作,把大量的文件寫入集群內,同時,有一個使用在進行搜尋,那麼,這兩個使用者的操作就會互相干擾。

若是有一個辦法讓單一使用者的資料只會存放在單一節點,那麼就可以有效的避免上述案例。頂多某個使用者在大量寫入時,自己的搜尋效能被影響而已。這樣的作法稱為客製化路由。

flowchart TD
    u1((User 1)) --bulk--> n1
    u2((User 2)) --search--> n2
    classDef colored fill:#f96
    subgraph n1 [Node 1]
       a1:::colored
       ar1[a1]
    end
    subgraph n2 [Node 2]
        a2:::colored
        ar2[a2]
    end

Elasticsearch是可以讓讀寫操作加入一個routing參數,這樣剛才提到的公式就會變成:

shard = hash(routing) % number_of_shards

儘管如此,我們仍要注意會不會有hotspot問題,也就是資料過度集中在某些節點。我之前有寫一篇文章有關sharding的設計,有解釋該怎麼做比較好。

Tip #3:大量寫入時應該關閉replicarefresh

在進行大量寫入時,先暫停複製以減少資源消耗,等大量寫入完成後再開啟副本複製。這樣的作法可以大幅降低寫入的消耗並提昇整個集群的效能。

關閉replica算是容易理解,那關閉refresh又是怎麼回事?

在解釋之前,讓我們先來了解refresh的機制。

flowchart LR
    u((User)) --write--> buf[[Mem buffer]]
    
    subgraph Shard
        buf --refresh--> fs[(Lucene)]
    end
    
    u --search--> fs

當使用者對shard寫入資料時,首先會先寫入記憶體緩衝內,這時候的資料對於搜尋操作來說是不可見的。接著,Elasticsearch會將記憶體緩衝內的資料透過refresh寫入硬碟,並轉換成Lucene的形式,這時候才真的能夠搜尋。

refresh有兩種形式。

  1. Index設定內的refresh_interval,時間到了會自動觸發refresh
  2. 顯式調用Refresh API

在大量寫入時,我們本來就不會主動調用Refresh API,因此這裡提到的關閉是指將refresh_interval調長甚至關閉。

當然,無論是關閉replica或關閉refresh都是為了減少資源消耗以便讓全部的資源都投入到處理大量寫入。

那麼,你也許會問,關閉refresh難道不會導致資料遺失嗎?

不,不會。

原因在於,Elasticsearch的持久化模型不僅僅只依賴refresh。讓我們再深入一點。

flowchart TD
    u((User)) --> Shard
    subgraph Shard
        buf[[Mem buffer]] --refresh--> fs[(Lucene)] --flush--> disk[Disk]
        log[[Trans log]] --disater recovery--> disk
    end

從上面的圖我們可以發現,即便是從記憶體暫存寫入硬碟的過程也還沒真正落入硬碟,還需要經過flush才會真的被持久化。但這樣的路徑很長,對於資料庫來說完全不可靠,因此實際上在寫入資料時會同時寫到兩個地方:記憶體暫存以及交易記錄。

一旦發生災難,還是可以透過交易記錄來回復整個寫入的資料集。但這樣當然有代價,少了一道保險機制,若是交易記錄損毀,就真的會資料遺失了。但我相信這機率絕對是低的多了,而且損失的也只是這次大量寫入的資料而已。

結論

這次介紹了三種方法能夠讓Elasticsearch集群的效能獲得顯著提昇。

我必須說,在大資料領域下,光會使用各種資料儲存明顯是不夠的。若是不深入了解資料儲存的底層實作,那麼就極有可能發生資源浪費。這不僅僅會造成集群效能低落,更可能會造成硬體成本上升,因為要用更多硬體來解決這些負擔。

在這篇文章中,我們先簡單解釋了Elasticsearch背後的幾個名詞,接著透過這些知識進一步了解相關的優化技巧,我相信,透過這樣的流程你們也許可以挖掘出更多細節。

若是有什麼好用秘訣,也歡迎與我分享。