Elasticsearch調優實踐

騰訊技術工程發表於2018-10-25


Elasticsearch調優實踐

背景

Elasticsearch(ES)作為NOSQL+搜尋引擎的有機結合體,不僅有近實時的查詢能力,還具有強大的聚合分析能力。因此在全文檢索、日誌分析、監控系統、資料分析等領域ES均有廣泛應用。而完整的Elastic Stack體系(Elasticsearch、Logstash、Kibana、Beats),更是提供了資料採集、清洗、儲存、視覺化的整套解決方案。 

本文基於ES 5.6.4,從效能和穩定性兩方面,從linux引數調優、ES節點配置和ES使用方式三個角度入手,介紹ES調優的基本方案。當然,ES的調優絕不能一概而論,需要根據實際業務場景做適當的取捨和調整,文中的疏漏之處也隨時歡迎批評指正。


Elasticsearch調優實踐

效能調優

一 Linux引數調優

1. 關閉交換分割槽,防止記憶體置換降低效能。 將/etc/fstab 檔案中包含swap的行註釋掉

sed -i '/swap/s/^/#/' /etc/fstab
swapoff -a

2. 磁碟掛載選項


  • noatime:禁止記錄訪問時間戳,提高檔案系統讀寫效能

  • data=writeback: 不記錄data journal,提高檔案系統寫入效能

  • barrier=0:barrier保證journal先於data刷到磁碟,上面關閉了journal,這裡的barrier也就沒必要開啟了

  • nobh:關閉buffer_head,防止核心打斷大塊資料的IO操作

mount -o noatime,data=writeback,barrier=0,nobh /dev/sda /es_data

3. 對於SSD磁碟,採用電梯排程演算法,因為SSD提供了更智慧的請求排程演算法,不需要核心去做多餘的調整 (僅供參考)

echo noop > /sys/block/sda/queue/scheduler

二 ES節點配置

conf/elasticsearch.yml檔案:

 1. 適當增大寫入buffer和bulk佇列長度,提高寫入效能和穩定性
indices.memory.index_buffer_size: 15%
thread_pool.bulk.queue_size: 1024

2. 計算disk使用量時,不考慮正在搬遷的shard

在規模比較大的叢集中,可以防止新建shard時掃描所有shard的後設資料,提升shard分配速度。

cluster.routing.allocation.disk.include_relocations: false

三 ES使用方式

1. 控制欄位的儲存選項

ES底層使用Lucene儲存資料,主要包括行存(StoreFiled)、列存(DocValues)和倒排索引(InvertIndex)三部分。 大多數使用場景中,沒有必要同時儲存這三個部分,可以透過下面的引數來做適當調整:

  • StoreFiled: 行存,其中佔比最大的是source欄位,它控制doc原始資料的儲存。在寫入資料時,ES把doc原始資料的整個json結構體當做一個string,儲存為source欄位。查詢時,可以透過source欄位拿到當初寫入時的整個json結構體。 所以,如果沒有取出整個原始json結構體的需求,可以透過下面的命令,在mapping中關閉source欄位或者只在source中儲存部分欄位,資料查詢時仍可透過ES的docvaluefields獲取所有欄位的值。

    注意:關閉source後, update, updatebyquery, reindex等介面將無法正常使用,所以有update等需求的index不能關閉source。

# 關閉 _source
PUT my_index 
{
  "mappings": {
    "my_type": {
      "_source": {
        "enabled": false
      }
    }
  }
}
# _source只儲存部分欄位,透過includes指定要儲存的欄位或者透過excludes濾除不需要的欄位
PUT my_index
{
  "mappings": {
    "_doc": {
      "_source": {
        "includes": [
          "*.count",
          "meta.*"
        ],
        "excludes": [
          "meta.description",
          "meta.other.*"
        ]
      }
    }
  }
}
  • docvalues:控制列存。

    ES主要使用列存來支援sorting, aggregations和scripts功能,對於沒有上述需求的欄位,可以透過下面的命令關閉docvalues,降低儲存成本。

PUT my_index
{
  "mappings": {
    "my_type": {
      "properties": {
        "session_id": { 
          "type": "keyword",
          "doc_values": false
        }
      }
    }
  }
}
  • index:控制倒排索引。

    ES預設對於所有欄位都開啟了倒排索引,用於查詢。對於沒有查詢需求的欄位,可以透過下面的命令關閉倒排索引。

