高效管理 Elasticsearch 中基於時間的索引

stormluke發表於2018-03-02

翻譯自:And the big one said "Rollover" — Managing Elasticsearch time-based indices efficiently

用 Elasticsearch 來索引諸如日誌事件等基於時間的資料的人可能已經習慣了“每日一索引”模式:使用以天為粒度的索引名字來存放當天的日誌資料,一天過去後再建一個新索引。新索引的屬性可以由索引模板來提前控制。

這種模式很容易理解並且易於實現,但是它粉飾了索引管理的一些複雜的地方:

  • 為了達到較高的寫入速度,活躍索引分片需要分佈在儘可能多的節點上。
  • 為了提高搜尋速度和降低資源消耗,分片數量需要儘可能地少,但是也不能有過大的單個分片進而不便操作
  • 一天一個索引確實易於清理陳舊資料,但是一天到底需要多少個分片呢?
  • 每天的寫入壓力是一成不變的嗎?還是一天分片過多,而下一天分片不夠用呢?

在這篇文章中我將介紹新的”滾動模式“和用來實現它的 API 們,這個模式可以更加簡單且高效地管理基於時間的索引。

滾動模式

滾動模式工作流程如下:

  • 有一個用於寫入的索引別名,其指向活躍索引
  • 另外一個用於讀取(搜尋)的索引別名,指向不活躍索引
  • 活躍索引具有和熱節點數量一樣多的分片,可以充分發揮昂貴硬體的索引寫入能力
  • 當活躍索引太滿或者太老的時候,它就會滾動:新建一個索引並且索引別名自動從老索引切換到新索引
  • 移動老索引到冷節點上並且縮小為一個分片,之後可以強制合併和壓縮。

入門

假設我們有一個具有 10 個節點和一個節點池的叢集。理想情況下我們的活躍索引(接收所有寫入的索引)應該在每個熱節點上均勻分佈一個分片,以此來儘可能地在多個機器上分散寫入壓力。

我們讓每個主分片都有一個複製分片來允許一個節點失效而不丟失資料。這意味著我們的活躍索引應該有 5 個主分片,加起來一共 10 個分片(每個節點一個)。我們也可以用 10 個主分片(包含冗餘一共 20 個分片),這樣每個節點兩個分片。

首先,為活躍索引建立一個索引模版

PUT _template/active-logs
{
  "template": "active-logs-*",
  "settings": {
    "number_of_shards": 5,
    "number_of_replicas": 1,
    "routing.allocation.include.box_type": "hot",
    "routing.allocation.total_shards_per_node": 2
  },
  "aliases": {
    "active-logs": {}, 
    "search-logs": {}
  }
}
複製程式碼

由這個模板建立的索引會被分配到標記為 box_type:hot 的節點上,而 total_shards_per_node 配置會保證將分片均勻分佈在節點中。我把其設定為 2 而不是 1,這樣當一個節點失效時也可以繼續分配分片。

我們將會用 active-logs 別名來寫入當前的活躍索引,用 search-logs 別名來查詢所有的日誌索引。

下面是非活躍索引的模板:

PUT _template/inactive-logs
{
  "template": "inactive-logs-*", 
  "settings": { 
    "number_of_shards": 1, 
    "number_of_replicas": 0,
    "routing.allocation.include.box_type": "cold",
    "codec": "best_compression"
  }
}
複製程式碼

歸檔的索引應該被分配到節點上並且使用 deflate 壓縮來節約磁碟空間。我會在之後解釋為什麼把 replicas 設定為 0

現在可以建立第一個活躍索引了:

PUT active-logs-1
複製程式碼

Rollover API 會將名字中的 -1 識別為一個計數器。

索引日誌事件

當建立 active-logs-1 索引時,我們也建立了 active-logs 別名。在此之後,我們應該僅使用別名來寫入,文件會被髮送到當前的活動索引:

POST active-logs/log/_bulk
{ "create": {}} { "text": "Some log message", "@timestamp": "2016-07-01T01:00:00Z" }
{ "create": {}} { "text": "Some log message", "@timestamp": "2016-07-02T01:00:00Z" }
{ "create": {}} { "text": "Some log message", "@timestamp": "2016-07-03T01:00:00Z" }
{ "create": {}} { "text": "Some log message", "@timestamp": "2016-07-04T01:00:00Z" }
{ "create": {}} { "text": "Some log message", "@timestamp": "2016-07-05T01:00:00Z" }
複製程式碼

滾動索引

在某個時間點,活躍索引變得過大或者過老,這時你想用一個新的空索引來替換它。Rollover API 允許你指定觸發滾動操作的具體大小或者時間限制。

多大才是過大?一如以往,看情況。這取決於你的硬體效能,你的搜尋操作的型別,你想要達到的效能效果和你能接受的分片恢復時間等等。可以從例如 1 億或者 10 億這種數字開始,依據搜尋效能、資料保留時間和可用磁碟空間來上下調整。

一個分片能包含的文件數有一個硬限制:2147483519。如果你打算把活躍索引縮小到一個分片,那麼活躍索引中的文件數不能超過 21 億。如果活躍索引中的文件一個分片放不下,你可以將活躍索引縮小到多個分片,只要目標分片數是原來分片數的因子,例如 6 到 3 或者 6 到 2。

基於時間來滾動索引很方便,因為可以按照小時、天或者星期來整理索引。但其實按照索引中的文件數來滾動索引更加高效。按照數量來滾動的優點之一就是所有的分片會具有大致相同的大小,這樣做負載均衡更加方便。

