轉載自:騰訊技術工程 微信公衆號html
背景node
Elasticsearch(ES)做爲NOSQL+搜索引擎的有機結合體,不只有近實時的查詢能力,還具備強大的聚合分析能力。所以在全文檢索、日誌分析、監控系統、數據分析等領域ES均有普遍應用。而完整的Elastic Stack體系(Elasticsearch、Logstash、Kibana、Beats),更是提供了數據採集、清洗、存儲、可視化的整套解決方案。 linux
本文基於ES 5.6.4,從性能和穩定性兩方面,從linux參數調優、ES節點配置和ES使用方式三個角度入手,介紹ES調優的基本方案。固然,ES的調優毫不能一律而論,須要根據實際業務場景作適當的取捨和調整,文中的疏漏之處也隨時歡迎批評指正。算法
性能調優數據庫
一 Linux參數調優json
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提供了更智能的請求調度算法,不須要內核去作多餘的調整 (僅供參考)session
- 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.*"
- ]
- }
- }
- }
- }
ES主要使用列存來支持sorting, aggregations和scripts功能,對於沒有上述需求的字段,能夠經過下面的命令關閉docvalues,下降存儲成本。
- PUT my_index
- {
- "mappings": {
- "my_type": {
- "properties": {
- "session_id": {
- "type": "keyword",
- "doc_values": false
- }
- }
- }
- }
- }
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參數調優
- # 修改系統資源限制
- # 單用戶能夠打開的最大文件數量,能夠設置爲官方推薦的65536或更大些
- echo "* - nofile 655360" >>/etc/security/limits.conf
- # 單用戶內存地址空間
- echo "* - as unlimited" >>/etc/security/limits.conf
- # 單用戶線程數
- echo "* - nproc 2056474" >>/etc/security/limits.conf
- # 單用戶文件大小
- echo "* - fsize unlimited" >>/etc/security/limits.conf
- # 單用戶鎖定內存
- echo "* - memlock unlimited" >>/etc/security/limits.conf
- # 單進程可使用的最大map內存區域數量
- echo "vm.max_map_count = 655300" >>/etc/sysctl.conf
- # TCP全鏈接隊列參數設置, 這樣設置的目的是防止節點數較多(好比超過100)的ES集羣中,節點異常重啓時全鏈接隊列在啓動瞬間打滿,形成節點hang住,整個集羣響應遲滯的狀況
- echo "net.ipv4.tcp_abort_on_overflow = 1" >>/etc/sysctl.conf
- echo "net.core.somaxconn = 2048" >>/etc/sysctl.conf
- # 下降tcp alive time,防止無效連接佔用連接數
- 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內存空間。