Elasticsearch 索引容量管理實踐

騰訊技術工程發表於2020-07-21

Elasticsearch 是目前大資料領域最熱門的技術棧之一,騰訊雲 Elasticsearch Service(ES)是基於開源搜尋引擎 Elasticsearch 打造的高可用、可伸縮的雲端全託管 Elasticsearch 服務,完善的高可用解決方案,讓業務可以放心的把重要資料儲存到騰訊雲 ES 中。

瞭解 ES 的索引管理方法有助於揚長避短,更好的利用 ES 的強大功能,特別是當遇到效能問題時,原因通常都可回溯至資料的索引方式以及叢集中的分片數量。如果未能在一開始做出最佳選擇,隨著資料量越來越大,便有可能會引發效能問題。叢集中的資料越多,要糾正這一問題就越難,本文旨在幫助大家瞭解 ES 容量管理的方法,在一開始就管理好索引的容量,避免給後面留坑。

1. 為什麼要做索引容量管理

  • 在生產環境使用 ES 要面對的第一個問題通常是索引容量的規劃,不合理的分片數,副本數和分片大小會對索引的效能產生直接的影響;
  • Elasticsearch 中的每個索引都由一個或多個分片組成的,每個分片都是一個 Lucene 索引例項,您可以將其視作一個獨立的搜尋引擎,它能夠對 Elasticsearch 叢集中的資料子集進行索引並處理相關查詢;
  • 查詢和寫入的效能與索引的大小是正相關的,所以要保證高效能,一定要限制索引的大小,具體來說是限制分片數量和單個分片的大小;
  • 關於分片數量,索引大小的問題這裡不再贅述,可以參考 ES 官方 blog 《我在 Elasticsearch 叢集內應該設定多少個分片?》
  • 直接說結論:ES 官方推薦分片的大小是 20G - 40G,最大不能超過 50G。

本文介紹 3 種管理索引容量的方法,從這 3 種方法可以瞭解到 ES 管理索引容量的演進過程:

2. 方法 1: 使用在索引名稱上帶上時間的方法管理索引

2.1 建立索引

索引名上帶日期的寫法:

<static_name{date_math_expr{date_format|time_zone}}>

日期格式就是 java 的日期格式:

yyyy:年
MM:月
dd:日
hh:1~12小時制(1-12)
HH:24小時制(0-23)
mm:分
ss:秒
S:毫秒
E:星期幾
D:一年中的第幾天
F:一月中的第幾個星期(會把這個月總共過的天數除以7)
w:一年中的第幾個星期
W:一月中的第幾星期(會根據實際情況來算)
a:上下午標識
k:和HH差不多,表示一天24小時制(1-24)。
K:和hh差不多,表示一天12小時制(0-11)。
z:表示時區

參考官方文件:Date math support in index names

例如:

<logs-{now{yyyyMMddHH|+08:00}}-000001>

在使用的時候,索引名要 urlencode 後再使用:

PUT /%3Cmylogs-%7Bnow%7ByyyyMMddHH%7C%2B08%3A00%7D%7D-000001%3E{  "aliases": {  "mylogs-read-alias": {}  }}

執行結果:

{
  "acknowledged" : true,
  "shards_acknowledged" : true,
  "index" : "mylogs-2020061518-000001"
}

2.2 寫入資料

寫入資料的時候也要帶上日期:

POST /%3Cmylogs-%7Bnow%7ByyyyMMddHH%7C%2B08%3A00%7D%7D-000001%3E/_doc
{"name":"xxx"}

執行結果:

{
  "_index" : "mylogs-2020061518-000001",
  "_type" : "_doc",
  "_id" : "VNZut3IBgpLCCHbxDzDB",
  "_version" : 1,
  "result" : "created",
  "_shards" : {
    "total" : 2,
    "successful" : 2,
    "failed" : 0
  },
  "_seq_no" : 0,
  "_primary_term" : 1
}

2.3 查詢資料

