ELK 性能優化實踐

一、背景介紹

近一年內對公司的 ELK 日誌系統做過性能優化,也對 SkyWalking 使用的 ES 存儲進行過性能優化,在此做一些總結。本篇主要是講 ES 在 ELK 架構中作爲日誌存儲時的性能優化方案。

ELK 架構作爲日誌存儲方案

ELK日誌架構.png
ELK日誌架構.png

二、現狀分析

1. 版本及硬件配置

  • JDK:JDK1.8_171-b11 (64位)
  • ES集羣:由3臺16核32G的虛擬機部署 ES 集羣,每個節點分配20G堆內存
  • ELK版本:6.3.0
  • 垃圾回收器:ES 默認指定的老年代(CMS)+ 新生代(ParNew)
  • 操作系統:CentOS Linux release 7.4.1708(Core)

2. 性能問題

隨着接入 ELK 的應用越來越多,每日新增索引約 230 個,新增 document 約 3000 萬到 5000 萬

每日上午和下午是日誌上傳高峯期,在 Kibana 上查看日誌,發現問題:

(1) 日誌會有 5-40 分鐘的延遲

(2) 有很多日誌丟失,無法查到

3. 問題分析

3.1 日誌延遲

首先了解清楚:數據什麼時候可以被查到?

數據先是存放在 ES 的內存 buffer,然後執行 refresh 操作寫入到操作系統的內存緩存 os cache,此後數據就可以被搜索到。

所以,日誌延遲可能是我們的數據積壓在 buffer 中沒有進入 os cache 。

3.2日誌丟失

查看日誌發現很多 write 拒絕執行的情況

write 線程池拒絕任務的日誌.png
write 線程池拒絕任務的日誌.png

從日誌中可以看出 ES 的 write 線程池已經滿負荷,執行任務的線程已經達到最大16個線程,而200容量的隊列也已經放不下新的task。

查看線程池的情況也可以看出 write 線程池有很多寫入的任務

GET /_cat/thread_pool?v&h=host,name,type,size,active,largest,rejected,completed,queue,queue_size
write 線程池拒絕任務的情況.png
write 線程池拒絕任務的情況.png

所以我們需要優化 ES 的 write 的性能。

4.解決思路

4.1 分析場景

ES 的優化分爲很多方面,我們要根據使用場景考慮對 ES 的要求。

根據個人實踐經驗,列舉三種不同場景下的特點

  • SkyWalking:一般配套使用 ES 作爲數據存儲,存儲鏈路追蹤數據、指標數據等信息。
  • ELK:一般用來存儲系統日誌,並進行分析,搜索,定位應用的問題。
  • 全文搜索的業務:業務中常用 ES 作爲全文搜索引擎,例如在外賣應用中,ES 用來存儲商家、美食的業務數據,用戶在客戶端可以根據關鍵字、地理位置等查詢條件搜索商家、美食信息。

這三類場景的特點如下:

SkyWalking ELK 全文搜索的業務
併發寫 高併發寫 高併發寫 併發一般不高
併發讀 併發低 併發低 併發高
實時性要求 5分鐘以內 30秒以內 1分鐘內
數據完整性 可容忍丟失少量數據 可容忍丟失少量數據 數據儘量100%不丟失

關於實時性

  • SkyWalking 在實際使用中,一般使用頻率不太高,往往是發現應用的問題後,再去 SkyWalking 查歷史鏈路追蹤數據或指標數據,所以可以接受幾分鐘的延遲。
  • ELK 不管是開發、測試等階段,時常用來定位應用的問題,如果不能快速查詢出數據,延遲太久,會耽誤很多時間,大大降低工作效率;如果是查日誌定位生產問題,那更是刻不容緩。
  • 全文搜索的業務中一般可以接受在1分鐘內查看到最新數據,比如新商品上架一分鐘後纔看到,但儘量實時,在幾秒內可以可看到。

4.2 優化的方向

