線上服務釋出抖動,該怎麼解決呢

2022-10-04 21:01:59

之前的文章分別講了優雅上線優雅下線,實際工作中做了優雅上下線後,服務釋出後還是會有短暫的「抖動」,介面的響應時間急劇升高後又恢復正常,就和下面的監控圖一樣,圖片來源於 得物 的InfoQ技術檔案服務釋出時網路「抖動」

背景

小卷現在負責的系統已經達到20萬QPS了,每天即使是在半夜,QPS依然過萬。每次系統升級釋出時,抖動比較頻繁,上游應用方都跑過來質問,怎麼服務又超時了啊,還能不能用了。。。(巴拉巴拉),小卷只能陪著笑臉的一番解釋。後來小卷加上了優雅上下線,想著這下發布應該沒問題了吧。哪知再次釋出,超時問題依然存在。。。小卷決定好好分析一下發布抖動問題的根因是啥

1.抖動問題分析

服務抖動問題需要根據具體場景分析,這裡列一下可能的原因:

  • redis、DB連線初始化耗時長,引起啟動後的介面RT升高
  • JIT即時編譯耗時長,造成CPU利用率高,引起介面RT升高

對於高並行的應用來說,這裡JIT即時編譯是通用的原因。

JIT是什麼?

JIT(just-in-time)即時編譯,是一種執行計算機程式碼的方法,這種方法涉及在程式執行過程中(在執行期)而不是在執行之前進行。關於JIT的歷史,摘抄一段維基百科上的內容

最早釋出的JIT編譯器是 約翰·麥卡錫在1960年對LISP的研究。在他的重要論文《符號表示式的遞迴函數及其在機器上的計算》(Recursive functions of symbolic expressions and their computation by machine, Part I)提到了在執行時被轉換的函數,因此不需要儲存編譯器輸出來打孔卡。在Self被Sun公司拋棄後,研究轉向了Java語言。「即時編譯」這個術語是從製造術語「及時」中借來的,並由Java普及,Java之父James Gosling從1993年開始使用這個術語。目前,大多數Java虛擬機器器的實現都使用JIT技術,而且使用廣泛。

瞭解JVM的都知道,Java的編譯分為兩部分:

  • javac.java檔案編譯為.class檔案,即轉換為位元組碼
  • 直譯器將.class位元組碼檔案解釋為機器碼(0、1)執行

但是解釋執行的缺點很明顯,執行速度慢。

Java早期使用解釋執行,將位元組碼逐條解釋執行,這種方式執行很慢。如果是快速反覆呼叫某段程式碼,執行效率大大降低。後來為了解決這種問題,JVM引入了JIT即時編譯,當Java虛擬機器器發現某段程式碼塊或是方法執行比較頻繁,超過設定的閾值時,就會把這些程式碼視為熱點程式碼(Hot Spot code)

為了提高熱點程式碼的執行效率,虛擬機器器會將其編譯為機器碼,並存到CodeCache裡,等到下次再執行這段程式碼時,直接從CodeCache裡取,直接執行,大大提升了執行效率,整個執行過程如下:

看上圖很容易理解JIT是什麼,然後思考下面的問題:

  • 怎麼判斷屬於熱點程式碼?
  • 閾值是怎麼設定的?
  • codeCache又是什麼?

怎麼判斷熱點程式碼

我們知道JIT是將熱點程式碼編譯成機器碼快取起來的,那麼什麼樣的程式碼才屬於熱點程式碼呢

HotSpot虛擬機器器使用的是基於計數器的熱點程式碼探測,JVM統計每個方法呼叫棧的彈出頻率作為指標,提供了2種次數級別熱點探測方法:

  1. 精確計數,超過閾值觸發編譯 (統計的是總呼叫量)
  2. 記錄一段時間內被呼叫的次數,超過閾值觸發編譯(類似QPS的含義)

JVM預設使用的第二種方法統計方法呼叫次數,因為第一種方法計算開銷大,第二種方法與呼叫時間有關,適用於大多數場景

閾值如何設定

上面說到超過閾值才觸發編譯,閾值是設定為多少了呢?