由於資料分佈在多個索引裡,查詢的時候要在符合條件的所有索引查詢,可以使用下面的方法查詢。

2.3.1 使用逗號分割指定多個索引
GET /mylogs-2020061518-000001,mylogs-2020061519-000001/_search
{"query":{"match_all":{}}}
2.3.2 使用萬用字元查詢
GET /mylogs-*/_search
{
  "query": {
    "match_all": {}
  }
}

執行結果:

{
  "took" : 0,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "mylogs-2020061518-000001",
        "_type" : "_doc",
        "_id" : "VNZut3IBgpLCCHbxDzDB",
        "_score" : 1.0,
        "_source" : {
          "name" : "xxx"
        }
      }
    ]
  }
}
2.3.3 使用別名查詢
GET /mylogs-read-alias/_search
{
  "query": {
    "match_all": {}
  }
}

執行結果同上。

2.4 使用帶日期的索引名稱的缺陷

這個方法的優點是比較直觀能夠透過索引名稱直接分辨出資料的新舊,缺點是:

  • 不是所有資料都適合使用時間分割,對於寫入之後還有修改的資料不適合;
  • 直接使用時間分割也可能存在某段時間資料量集中,導致索引分片超過設計容量的問題,從而影響效能;
  • 為了解決上述問題還需要配合 rollover 策略使用,索引的維護比較複雜。

3. 方法 2: 使用 Rollover 管理索引

Rollover 的原理是使用一個別名指向真正的索引,當指向的索引滿足一定條件(文件數或時間或索引大小)更新實際指向的索引。

3.1 建立索引並且設定別名

注意: 索引名稱的格式為 {.*}-d 這種格式的,數字預設是 6 位:

PUT myro-000001
{
  "aliases": {
    "myro_write_alias":{}
  }
}

3.2 透過別名寫資料

使用 bulk 一次寫入了 3 條記錄:

POST /myro_write_alias/_bulk?refresh=true
{"create":{}}
{"name":"xxx"}
{"create":{}}
{"name":"xxx"}
{"create":{}}
{"name":"xxx"}

執行結果:

{
  "took" : 37,
  "errors" : false,
  "items" : [
    {
      "create" : {
        "_index" : "myro-000001",
        "_type" : "_doc",
        "_id" : "wVvFtnIBUTVfQxRWwXyM",
        "_version" : 1,
        "result" : "created",
        "forced_refresh" : true,
        "_shards" : {
          "total" : 2,
          "successful" : 2,
          "failed" : 0
        },
        "_seq_no" : 0,
        "_primary_term" : 1,
        "status" : 201
      }
    },
    {
      "create" : {
        "_index" : "myro-000001",
        "_type" : "_doc",
        "_id" : "wlvFtnIBUTVfQxRWwXyM",
        "_version" : 1,
        "result" : "created",
        "forced_refresh" : true,
        "_shards" : {
          "total" : 2,
          "successful" : 2,
          "failed" : 0
        },
        "_seq_no" : 1,
        "_primary_term" : 1,
        "status" : 201
      }
    },
    {
      "create" : {
        "_index" : "myro-000001",
        "_type" : "_doc",
        "_id" : "w1vFtnIBUTVfQxRWwXyM",
        "_version" : 1,
        "result" : "created",
        "forced_refresh" : true,
        "_shards" : {
          "total" : 2,
          "successful" : 2,
          "failed" : 0
        },
        "_seq_no" : 2,
        "_primary_term" : 1,
        "status" : 201
      }
    }
  ]
}

記錄都寫到了 myro-000001 索引下。

3.3 執行 rollover 操作

rollover 的 3 個條件是並列關係,任意一個條件滿足就會發生 rollover:

POST /myro_write_alias/_rollover
{
  "conditions": {
    "max_age":   "7d",
    "max_docs":  3,
    "max_size": "5gb"
  }
}

執行結果:

{
  "acknowledged" : true,
  "shards_acknowledged" : true,
  "old_index" : "myro-000001",
  "new_index" : "myro-000002",
  "rolled_over" : true,
  "dry_run" : false,
  "conditions" : {
    "[max_docs: 3]" : true,
    "[max_size: 5gb]" : false,
    "[max_age: 7d]" : false
  }
}

分析一下執行結果:

 "new_index" : "myro-000002"
 "[max_docs: 3]" : true,

從結果看出滿足了條件("[max_docs: 3]" : true)發生了 rollover,新的索引指向了 myro-000002。

再寫入一條記錄:

POST /myro_write_alias/_doc
{"name":"xxx"}

已經寫入了新的索引,結果符合預期:

{
  "_index" : "myro-000002",
  "_type" : "_doc",
  "_id" : "BdbMtnIBgpLCCHbxhihi",
  "_version" : 1,
  "result" : "created",
  "_shards" : {
    "total" : 2,
    "successful" : 2,
    "failed" : 0
  },
  "_seq_no" : 0,
  "_primary_term" : 1
}

3.4 使用 Rollover 的缺點

  • 必須明確執行了 rollover 指令才會更新 rollover 的別名對應的索引;
  • 通常可以在寫入資料之後 再執行一下 rollover 命令,或者採用配置系統 cron 指令碼的方式;
  • 增加了使用的 rollover 的成本,對於開發者來說不夠自動化。

4. 方法 3: 使用 ILM(Index Lifecycle Management ) 管理索引

ES 一直在索引管理這塊進行最佳化迭代,從 6.7 版本推出了索引生命週期管理(Index Lifecycle Management ,簡稱 ILM)機制,是目前官方提供的比較完善的索引管理方法。所謂 Lifecycle(生命週期)是把索引定義了四個階段:

Elasticsearch 索引容量管理實踐
lifecycle
  • Hot:索引可寫入,也可查詢,也就是我們通常說的熱資料,為保證效能資料通常都是在記憶體中的;
  • Warm:索引不可寫入,但可查詢,介於熱和冷之間,資料可以是全記憶體的,也可以是在 SSD 的硬碟上的;
  • Cold:索引不可寫入,但很少被查詢,查詢的慢點也可接受,基本不再使用的資料,資料通常在大容量的磁碟上;
  • Delete:索引可被安全的刪除。

這 4 個階段是 ES 定義的一個索引從生到死的過程, Hot -> Warm -> Cold -> Delete 4 個階段只有 Hot 階段是必須的,其他 3 個階段根據業務的需求可選。

使用方法通常是下面幾個步驟:

4.1 建立 Lifecycle 策略

這一步通常在 Kibana 上操作,需要的時候再匯出 ES 語句,例如下面這個策略:

Elasticsearch 索引容量管理實踐
在 Kibina 中建立 lifecycle 策略
  • 暫時只配置了 Hot 階段;
  • 為了方便驗證,最大文件數(max_docs) 超過 2 個時就 rollover。

匯出的語句如下:

PUT _ilm/policy/myes-lifecycle
{
  "policy": {
    "phases": {
      "hot": {
        "min_age": "0ms",
        "actions": {
          "rollover": {
            "max_age": "30d",
            "max_size": "50gb",
            "max_docs": 2
          },
          "set_priority": {
            "priority": 100
          }
        }
      }
    }
  }
}

4.2 建立索引模版

ES 語句如下:

PUT /_template/myes_template
{
  "index_patterns": [
    "myes-*"
  ],
  "aliases": {
    "myes_reade_alias": {}
  },
  "settings": {
    "index": {
      "lifecycle": {
        "name": "myes-lifecycle",
        "rollover_alias": "myes_write_alias"
      },
      "refresh_interval": "30s",
      "number_of_shards": "12",
      "number_of_replicas": "1"
    }
  },
  "mappings": {
    "properties": {
      "name": {
        "type": "keyword"
      }
    }
  }
}