可以從三方面進行優化:JVM性能調優、ES性能調優、控制數據來源

三、ES性能優化

可以從三方面進行優化:JVM 性能調優、ES 性能調優、控制數據來源

1. JVM調優

第一步是 JVM 調優。

因爲 ES 是依賴於 JVM 運行,沒有合理的設置 JVM 參數,將浪費資源,甚至導致 ES 很容易 OOM 而崩潰。

1.1 監控 JVM 運行情況

(1) 查看 GC 日誌 GC日誌中很多Young GC.png 問題:Young GC 和 Full GC 都很頻繁,特別是 Young GC 頻率高,累積耗時非常多。

(2) 使用 jstat 看下每秒的 GC 情況 jstat發現Young GC頻繁.png 參數說明

  • S0:倖存1區當前使用比例
  • S1:倖存2區當前使用比例
  • E:伊甸園區使用比例
  • O:老年代使用比例
  • M:元數據區使用比例
  • CCS:壓縮使用比例
  • YGC:年輕代垃圾回收次數
  • FGC:老年代垃圾回收次數
  • FGCT:老年代垃圾回收消耗時間
  • GCT:垃圾回收消耗總時間 問題:從 jstat gc 中也可以看出,每秒的 eden 增長速度非常快,很快就滿了。

1.2 定位 Young GC 頻繁的原因

1.2.1 檢查是否新生代的空間是否太小

用下面幾種方式都可查看新、老年代內存大小 (1) 使用 jstat -gc pid 查看 Eden 區、老年代空間大小 (2) 使用 jmap -heap pid 查看 Eden 區、老年代空間大小 (3) 查看 GC 日誌中的 GC 明細 GC明細.png 其中 996800K 爲新生代可用空間大小,即 Eden 區 +1 個 Survivor 區的空間大小,所以新生代總內存是996800K/0.9, 約1081M

上面的幾種方式都查詢出,新生代總內存約1081M,即1G左右;老年代總內存爲19864000K,約19G。新、老比例約1:19,出乎意料。

1.2.1 新老年代空間比例爲什麼不是 JDK 默認的1:2【重點!】

這真是一個容易踩坑的地方。 如果沒有顯示設置新生代大小,JVM 在使用 CMS 收集器時會自動調參,新生代的大小在沒有設置的情況下是通過計算得出的,其大小可能與 NewRatio 的默認配置沒什麼關係而與 ParallelGCThreads 的配置有一定的關係。

參考文末鏈接:?CMS GC 默認新生代是多大?

所以:新生代大小有不確定性,最好配置 JVM 參數 -XX:NewSize、-XX:MaxNewSize 或者 -xmn ,免得遇到一些奇怪的 GC,讓人措手不及。

1.3 上面現象造成的影響

新生代過小,老年代過大的影響

  • 新生代過小: (1) 會導致新生代 Eden 區很快用完,而觸發 Young GC,Young GC 的過程中會 STW(Stop The World),也就是所有工作線程停止,只有 GC 的線程在進行垃圾回收,這會導致 ES 短時間停頓。頻繁的 Young GC,積少成多,對系統性能影響較大。 (2) 大部分對象很快進入老年代,老年代很容易用完而觸發 Full GC。
  • 老年代過大:會導致 Full GC 的執行時間過長,Full GC 雖然有並行處理的步驟,但是還是比 Young GC 的 STW 時間更久,而 GC 導致的停頓時間在幾十毫秒到幾秒內,很影響 ES 的性能,同時也會導致請求 ES 服務端的客戶端在一定時間內沒有響應而發生 timeout 異常,導致請求失敗。

1.4 JVM優化

1.4.1 配置堆內存空間大小

32G 的內存,分配 20G 給堆內存是不妥當的,所以調整爲總內存的50%,即16G。 修改 elasticsearch 的 jvm.options 文件

-Xms16g
-Xmx16g