可以用定時任務來定期呼叫 rollover API 去檢查是否到達了 max_docs 或者 max_age 限制。當超過某個限制時,索引就會被滾動。因為我們在例子中只索引了 5 個文件,我們將 max_docs 值設定為 5,並且(為了完整性)將 max_age 設定為一週:

POST active-logs/_rollover
{
  "conditions": {
    "max_age": "7d",
    "max_docs": 5
  }
}
複製程式碼

這個請求告訴 Elasticsearch 去滾動 active-logs 別名指向的索引,如果這個索引至少在七天之前建立或者至少包含 5 個文件。應答如下:

{
  "old_index": "active-logs-1",
  "new_index": "active-logs-2",
  "rolled_over": true,
  "dry_run": false,
  "conditions": {
    "[max_docs: 5]": true,
    "[max_age: 7d]": false
  }
}
複製程式碼

因為滿足了 max_docs: 5 條件,active-logs-1 索引被滾動到 active-logs-2 索引。這意味著一個叫做 active-logs-2 的索引被建立(基於 active-logs 模板),並且 active-logs 別名從 active-logs-1 切換到 active-logs-2

順帶一提,如果你想覆寫索引模板中的某些值(例如 settings 或者 mappings),只需要把它們放在 _rollover 的請求體中就可以(和建立索引 API 一樣)。

為什麼不支援 max_size 限制?

既然想盡可能地讓分片大小相似,為什麼不在 max_docs 之外在加上支援 max_size 限制呢?答案是分片的大小並不是一個可靠的測量標準,因為正在進行中的合併會產生大量的臨時分片大小增長,而當合並結束後這些增長會消失掉。五個主分片,每個都在合併到一個 5GB 分片的過程中,那麼此時索引大小會臨時增多 25GB!而對於文件數量來說,它的增長則是可以預測的。

縮小索引

此時 acitve-logs-1 不再用於寫入,我們可以把它移到冷節點上並且把它縮小到一個分片,這個新索引叫做 inactive-logs-1。在縮小之前,我們必須:

  • 設定索引為只讀
  • 將所有分片移動到同一個節點上。可以任意選擇目標節點,比如選擇具有最大剩餘空間的節點

用以下命令來做這些事情:

PUT active-logs-1/_settings
{
  "index.blocks.write": true,
  "index.routing.allocation.require._name": "some_node_name"
}
複製程式碼

allocation 配置保證了每個分片的至少一個拷貝會被移動到 some_node_name 節點上。這並不會移動所有分片——因為複製分片不能和主分片分配在同一個節點上——但它會保證至少一個主分片或者複製分片會被移動。

當索引完成遷移後(用叢集健康 API 來檢查),使用一下請求來縮小索引:

POST active-logs-1/_shrink/inactive-logs-1
複製程式碼

如果你的檔案系統支援硬連結,那麼縮小會瞬間完成。如果你的檔案系統不支援硬連結,那你就得等待所有的分段檔案從一個索引拷貝到另一個索引……

你可以用 恢復狀態查詢 API 或者叢集健康 API 來監控縮小過程:

GET _cluster/health/inactive-logs-1?wait_for_status=yellow
複製程式碼

當縮小完成後,你就可以從 search-logs 別名中刪除老索引並加入新索引:

POST _aliases
{
  "actions": [
    {
      "remove": {
        "index": "active-logs-1",
        "alias": "search-logs"
      } 
    },
    {
      "add": {
        "index": "inactive-logs-1",
        "alias": "search-logs"
      }
    }
  ]
}
複製程式碼

節約空間

我們的索引已經縮小到單個分片,但它依舊包含和之前相同數量的段檔案,並且 best_compression 設定並沒有生效,因為沒有任何寫入操作。我們可以用強制合併來將單分片索引優化為單分段索引,如下:

POST inactive-logs-1/_forcemerge?max_num_segments=1
複製程式碼

這個請求會建立一個新的分段來替換之前的多個分段。並且因為 Elasticsearch 必須要寫入新分段,best_compression 設定就會起作用,新分段會用 deflate 壓縮寫入。

在主分片和複製分片上分別執行強制合併是沒有意義的,這就是為什麼我們的非活躍索引模板中 number_of_replicas 設定被為 0。現在當強制合併結束後,我們可以開啟復制分片以獲得冗餘:

PUT inactive-logs-1/_settings
{ "number_of_replicas": 1 }
複製程式碼

當複製分片被分配之後(用 ?wait_for_status=green API 查詢),我們就可以確定擁有了一個冗餘,此時便可以安全地刪掉 active-logs-1 索引:

DELETE active-logs-1
複製程式碼

刪除舊索引

在使用老的每日一索引模式時,決定刪除哪些索引十分方便。而在使用滾動模式時,似乎並不好確定索引包含了什麼時間段的資料。

幸運的是,欄位統計 API 可以輕鬆確定這些。我們只需要具有找出超過我們閾值的最大 @timestamp 欄位的索引列表就可以了:

GET search-logs/_field_stats?level=indices
{
  "fields": ["@timestamp"],
  "index_constraints": {
    "@timestamp": {
      "max_value": {
        "lt": "2016/07/03",
        "format": "yyyy/MM/dd"
      }
    }
  }
}
複製程式碼

這個請求返回的索引都可以刪除。

未來的改進

通過滾動縮小強制合併欄位統計 API,我們向你提供了高效管理基於時間的索引的基礎工具。

當然,這裡有許多步驟可以被自動化來讓生活更美好。這些步驟並在 Elasticsearch 中並不是很容易內建,因為我們需要在發生意料之外的情況時通知別人。這是在 Elasticsearch 之上構建的工具或應用程式的職責。

期待可以在 Curator index management toolX-Pack 中看到相應的工作流和 UI。

相關文章