ElasticSearch6.3.2 中的JVM性能調優

 

前一段時間被人問了個問題:在使用ES的過程中有沒有做過什麼JVM調優措施?

在我搭建ES集羣過程中,參照important-settings官方文檔來的,並沒有對JVM參數做過多的調整。但談到JVM配置參數,少不了操作系統層面上的一些配置參數,比如 page cache 和文件描述符的個數:(/etc/security/limits.conf)。另外ES jvm.options配置文件也針對JVM參數做了一些優化,這裏簡要介紹一下ElasticSearch中與jvm相關的各個配置參數:

  • 將 Xms 和 Xmx 設置成一樣大 避免JVM堆的動態調整給應用進程帶來"不穩定"。參考:Heap Tuning Parameters

    By default, the JVM grows or shrinks the heap at each GC to try to keep the proportion of free space to the living objects at each collection within a specific range. This range is set as a percentage by the parameters -XX:MinHeapFreeRatio=minimum and -XX:MaxHeapFreeRatio=maximum; and the total size bounded by -Xms and -Xmx

  • 預留足夠的內存空間給page cache。假設一臺32GB內存的機器,配置16GB內存給ES進程使用,另外16GB給page cache,而不是將 xmx 設置成32GB。

  • 禁用內存交換(/proc/sys/vm/swappiness),後面解釋爲什麼要禁用內存交換?

  • 配置JVM參數:-XX:+AlwaysPreTouch 減少新生代晉升到老年代時停頓。JDK官方文檔關於 AlwaysPreTouch 的解釋是:Pre-touch the Java heap during JVM initialization. Every page of the heap is thus demand-zeroed during initialization rather than incrementally during application execution.
    也就是說:在啓動時就把參數裏說好了的內存全部都分配了,會使得啓動慢上一點,但後面訪問時會更流暢,比如頁面會連續分配,比如不會在晉升新生代到老生代時纔去訪問頁面,導致GC停頓時間加長

既然提到了這個參數,我想說一下Linux內存分配、Linux OOM Killer、內存交換vm.swappiness參數 之間的一些理解:當ES進程向操作系統MMU申請分配內存時,操作系統內核分配的是虛擬內存,比如指定 -Xms=16G 那麼只是告訴內核這個ES進程在啓動的時候最需要16G內存,但是ES進程在啓動後並不是立即就用了16G的內存。因此,隨着ES的運行,ES進程訪問虛擬內存時產生缺頁錯誤(page fault),然後內核爲之分配實際的物理頁面(這個過程也是需要開銷的)。而如果在JVM啓動時指定了AlwaysPreTouch,就會分配實際的物理內存,這樣在發生YGC的時候,新生代對象晉升到老年代,減少老年代空間分配產生的缺頁異常,從而減少YGC停頓時間。

正是由於Linux內存管理機制,應用程序進程可以申請比物理內存大得多的內存,而進程而言,它看到的是邏輯地址空間,因爲通過內存交換(vm.swapiness),操作系統允許將那些長期不用的物理頁面交換到磁盤上去,因此程序能夠申請的內存空間範圍是很大的。由於進程可以申請到一塊很大的地址空間,但不一定把這些空間都使用了。但萬一進程真的實實在在地佔用了這麼多空間,怎麼辦?於是OOM Killer 就出馬了,OOM Killer 會選擇一個進程將之殺死

在使用CMS垃圾回收器時,jmap -heap 查看jvm實際爲ES進程分配的新生代的大小。因爲,影響新生代大小的參數有三個:第一個是:-XX:NewSize和-XX:MaxNewSize、第二個是:-Xmn、第三個是:-XX:NewRatio=2。根據參數XX:NewRatio=2 ,ES 進程 jvm新生代堆大小佔配置的總的堆的1/3,但其實並沒有,就是因爲參數:-XX:NewSize和-XX:MaxNewSize 優先級比XX:NewRatio=2高,覆蓋了這個參數配置的值。

  • -XX:CMSInitiatingOccupancyFraction 設置成75%。主要是因爲CMS是併發收集,垃圾回收線程和用戶線程同時運行,用戶線程運行時可能繼續無用的垃圾對象,如果到90%再去回收就太晚了。老年代使用到75%就回收可減少OOM異常發生的概率。

  • -XX:MaxTenuringThreshold 設置成6。這個值默認爲15,即Survivor區對象經歷15次Young GC後才進入老年代,設置成6就只需6次YGC就晉升到老年代了。默認情況下ES新生代採用 -XX:+UseParNewGC,老年代採用CMS垃圾回收器,關於這個參數的調優說明,可參考:關鍵業務系統的JVM參數推薦(2018仲夏版)

    Young GC是最大的應用停頓來源,而新生代裏GC後存活對象的多少又直接影響停頓的時間,所以如果清楚Young GC的執行頻率和應用裏大部分臨時對象的最長生命週期,可以把它設的更短一點,讓其實不是臨時對象的新生代對象趕緊晉升到年老代

  • -Xss 配置爲1M。線程佔用棧內存,默認每條線程爲1M。從這個參數可看出一個進程下可創建的線程數量是有限制的,可視爲創建線程的開銷。