設置要求:

  • Xms 與 Xmx 大小相同。

    在 jvm 的參數中 -Xms 和 -Xmx 設置的不一致,在初始化時只會初始 -Xms 大小的空間存儲信息,每當空間不夠用時再向操作系統申請,這樣的話必然要進行一次 GC,GC會帶來 STW。而剩餘空間很多時,會觸發縮容。再次不夠用時再擴容,如此反覆,這些過程會影響系統性能。同理在 MetaSpace 區也有類似的問題。

  • jvm 建議不要超過 32G,否則 jvm 會禁用內存對象指針壓縮技術,造成內存浪費

  • Xmx 和 Xms 不要超過物理 RAM 的50%。 參考文末:?官方堆內存設置的建議

    Xmx 和 Xms 不要超過物理內存的50%。Elasticsearch 需要內存用於JVM堆以外的其他用途,爲此留出空間非常重要。例如,Elasticsearch 使用堆外緩衝區進行有效的網絡通信,依靠操作系統的文件系統緩存來高效地訪問文件,而 JVM 本身也需要一些內存。

1.4.2 配置堆內存新生代空間大小

因爲指定新生代空間大小,導致 JVM 自動調參只分配了 1G 內存給新生代。

修改 elasticsearch 的 jvm.options 文件,加上

-XX:NewSize=8G
-XX:MaxNewSize=8G

老年代則自動分配 16G-8G=8G 內存,新生代老年代的比例爲 1:1。修改後每次 Young GC 頻率更低,且每次 GC 後只有少數數據會進入老年代。

2.3 使用G1垃圾回收器(未實踐)

G1垃圾回收器讓系統使用者來設定垃圾回收堆系統的影響,然後把內存拆分爲大量的小 Region,追蹤每個 Region 中可以回收的對象大小和回收完成的預計花費的時間, 最後在垃圾回收的時候,儘量把垃圾回收對系統造成的影響控制在我們指定的時間範圍內,同時在有限的時間內儘量回收更多的垃圾對象。 G1垃圾回收器一般在大數量、大內存的情況下有更好的性能。

ES默認使用的垃圾回收器是:老年代(CMS)+ 新生代(ParNew)。如果是JDK1.9,ES 默認使用 G1 垃圾回收器。

因爲使用的是 JDK1.8,所以並未切換垃圾回收器。後續如果再有性能問題再切換G1垃圾回收器,測試是否有更好的性能。

1.5 優化的效果

1.5.1 新生代使用內存的增長率更低

優化前

優化前.png
優化前.png

每秒打印一次 GC 數據。可以看出,年輕代增長速度很快,幾秒鐘年輕代就滿了,導致 Young GC 觸發很頻繁,幾秒鐘就會觸發一次。而每次 Young GC 很大可能有存活對象進入老年代,而且,存活對象多的時候(看上圖中第一個紅框中的old gc數據),有(51.44-51.08)/100 * 19000M = 約68M。每次進入老年代的對象較多,加上頻繁的 Young GC,會導致新老年代的分代模式失去了作用,相當於老年代取代了新生代來存放近期內生成的對象。當老年代滿了,觸發 Full GC,存活的對象也會很多,因爲這些對象很可能還是近期加入的,還存活着,所以一次 Full GC 回收對象不多。而這會惡性循環,老年代很快又滿了,又 Full GC,又殘留一大部分存活的,又很容易滿了,所以導致一直頻繁 Full GC。

優化後

優化後.png
優化後.png

每秒打印一次 GC 數據。可以看出,新生代增長速度慢了許多,至少要 60 秒纔會滿,如上圖紅框中數據,進入老年代的對象約(15.68-15.60)/100 * 10000 = 8M,非常的少。所以要很久纔會觸發一次 Full GC 。而且等到 Full GC 時,老年代裏很多對象都是存活了很久的,一般都是不會被引用,所以很大一部分會被回收掉,留一個比較乾淨的老年代空間,可以繼續放很多對象。

1.5.2 新生代和老年代 GC 頻率更低