PUT my_index
{
  "mappings": {
    "my_type": {
      "properties": {
        "session_id": { 
          "type": "keyword",
          "index": false
        }
      }
    }
  }
}
  • all:ES的一個特殊的欄位,ES把使用者寫入json的所有欄位值拼接成一個字串後,做分詞,然後儲存倒排索引,用於支援整個json的全文檢索。

    這種需求適用的場景較少,可以透過下面的命令將all欄位關閉,節約儲存成本和cpu開銷。(ES 6.0+以上的版本不再支援_all欄位,不需要設定)

PUT /my_index
{
  "mapping": {
    "my_type": {
      "_all": {
        "enabled": false   
      }
    }
  }
}
  • fieldnames:該欄位用於exists查詢,來確認某個doc裡面有無一個欄位存在。若沒有這種需求,可以將其關閉。

PUT /my_index
{
  "mapping": {
    "my_type": {
      "_field_names": {
        "enabled": false   
      }
    }
  }
}

2. 開啟最佳壓縮

對於開啟了上述_source欄位的index,可以透過下面的命令來把lucene適用的壓縮演算法替換成 DEFLATE,提高資料壓縮率。

PUT /my_index/_settings
{
    "index.codec": "best_compression"
}

3. bulk批次寫入

寫入資料時儘量使用下面的bulk介面批次寫入,提高寫入效率。每個bulk請求的doc數量設定區間推薦為1k~1w,具體可根據業務場景選取一個適當的數量。

POST _bulk
{ "index" : { "_index" : "test", "_type" : "type1" } }
{ "field1" : "value1" }
{ "index" : { "_index" : "test", "_type" : "type1" } }
{ "field1" : "value2" }

4. 調整translog同步策略

預設情況下,translog的持久化策略是,對於每個寫入請求都做一次flush,重新整理translog資料到磁碟上。這種頻繁的磁碟IO操作是嚴重影響寫入效能的,如果可以接受一定機率的資料丟失(這種硬體故障的機率很小),可以透過下面的命令調整 translog 持久化策略為非同步週期性執行,並適當調整translog的刷盤週期。

PUT my_index
{
  "settings": {
    "index": {
      "translog": {
        "sync_interval": "5s",
        "durability": "async"
      }
    }
  }
}

5. 調整refresh_interval

寫入Lucene的資料,並不是實時可搜尋的,ES必須透過refresh的過程把記憶體中的資料轉換成Lucene的完整segment後,才可以被搜尋。預設情況下,ES每一秒會refresh一次,產生一個新的segment,這樣會導致產生的segment較多,從而segment merge較為頻繁,系統開銷較大。如果對資料的實時可見性要求較低,可以透過下面的命令提高refresh的時間間隔,降低系統開銷。

PUT my_index
{
  "settings": {
    "index": {
        "refresh_interval" : "30s"
    }
  }
}

6. merge併發控制

ES的一個index由多個shard組成,而一個shard其實就是一個Lucene的index,它又由多個segment組成,且Lucene會不斷地把一些小的segment合併成一個大的segment,這個過程被稱為merge。預設值是Math.max(1, Math.min(4, Runtime.getRuntime().availableProcessors() / 2)),當節點配置的cpu核數較高時,merge佔用的資源可能會偏高,影響叢集的效能,可以透過下面的命令調整某個index的merge過程的併發度:

PUT /my_index/_settings
{
    "index.merge.scheduler.max_thread_count": 2
}

7. 寫入資料不指定_id,讓ES自動產生

當使用者顯示指定id寫入資料時,ES會先發起查詢來確定index中是否已經有相同id的doc存在,若有則先刪除原有doc再寫入新doc。這樣每次寫入時,ES都會耗費一定的資源做查詢。如果使用者寫入資料時不指定doc,ES則透過內部演算法產生一個隨機的id,並且保證id的唯一性,這樣就可以跳過前面查詢id的步驟,提高寫入效率。 所以,在不需要透過id欄位去重、update的使用場景中,寫入不指定id可以提升寫入速率。基礎架構部資料庫團隊的測試結果顯示,無id的資料寫入效能可能比有_id的高出近一倍,實際損耗和具體測試場景相關。

# 寫入時指定_id
POST _bulk
{ "index" : { "_index" : "test", "_type" : "type1", "_id" : "1" } }
{ "field1" : "value1" }
# 寫入時不指定_id
POST _bulk
{ "index" : { "_index" : "test", "_type" : "type1" } }
{ "field1" : "value1" }

8. 使用routing