:warning:注意:

  • 模版匹配以索引名稱 myes- 開頭的索引;
  • 所有使用此模版建立的索引都有一個別名 myes_reade_alias 用於方便查詢資料;
  • 模版繫結了上面建立的 Lifecycle 策略,並且用於 rollover 的別名是 myes_write_alias。

4.3 建立索引

ES 語句:

PUT /myes-testindex-000001
{
  "aliases": {
    "myes_write_alias":{}
  }
}

:warning:注意:

  • 索引的名稱是 .*-d 的形式;
  • 索引的別名用於 lifecycle 做 rollover。

4.4 檢視索引配置

GET /myes-testindex-000001
{}

執行結果:

{
  "myes-testindex-000001" : {
    "aliases" : {
      "myes_reade_alias" : { },
      "myes_write_alias" : { }
    },
    "mappings" : {
      "dynamic_templates" : [
        {
          "message_full" : {
            "match" : "message_full",
            "mapping" : {
              "fields" : {
                "keyword" : {
                  "ignore_above" : 2048,
                  "type" : "keyword"
                }
              },
              "type" : "text"
            }
          }
        },
        {
          "message" : {
            "match" : "message",
            "mapping" : {
              "type" : "text"
            }
          }
        },
        {
          "strings" : {
            "match_mapping_type" : "string",
            "mapping" : {
              "type" : "keyword"
            }
          }
        }
      ],
      "properties" : {
        "name" : {
          "type" : "keyword"
        }
      }
    },
    "settings" : {
      "index" : {
        "lifecycle" : {
          "name" : "myes-lifecycle",
          "rollover_alias" : "myes_write_alias"
        },
        "refresh_interval" : "30s",
        "number_of_shards" : "12",
        "translog" : {
          "sync_interval" : "5s",
          "durability" : "async"
        },
        "provided_name" : "myes-testindex-000001",
        "max_result_window" : "65536",
        "creation_date" : "1592222799955",
        "unassigned" : {
          "node_left" : {
            "delayed_timeout" : "5m"
          }
        },
        "priority" : "100",
        "number_of_replicas" : "1",
        "uuid" : "tPwDbkuvRjKtRHiL4fKcPA",
        "version" : {
          "created" : "7050199"
        }
      }
    }
  }
}

:warning:注意:

  • 索引使用了之前建立的索引模版;
  • 索引繫結了 lifecycle 策略並且寫入別名是 myes_write_alias。

4.5 寫入資料

POST /myes_write_alias/_bulk?refresh=true
{"create":{}}
{"name":"xxx"}
{"create":{}}
{"name":"xxx"}
{"create":{}}
{"name":"xxx"}

執行結果:

{
  "took" : 18,
  "errors" : false,
  "items" : [
    {
      "create" : {
        "_index" : "myes-testindex-000001",
        "_type" : "_doc",
        "_id" : "jF3it3IBUTVfQxRW1Xys",
        "_version" : 1,
        "result" : "created",
        "forced_refresh" : true,
        "_shards" : {
          "total" : 2,
          "successful" : 2,
          "failed" : 0
        },
        "_seq_no" : 0,
        "_primary_term" : 1,
        "status" : 201
      }
    },
    {
      "create" : {
        "_index" : "myes-testindex-000001",
        "_type" : "_doc",
        "_id" : "jV3it3IBUTVfQxRW1Xys",
        "_version" : 1,
        "result" : "created",
        "forced_refresh" : true,
        "_shards" : {
          "total" : 2,
          "successful" : 2,
          "failed" : 0
        },
        "_seq_no" : 0,
        "_primary_term" : 1,
        "status" : 201
      }
    },
    {
      "create" : {
        "_index" : "myes-testindex-000001",
        "_type" : "_doc",
        "_id" : "jl3it3IBUTVfQxRW1Xys",
        "_version" : 1,
        "result" : "created",
        "forced_refresh" : true,
        "_shards" : {
          "total" : 2,
          "successful" : 2,
          "failed" : 0
        },
        "_seq_no" : 0,
        "_primary_term" : 1,
        "status" : 201
      }
    }
  ]
}