先說說JVM的分層編譯器,Hotspot虛擬機器器中,JIT有2種編譯器C1編譯器(使用者端模式)、C2編譯器(伺服器端模式)。

C1編譯器:簡單快速,蒐集資訊較少,主要關注點在區域性化的優化,編譯速度快,適用於對啟動效能有要求的應用。缺點是編譯後的程式碼執行效率低;

C2編譯器:需要蒐集大量的統計資訊在編譯時進行優化,為長期執行的應用程式做效能優化的編譯器,優化手段複雜,編譯時間長,編譯出來的機器碼執行效率高。代價是啟動時間變長,程式需要執行較長時間後,才能達到最佳效能;

JAVA8之後預設開啟了分層編譯,即:應用啟動初期使用C1編譯器快取熱點程式碼,在系統穩定後使用C2編譯器繼續優化效能。

可通過一些引數進行設定

在 Java8 中預設開啟分層編譯(-XX:+TieredCompilation預設為true)

  • 如果只想用 C1,可以在開啟分層編譯的同時使用引數「-XX:TieredStopAtLevel=1」
  • 如果只想用 C2,使用引數「-XX:-TieredCompilation=false」關閉分層編譯即可

通過java -version可看到當前JVM使用的編譯模式

方法被呼叫的次數,在 C1 模式下預設閾值是 1500 次,在 C2 模式是 10000 次,可通過引數-XX: CompileThreshold 手動設定,在分層編譯的情況下,-XX: CompileThreshold 指定的閾值將失效,此時將會根據當前待編譯的方法數以及編譯執行緒數來動態調整。超過閾值觸發編譯,編譯完成後系統會把方法呼叫入口改為最新地址,下次直接使用機器碼。

需要注意的是,計數器統計的是一段時間內的呼叫次數,當超過時間限度呼叫次數仍然未達到閾值,那麼該方法的呼叫次數就會減半,並不是一直累加的,這段時間稱為該方法的統計半衰週期,可以使用虛擬機器器引數-XX:-UseCounterDecay 關閉熱度衰減,引數-XX:CounterHalfLifeTime 設定半衰週期的時間,需要注意進行熱度衰減的動作是在虛擬機器器進行垃圾收集時順便進行的。

CodeCache是什麼

CodeCache主要用於儲存JIT編譯後的機器碼,隨著程式的執行,大部分熱點程式碼都會編譯為機器碼來執行。所以Java的執行速度比較快,除了JIT編譯的程式碼外,本地方法程式碼(JNI)也會儲存在Codecache內。可設定一些引數設定Codecache的屬性

  • -XX:ReservedCodeCacheSize:codeCache最大大小
  • -XX:InitialCodeCacheSize:codeCache初始大小

在Linux環境下,Codecache預設大小是2.4375M,可通過jinfo -flag InitialCodeCacheSize [java程序ID]檢視,如圖

2.為什麼應用剛啟動時會抖動?

上面已經講了JIT即時編譯,這樣也好理解為什麼剛啟動完的應用,RT突然升高,CPU利用率也很高。在高並行場景下,一個方法的呼叫次數激增,會瞬間達到JIT編譯的閾值,JVM會執行即時編譯,講熱點程式碼轉為機器碼。熱點程式碼過多時,JIT編譯的壓力會增大,造成系統的load升高,CPU利用率跟著升高,導致服務的整體效能下降

3.解決方案

這裡小卷列了一些解決方案,需要根據具體場景具體使用,如圖

JWarmup

AJDK內嵌的功能模組,相關wiki在阿里的Github上阿里巴巴Dragonwell8使用者指南

其原理是先發布beta伺服器,等到beta伺服器的JIT編譯完成後,將熱點方法dump下來,然後production環境釋出時直接載入dump檔案,不需要再進行JIT編譯了。從JVM層面解決了該問題,但是接入門檻較高,可能會踩一些坑。

平臺預熱

藉助流量排程平臺的能力,小流量預熱後再放開,把JIT編譯的影響降低。是綜合考慮接入成本以及推廣維護最合適的方案。這裡阿里雲微服務引擎MSE已提供功能小流量預熱服務,但是是收費的哦~

關注我

我是卷福同學,公眾號同名,在福報廠修福報的小卷哦~