對於資料量較大的index,一般會配置多個shard來分攤壓力。這種場景下,一個查詢會同時搜尋所有的shard,然後再將各個shard的結果合併後,返回給使用者。對於高併發的小查詢場景,每個分片通常僅抓取極少量資料,此時查詢過程中的排程開銷遠大於實際讀取資料的開銷,且查詢速度取決於最慢的一個分片。開啟routing功能後,ES會將routing相同的資料寫入到同一個分片中(也可以是多個,由index.routingpartitionsize引數控制)。如果查詢時指定routing,那麼ES只會查詢routing指向的那個分片,可顯著降低排程開銷,提升查詢效率。 routing的使用方式如下:

# 寫入
PUT my_index/my_type/1?routing=user1
{
  "title": "This is a document"
}
# 查詢
GET my_index/_search?routing=user1,user2 
{
  "query": {
    "match": {
      "title": "document"
    }
  }
}

9. 為string型別的欄位選取合適的儲存方式

  • 存為text型別的欄位(string欄位預設型別為text): 做分詞後儲存倒排索引,支援全文檢索,可以透過下面幾個引數最佳化其儲存方式:

    • norms:用於在搜尋時計算該doc的_score(代表這條資料與搜尋條件的相關度),如果不需要評分,可以將其關閉。

    • indexoptions:控制倒排索引中包括哪些資訊(docs、freqs、positions、offsets)。對於不太注重score/highlighting的使用場景,可以設為 docs來降低記憶體/磁碟資源消耗。

    • fields: 用於新增子欄位。對於有sort和聚合查詢需求的場景,可以新增一個keyword子欄位以支援這兩種功能。

PUT my_index
{
  "mappings": {
    "my_type": {
      "properties": {
        "title": { 
          "type": "text",
          "norms": false,
          "index_options": "docs",
          "fields": {
            "raw": { 
              "type":  "keyword"
            }
          }
        }
      }
    }
  }
}


  • 存為keyword型別的欄位: 不做分詞,不支援全文檢索。text分詞消耗CPU資源,冗餘儲存keyword子欄位佔用儲存空間。如果沒有全文索引需求,只是要透過整個欄位做搜尋,可以設定該欄位的型別為keyword,提升寫入速率,降低儲存成本。 設定欄位型別的方法有兩種:一是建立一個具體的index時,指定欄位的型別;二是透過建立template,控制某一類index的欄位型別。

# 1. 透過mapping指定 tags 欄位為keyword型別
PUT my_index
{
  "mappings": {
    "my_type": {
      "properties": {
        "tags": {
          "type":  "keyword"
        }
      }
    }
  }
}
# 2. 透過template,指定my_index*類的index,其所有string欄位預設為keyword型別
PUT _template/my_template
{
    "order": 0,
    "template": "my_index*",
    "mappings": {
      "_default_": {
        "dynamic_templates": [
          {
            "strings": {
              "match_mapping_type": "string",
              "mapping": {
                "type": "keyword",
                "ignore_above": 256
              }
            }
          }
        ]
      }
    },
    "aliases": {}
  }

10. 查詢時,使用query-bool-filter組合取代普通query

預設情況下,ES透過一定的演算法計算返回的每條資料與查詢語句的相關度,並透過score欄位來表徵。但對於非全文索引的使用場景,使用者並不care查詢結果與查詢條件的相關度,只是想精確的查詢目標資料。此時,可以透過query-bool-filter組合來讓ES不計算score,並且儘可能的快取filter的結果集,供後續包含相同filter的查詢使用,提高查詢效率。

# 普通查詢
POST my_index/_search
{
  "query": {
    "term" : { "user" : "Kimchy" } 
  }
}
# query-bool-filter 加速查詢
POST my_index/_search
{
  "query": {
    "bool": {
      "filter": {
        "term": { "user": "Kimchy" }
      }
    }
  }
}

11. index按日期滾動,便於管理

寫入ES的資料最好透過某種方式做分割,存入不同的index。常見的做法是將資料按模組/功能分類,寫入不同的index,然後按照時間去滾動生成index。這樣做的好處是各種資料分開管理不會混淆,也易於提高查詢效率。同時index按時間滾動,資料過期時刪除整個index,要比一條條刪除資料或deletebyquery效率高很多,因為刪除整個index是直接刪除底層檔案,而deletebyquery是查詢-標記-刪除。

