首頁>技術>

一、背景介紹

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

ELK 架構作為日誌儲存方案二、現狀分析1. 版本及硬體配置JDK:JDK1.8_171-b11 (64 位)ES叢集:由3臺16核32G的虛擬機器部署 ES 叢集,每個節點分配 20 G 堆記憶體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 拒絕執行的情況

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

檢視執行緒池的情況也可以看出 write 執行緒池有很多寫入的任務

GET /_cat/thread_pool?v&h=host,name,type,size,active,largest,rejected,completed,queue,queue_size

所以我們需要最佳化 ES 的 write 的效能。

4.解決思路4.1 分析場景

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

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

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

這三類場景的特點如下:

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

關於實時性

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

可以從三方面進行最佳化:JVM 效能調優、ES 效能調優、控制資料來源

1. JVM調優

第一步是 JVM 調優。

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

1.1 監控 JVM 執行情況

(1)檢視 GC 日誌

問題:Young GC 和 Full GC 都很頻繁,特別是 Young GC 頻率高,累積耗時非常多。

(2) 使用 jstat 看下每秒的 GC 情況

引數說明

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 明細

其中 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 新生代使用記憶體的增長率更低

最佳化前

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

最佳化後

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

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

ES 啟動後,執行14個小時

最佳化前

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 頻率非常頻繁。

最佳化後

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

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

2.ES 調優

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

2.1 最佳化 ES 索引設定2.2.1 ES 寫資料底層原理

refreshES 接收資料請求時先存入 ES 的記憶體中,預設每隔一秒會從記憶體 buffer 中將資料寫入作業系統快取 os cache,這個過程叫做 refresh;

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

fsynctranslog 會每隔 5 秒或者在一個變更請求完成之後執行一次 fsync 操作,將 translog 從快取刷入磁碟,這個操作比較耗時,如果對資料一致性要求不是很高時建議將索引改為非同步,如果節點宕機時會有5秒資料丟失;

flushES 預設每隔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 進行檢視。

經過上面的各種最佳化,拒絕的資料量少了很多,但是還是存在拒絕的情況,如下圖:

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

最佳化後效果

可以看到,已經沒有拒絕的情況,這樣也就是解決了日誌丟失的問題。

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 和記憶體使用率都不會過高,如下圖

13
最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • 群暉Docker套件下搭建執行MSSQL