:warning:注意:

  • 3 條記錄都寫到了 myes-testindex-000001 中, Lifecycle 策略明明設定的是 2 條記錄就 rollover 為什麼會三條都寫到同一個索引了呢?

再次執行上面的語句,寫入 3 條記錄發現新的資料都寫到了 myes-testindex-000002 中, 結果符合預期。

{
  "took" : 17,
  "errors" : false,
  "items" : [
    {
      "create" : {
        "_index" : "myes-testindex-000002",
        "_type" : "_doc",
        "_id" : "yl0JuHIBUTVfQxRWvsv5",
        "_version" : 1,
        "result" : "created",
        "forced_refresh" : true,
        "_shards" : {
          "total" : 2,
          "successful" : 2,
          "failed" : 0
        },
        "_seq_no" : 0,
        "_primary_term" : 1,
        "status" : 201
      }
    },
    {
      "create" : {
        "_index" : "myes-testindex-000002",
        "_type" : "_doc",
        "_id" : "y10JuHIBUTVfQxRWvsv5",
        "_version" : 1,
        "result" : "created",
        "forced_refresh" : true,
        "_shards" : {
          "total" : 2,
          "successful" : 2,
          "failed" : 0
        },
        "_seq_no" : 0,
        "_primary_term" : 1,
        "status" : 201
      }
    },
    {
      "create" : {
        "_index" : "myes-testindex-000002",
        "_type" : "_doc",
        "_id" : "zF0JuHIBUTVfQxRWvsv5",
        "_version" : 1,
        "result" : "created",
        "forced_refresh" : true,
        "_shards" : {
          "total" : 2,
          "successful" : 2,
          "failed" : 0
        },
        "_seq_no" : 0,
        "_primary_term" : 1,
        "status" : 201
      }
    }
  ]
}

:warning:注意:如果按照這個步驟沒有發生自動 rollover 資料仍然寫到了 myes-testindex-000001 中,需要 配置 Lifecycle 自動 Rollover 的時間間隔, 參考下文。

4.6 配置 Lifecycle 自動 Rollover 的時間間隔

  • 由於 ES 是一個準實時系統,很多操作都不能實時生效;
  • Lifecycle 的 rollover 之所以不用每次手動執行 rollover 操作是因為 ES 會隔一段時間判斷一次索引是否滿足 rollover 的條件;
  • ES 檢測 ILM 策略的時間預設為 10min。

修改 Lifecycle 配置:

PUT _cluster/settings
{
  "transient": {
    "indices.lifecycle.poll_interval": "3s"
  }
}

5. ES 在 QQ 家校群作業統計功能上的實踐

疫情期間線上教學需求爆發,QQ 的家校群功能也迎來了一批發展紅利,家校群的作業功能可以輕鬆在 QQ 群裡實現作業佈置,提交,批改等功能,深受師生們的喜愛。

5.1 使用場景簡介

近期推出的作業統計功能,可以指定時間段+指定科目動態給出排名,有效提高了學生答題的積極性。在功能的實現上如果用傳統的 SQL + KV 的方式實現成本比較高,要做到高效能也需要花不少精力,藉助 ES 強大的統計聚合能力大大降低了開發成本,實現了需求的快速上線。

Elasticsearch 索引容量管理實踐

Elasticsearch 索引容量管理實踐


5.2 申請資源

  • ES 版本:7.5.1
  • 高階特性:騰訊雲 ES 白金版
  • 單節點容量:1000GB
  • 節點數:3
  • 總容量:3000GB

5.3 索引使用方案

  • 按群尾號 % 100 把資料分為 100 個索引
  • 每個索引 12 個分片
  • 每 40000W(120GB)發生一次 Rollover
  • 單個分片最大大小 10GB

5.4 實際耗時情況

  • 插入:~ 25ms
  • 更新:~ 15ms
  • 聚合:200ms 以內
Elasticsearch 索引容量管理實踐


相關文章