ES 啓動後,運行14個小時

優化前

優化前.png
優化前.png

Young GC 每次的時間是不長的,從上面監控數據中可以看出每次GC時長 1467.995/27276 約等於 0.05 秒。那一秒鐘有多少時間實在處理 Young GC ?

計算公式:1467 秒/( 60 秒× 60 分 14 小時)= 約 0.028 秒,也就是 100 秒中就有 2.8 秒在Young GC,也就是有 2.8S 的停頓,這對性能還是有很大消耗的。同時也可以算出多久一次 Young GC, 方程是: 60秒×60分*14小時/ 27276次 = 1次/X秒,計算得出X = 0.54,也就是 0.54 秒就會有一次Young GC,可見 Young GC 頻率非常頻繁。

優化後

優化後效果.png
優化後效果.png

Young GC 次數只有修改前的十分之一,Young GC 時間也是約八分之一。Full GC 的次數也是隻有原來的八分之一,GC 時間大約是四分之一。

GC 對系統的影響大大降低,性能已經得到很大的提升。

2.ES 調優

上面已經分析過 ES 作爲日誌存儲時的特性是:高併發寫、讀少、接受 30 秒內的延時、可容忍部分日誌數據丟失。 下面我們針對這些特性對ES進行調優。

2.1 優化 ES 索引設置

2.2.1 ES 寫數據底層原理
ES寫入數據的原理.png
ES寫入數據的原理.png

refresh ES 接收數據請求時先存入 ES 的內存中,默認每隔一秒會從內存 buffer 中將數據寫入操作系統緩存 os cache,這個過程叫做 refresh;

到了 os cache 數據就能被搜索到(所以我們才說 ES 是近實時的,因爲 1 s 的延遲後執行 refresh 便可讓數據被搜索到)

fsync translog 會每隔 5 秒或者在一個變更請求完成之後執行一次 fsync 操作,將 translog 從緩存刷入磁盤,這個操作比較耗時,如果對數據一致性要求不是跟高時建議將索引改爲異步,如果節點宕機時會有5秒數據丟失;

flush ES 默認每隔30分鐘會將 os cache 中的數據刷入磁盤同時清空 translog 日誌文件,這個過程叫做 flush。

merge

ES 的一個 index 由多個 shard 組成,而一個 shard 其實就是一個 Lucene 的 index ,它又由多個 segment 組成,且 Lucene 會不斷地把一些小的 segment 合併成一個大的 segment ,這個過程被稱爲?段merge(參考文末鏈接)。執行索引操作時,?ES會先生成小的segment,ES 有離線的邏輯對小的 segment 進行合併,優化查詢性能。但是合併過程中會消耗較多磁盤 IO,會影響查詢性能。

2.2.2 優化方向
2.2.2.1 優化 fsync

爲了保證不丟失數據,就要保護 translog 文件的安全:

Elasticsearch 2.0 之後, 每次寫請求(如 index 、delete、update、bulk 等)完成時, 都會觸發fsync將 translog 中的 segment 刷到磁盤, 然後纔會返回200 OK的響應;

或者: 默認每隔5s就將 translog 中的數據通過fsync強制刷新到磁盤.

該方式提高數據安全性的同時, 降低了一點性能.

==> 頻繁地執行 fsync 操作, 可能會產生阻塞導致部分操作耗時較久. 如果允許部分數據丟失, 可設置異步刷新 translog 來提高效率,還有降低 flush 的閥值,優化如下:

"index.translog.durability""async",
"index.translog.flush_threshold_size":"1024mb",
"index.translog.sync_interval""120s"
2.2.2.2 優化 refresh

寫入 Lucene 的數據,並不是實時可搜索的,ES 必須通過 refresh 的過程把內存中的數據轉換成 Lucene 的完整 segment 後,纔可以被搜索。

