ElasticSearch 索引設計指南

  1. 何爲 ES 索引設計

ElasticSearch(以下簡稱:ES )是一個分佈式、RESTful 風格的搜索和數據分析引擎。區別於傳統關係型數據庫(比如:MySQL、Oracle 等),ES 在定義數據模型和搜索方式上非常靈活,數據模型可以採用靜態數據映射與動態數據映射,搜索方式也支持多維度類型的搜索(結構化數據、非結構化數據、地理位置、指標參數等)。

如果對事務性要求不高或者事務性操作在 RDMS 端,可以考慮使用 ES 作爲海量數據存儲與檢索分析的平臺。用戶在創建新索引時,可以直接寫數據,所有配置走默認,收縮自如、十分靈活;也可以精細化評估,按照自己的業務需求,提前做好索引的 settings 和 mappings 的設置,未雨綢繆、盡在掌握。

針對 ES 索引設計還是有一些問題困擾着我們:

1.1 ES 索引設計

ES 索引設計主要是根據在新索引數據寫入前,提前手動創建索引的配置與數據結構,以達到極致的穩定性和資源利用的最大化。一個完備的 ES 索引設計,通常會涉及以下幾個點:

1.2 ES 索引設計可能遇到的問題

如果採用的是索引動態創建,使用的將是默認的配置。對於數據量大,業務複雜的系統,可能會出現如下問題:

分片數無法修改,數據膨脹後,導致無法使用: ES 7.x 後默認分片數爲 1 ,而單個分片的存儲 doc 數爲 2,147,483,519 。超過則無法寫入。而分片數設置後,無法修改,只能通過 reindex 進行調整,但是會有很大的性能隱患和時間成本。

影響寫入性能: 對於日誌類的寫多讀少的業務類型,沒必要按照默認的每秒進行 refresh 刷新,可以根據業務情況,調整爲 60s 甚至 120s ,這樣可以大大提升寫入性能。

影響恢復性能: 當集羣有節點 left 集羣的時候,集羣需要對 unassigned shards 進行恢復,如果是默認的配置,則會在 1min 後進行恢復(將宕機節點的 shards 在集羣的副本進行恢復),這樣會大大提升集羣節點之間的 IO 操作,當本節點加入進來後,還會繼續進行大量的節點間 shards 同步,會大大影響恢復進度,繼而影響集羣性能。

如果是調高 "index.unassigned.node_left.delayed_timeout" 這個參數,集羣則會在一段時間內不做分配,在宕機節點重啓後,由宕機節點本機磁盤數據進行恢復,在大型集羣調優中,能提到數倍的恢復差異。

數據結構無法更改:  ES 的字段類型一旦創建無法更改,當動態映射的類型不滿足業務需求時,則會出現隱患。

舉例,下圖是動態創建寫入數據:

以上數據對應的動態映射的 settings 如下:

可以看到如果直接動態映射,很多索引級別和 mapping 級別的設置都不可控。

而靜態映射,在設置的時候就能把握很多屬性細節:

  1. 索引 Settings 設計

索引的 Settings 部分主要針對的是索引和分片層面的設置,重點討論的是索引的分片大小與分片數的評估(涉及到存儲評估設置)、索引恢復設置與性能優化設置。相信瞭解了這些可以對索引的 Settings 設置更加遊刃有餘。

要點:索引大小預估、分片大小設計、shards_per_nodes、refresh、flush 設置、node_left(delayed_timeout)、replicas

2.1 存儲評估設置

在創建索引前,應該做一個短暫的評估,重點在於索引的業務類型存儲週期分片配置這幾塊。

2.1.1 業務類型

一般日誌類的數據是寫多讀少,壓力主要在寫入端,且數據量很大。索引通常以日期爲單位(常見的有按天、也有按周的),比如 log_appcode-yyyy.mm.dd 或者 log_appcode-yyyy.ww。

對於寫入量特別大的索引,建議使用 rollover 進行索引滾動創建管理。這樣能保證單個索引存儲量可控,單個分片存儲也可控。動態索引的使用效果如下:

log_appcode-2021-10-01-000001
log_appcode-2021-10-01-000002
log_appcode-2021-10-01-000003

除日誌類外,很多業務類的數據是寫少讀多,也即數據產生並不是週期性的,可以根據業務和壓測進行數據量來進行評估。

對於查詢量大的索引,應保證查詢性能優先,可以設置儘可能多的分片數,以保證請求平攤提升性能。

2.1.1.1  ollover 操作實踐

a. 創建需要 rollover 的索引,並設置對應的別名 alias

PUT log_xxx-20211020-000001
{
  "aliases"{
    "log_xxx-alias"{
      "is_write_index"true
   }
 }
}