page cache 與 應用程序內存

Linux內存可分成2種類型:Page cache和應用程序內存。應用程序內存會被Linux的swap機制交換出去,而page cache則是由Linux後臺的異步flush策略刷盤。當OS內存滿了時,就需要把一部分內存數據寫入磁盤,因此就需要決定是swap應用程序內存呢?還是清理page cache?OS有個系統參數/proc/sys/vm/swappiness默認值60,一般將之設置成0,表示優先刷新page cache而不是將應用程序的內存交換出去。

那Linux的page cache的清除策略是什麼呢?--優化了的LRU算法。LRU存在缺點:那些新讀取的但卻只使用一次的數據佔滿了LRU列表,反而把熱點數據給evict 到磁盤上去了,因此還需要考慮數據的訪問次數(頻率)

很多存儲系統針對LRU做了一些優化,比如Redis的鍵過期策略是基於LRU算法的思想,用若干個位記錄每個key的idle time(空閒時間),將空閒時間較大的key存入pool,從pool中選擇key evict出去,具體可參考:Redis的LRU算法。 再比如Mysql innodb 緩衝池管理page也是基於LRU,當新讀入一頁數據時不是立即放到LRU鏈表頭,而是設置mid point(默認37%),即放到鏈表37%位置處 。關於如何理解page cache 與應用程序內存之間的區別,可參考這篇文章:從Apache Kafka 重溫文件高效讀寫

Linux總會把系統中還沒被應用使用的內存挪來給Page Cache,在命令行輸入free,或者cat /proc/meminfo,"Cached"的部分就是Page Cache。

當Linux系統內存不足時,要麼就回收page cache,要麼就內存交換(swap),而內存交換就有可能把應用程序的數據換出到磁盤上去了,這就有可能造成應用的長時間停頓了。參考:在你的代碼之外,服務時延過長的三個追查方向(上)

Linux有個很怪的癖好,當內存不足時,有很大機率不是把用作IO緩存的Page Cache收回,而是把冷的應用內存page out到磁盤上。當這段內存重新要被訪問時,再把它重新page in回內存(所謂的主缺頁錯誤),這個過程進程是停頓的。增長緩慢的老生代,池化的堆外內存,都可能被認爲是冷內存,用 cat /proc/[pid]/status 看看 VmSwap的大小, 再dstat裏看看監控page in發生的時間。

Linux free 內存參數

free -m 查看機器的內存使用情況時,其實一直被以下幾個概念所困擾。Google了一圈,大部分只是"翻譯",沒有解釋。其實我是想了解這些參數對系統的影響是什麼?
man free 查看註釋如下,free 命令輸出的內容其實就是 /proc/meminfo 中的內容,因此真正的是要理解 /proc/meminfo 中每一行的內容。
free displays the total amount of free and used physical and swap memory in the system, as well as the buffers and caches used by the kernel

  1. 如何理解操作系統的交換內存?
  2. 命令輸出的 free 和 available 之間的區別是什麼?free表示:Unused memory (MemFree and SwapFree in /proc/meminfo)。而available 一般都比free字段數值大,這是因爲:available 還包含了那些已經被使用但是可以被回收的內存

一個JVM長時間停頓的示例

隨着系統的運行,JVM堆內存會被使用,引用不可達對象就是垃圾對象,而操作系統有可能會把這些垃圾對象(長期未用的物理頁面)交換到磁盤上去。當JVM堆使用到一定程度時,觸發FullGC,FullGC過程中發現這些未引用的對象都被交換到磁盤上去了,於是JVM垃圾回收進程得把它們重新讀回來到內存中,然而戲劇性的是:這些未引用的對象本身就是垃圾,讀到內存中的目的是回收被丟棄它們!而這種來回讀取磁盤的操作導致了FullGC 消耗了大量時間,(有可能)發生長時間的stop the world。因此,需要禁用內存交換以避免這種現象,這也是爲什麼在部署Kafka和ES的機器上都應該要禁用內存交換的原因吧(如果因長時間STW導致node與master之間的"心跳包"失效,集羣的master節點認爲該節點發生了故障,於是是自動進行failover,對於ES來說可能就發生自動rebalance分片遷移)。具體可參考這篇文章:Just say no to swapping!

總結

本文以ES使用的JVM配置參數爲示例,記錄了一些關於JVM調優的參數理解,以及涉及到的一些關於page cache、應用程序內存區別,LRU算法思想等,它們在Mysql、Kafka、ElasticSearch都有所應用。文中所引用的參考鏈接都非常好,對深入理解系統底層運行原理有幫助。
ElasticSearch官方文檔HowTo也給出了ElasticSearch搜索、索引(Indexing)的調優方案,也值得一看:general-recommendations

一個統計gc日誌的工具:gcplot,以圖形化方式顯示gc情況。(分析一個ES進程的gc日誌得到:99%的gc STW 時間控制在77ms以內)

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