:JVM調(diào)優(yōu)解決Kafka積壓)
說句實話以前看到“JVM調(diào)優(yōu)”這四個字我腦子里第一時間蹦出來的也是“八股文”內(nèi)存模型、GC算法、類加載機制、雙親委派……背得滾瓜爛熟但真正到了線上面對一個“瘋了一樣吞CPU卻不吐結(jié)果”的Java進程還是會有點手足無措。這次Logstash性能問題排查記錄就是一次把“面試題”重新拆成“排查工具”的完整過程——我打算把它原原本本寫出來如果你也在維護ELK這套日志管道或者正在被Kafka消費延遲、日志積壓這類問題折磨這篇應(yīng)該能給你省下不少彎路。這篇文章不會講太多花哨的理論重點是一臺Logstash在高峰期卡死之后我如何通過JVM自帶工具、GC日志分析、啟動參數(shù)調(diào)整一步步找到根因并最終恢復(fù)吞吐量的全過程。中間會穿插我踩過的坑、查過的資料、以及最終沉淀下來的一套參數(shù)配置。不管你是剛接觸Logstash的運維新手還是后端Java開發(fā)想借實例真正理解JVM調(diào)優(yōu)這篇文章都按“現(xiàn)象 → 分析 → 操作 → 復(fù)盤”的順序來寫可以直接照著做。1. 問題是怎么發(fā)生的Kafka Lag 突然翹了上來先說下當時的架構(gòu)線上日志通過Filebeat采集發(fā)到Kafka再由Logstash消費并清洗最后寫入Elasticsearch做檢索。這套鏈路跑了大半年一直很穩(wěn)直到某一天流量高峰監(jiān)控面板上的Kafka消費延遲Consumer Lag曲線突然像坐了火箭一樣往上竄。一開始我以為是Elasticsearch寫入變慢了因為Logstash的寫入端和數(shù)據(jù)量直接掛鉤但查了一圈ES的bulk隊列、線程池、慢日志都正常索引速率也沒掉那問題大概率就出在Logstash本身。1.1 現(xiàn)象描述進程活著但就是不吐數(shù)據(jù)登錄服務(wù)器敲幾條命令CPU占用直接飆到百分之八九百但看一眼Logstash的監(jiān)控指標事件處理速率卻不升反降Kafka里積壓的消息越來越多。最氣人的是進程沒有掛日志里也沒有明顯異??雌饋怼耙磺姓!睂嶋H上數(shù)據(jù)就是卡在里面出不去。這是最典型的“假死”狀態(tài)——進程活著線程也占著但真正的處理能力已經(jīng)崩潰了。我當時的第一個反應(yīng)是檢查Logstash的管道配置懷疑是不是filter里的grok正則寫得太復(fù)雜或者某個插件在高峰時段出現(xiàn)了阻塞。但把pipeline的worker數(shù)和batch size隨便改了幾下重啟之后問題并沒有實質(zhì)改善。這時候我才意識到問題的根源可能不在Logstash的業(yè)務(wù)處理邏輯上而是承載它的JVM環(huán)境已經(jīng)處于“亞健康”狀態(tài)了。1.2 初步判斷Logstash性能問題先別急著懷疑管道很多人有一個誤區(qū)Logstash處理性能慢了第一反應(yīng)就是加filter、調(diào)grok、換插件很少有人會去想JVM本身是不是已經(jīng)不行了。但Logstash的本質(zhì)是一個跑在JRuby上的Java應(yīng)用它的所有事件處理、隊列緩沖、內(nèi)存分配全部依賴底層JVM的健康程度。JVM堆內(nèi)存、垃圾回收、JIT編譯這些“聽起來像八股文”的東西在高壓場景下會直接決定Logstash是每秒處理一萬條還是每秒處理一千條。所以排查思路從這一步開始轉(zhuǎn)變不再盯著Logstash的管道配置看而是把它當成一個普通的Java進程先用JVM的視角去體檢。這里也順帶說明一下網(wǎng)上經(jīng)常有人問“JDK、JVM、JRE有什么區(qū)別”——簡單來說JRE是Java運行環(huán)境JVM是JRE核心里真正負責執(zhí)行字節(jié)碼的虛擬機JDK則是開發(fā)工具包。Logstash自帶JRuby和Java運行時所以它本質(zhì)上就是一個“套了Ruby皮的Java程序”所有Java系的JVM調(diào)優(yōu)知識在它身上完全適用。2. 把Logstash當Java應(yīng)用來查先看懂JVM在干什么既然決定按Java應(yīng)用的思路排查那第一步就是把JVM的內(nèi)存模型、GC機制這些“八股文基礎(chǔ)”重新?lián)炱饋硪驗楹竺嫠蟹治龆冀⒃谶@上面。很多情況下不懂原理的人拿到j(luò)stat輸出只會看數(shù)字而懂原理的人能從數(shù)字倒推出一整套現(xiàn)場還原。2.1 JVM內(nèi)存模型快速復(fù)習(xí)堆、元空間、線程棧和直接內(nèi)存JVM運行時數(shù)據(jù)區(qū)可以粗略分成四塊堆內(nèi)存Heap用來存放對象實例是GC回收的主戰(zhàn)場元空間Metaspace存放類元數(shù)據(jù)在Java 8之后替代了永久代默認情況下只受本機內(nèi)存限制但類加載過多也可能撐爆虛擬機棧Stack是線程私有的每個線程執(zhí)行方法時都會創(chuàng)建棧幀棧深度過大就會拋StackOverflowError還有一塊是直接內(nèi)存Direct MemoryNIO和Netty用得最多不在堆內(nèi)但在物理內(nèi)存里如果做網(wǎng)絡(luò)傳輸時分配太多同樣可能觸發(fā)OOM。Logstash這種數(shù)據(jù)處理型應(yīng)用最敏感的就是堆內(nèi)存。因為每條日志進來都會被包裝成Logstash::Event對象還有中間的各種String、Hash全都住在堆上。堆如果設(shè)置得不對垃圾回收就會頻繁發(fā)生輕則吞吐下降重則進程假死。2.2 排查工具組合拳top、jstat、jmap、jstack怎么配合登錄到Logstash所在機器我會按以下順序操作先用top -Hp查看進程的CPU和內(nèi)存占用確認是不是某個線程在瘋狂自旋。Logstash啟動后會有一個獨立的JRuby運行時很多線程名都帶jruby關(guān)鍵字如果發(fā)現(xiàn)某個線程CPU占用異常高說明它正在反復(fù)執(zhí)行代碼或頻繁觸發(fā)GC。用jps -l確認Logstash進程的PID然后jstat -gcutil 1000觀察每秒的GC情況。這一步能立刻判斷出當前GC頻率、單次GC耗時、老年代使用率等關(guān)鍵指標。用jmap -heap 看當前堆的分配情況和各分代使用率特別是Eden區(qū)和老年代的比例是否合理。如果懷疑線程卡住用jstack 抓線程快照看看JRuby的工作線程是不是阻塞在某個地方。這里我想重點說一下gcutil輸出里的幾個字段E代表Eden區(qū)使用率O代表老年代使用率FGC是Full GC發(fā)生次數(shù)FGCT是Full GC累計耗時。如果FGC在短時間內(nèi)快速增長FGCT占比很高基本就能斷定系統(tǒng)正在被垃圾回收拖死。GC本身是為了騰出空間但Full GC觸發(fā)時會“停止整個世界”Stop The World簡稱STW應(yīng)用線程全部暫停這期間Logstash完全無法處理數(shù)據(jù)。2.3 現(xiàn)場抓到的證據(jù)GC日志不會騙人當時我連續(xù)觀察了差不多五分鐘jstat的輸出大致是下面這個樣子S0 S1 E O M CCS YGC YGCT FGC FGCT GCT 0.00 0.00 92.34 95.67 98.12 96.45 1123 18.23 47 23.81 42.04 0.00 0.00 94.12 96.02 98.20 96.50 1124 18.25 48 24.95 43.20 0.00 0.00 95.78 96.89 98.25 96.52 1125 18.28 49 26.03 44.31看得出什么Eden區(qū)使用率一直維持在90%以上老年代也一直在95%左右徘徊Full GC幾乎每10秒就會來一次而且每次FGCT都在1秒以上。翻譯成人話就是堆內(nèi)存不夠用了JVM疲于奔命地做垃圾回收但回收完了馬上又填滿根本沒有足夠的空間讓數(shù)據(jù)處理線程順暢跑。這就像一個人一邊工作一邊不停地打掃房間房間卻永遠堆滿東西工作效率能高才怪。3. 根因分析為什么默認配置撐不住高峰日志量拿到這些數(shù)據(jù)之后我心里基本有數(shù)了但還是要搞清楚一個最關(guān)鍵的問題為什么之前一直沒事偏偏這段時間出問題這就得從頭檢查Logstash的默認JVM配置和數(shù)據(jù)量增長情況。3.1 堆內(nèi)存太小默認1GB根本只夠“自家日用”Logstash的默認JVM堆大小是1GB這個數(shù)值在官方文檔里寫得很清楚除非你通過LS_HEAP_SIZE或jvm.options里的-Xmx參數(shù)顯式指定否則它就老老實實用1GB。對很多小規(guī)模日志場景來說1GB確實夠用因為日志量小事件在高頻GC完成之前就被消費掉了。可一旦業(yè)務(wù)量漲上去比如高峰期每秒涌入幾千上萬條日志這1GB的堆就會變成瓶頸。為什么會這樣我們簡單估算一下假設(shè)一條日志原始文本平均2KB經(jīng)過系統(tǒng)解析后會變成Logstash::Event對象除了message字段還有host、path、timestamp等一堆元數(shù)據(jù)。JRuby環(huán)境下每個Ruby對象在Java層的開銷都比較大換算下來一條日志在堆里實際占用的空間差不多是原始文本的3到5倍也就是6到10KB。如果配置的批處理大小是默認的125條一批數(shù)據(jù)在堆里也就1MB左右看起來不多但架不住Logstash是持續(xù)不斷地消費和寫入在1GB堆里同時存在幾十批數(shù)據(jù)在排隊、在轉(zhuǎn)換、在等待寫入ES內(nèi)存壓力一下就上來了。3.2 G1收集器默認參數(shù)不一定適合你的場景大多數(shù)現(xiàn)代JVM默認的垃圾收集器是G1Logstash也不例外。G1的設(shè)計目標是“可預(yù)測的停頓時間”它把堆分成一個個Region然后根據(jù)各Region垃圾比例來優(yōu)先回收垃圾最多的區(qū)域理論上很優(yōu)雅。但G1對參數(shù)的敏感度也很高默認的MaxGCPauseMillis是200ms意思是JVM會盡量把每次GC停頓時長控制在200毫秒左右但這只是一個“目標”如果堆太小、對象分配太快G1為了追上分配速度依然會觸發(fā)Full GC。更麻煩的是當老年代占滿之后G1會退化到Serial Old式的Full GC這種回收是單線程執(zhí)行大范圍全堆掃描的停頓時間可能是好幾秒。這個過程對Logstash來說是毀滅性的一次幾秒鐘的STW管道里堆積的數(shù)據(jù)會成倍增長等到GC結(jié)束后瞬間把內(nèi)存又填滿然后再次觸發(fā)FGC形成惡性循環(huán)。我當時從jstat里看到FGC之后老年代使用率幾乎沒降多少就猜到已經(jīng)進入這種退化狀態(tài)了。3.3 JRuby執(zhí)行模型與批量參數(shù)Logstash性能的“命門”Logstash的pipeline配置里有幾個非常重要的參數(shù)pipeline.batch.size默認125代表每次從隊列里取多少個事件交給filter和output處理pipeline.batch.delay默認50ms代表即使沒有湊夠batch.size最多等多久也要把現(xiàn)有事件送出去pipeline.workers默認是CPU核數(shù)代表并行處理管道的線程數(shù)。這三個參數(shù)加在一起決定了Logstash處理數(shù)據(jù)的“節(jié)奏”。批處理的好處是減少JRuby和JVM之間的交互次數(shù)讓一批事件一次性完成filter和output提升吞吐。但batch.size調(diào)大的同時內(nèi)存占用也會線性上漲如果堆空間跟不上就會誘發(fā)更頻繁的GC。這就像把一個餐廳的翻臺率公式里加了一個“每桌人數(shù)”的變量每桌人越多廚房備菜的效率越高但倉庫如果不夠大食材堆不下了就只能扔掉重買——扔食材就是垃圾回收買食材就是重新加載成本全在這上面。所以批量調(diào)整和JVM堆配置必須同步考慮不能只看一頭。3.4 順帶說下-XX:CompileThreshold這類JIT參數(shù)在排查過程中我也重新研究了一個常見的面試參數(shù)-XX:CompileThreshold。它的作用是控制熱點代碼被JIT編譯為本地代碼的觸發(fā)閾值默認值在C1編譯器下是1500次方法調(diào)用在C2編譯器下是10000次左右。JRuby本身是Ruby代碼JVM對JRuby的調(diào)用棧進行C2編譯時也需要時間如果CompileThreshold設(shè)置得過高熱點方法會長時間停留在解釋執(zhí)行階段性能自然上不去。反過來如果把這個閾值調(diào)低可以讓高頻執(zhí)行的方法盡早編譯成機器碼減少解釋執(zhí)行的CPU開銷。不過說實話在實際排查過程中這個參數(shù)不是主要矛盾屬于“錦上添花”的優(yōu)化項。如果GC問題不解決把CompileThreshold調(diào)到天上也沒有意義。但了解它的存在是很重要的因為很多面試題里都會問“JVM是如何判斷熱點代碼的”這個問題放到Logstash這種解釋型語言套殼應(yīng)用上有完全不同的解讀——JRuby代碼在JVM里到底算不算“熱點”取決于這段Ruby邏輯被調(diào)用的頻率以及是否觸發(fā)了編譯閾值。4. 調(diào)優(yōu)落地一套我正在用的Logstash JVM配置確定了問題是JVM堆內(nèi)存和GC、批量參數(shù)配置不匹配之后我重新整理了整個Logstash環(huán)境。這里要特別注明以下配置是從實際場景出發(fā)做的調(diào)整不一定適合所有人但思路可復(fù)現(xiàn)先根據(jù)業(yè)務(wù)量估算內(nèi)存需求再反推JVM參數(shù)最后通過監(jiān)控驗證。4.1 堆內(nèi)存怎么定不能拍腦袋要看數(shù)據(jù)量和物理機器這臺機器是32GB內(nèi)存Elasticsearch部署在另一臺機器上所以Logstash可以安心地使用這臺機器的內(nèi)存。我第一反應(yīng)是直接把堆調(diào)到16GB但冷靜下來想了想不妥。為什么因為JVM堆之外還有Metaspace、JRuby本身的原生內(nèi)存、線程棧、網(wǎng)絡(luò)緩沖區(qū)等非堆內(nèi)存如果堆占了16GB整個進程實際占用可能輕松超過20GB。萬一物理機器還有其他任務(wù)在跑就容易觸發(fā)系統(tǒng)級別的內(nèi)存壓力甚至被內(nèi)核OOM Killer直接殺掉。Logstash官方建議堆大小不要超過系統(tǒng)內(nèi)存的50%這個建議是合理的。我最后選擇把-Xmx和-Xms都設(shè)為8GB理由也很簡單高峰期Logstash單批事件batch_size2000在堆里占用的空間大約幾十MB8GB堆可以容納足夠多的批次在隊列里排隊同時給JRuby解釋器、Metaspace、線程棧留出充足余地。Xms和Xmx設(shè)置為相同值是為了避免JVM在運行期動態(tài)擴容堆時產(chǎn)生額外開銷。對追求穩(wěn)定吞吐的在線服務(wù)來說啟動時就把堆分配好比運行中頻繁擴縮容要靠譜得多。4.2 批量參數(shù)和worker數(shù)量怎么配合關(guān)鍵是找到平衡點調(diào)整JVM參數(shù)的同時我把pipeline配置中的batch.size從125調(diào)到了2000batch.delay保持默認的50mspipeline.workers從默認值當時機器是16核所以默認16調(diào)到了8。為什么worker要減半因為數(shù)據(jù)管道不是無腦并行就能提升性能的當batch.size變大之后單個worker在一輪處理中的數(shù)據(jù)量已經(jīng)很大了如果還開16個worker并行對堆內(nèi)存的壓力和對下游ES的沖擊都會成倍增加。尤其在filter階段跑的是grok、dissect這類CPU密集型插件時線程過多反而會導(dǎo)致上下文切換開銷和頻繁GC。我建議一個比較穩(wěn)妥的起步點是pipeline.workers設(shè)置為CPU核數(shù)的一半到三分之二batch.size從500開始壓測逐步往上調(diào)到1000、1500、2000同時盯著GC監(jiān)控。如果發(fā)現(xiàn)FGC頻率上升就說明batch.size調(diào)得太激進需要回退一點。批量調(diào)優(yōu)不是越大越好這句話需要寫在墻上。4.3 完整配置參考jvm.options和pipelines.yml這是我這套環(huán)境最終使用的配置供參考# jvm.options -Xms8g -Xmx8g -XX:UseG1GC -XX:MaxGCPauseMillis200 -XX:UseStringDeduplication -XX:ParallelRefProcEnabled -XX:DisableExplicitGC -Xlog:gc*info,gcheapdebug,gcergodebug:file/usr/share/logstash/logs/gc.log:utctime,uptimemillis,level,tags:filecount5,filesize50m這里解釋幾個關(guān)鍵參數(shù)-XX:UseG1GC顯式指定G1作為垃圾回收器避免不同JDK版本默認GC行為不一致的坑。-XX:MaxGCPauseMillis200把目標停頓時間設(shè)為200ms這是Logstash默認值但顯式寫出來便于后續(xù)審計。-XX:UseStringDeduplication日志處理場景會產(chǎn)生大量重復(fù)字符串開啟去重可以降低堆占用。-XX:ParallelRefProcEnabled并行處理引用對象減少GC停頓時間。-XX:DisableExplicitGC防止第三方庫調(diào)用System.gc()觸發(fā)不必要的Full GC。Xlog參數(shù)把GC日志輸出到文件方便事后追溯這是整個調(diào)優(yōu)過程中我最后悔沒有早點做的事——日志是最好的老師。然后在pipelines.yml里核心配置如下pipeline: batch: size: 2000 delay: 50 workers: 8 max_inflight: 8192max_inflight代表管道內(nèi)允許同時存在的最大事件數(shù)默認值為batch.size * workers的兩倍我顯式設(shè)置成8192是為了給突發(fā)流量留緩沖同時避免內(nèi)存無限增長。4.4 調(diào)優(yōu)效果前后數(shù)據(jù)對比調(diào)整完并重啟Logstash之后我盯著監(jiān)控面板觀察了好久數(shù)據(jù)變化非常明顯指標調(diào)優(yōu)前調(diào)優(yōu)后Full GC頻率每10秒一次左右基本為0偶發(fā)一次Full GC平均耗時3秒以上無事件處理速率約2000 events/s波動大穩(wěn)定在10000 events/sKafka消費延遲持續(xù)增長達到分鐘級穩(wěn)定歸零CPU使用率800%以上空轉(zhuǎn)明顯300%左右各線程干實事這里我想特別提醒CPU使用率從800%降到300%看起來像是“占用變低了效率變低了”但其實恰恰相反。之前的高CPU有很大一部分是垃圾回收線程和內(nèi)存分配的消耗真正處理數(shù)據(jù)的線程一直在被STW打斷屬于“忙但不出活”。調(diào)優(yōu)后CPU降下來了但事件處理速率反而提升了五倍這才是健康的CPU占用。5. 常見報錯與避坑指南調(diào)優(yōu)過程中我還順手解決了一些Logstash運行中的經(jīng)典報錯這里整理成速查省得大家再走彎路。5.1 “stopped processing because of an error: (SystemExit) exit org.jruby”這個報錯在Logstash社區(qū)里出現(xiàn)頻率很高很多人一看到就慌了以為是管道崩潰。其實它翻譯過來的意思就是“JRuby進程主動退出了”SystemExit是一種JRuby主動拋出的退出信號。最常見的原因有三個一是進程被系統(tǒng)的OOM Killer殺掉JVM被迫退出二是Logstash啟動腳本在執(zhí)行過程中檢測到配置錯誤主動調(diào)用了System.exit三是堆內(nèi)存設(shè)置不合理導(dǎo)致OutOfMemoryErrorJRuby捕獲后以SystemExit的形式退出。排查思路分三步先看dmesg | tail確認有沒有Out of memory: Kill process的記錄再看Logstash日志里有沒有Java heap space相關(guān)的異常最后檢查jvm.options里有沒有寫錯參數(shù)。我遇到過有人把-Xmx寫成-Xms然后進程一啟動就報錯退出很隱蔽。5.2 Docker容器部署時怎么定位JVM異常重啟和GC日志現(xiàn)在很多人用Docker部署Logstash如果容器異常重啟第一反應(yīng)是docker logs看標準輸出但JVM的GC日志默認不會打到stdout而是輸出到文件或丟棄。針對這類問題我建議啟動時通過LS_JAVA_OPTS環(huán)境變量顯式指定GC日志路徑然后把對應(yīng)目錄掛載為Volume這樣即使容器重建日志也能保存下來。docker run -d \ -e LS_JAVA_OPTS-Xlog:gc*:file/usr/share/logstash/logs/gc.log:time,uptime,level,tags:filecount2,filesize20m \ -v /data/logstash/logs:/usr/share/logstash/logs \ docker.elastic.co/logstash/logstash:8.11.0另外容器里的jmap、jstack這類JDK工具可能沒有打包進鏡像。遇到這種情況可以先用docker exec -it bash看看有沒有如果沒有就只能在啟動時掛載宿主機JDK目錄進去或者用docker cp把JVM自帶的工具拷進容器。更簡單的辦法是讓Logstash開啟JMX遠程監(jiān)控從本機用jconsole或VisualVM遠程連接查看。但JMX端口暴露在公網(wǎng)有安全風險生產(chǎn)環(huán)境建議走內(nèi)網(wǎng)。5.3 集成自定義插件時容易忽略的JVM因素如果你像很多團隊一樣給Logstash集成了自定義插件那還要注意JVM層面對插件代碼的影響。JRuby調(diào)用Java API時會頻繁發(fā)生Ruby對象和Java對象的互轉(zhuǎn)這個轉(zhuǎn)換過程是有內(nèi)存開銷的。如果插件內(nèi)部創(chuàng)建了大量臨時數(shù)組或列表沒有及時釋放引用就會造成堆內(nèi)存泄漏。調(diào)優(yōu)時可以用jmap -histo:live 查看當前堆里哪些對象占用了最大空間如果發(fā)現(xiàn)自己的插件類名列前茅那基本可以斷定插件存在內(nèi)存管理問題。這在Logstash集成自定義插件時是特別容易踩的暗坑——插件功能沒毛病但內(nèi)存損耗拖垮了整個管道。5.4 調(diào)優(yōu)誤區(qū)速查表面試常見參數(shù) vs 線上實際操作最后整理一個我個人的對照表很多概念在面試題里是“考記憶”在真實場景里則要“看語境”千萬別混淆面試常見問題八股文標準回答線上真實操作JVM默認堆大小是多少物理內(nèi)存1/4要根據(jù)進程實際數(shù)據(jù)量重設(shè)別信默認值G1和CMS哪個好低停頓選G1高吞吐選Parallel先看你的停頓敏感性和內(nèi)存大小G1不是銀彈-XX:CompileThreshold有什么用控制JIT編譯觸發(fā)次數(shù)對JRuby型應(yīng)用有點幫助但優(yōu)先級靠后Full GC為什么慢要掃描整個堆實際上更可能是堆太小導(dǎo)致G1退化成了Serial OldJDK、JVM、JRE啥區(qū)別一個是開發(fā)包一個是虛擬機一個是運行環(huán)境排查時你只需要關(guān)心JVM的運行時參數(shù)和GC狀態(tài)順帶提一句網(wǎng)上經(jīng)??吹健癕axKB知識庫如何調(diào)優(yōu)”這類問題雖然MaxKB不是Logstash但它同樣是Java系服務(wù)底層邏輯一樣先看GC再看堆匹配再看線程池和批量配置。核心方法論是通用的。6. 寫在最后的個人體會這次排查前后花了大半天時間其中最浪費時間的一環(huán)恰恰是最開始“不想去碰JVM配置”的心理——總覺得Logstash日志不報錯就等于沒問題結(jié)果監(jiān)控數(shù)據(jù)狠狠打了臉。JVM調(diào)優(yōu)在面試里是八股文在線上就是保命技能差別只在于你有沒有真的拿著jstat輸出逐行分析過。調(diào)優(yōu)完成之后我痛定思痛做了一件事給所有Java系服務(wù)統(tǒng)一開啟了GC日志并且用腳本定時掃描FGCT增長率只要Full GC累計耗時在短時間內(nèi)翻倍就立刻告警。這樣一來再遇到類似問題我不用再從零開始抓現(xiàn)場直接從監(jiān)控面板和GC日志里就能把病根揪出來。我也建議大家排查性能問題時盡量“每次只改一個變量”改完給足觀察時間不要同時動堆大小、GC算法、batch size和worker數(shù)——不然你根本不知道是哪個調(diào)整起了作用又回到了猜謎游戲。最后分享一個小技巧調(diào)優(yōu)JVM參數(shù)后別急著馬上看吞吐量先看GC曲線穩(wěn)不穩(wěn)定再看Kafka Lag有沒有下降趨勢。如果GC穩(wěn)定了但吞吐還沒上來那再考慮是filter邏輯還是下游ES的問題如果GC還是像過山車一樣那堆內(nèi)存設(shè)置就還沒到位繼續(xù)調(diào)。這套判斷順序是我這次踩過坑之后總結(jié)出來的最可靠的節(jié)奏。