b. 向別名批量插入若干數據

PUT log_xxx-alias/_bulk
{"index":{"_id":1}}
{"name":"zhangsan"}
{"index":{"_id":2}}
{"name":"lisi"}
{"index":{"_id":3}}
{"name":"wangwu"}
{"index":{"_id":4}}
{"name":"zhaoliu"}
{"index":{"_id":5}}
{"name":"qinqi"}

c. 調用 rollover api ,觸發調整邏輯,設置三個維度,其中 max_docs 生效

POST log_xxx-alias/_rollover
{
  "conditions"{
    "max_age""7d",
    "max_docs": 5,
    "max_size""5gb"
 }
}

運行結果:

d. 繼續插數據,可以看出數據已經寫到新的 index 中

PUT log_xxx-alias/_bulk
{"index":{"_id":6}}
{"title":"testing 06"}

運行結果:

以上 rollover 的流程可以用如下圖展示:

2.1.2 存儲週期

存儲週期決定了索引的存儲量上限,用戶可以在創建前評估存儲週期,可參考業務類型中的來一併評估。

2.1.3 分片配置

分片:分片( shard )是 ES 中存儲數據的文件塊,也是 ES 數據存儲的最小單位。通常一個索引會分爲多個分片存儲,不同分片存儲的數據不同。分片可以被放置在任何節點上。

分片主要有以下兩種作用:

分片也有侷限:一旦創建了分片數,無法更改。 因爲數據寫入時,每個 doc 都會進行路由,將 routing 的 hash 值與分片數取模來確定 doc 對應的分片。如果更改分片數,則無法確定 doc 對應的分片。算法很簡單,如下:

shard = hash(routing)%number_of_primary_shards

分片數設置主要調整如下參數:

Elastic 官方推薦的單分片大小存儲控制在 20GB~40GB 之間,不要超過 50GB 。如果單分片存儲量超出範圍過大,會出現如下問題:

基於上述,分片數的設置是索引設計的重中之重。根據實戰經驗與官方論壇,總結出幾點設計思路:

2.1.4 副本設置

副本( replicas )主要用來提供集羣的高可用。

副本的缺點:影響集羣寫入性能(寫入的時候會主副分片都寫入數據)與提升一倍的存儲成本副本的優點:提升查詢性能(查詢的時候可以主、副分片並行執行)

一般評估副本的數量,需要評估業務的場景和集羣節點規模。建議:普通業務 1 個副本即可,安全等級高、重要性大的,可適當增加副本數。

副本數設置主要調整如下參數:

例如,ES 7.x 中的.security 索引是用來保存認證賬號信息的,極爲重要,集羣內部設置的副本就非常多。公司內部的一個日誌大集羣,管理公司所有實時日誌,其中 .security 保存全員賬號信息。設置的副本就爲 9 。

但是如果要保證高可用,一定要保證最少爲 1

副本設置爲 0 ,可能會出現以下問題:

分片和副本設置參考如下:

PUT /index/_settings
{
  "settings"{
    "index.number_of_shards""10",
    "index.number_of_replicas""1"
 }
}

2.2 索引恢復設置

索引恢復是一個複雜的過程,通常如果有節點 left 集羣后,會在一分鐘後,集羣開始進行恢復。

恢復期間主要會根據分片元數據信息,其餘節點對應的宕機節點的分片,置這些分片爲主分片,同時進行副本分片數據傳輸和同步,整體過程會消耗大量的 IO 資源,影響集羣穩定性。

且當宕機節點重啓後,集羣還會進行 rebalance shard ,進行進一步的平衡分配分片。整體而言時間很長,苦不堪言。

index.unassigned.node_left.delayed_timeout : 節點脫離集羣后多久分配 unassigned shards(默認 1min)

可以調整上述參數來保證節點恢復期間,不要進行 unassigned 的分配。可以根據索引的存儲量、分片數爲不同的索引進行設置,如下的設置就能保證節點 left 集羣 5 分鐘內不進行分配:

PUT /index/_settings
{
  "settings"{
    "index.unassigned.node_left.delayed_timeout""5m"
 }
}

2.3 性能優化設置

索引 Settings 中有很多參數可以直接影響到集羣的讀寫性能,調整到位則事半功倍,一招調錯也可能影響全局。

一般來說,集羣默認的配置對性能影響不大,但是遇到數據海量的集羣可能會出現一些問題,需要調整相關的參數來提高集羣的穩定性和讀寫性能。

文中選取實踐中使用最頻繁的,效果最佳的兩個參數:

total_shards_per_node 參數主要影響的是集羣寫入的情況,當一個索引在同一個節點上有多個分片時,海量寫入時會極大地飆高此節點的 CPU 佔用和 Load 使用量。可以根據節點數、分片數調整此參數。例如:

比如數據節點有 10 個,索引分片數有 8 個,副本數 1 個。則設置爲 2 則可。

比如數據節點有 20 個,索引分片數有 5 個,副本數 1 個。則設置爲 1 則可。

refresh_interval 參數也是影響寫入的性能,默認的 1s 屬於近實時的檢索,對於實時性要求高的可以適用。

但是對於寫多讀少的日誌類數據則不適合,集羣要在每秒鐘都要生成一個 Segment ,非常耗費資源,影響寫入性能。推薦根據業務使用頻次來設置此參數:

如果可以接受一定時間的查詢延遲,則可以適當調大 refresh_interval 。

我們這邊 nginx 的日誌量很大,就單獨做了設置,total_shards_per_node 設置爲 1、refresh_interval 設置爲 120s 。參考如下:

  1. 索引 Mapping 設計

Mapping 是定義文檔以及其包含的字段如何存儲和索引的。Mapping 可以用來做如下定義:

3.1 索引 Mapping 存儲概要

索引映射(Mapping) 分爲動態映射和靜態映射。其中動態映射指數據寫入 ES 索引時進行動態映射匹配。

如果不確定寫入的數據字段或者類型,可以使用動態映射,等業務穩定後,視情況調整索引的 mapping 即可。

動態映射適合新字段和不確定類型的字段映射,官網推薦了兩種方式來擴展和完善動態映射規則:

動態字段映射保證了基本的字段匹配規則,只要是 "dynamic" : "true"(默認),就會開啓字段映射,匹配規則如下:

動態模板可以爲字段動態添加各種映射規則,比如可以匹配到包含特定字符的字段進行數據類型映射。

也可以直接根據 match_mapping_type 匹配到的數據字段進行映射。適合於特定類型的映射,可以作爲靜態 mapping 的有效補充。

3.2 索引 Mapping 設計流程

設置字段設計的時候,可以參考幾點來進行:

根據以上幾點基本可以最準確的定義字段的映射關係與存儲結構,達到資源的利用最大化。

3.3 Mappings 設計關鍵性參數

Mapping 設置中,除了數據類型,還有一些參數屬性可以設置,挑選出核心影響的幾個,整理如下:

以上的表格是截取了生產實踐中使用頻率較多的參數,其餘更多的參數配置,可以參考官網:

https://www.elastic.co/guide/en/elasticsearch/reference/7.7/mapping-params.html

3.4 Mapping 的注意事項

如果做動態映射,可能會出現使用過程中字段類型衝突的情況,此時只能 reindex 進行重新重置索引,並進行數據同步。如果數據量特別大的情況, reindex 期間會極大地影響集羣性能。一定要慎重。

  1. 複雜類型的設計

4.1 寬表模式

不同於 MySQL 類的傳統關係型數據庫,ES 對於多表關聯查詢不佔優勢。但是由於其靈活擴展的字段設計,可以採用寬表模式來有效的解決。

比如:一個用戶發表多篇旅行遊記,存儲遊記列表,設計對比如下:

按照 MySQL 建表思維:需要創建兩個表(用戶表、博客內容表),通過 userid 進行關聯;

按照 ES 索引思維:可以創建遊記索引,在每個遊記內容中攜帶用戶信息即可。內容有所增加,但是一次檢索就可。

寬表模式:顧名思義,就是字段很多的數據表,通常是指業務相關的各指標、維度、屬性關聯在一起的一張數據表。寬表包含維度層次比較多,也容易造成冗餘,但是有利於進行海量數據提取與分析,廣泛應用於 olap 領域。

舉例:ES 中游記索引設計如下:

4.2 Nested 類型

nested 類型屬於 ES 的對象類型,允許存儲複雜嵌套的對象模型。nested 的出現提供了對象類型數組中的獨立查詢,保證了讀取精度。

nested 可以保證數組中每個對象的獨立性。nested 缺陷也比較明顯,因爲更新子文檔需要更新整個對象,所以更新效率低。

4.3 父子文檔類型

在 ES 6.x 版本後父子文檔使用 join 來替代。父子文檔解決了 nested 的子文檔不能獨立更新的問題,保證了父與子更新的獨立性。比較適合 1 個父對多個子,且子文檔更新頻繁的場景。

父子文檔更新便捷,但是由於內部維護了父子的複雜關聯關係,需要佔據更多內存,查詢性能較之 nested 也較低。

