攜程十個(gè)有效降低客戶端超時(shí)的方法
一、背景
在現(xiàn)今的信息時(shí)代,微服務(wù)技術(shù)已成為一種重要的解決方案,微服務(wù)技術(shù)可以使系統(tǒng)的規(guī)模和功能變的更加靈活,從而獲得更高的可擴(kuò)展性和可用性。然而,微服務(wù)調(diào)用中出現(xiàn)的超時(shí)問題,卻也成為系統(tǒng)可用性的一大隱患。超時(shí)會(huì)導(dǎo)致客戶端的性能下降,甚至可能無(wú)法正常工作。本文針對(duì)超時(shí)問題,提出相關(guān)的優(yōu)化手段,降低微服務(wù)調(diào)用超時(shí)的風(fēng)險(xiǎn)。
1.1 誤區(qū)
當(dāng)我們遇到超時(shí)或執(zhí)行慢的問題時(shí),我們往往會(huì)認(rèn)為是依賴方出現(xiàn)了問題。
例如:訪問 Redis、DB、 RPC 接口變慢、超時(shí),第一時(shí)間找依賴方排查問題,對(duì)方反饋的結(jié)論是,我這邊(服務(wù)端)沒有問題,請(qǐng)檢查一下你那邊(客戶端)是否有問題。
實(shí)際上,性能下降是一個(gè)非常復(fù)雜的問題,它可能涉及多個(gè)方面,包括服務(wù)端和客戶端。例如:代碼質(zhì)量、硬件資源、網(wǎng)絡(luò)狀況等問題都會(huì)導(dǎo)致性能下降,從而引發(fā)響應(yīng)慢、超時(shí)等問題。因此,我們需要全面地分析問題,找出影響性能的各種因素。
1.2 分享的目的
本文將詳細(xì)介紹我們?cè)谏a(chǎn)環(huán)境中遇到的慢執(zhí)行和超時(shí)等問題,并提出相關(guān)的優(yōu)化手段,通過優(yōu)化長(zhǎng)尾性能,降低變慢或超時(shí)的風(fēng)險(xiǎn),提升系統(tǒng)的穩(wěn)定性。
二、超時(shí)的分類
常見的超時(shí)一般有兩類:
a. 連接超時(shí)(ConnectTimeout):指建立網(wǎng)絡(luò)連接所需要的時(shí)間超出了設(shè)定的等待時(shí)間。
b. Socket 超時(shí)(SocketTimeout):指在數(shù)據(jù)傳輸過程中,客戶端等待服務(wù)端響應(yīng)的時(shí)間超出了設(shè)定的等待時(shí)間。
如下圖,①就是連接超時(shí)關(guān)注的時(shí)間,②就是 Socket 超時(shí)關(guān)注的時(shí)間,本文講解的超時(shí)為 Socket 超時(shí)。
圖1 客戶端請(qǐng)求過程
三、超時(shí)問題分析與優(yōu)化
3.1 設(shè)置合理的超時(shí)時(shí)間
根據(jù)實(shí)際情況設(shè)置合理的超時(shí)時(shí)間,避免因?yàn)槌瑫r(shí)時(shí)間設(shè)置不合理導(dǎo)致的接口超時(shí)。
1)分析
看下客戶端設(shè)置的超時(shí)時(shí)間是否合理。比如調(diào)用服務(wù)端 P99.9 是100ms ,客戶端設(shè)置的超時(shí)時(shí)間是 100ms ,就會(huì)有 0.1% 的請(qǐng)求會(huì)超時(shí)。
2)優(yōu)化方案
我們?cè)谠O(shè)置超時(shí)時(shí)間需要綜合考慮網(wǎng)絡(luò)延遲、服務(wù)響應(yīng)時(shí)間、GC 等情況。
以門票活動(dòng)查詢引擎為例:
- 核心接口:最小值( P99.9*3 ,用戶可接受的等待時(shí)間),核心會(huì)影響到訂單,在用戶可接受范圍內(nèi)盡可能出結(jié)果。
- 非核心接口:最小值( P99.9*1.5,用戶可接受的等待時(shí)間),非核心不影響訂單,不展示也沒關(guān)系。
3.2 限流
當(dāng)系統(tǒng)遇到突發(fā)流量時(shí),通過限流的方式,控制流量的訪問速度,避免系統(tǒng)崩潰或超時(shí)。
1)分析
看下超時(shí)時(shí)間點(diǎn)的請(qǐng)求量是否有突增,比如有某些突然的活動(dòng),這個(gè)時(shí)候應(yīng)用沒有提前擴(kuò)容,面對(duì)突增流量會(huì)導(dǎo)致應(yīng)用負(fù)載比較高,從而導(dǎo)致超時(shí)問題。
2)優(yōu)化方案
評(píng)估當(dāng)前應(yīng)用最大可承載的流量,配置限流,維度可以是單機(jī)+集群。
單機(jī)限流:在面對(duì)突增流量時(shí)避免單機(jī)崩潰。
集群限流:在有限的資源下提供最大化的服務(wù)能力,保證系統(tǒng)穩(wěn)定性,不會(huì)出現(xiàn)崩潰或故障。
3.3 提升緩存命中率
提升緩存命中率,可以提高接口的響應(yīng)速度,降低接口的響應(yīng)時(shí)間,從而減少超時(shí)的發(fā)生。
1)分析
分析調(diào)用鏈路,找到慢的地方對(duì)其進(jìn)行優(yōu)化,提升服務(wù)端的響應(yīng)速度。
如下圖所示,很明顯可以看到服務(wù)端執(zhí)行時(shí)間超過了客戶端配置的超時(shí)時(shí)間 200ms 導(dǎo)致超時(shí)。
圖2 客戶端調(diào)用服務(wù)端超時(shí)鏈路
繼續(xù)分析服務(wù)端執(zhí)行鏈路,發(fā)現(xiàn)是因?yàn)榫彺鏇]有命中導(dǎo)致的。
圖3 緩存未命中鏈路
2)優(yōu)化方案
對(duì)于高并發(fā)系統(tǒng)來(lái)說,常見的是使用緩存來(lái)提升性能。
如下圖是之前的緩存架構(gòu),這種緩存架構(gòu)有兩個(gè)風(fēng)險(xiǎn)。
a. 緩存是固定過期的,會(huì)導(dǎo)致某個(gè)時(shí)間大量 key 失效直接擊穿到數(shù)據(jù)庫(kù)。
b. 主動(dòng)刷新機(jī)制是刪除緩存,監(jiān)聽數(shù)據(jù)庫(kù) binlog 消息刪除緩存,如果大批量刷數(shù)據(jù)會(huì)導(dǎo)致大量 key 失效。
圖4 固定過期+懶加載模式
針對(duì)上面的風(fēng)險(xiǎn)我們優(yōu)化了緩存架構(gòu),固定過期改為主動(dòng)續(xù)期緩存,主動(dòng)監(jiān)聽消息刷新緩存的方案,如下圖所示。
圖5 緩存前后架構(gòu)對(duì)比
3)效果
緩存命中率提升到 98% 以上,接口性能(RT)提升 50% 以上。
圖6 處理性能提升50%
這個(gè)緩存優(yōu)化方案在我們團(tuán)隊(duì)之前寫的一篇文章《1分鐘售票8萬(wàn)張!門票搶票背后的技術(shù)思考》中有詳細(xì)的介紹,具體細(xì)節(jié)這個(gè)地方不再展開,有興趣的同學(xué)可以自行閱讀。
3.4 優(yōu)化線程池
減少不合理的線程,降低線程切換帶來(lái)的超時(shí)。
1)分析
a. HTTP 線程數(shù)
先看下服務(wù)端 HTTP 線程數(shù)是否有明顯增加,且流量沒有增長(zhǎng),要確認(rèn) HTTP 線程數(shù)增加不是因?yàn)榱髁吭鲩L(zhǎng)導(dǎo)致的。如下面兩張圖,流量正常的情況下 HTTP 線程數(shù)增加,說明是服務(wù)端響應(yīng)變慢導(dǎo)致,可以確認(rèn)超時(shí)是服務(wù)端原因。
圖7 服務(wù)流量平穩(wěn)
圖8 HTTP線程數(shù)突增
b. 總線程數(shù)
再看下總線程數(shù)是否有增加(排除 HTTP 線程數(shù)),如果有,說明有使用多線程導(dǎo)致線程數(shù)量增加。這個(gè)時(shí)候需要 Dump 下線程,看下哪些線程使用的比較多。
2)解決方案
a. 統(tǒng)一管理線程池:動(dòng)態(tài)配置參數(shù)+監(jiān)控能力
通過工具類封裝統(tǒng)一的線程池,提供動(dòng)態(tài)配置參數(shù)和線程池監(jiān)控能力。
- 效果
線程池具備監(jiān)控能力,如下圖是最小值(核心線程數(shù))、最大值(最大線程數(shù))和當(dāng)前線程池中線程數(shù)量的監(jiān)控,可以參考這個(gè)來(lái)調(diào)整線程池參數(shù)。
圖9 線程池水位線監(jiān)控
b. 異步改同步:小于10ms 的不使用多線程
高并發(fā)的場(chǎng)景下線程太多,線程調(diào)度時(shí)間得不到保障,一次任務(wù)需要多個(gè) CPU 時(shí)間片,下一次調(diào)度的時(shí)間無(wú)法得到保障。
如下圖是一個(gè)線程池執(zhí)行耗時(shí)埋點(diǎn),通過埋點(diǎn)發(fā)現(xiàn) A 在線程池中執(zhí)行的比較快,平均線和 P95 都在 10ms 以下,沒有必要使用線程池,改成同步執(zhí)行。
圖10 優(yōu)化前執(zhí)行耗時(shí)
- 效果
接口性能提升明顯,平均線從 2.7ms 降低到 1.6ms,P99.9 從 23.7ms 降低到 1.7ms。
之前使用多線程,請(qǐng)求量有波動(dòng)的時(shí)候線程增加比較多,導(dǎo)致線程調(diào)度時(shí)間得不到保障,體現(xiàn)到 P99.9 就很高。
圖11 優(yōu)化前后耗時(shí)對(duì)比
另外可以明顯看到總線程數(shù)也相應(yīng)減少了很多。
圖12 異步改同步后總線程數(shù)減少
3.5 優(yōu)化 GC
優(yōu)化 GC,減少 GC 的停頓時(shí)間,提高接口的性能。
1 )分析
首先看超時(shí)時(shí)間點(diǎn)是否有 Full GC,沒有再看下 Yong GC 是否有明顯的毛刺,如下圖可以看到 3 個(gè)明顯的毛刺。
圖13 Yong GC時(shí)間
如果超時(shí)的時(shí)間點(diǎn)(如下圖)可以對(duì)應(yīng)上 GC 毛刺時(shí)間點(diǎn),那可以確認(rèn)問題是由于Yong GC 導(dǎo)致。
圖14 客戶端調(diào)用服務(wù)端超時(shí)次數(shù)
2)解決方案
a. 通用性 JVM 參數(shù)調(diào)優(yōu)
檢查 -Xmx -Xms 這兩個(gè)值設(shè)置的是否一樣,如果不一樣 JVM 在運(yùn)行時(shí)會(huì)根據(jù)實(shí)際情況來(lái)動(dòng)態(tài)調(diào)整堆大小,這個(gè)調(diào)整頻繁會(huì)有性能開銷,并且初始化堆較小的話,GC 次數(shù)會(huì)比較頻繁。
- 效果
-Xmx3296m -Xms1977m 改成 -Xmx3296m -Xms3296m 后效果如下圖所示,頻率和時(shí)間都有明顯的下降。
圖15 通用性JVM參數(shù)調(diào)優(yōu)后效果
b. G1 垃圾回收器參數(shù)調(diào)優(yōu)
背景:如果沒有設(shè)置新生代最大值和最小值或者只設(shè)置了最大值和最小值中的一個(gè),那么 G1 將根據(jù)參數(shù) G1MaxNewSizePercent(默認(rèn)值為60)和 G1NewSizePercent(默認(rèn)值為5)占整個(gè)堆空間的比例來(lái)計(jì)算最大值和最小值,會(huì)動(dòng)態(tài)平衡來(lái)分配新生代空間。
JVM剛啟動(dòng)默認(rèn)分配新生代空間是總堆的 5%,隨著流量的增加,新生代很容易就滿了,從而發(fā)生 Yong GC,下一次重新分配更多新生代空間,直到從默認(rèn)的 5% 動(dòng)態(tài)擴(kuò)容和合適的初始值。這種配置在發(fā)布接入流量或者大流量涌入時(shí)容易發(fā)生頻繁的 Yong GC。
針對(duì)這類問題,優(yōu)化方案是調(diào)大 G1NewSizePercent,調(diào)大初始值,讓 GC 更加平穩(wěn)。這個(gè)值需要根據(jù)業(yè)務(wù)場(chǎng)景參考GC日志中Eden初始大小分布來(lái)設(shè)置,太大可能會(huì)導(dǎo)致 Full GC 問題。
以查詢引擎為例,根據(jù) GC 日志分析,新生代大小占堆比例在 35% 后相對(duì)平穩(wěn),設(shè)置的參數(shù)為:
-XX:+UnlockExperimentalVMOptions -XX:G1NewSizePercent=35
- 效果
優(yōu)化后效果如下圖,可以看到優(yōu)化之后 GC 次數(shù)從 27次/min 降低到 11次/min,GC 時(shí)間從 560ms 降低到 250ms。
圖16 G1參數(shù)調(diào)優(yōu)后效果
3.6 線程異步改成 NIO 異步編程
NIO(非阻塞 IO)可以減少線程數(shù)量,提高線程的利用率,從而降低線程切換帶來(lái)的超時(shí)。
1)分析:CPU 指標(biāo)
分析 CPU 相關(guān)指標(biāo),如果出現(xiàn) CPU 利用率正常,CPU Load 高需要重點(diǎn)關(guān)注(如果是CPU 利用率高的情況,說明 CPU 本身就很繁忙,那 CPU Load 高也比較正常)。
在分析之前,先介紹幾個(gè)概念:
a. CPU時(shí)間片
CPU 將時(shí)間分成若干個(gè)時(shí)間片,每個(gè)時(shí)間片分配給一個(gè)線程使用。當(dāng)一個(gè)時(shí)間片用完后,CPU 會(huì)停止當(dāng)前線程的執(zhí)行,進(jìn)行上下文切換到下一個(gè)任務(wù),以此類推。
這樣可以讓多個(gè)任務(wù)在同一時(shí)間內(nèi)并發(fā)執(zhí)行,提高系統(tǒng)的效率和響應(yīng)速度。
下圖模擬了單核 CPU 執(zhí)行的過程,需要注意的進(jìn)行上下文切換是需要開銷的,但實(shí)際一次上下文切換需要的時(shí)間很短(一般是微秒級(jí)別)。
圖17 CPU執(zhí)行線程流程
b. CPU利用率
按時(shí)間片維度來(lái)理解,假設(shè)每次時(shí)間片都正好被使用完。
c. CPU Load
從上面概念分析,如果出現(xiàn) CPU 利用率正常,但是 CPU Load 高,那說明 CPU 空閑時(shí)間片、等待線程數(shù)很多,正在使用的時(shí)間片很少,這種情況要減少 CPU Load 需要減少等待線程數(shù)。
2)分析:實(shí)際案例
我們之前生產(chǎn)遇到過多次 CPU Load 高 CPU 利用率正常的情況。問題出現(xiàn)前后代碼沒有變動(dòng),比較明顯的變化是流量有上漲。排查代碼發(fā)現(xiàn)有使用線程池并發(fā)調(diào)用接口的地方,調(diào)用方式如下圖。
圖18 線程池執(zhí)行模型
這種方式在流量較低的情況下看不出什么問題,流量變高會(huì)導(dǎo)致需要的線程數(shù)量成倍增加。
例如:一次請(qǐng)求 A 需要調(diào)用 BCD 3個(gè)接口,那100個(gè)并發(fā)需要的線程數(shù)就是100 + 3*100=400(第一個(gè)100是A對(duì)應(yīng)的主線程,后面的3*100是 BCD 需要的100個(gè)線程)。
3)解決方案
線程池并發(fā)調(diào)用改成 NIO 異步調(diào)用,如下圖所示。
和之前對(duì)比,100 個(gè)并發(fā),需要的線程數(shù)也是 100(這個(gè)地方不考慮 NIO 本身的線程,這個(gè)是全局的,并且是相對(duì)固定很少的線程數(shù))。
圖19 NIO異步調(diào)用執(zhí)行模型
4)效果
超時(shí)問題沒再出現(xiàn),CPU Load 平均下降50%,之前 Load 經(jīng)常超過2(CPU 核數(shù)為2),改造之后 Load 降到 0.5 左右。
圖20 CPU Load優(yōu)化后效果
3.7 啟動(dòng)階段預(yù)熱
啟動(dòng)階段預(yù)熱可以提前建立鏈接,減少流量接入時(shí)的鏈接建立,從而降低超時(shí)的發(fā)生。
1)分析
應(yīng)用拉入后出現(xiàn)大量超時(shí),并且 CPU Load 高 CPU 利用率正常,說明有很多等待線程,這種是拉入后有大量請(qǐng)求在等待被處理。
之前我們生產(chǎn)遇到過是在等待 Redis 建立鏈接,建鏈的過程是同步的,應(yīng)用剛拉入請(qǐng)求量瞬間涌入就會(huì)導(dǎo)致大量請(qǐng)求在等待 Redis 建鏈完成。
2)解決方案
啟動(dòng)階段預(yù)熱提前建立鏈接,或者是配置 Redis 的最小空閑連接數(shù)。其他資源準(zhǔn)備也可以通過啟動(dòng)階段預(yù)熱完成,比如 DB 鏈接、本地緩存加載等。
3.8 優(yōu)化 JIT
JIT(Just-In-Time)編譯可以提高程序的運(yùn)行效率,灰度接入流量將字節(jié)碼編譯成本地機(jī)器碼,避免對(duì)接口性能的影響。
1)JIT 介紹
JIT 是 Just-In-Time 的縮寫,意為即時(shí)編譯。JIT 是一種在程序運(yùn)行時(shí)將字節(jié)碼編譯為本地機(jī)器碼的技術(shù),可以提高程序的執(zhí)行效率。
在 Java 中,程序首先被編譯為字節(jié)碼,然后由 JVM 解釋執(zhí)行。但是,解釋執(zhí)行的效率較低,因?yàn)槊看螆?zhí)行都需要解釋一遍字節(jié)碼。為了提高程序的執(zhí)行效率,JIT 技術(shù)被引入到 Java 中。JIT 會(huì)在程序運(yùn)行時(shí),將頻繁執(zhí)行的代碼塊編譯為本地機(jī)器碼,然后再執(zhí)行機(jī)器碼,這樣可以大大提高程序的執(zhí)行效率。
2)分析
JIT 技術(shù)可以根據(jù)程序的實(shí)際運(yùn)行情況,動(dòng)態(tài)地優(yōu)化代碼,使得程序的性能更好。但是JIT 編譯過程需要一定的時(shí)間,因此在程序剛開始運(yùn)行時(shí),可能會(huì)出現(xiàn)一些性能瓶頸。
如下圖應(yīng)用拉入后 JIT 時(shí)間很久,那可以確認(rèn)是 JIT 導(dǎo)致超時(shí)。
圖21 JIT執(zhí)行時(shí)間
3)解決方案
優(yōu)化 JIT 一個(gè)比較好的方案是開啟服務(wù)預(yù)熱(預(yù)熱功能攜程 RPC 框架是支持的)。
原理是讓應(yīng)用拉入后不是立馬接入100% 流量,而是隨著時(shí)間移動(dòng)來(lái)逐漸增加流量,最終接入100% 流量,這種會(huì)讓小部分流量將熱點(diǎn)代碼提前編譯好。
4)效果
開啟服務(wù)預(yù)熱后,如下圖所示,應(yīng)用流量是逐漸增加的,可以看到響應(yīng)時(shí)間隨著時(shí)間越來(lái)越低,這就達(dá)到了預(yù)熱的效果。
圖22 服務(wù)拉入后請(qǐng)求量和響應(yīng)時(shí)間
3.9 換宿主機(jī)
當(dāng)宿主機(jī)負(fù)載過高時(shí),可以考慮更換宿主機(jī),避免宿主機(jī)負(fù)載過高影響容器負(fù)載。
1)分析
a. CPU Throttled 指標(biāo)
看下應(yīng)用 CPU 節(jié)流指標(biāo),CPU 節(jié)流會(huì)導(dǎo)致 CPU 休眠引起服務(wù)停頓。如果 CPU 利用率正常還是出現(xiàn)了 CPU 節(jié)流,這種大多數(shù)都是宿主機(jī)問題導(dǎo)致。
圖23 CPU節(jié)流情況
b. 宿主機(jī)指標(biāo)
排查宿主機(jī) CPU 利用率、CPU Load、磁盤、IO 等指標(biāo)是否正常,如下圖 CPU Load在某個(gè)時(shí)刻后大于1說明宿主機(jī)負(fù)載較大。
圖24 宿主機(jī)CPU Load監(jiān)控
2)解決方案
重啟機(jī)器換宿主機(jī)。
3.10 優(yōu)化網(wǎng)絡(luò)
排查不穩(wěn)定的網(wǎng)絡(luò)線路,保證網(wǎng)絡(luò)的穩(wěn)定性。
1)分析
網(wǎng)絡(luò)的重點(diǎn)看下 TCPLostRetransmit(丟失的重傳包)指標(biāo)。比如下圖,某個(gè)點(diǎn)指標(biāo)異常,而這個(gè)點(diǎn)其他指標(biāo)都正常,那可以初步懷疑是網(wǎng)絡(luò)問題導(dǎo)致,最終確認(rèn)需要找網(wǎng)絡(luò)相關(guān)團(tuán)隊(duì)確認(rèn)。
圖25 TCPLostRetransmit指標(biāo)
2)解決方案
找網(wǎng)絡(luò)相關(guān)團(tuán)隊(duì)排查優(yōu)化。
四、總結(jié)
回顧全文,主要講解遇到超時(shí)問題怎么分析、怎么定位、怎么優(yōu)化,從簡(jiǎn)單到復(fù)雜總結(jié)了 10 種常見的優(yōu)化方法。這些方法不一定能解決其他不同業(yè)務(wù)場(chǎng)景的超時(shí)問題,具體需要結(jié)合自己的實(shí)際業(yè)務(wù)場(chǎng)景來(lái)驗(yàn)證。
本文總結(jié)的方法都是我們?cè)谏a(chǎn)中遇到的真實(shí)情況,通過不斷實(shí)踐總結(jié)出來(lái)的,希望這些內(nèi)容能夠給閱讀本文的同學(xué)帶來(lái)一定的收獲。
4.1 優(yōu)化注意事項(xiàng)
- 超時(shí)時(shí)間設(shè)置和 GC 調(diào)優(yōu)需要結(jié)合自己業(yè)務(wù)場(chǎng)景來(lái)優(yōu)化。
- NIO 異步編程改造成本、復(fù)雜度較高,我們也在探索更簡(jiǎn)單的方式,例如 JDK19 引入的虛擬線程(類似 Go 協(xié)程),可以用同步編程方式來(lái)實(shí)現(xiàn)異步的效果。