默認 1秒後,寫入的數據可以很快被查詢到,但勢必會產生大量的 segment,檢索性能會受到影響。所以,加大時長可以降低系統開銷。對於日誌搜索來說,實時性要求不是那麼高,設置爲 5 秒或者 10s;對於 SkyWalking,實時性要求更低一些,我們可以設置爲30s。

設置如下:

"index.refresh_interval":"5s"
2.2.2.3 優化 merge

index.merge.scheduler.max_thread_count 控制併發的 merge 線程數,如果存儲是併發性能較好的 SSD,可以用系統默認的 max(1, min(4, availableProcessors / 2)),當節點配置的 cpu 核數較高時,merge 佔用的資源可能會偏高,影響集羣的性能,普通磁盤的話設爲1,發生磁盤 IO 堵塞。設置max_thread_count 後,會有 max_thread_count + 2 個線程同時進行磁盤操作,也就是設置爲 1 允許3個線程。

設置如下:

"index.merge.scheduler.max_thread_count":"1"
2.2.2 優化設置
2.2.2.1 對現有索引做索引設置
# 需要先 close 索引,然後再執行,最後成功之後再打開
# 關閉索引
curl -XPOST 'http://localhost:9200/_all/_close'

# 修改索引設置
curl -XPUT -H "Content-Type:application/json" 'http://localhost:9200/_all/_settings?preserve_existing=true' -d '{"index.merge.scheduler.max_thread_count" : "1","index.refresh_interval" : "10s","index.translog.durability" : "async","index.translog.flush_threshold_size":"1024mb","index.translog.sync_interval" : "120s"}'

# 打開索引
curl -XPOST 'http://localhost:9200/_all/_open'

該方式可對已經生成的索引做修改,但是對於後續新建的索引不生效,所以我們可以製作 ES 模板,新建的索引按模板創建索引。

2.2.2.2 製作索引模板
# 製作模板 大部分索引都是業務應用的日誌相關的索引,且索引名稱是 202* 這種帶着日期的格式
PUT _template/business_log
{
  "index_patterns": ["*202*.*.*"],
  "settings": {
  "index.merge.scheduler.max_thread_count" : "1","index.refresh_interval" : "5s","index.translog.durability" : "async","index.translog.flush_threshold_size":"1024mb","index.translog.sync_interval" : "120s"}
}

# 查詢模板是否創建成功
GET _template/business_log

因爲我們的業務日誌是按天維度創建索引,索引名稱示例:user-service-prod-2020.12.12,所以用通配符*202..**匹配對應要創建的業務日誌索引。

2.2 優化線程池配置

前文已經提到過,write 線程池滿負荷,導致拒絕任務,而有的數據無法寫入。

而經過上面的優化後,拒絕的情況少了很多,但是還是有拒絕任務的情況。

所以我們還需要優化 write 線程池。

從 prometheus 監控中可以看到線程池的情況:

爲了更直觀看到 ES 線程池的運行情況,我們安裝了 elasticsearch_exporter 收集 ES 的指標數據到 prometheus,再通過 grafana 進行查看。

經過上面的各種優化,拒絕的數據量少了很多,但是還是存在拒絕的情況,如下圖:

image.png
image.png

write 線程池如何設置:

參考文末鏈接:?ElasticSearch線程池

write

For single-document index/delete/update and bulk requests. Thread pool type is fixed with a size of # of available processors, queue_size of 200. The maximum size for this pool is 1 + # of available processors.

write 線程池採用 fixed 類型的線程池,也就是核心線程數與最大線程數值相同。線程數默認等於 cpu 核數,可設置的最大值只能是 cpu 核數加 1,也就是 16 核 CPU,能設置的線程數最大值爲 17。

優化的方案:

  • 線程數改爲 17,也就是 cpu 總核數加 1
  • 隊列容量加大。隊列在此時的作用是消峯。不過隊列容量加大本身不會提升處理速度,只是起到緩衝作用。此外,隊列容量也不能太大,否則積壓很多任務時會佔用過多堆內存。