舉例說明,假如有[modulea,moduleb]兩個模組產生的資料,那麼index規劃可以是這樣的:一類index名稱是modulea + {日期},另一類index名稱是module_b+ {日期}。對於名字中的日期,可以在寫入資料時自己指定精確的日期,也可以透過ES的ingest pipeline中的index-name-processor實現(會有寫入效能損耗)。

# module_a 類index
- 建立index:
PUT module_a@2018_01_01
{
    "settings" : {
        "index" : {
            "number_of_shards" : 3, 
            "number_of_replicas" : 2 
        }
    }
}
PUT module_a@2018_01_02
{
    "settings" : {
        "index" : {
            "number_of_shards" : 3, 
            "number_of_replicas" : 2 
        }
    }
}
...
- 查詢資料:
GET module_a@*/_search
#  module_b 類index
- 建立index:
PUT module_b@2018_01_01
{
    "settings" : {
        "index" : {
            "number_of_shards" : 3, 
            "number_of_replicas" : 2 
        }
    }
}
PUT module_b@2018_01_02
{
    "settings" : {
        "index" : {
            "number_of_shards" : 3, 
            "number_of_replicas" : 2 
        }
    }
}
...
- 查詢資料:
GET module_b@*/_search

12. 按需控制index的分片數和副本數

分片(shard):一個ES的index由多個shard組成,每個shard承載index的一部分資料。

副本(replica):index也可以設定副本數(numberofreplicas),也就是同一個shard有多少個備份。對於查詢壓力較大的index,可以考慮提高副本數(numberofreplicas),透過多個副本均攤查詢壓力。

shard數量(numberofshards)設定過多或過低都會引發一些問題:shard數量過多,則批次寫入/查詢請求被分割為過多的子寫入/查詢,導致該index的寫入、查詢拒絕率上升;對於資料量較大的inex,當其shard數量過小時,無法充分利用節點資源,造成機器資源利用率不高 或 不均衡,影響寫入/查詢的效率。

對於每個index的shard數量,可以根據資料總量、寫入壓力、節點數量等綜合考量後設定,然後根據資料增長狀態定期檢測下shard數量是否合理。基礎架構部資料庫團隊的推薦方案是:

  • 對於資料量較小(100GB以下)的index,往往寫入壓力查詢壓力相對較低,一般設定3~5個shard,numberofreplicas設定為1即可(也就是一主一從,共兩副本) 。

  • 對於資料量較大(100GB以上)的index:

    • 一般把單個shard的資料量控制在(20GB~50GB)

    • 讓index壓力分攤至多個節點:可透過index.routing.allocation.totalshardsper_node引數,強制限定一個節點上該index的shard數量,讓shard儘量分配到不同節點上

    • 綜合考慮整個index的shard數量,如果shard數量(不包括副本)超過50個,就很可能引發拒絕率上升的問題,此時可考慮把該index拆分為多個獨立的index,分攤資料量,同時配合routing使用,降低每個查詢需要訪問的shard數量。


穩定性調優

一 Linux引數調優


  1. # 修改系統資源限制

  2. # 單使用者可以開啟的最大檔案數量,可以設定為官方推薦的65536或更大些

  3. echo "* - nofile 655360" >>/etc/security/limits.conf

  4. # 單使用者記憶體地址空間

  5. echo "* - as unlimited" >>/etc/security/limits.conf

  6. # 單使用者執行緒數

  7. echo "* - nproc 2056474" >>/etc/security/limits.conf

  8. # 單使用者檔案大小

  9. echo "* - fsize unlimited" >>/etc/security/limits.conf

  10. # 單使用者鎖定記憶體

  11. echo "* - memlock unlimited" >>/etc/security/limits.conf

  12. # 單程式可以使用的最大map記憶體區域數量

  13. echo "vm.max_map_count = 655300" >>/etc/sysctl.conf

  14. # TCP全連線佇列引數設定, 這樣設定的目的是防止節點數較多(比如超過100)的ES叢集中,節點異常重啟時全連線佇列在啟動瞬間打滿,造成節點hang住,整個叢集響應遲滯的情況

  15. echo "net.ipv4.tcp_abort_on_overflow = 1" >>/etc/sysctl.conf

  16. echo "net.core.somaxconn = 2048" >>/etc/sysctl.conf

  17. # 降低tcp alive time,防止無效連結佔用連結數

  18. echo 300 >/proc/sys/net/ipv4/tcp_keepalive_time



二 ES節點配置

1. jvm.options