Nested 和父子文檔都屬於文檔嵌套的模型,但是使用方式和使用場景各有不同,詳細對比如下:

  1. 索引設計實戰

本章節筆者通過生產的實時日誌集羣(簡稱:日誌集羣)的真實案例來剖析在生產中是如何使用上述的設計架構要點進行實戰的。

重點圍繞索引的生命週期管理、分片的維護與評估、templates 的靈活設計運用,以及實際的一個設計後的生產情況。力求給讀者一個更直觀的綜合感受。

5.1 索引生命週期管理

索引生命週期管理通常指 ES 的各索引的存儲週期管理維護,是維護 ES 集羣穩定,存儲成本,分片管理的重要方案。

日誌集羣採用了自研的方案,構成了 “申請 + 維護 + 清理” 三位一體的生命週期管理方案。下面詳細介紹一下:

審批過後,該 appcode 對應的索引生命週期自動變成新的週期。

注意:由於每週需要刪除的索引衆多(大約 900~1000),不能一次刪除,需要一個一個刪除,並且中間進行 3-5s 的休眠。

清理過程中,可以觀察集羣的 pending_tasks 是否飆高,集羣 load 是否變高,來保障穩定性。可以選擇集羣使用率低的時間段來進行索引的清理操作。

5.2 索引 settings 設計

日誌集羣中索引 settings 設計主要需要考慮的點有:分片的週期評估與調整、性能優化的整體調整(主要包括 total_shards_per_node 、refresh)、索引的提前創建。

5.2.1 分片週期調整

每週會有 airflow 定時任務進行索引分片評估計算。具體的邏輯如下:

a. 拿到本週的各 appcode 對應索引的主分片存儲量,按照分片 SIZE (40G)來計算出分片數,可以參考如下 api 來執行_cat/indices?format=json&pretty&bytes=b&h=index,pri,docs.count,pri.store.size

b. 獲取到數據節點實例數,在 a 步驟得到的分片數和實例數取小值。如下:計算方式:shard_count = MIN(shard_count, node_count)
api:_cat/nodes?format=json&h=name,ip,node.role

c. 計算 buffer ,buffer + shard_count 爲最終的分片數。通常比例係數爲 30%,buffer 爲比例係數 * shard_count

日誌類型數據分片大小建議在 30~50G 之間爲佳。

具體 templates 的維護與設計管理,請見下章節。

5.2.2 total_shards_per_node 週期調整

total_shards_per_node 的週期調整可以和分片調整放到一起。以減少維護成本。

total_shards_per_node 默認爲無限量。

調整原則爲:保證單個索引在單個節點上保留最少的分片數,以避免數據分片傾斜的情況。

total_shards_per_node 的調整邏輯如下:

a. 首先設置 total_shards_per_node 幾個範圍,日誌集羣這邊根據數據、節點情況設置了三個範圍

total_shards_per_node: 1 => shard_count < node_count
total_shards_per_node: 2 => node_count < shard_count < node_count * 1.3(比例係數)
total_shards_per_node: 3 => node_count * 1.3 < shard_count

b. 根據上述的範圍來計算不同索引的 total_shards_per_node 。

5.2.3 refresh 週期調整

refresh 的調整相對比較簡單:默認所有的索引 refresh_interval 設置爲 60s

對於非高優先級的 appcode,total_shards_per_node >1 的,設置 refresh_interval 爲 120s 。

5.2.4 索引週期創建

確立好上述索引的配置信息後,可以提前創建下一週期的索引。

索引創建時機很重要,日誌集羣按周來索引數據,需要週一之前創建好下一週期的索引。

創建索引主要注意的點:

a. 爲節省資源,需要在創建前判斷索引在當前周是否有索引數據,如果沒有,則不創建下一週期(如果已經下線,則就不會創建新的索引)

b. 創建索引需要串行執行,且每個索引創建後,需要休眠 3-5s 

c. 索引週期創建同樣需要 airflow 定時任務。根據每週的索引數和執行時長,評估創建的時間範圍,來進行定時任務的調整。

5.3 templates 設計管理

templates 是索引模板,可以定義好一類 index-pattern 的 settings 、mappings。而日誌類索引正式由於得天獨厚的分類優勢可以劃分爲各個不同的 index-pattern ,進而創建 templates 。

日誌集羣的 templates 架構主要分爲兩塊:default templates、index-pattern templates。

前者是提取日誌數據的公共內容寫入到統一的 templates ,供所有日誌索引數據

5.3.1 default templates

default templates 是默認的模板,用來作爲根模板使用,適用於放一些通用的配置,以達到方便配置管理的作用。

日誌集羣的 default templates 主要做了以下幾點:

a. 提供默認的分片、refresh

注:使用 default templates 的分片數和 refresh 刷新頻率主要是爲了新增的日誌提供默認的配置。日誌集羣做過統計基本上 95% 以上的索引周存儲量都在 1TB 以內,所以默認創建 20 分片的索引能滿足絕大多數需求。

number_of_shards: 20
refresh_interval: 60s

b. 日誌統一字段配置

根據各個業務線寫入的數據字段類型,和 kafka 存儲的日誌源數據信息,提取了一些共用字段,舉例如下:

@timestamp:date
send_time :long (時間戳)
app_code : keyword
source_ip : keyword
content   : text
log_name : keyword

更進一步,我們可以根據字段類型進行自身的數據內容,使用情況來進行字段屬性方面的調整,比如:

app_code : 字段存儲的是每個 appcode ,在單個 appcode 下沒有檢索意義。故而設置 index: false

content : 字段存儲的是日誌的主要內容,內容通常比較冗長。設置 norams: false, 不對 content 計算得分

根據上述,還對 source_ip、send_time 等字段都做了 index:false 的設置

一個較爲完整的 default template 如下:

{
  "order": 1,
  "index_patterns"[
    "log_*"
 ],
  "settings"{
    "index"{
      "routing"{
        "allocation"{
          "total_shards_per_node""1"
       }
     },
      "refresh_interval""60s",
      "number_of_shards""20",
      "translog"{
        "durability""async"
     },
      "number_of_replicas""1"
   }
 },
  "mappings"{
    "properties"{
      "@timestamp"{
        "type""date"
     },
      "send_time"{
        "index": false,
        "type""long"
     },
      "source_host"{
        "ignore_above": 256,
        "type""keyword"
     },
      "log_name"{
        "ignore_above": 256,
        "type""keyword"
     },
      "content"{
        "norms": false,
        "type""text"
     },
      "app_code"{
        "ignore_above": 256,
        "index": false,
        "type""keyword"
     },
      "source_ip"{
        "ignore_above": 256,
        "index": false,
        "type""keyword"
     }
   }
 },
  "aliases"{}
}

5.3.2 建立各自的 template

一般有如下情況下需要用到獨立的 template :

a. 有獨立於 default templates 的字段需要提前設置,比如增加字段

b. 針對 default templates 的現有字段有屬性調整設置,比如調整 date 類型的 format

c. 創建獨立的 shard_count、total_shards_per_node ,如上文 5.2 所示

比如其中一個日誌索引存儲的是 city 相關的日誌數據,所以會增加 cityName 字段;@timestamp(date 類型) 寫入的格式爲 yyyy-MM-dd HH:mm:ss.SSS , 則修改了 date 的 format ;而分片相關的設置會在定時任務中進行自動更新。一個獨立的 template 參考如下:

{
  "order": 99,
  "index_patterns"[
    "log_order_info-*"
 ],
  "settings"{
    "index"{
      "number_of_shards""256",
      "routing"{
        "allocation"{
          "total_shards_per_node""2"
       }
     },
      "refresh_interval""120s"
   }
 },
  "mappings"{
    "properties"{
      "cityName"{
        "type""keyword"
     },
      "@timestamp"{
        "format""strict_date_optional_time||epoch_millis||yyyy-MM-dd HH:mm:ss.SSS",
        "type""date"
     }
   }
 },
  "aliases"{}
}

templates 的重要字段說明:

5.4 實戰總結

總結上述的實戰,主要涉及到了三塊:索引生命週期管理、索引 settings 設計管理、索引的 templates 管理,這幾塊既有配置類的設計管理,也有性能方面的考慮,還加入了生產中使用的 templates 進行設計管理,提升可維護性。

並統一通過 airflow 定時任務進行自動化維護,保證了索引週期性設計的穩定性。具體流程參考下圖:

  1. 最佳實戰建議

Settings 層面

Mappings 層面

  1. 總結

文末,總結一下 ES 索引設計的核心考量因素:

相信讀者認真看完此篇文章,結合業務場景,仔細思考,定能設計出性能更好、穩定性更強的索引。

  1. 參考文獻

  1. https://www.elastic.co/guide/cn/elasticsearch/guide/current/index-management.html

  2. https://www.elastic.co/guide/cn/elasticsearch/guide/current/mapping-intro.html

  3. https://www.elastic.co/guide/en/elasticsearch/reference/7.7/mapping.html

  4. https://www.elastic.co/guide/en/elasticsearch/reference/7.7/indices-rollover-index.html

  5. https://blog.csdn.net/laoyang360/article/details/83717399

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/EmbqZUUQKTPNllimstiw7g