config/elasticsearch.yml文件增加配置

# 線程數設置
thread_pool:
  write:
    # 線程數默認等於cpu核數,即16  
    size: 17
    # 因爲任務多時存在任務拒絕的情況,所以加大隊列大小,可以在間歇性任務量陡增的情況下,緩存任務在隊列,等高峯過去逐步消費完。
    queue_size: 10000

優化後效果 線程池拒絕的情況.png 可以看到,已經沒有拒絕的情況,這樣也就是解決了日誌丟失的問題。

2.3 鎖定內存,不讓 JVM 使用 Swap

Swap 交換分區:

當系統的物理內存不夠用的時候,就需要將物理內存中的一部分空間釋放出來,以供當前運行的程序使用。那些被釋放的空間可能來自一些很長時間沒有什麼操作的程序,**這些被釋放的空間被臨時保存到 Swap 中,等到那些程序要運行時,再從 Swap 中恢復保存的數據到內存中。**這樣,系統總是在物理內存不夠時,才進行 Swap 交換。

參考文末鏈接:?ElasticSearch官方解釋爲什麼要禁用交換內存

Swap 交換分區對性能和節點穩定性非常不利,一定要禁用。它會導致垃圾回收持續幾分鐘而不是幾毫秒,並會導致節點響應緩慢,甚至與集羣斷開連接。

有三種方式可以實現 ES 不使用 Swap 分區

2.3.1 Linux 系統中的關閉 Swap (臨時有效)

執行命令

sudo swapoff -a

可以臨時禁用 Swap 內存,但是操作系統重啓後失效

2.3.2 Linux 系統中的儘可能減少 Swap 的使用(永久有效)

執行下列命令

echo "vm.swappiness = 1">> /etc/sysctl.conf

正常情況下不會使用 Swap,除非緊急情況下才會 Swap。

2.3.3 啓用 bootstrap.memory_lock

config/elasticsearch.yml 文件增加配置

#鎖定內存,不讓 JVM 寫入 Swap,避免降低 ES 的性能
bootstrap.memory_lock: true

2.4 減少分片數、副本數

分片

索引的大小取決於分片與段的大小,分片過小,可能導致段過小,進而導致開銷增加;分片過大可能導致分片頻繁 Merge,產生大量 IO 操作,影響寫入性能。

因爲我們每個索引的大小在 15G 以下,而默認是 5 個分片,沒有必要這麼多,所以調整爲 3 個。

"index.number_of_shards""3"

分片的設置我們也可以配置在索引模板。

副本數

減少集羣副本分片數,過多副本會導致 ES 內部寫擴大。副本數默認爲 1,如果某索引所在的 1 個節點宕機,擁有副本的另一臺機器擁有索引備份數據,可以讓索引數據正常使用。但是數據寫入副本會影響寫入性能。對於日誌數據,有 1 個副本即可。對於大數據量的索引,可以設置副本數爲0,減少對性能的影響。

"index.number_of_replicas""1"

分片的設置我們也可以配置在索引模板。

3.控制數據來源

3.1 應用按規範打印日誌

有的應用 1 天生成 10G 日誌,而一般的應用只有幾百到 1G。一天生成 10G 日誌一般是因爲部分應用日誌使用不當,很多大數量的日誌可以不打,比如大數據量的列表查詢接口、報表數據、debug 級別日誌等數據是不用上傳到日誌服務器,這些即影響日誌存儲的性能,更影響應用自身性能

四、ES性能優化後的效果

優化後的兩週內ELK性能良好,沒有使用上的問題:

  • ES 數據不再丟失

  • 數據延時在 10 秒之內,一般在 5 秒可以查出

  • 每個 ES 節點負載比較穩定,CPU 和內存使用率都不會過高,如下圖

    ES節點運行情況.png
    ES節點運行情況.png

五、參考文檔

參考

歡迎大家掃碼關注「碼海」,接收後續更多精彩內容!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章