-Xms和-Xmx設定為相同的值,推薦設定為機器記憶體的一半左右,剩餘一半留給系統cache使用。

  • jvm記憶體建議不要低於2G,否則有可能因為記憶體不足導致ES無法正常啟動或OOM

  • jvm建議不要超過32G,否則jvm會禁用記憶體物件指標壓縮技術,造成記憶體浪費

2. elasticsearch.yml

  • 設定記憶體熔斷引數,防止寫入或查詢壓力過高導致OOM,具體數值可根據使用場景調整。 indices.breaker.total.limit: 30% indices.breaker.request.limit: 6% indices.breaker.fielddata.limit: 3%

  • 調小查詢使用的cache,避免cache佔用過多的jvm記憶體,具體數值可根據使用場景調整。 indices.queries.cache.count: 500 indices.queries.cache.size: 5%

  • 單機多節點時,主從shard分配以ip為依據,分配到不同的機器上,避免單機掛掉導致資料丟失。 cluster.routing.allocation.awareness.attributes: ip node.attr.ip: 1.1.1.1

三 ES使用方式

1. 節點數較多的叢集,增加專有master,提升叢集穩定性

ES叢集的元資訊管理、index的增刪操作、節點的加入剔除等叢集管理的任務都是由master節點來負責的,master節點定期將最新的叢集狀態廣播至各個節點。所以,master的穩定性對於叢集整體的穩定性是至關重要的。當叢集的節點數量較大時(比如超過30個節點),叢集的管理工作會變得複雜很多。此時應該建立專有master節點,這些節點只負責叢集管理,不儲存資料,不承擔資料讀寫壓力;其他節點則僅負責資料讀寫,不負責叢集管理的工作。

這樣把叢集管理和資料的寫入/查詢分離,互不影響,防止因讀寫壓力過大造成叢集整體不穩定。 將專有master節點和資料節點的分離,需要修改ES的配置檔案,然後滾動重啟各個節點。

# 專有master節點的配置檔案(conf/elasticsearch.yml)增加如下屬性:
node.master: true 
node.data: false 
node.ingest: false 
# 資料節點的配置檔案增加如下屬性(與上面的屬性相反):
node.master: false 
node.data: true 
node.ingest: true

2. 控制index、shard總數量

上面提到,ES的元資訊由master節點管理,定期同步給各個節點,也就是每個節點都會儲存一份。這個元資訊主要儲存在clusterstate中,如所有node元資訊(indices、節點各種統計引數)、所有index/shard的元資訊(mapping, location, size)、後設資料ingest等。

ES在建立新分片時,要根據現有的分片分佈情況指定分片分配策略,從而使各個節點上的分片數基本一致,此過程中就需要深入遍歷clusterstate。當叢集中的index/shard過多時,clusterstate結構會變得過於複雜,導致遍歷clusterstate效率低下,叢集響應遲滯。基礎架構部資料庫團隊曾經在一個20個節點的叢集裡,建立了4w+個shard,導致新建一個index需要60s+才能完成。 當index/shard數量過多時,可以考慮從以下幾方面改進:

  • 降低資料量較小的index的shard數量

  • 把一些有關聯的index合併成一個index

  • 資料按某個維度做拆分,寫入多個叢集

3. Segment Memory最佳化

前面提到,ES底層採用Lucene做儲存,而Lucene的一個index又由若干segment組成,每個segment都會建立自己的倒排索引用於資料查詢。Lucene為了加速查詢,為每個segment的倒排做了一層字首索引,這個索引在Lucene4.0以後採用的資料結構是FST (Finite State Transducer)。Lucene載入segment的時候將其全量裝載到記憶體中,加快查詢速度。這部分記憶體被稱為SegmentMemory, 常駐記憶體,佔用heap,無法被GC

前面提到,為利用JVM的物件指標壓縮技術來節約記憶體,通常建議JVM記憶體分配不要超過32G。當叢集的資料量過大時,SegmentMemory會吃掉大量的堆記憶體,而JVM記憶體空間又有限,此時就需要想辦法降低SegmentMemory的使用量了,常用方法有下面幾個:

  • 定期刪除不使用的index

  • 對於不常訪問的index,可以透過close介面將其關閉,用到時再開啟

  • 透過force_merge介面強制合併segment,降低segment數量

基礎架構部資料庫團隊在此基礎上,對FST部分進行了最佳化,釋放高達40%的Segment Memory記憶體空間。

Elasticsearch調優實踐

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31559354/viewspace-2217519/,如需轉載,請註明出處,否則將追究法律責任。

相關文章