
1. 問題背景與現(xiàn)象分析最近在排查一個(gè)線上服務(wù)異常時(shí)遇到了一個(gè)典型的OOMOutOfMemoryError問題。這個(gè)案例非常有意思因?yàn)樗粌H涉及到內(nèi)存溢出本身還引發(fā)了Tomcat線程模型的異常表現(xiàn)最終導(dǎo)致服務(wù)不可用。讓我們先還原一下問題現(xiàn)場。1.1 問題現(xiàn)象還原開發(fā)同學(xué)使用EasyExcel處理文件上傳時(shí)沒有做分頁處理導(dǎo)致內(nèi)存溢出。這在EasyExcel官方文檔中也有明確警告大文件解析必須使用分頁模式。內(nèi)存溢出后JVM頻繁觸發(fā)Full GC但由于內(nèi)存壓力過大GC無法有效回收內(nèi)存最終拋出Java heap space錯(cuò)誤。重要提示生產(chǎn)環(huán)境使用EasyExcel等大數(shù)據(jù)量處理工具時(shí)必須實(shí)現(xiàn)分頁讀取機(jī)制這是避免OOM的基本要求。1.2 OOM類型深度解析在Java中OOM錯(cuò)誤有多種類型每種都對應(yīng)不同的場景Java heap space最常見的內(nèi)存溢出通常是對象分配過多或單個(gè)對象過大導(dǎo)致。我們的案例就屬于這種。GC Overhead limit exceeded當(dāng)JVM花費(fèi)98%以上的時(shí)間進(jìn)行GC但只回收不到2%的內(nèi)存時(shí)會(huì)拋出。這通常是內(nèi)存泄漏的標(biāo)志。Metaspace元數(shù)據(jù)區(qū)存放類信息溢出常見于動(dòng)態(tài)生成大量類的場景。Direct buffer memory直接內(nèi)存溢出常見于NIO的ByteBuffer使用不當(dāng)。Unable to create new native thread線程創(chuàng)建過多通常與系統(tǒng)ulimit設(shè)置有關(guān)。Requested array size exceeds VM limit嘗試創(chuàng)建過大的數(shù)組。CodeCacheJIT編譯代碼緩存區(qū)溢出。在我們的案例中需要特別注意內(nèi)存溢出和內(nèi)存泄漏的區(qū)別內(nèi)存溢出內(nèi)存不足但對象仍可回收如大文件解析時(shí)的臨時(shí)內(nèi)存占用內(nèi)存泄漏對象無法回收內(nèi)存被永久占用如靜態(tài)集合持續(xù)增長2. 異?,F(xiàn)象的矛盾點(diǎn)2.1 表面現(xiàn)象的矛盾當(dāng)OOM發(fā)生后我們觀察到幾個(gè)看似矛盾的現(xiàn)象內(nèi)存確實(shí)被回收了Full GC停止內(nèi)存使用下降但HTTP請求仍然失敗報(bào)connection reset by peer錯(cuò)誤Tomcat線程看似正常處于WAITING狀態(tài)連接數(shù)監(jiān)控顯示backlog隊(duì)列已滿101/1002.2 關(guān)鍵問題定位這引出了兩個(gè)核心問題什么是connection reset by peer這是TCP層面的錯(cuò)誤表示對端重置了連接在我們的場景中是因?yàn)門omcat的accept隊(duì)列已滿Tomcat線程在做什么通過arthas的thread命令查看發(fā)現(xiàn)工作線程都在等待任務(wù)但Acceptor和Poller線程神秘消失了3. Tomcat線程模型深度解析3.1 Reactor模式基礎(chǔ)要理解這個(gè)問題必須深入Tomcat的線程模型。Tomcat的NIO實(shí)現(xiàn)基于Reactor模式這是高性能網(wǎng)絡(luò)編程的經(jīng)典模式。Doug Lee在《Scalable IO in Java》中總結(jié)了三種Reactor模型單Reactor單線程Redis 6.0前采用簡單但無法利用多核且容易阻塞單Reactor多線程IO操作單線程業(yè)務(wù)處理多線程避免了業(yè)務(wù)阻塞IO多Reactor多線程Main Reactor負(fù)責(zé)acceptSub Reactor負(fù)責(zé)IO業(yè)務(wù)線程池處理請求Netty采用這種模型3.2 Tomcat的NIO實(shí)現(xiàn)Tomcat的NIO實(shí)現(xiàn)可以看作是多Reactor多線程的變種包含三類線程Acceptor線程負(fù)責(zé)接受新連接默認(rèn)只有1個(gè)線程Poller線程負(fù)責(zé)檢測就緒的IO事件默認(rèn)CPU核心數(shù)Worker線程處理HTTP請求數(shù)量由maxThreads控制關(guān)鍵源碼路徑啟動(dòng)入口org.apache.tomcat.util.net.NioEndpoint#startInternalAcceptor實(shí)現(xiàn)org.apache.tomcat.util.net.Acceptor#runPoller實(shí)現(xiàn)org.apache.tomcat.util.net.NioEndpoint.Poller#run3.3 Tomcat線程池特點(diǎn)Tomcat的線程池與JDK的ThreadPoolExecutor有所不同優(yōu)先創(chuàng)建線程直到maxThreads只有達(dá)到maxThreads后才會(huì)排隊(duì)隊(duì)列使用TaskQueue繼承LinkedBlockingQueue但重寫了offer邏輯這種設(shè)計(jì)是為了快速響應(yīng)突發(fā)流量但需要合理設(shè)置maxThreads避免資源耗盡。4. 問題根因分析4.1 線程消失之謎通過arthas的thread命令我們發(fā)現(xiàn)Acceptor和Poller線程不見了。查看源碼發(fā)現(xiàn)// Acceptor.run()片段 try { while (running) { // 接受連接... } } catch (Throwable t) { // 對于OOM等Error會(huì)直接拋出導(dǎo)致線程終止 }關(guān)鍵點(diǎn)Acceptor捕獲Throwable但對Error直接拋出OOM屬于Error會(huì)導(dǎo)致線程終止日志打印在異常處理之后所以看不到錯(cuò)誤日志4.2 現(xiàn)象解釋現(xiàn)在可以解釋所有現(xiàn)象了OOM導(dǎo)致Acceptor/Poller線程崩潰沒有Acceptor新連接堆積在OS層面backlogbacklog滿后新連接被拒絕connection resetWorker線程無事可做WAITING狀態(tài)內(nèi)存回收后線程不會(huì)自動(dòng)恢復(fù)5. 解決方案與最佳實(shí)踐5.1 立即解決方案修復(fù)內(nèi)存問題實(shí)現(xiàn)EasyExcel的分頁讀取增加內(nèi)存監(jiān)控和報(bào)警增強(qiáng)OOM處理Thread.setDefaultUncaughtExceptionHandler((t, e) - { if (t.getName().contains(Acceptor)) { log.error(Tomcat acceptor thread died, e); // 可以考慮重啟服務(wù) } });JVM參數(shù)優(yōu)化-XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/path/to/dumps -XX:OnOutOfMemoryErrorkill -9 %p # 強(qiáng)制重啟5.2 長期預(yù)防措施文件處理規(guī)范所有文件處理必須實(shí)現(xiàn)分頁/流式處理設(shè)置單個(gè)請求內(nèi)存上限Tomcat優(yōu)化# 適當(dāng)增大acceptCount server.tomcat.accept-count200 # 監(jiān)控關(guān)鍵線程 management.endpoint.health.probes.enabledtrue監(jiān)控體系監(jiān)控Tomcat關(guān)鍵線程狀態(tài)實(shí)現(xiàn)OOM自動(dòng)重啟機(jī)制建立內(nèi)存使用趨勢監(jiān)控6. 深度思考與經(jīng)驗(yàn)總結(jié)6.1 為什么Tomcat不捕獲Error這是一個(gè)設(shè)計(jì)權(quán)衡Error通常表示不可恢復(fù)狀態(tài)繼續(xù)運(yùn)行可能導(dǎo)致更嚴(yán)重問題犧牲單個(gè)請求保全整個(gè)服務(wù)6.2 生產(chǎn)環(huán)境診斷技巧Arthas高級用法# 監(jiān)控線程創(chuàng)建/銷毀 thread -b # 統(tǒng)計(jì)線程狀態(tài) thread --state BLOCKEDOS層面檢查# 查看連接隊(duì)列 ss -ltnp | grep java # 監(jiān)控backlog使用 netstat -s | grep overflowed日志增強(qiáng)// 在Spring Boot中增加Tomcat生命周期監(jiān)聽 Bean public TomcatConnectorCustomizer connectorCustomizer() { return connector - { connector.addLifecycleListener(event - { if (event.getType().equals(AFTER_START)) { log.info(Tomcat started with protocol: {}, connector.getProtocolHandler().getClass().getName()); } }); }; }6.3 性能優(yōu)化黃金法則理解比配置更重要先理解原理再調(diào)整參數(shù)避免盲目復(fù)制優(yōu)化配置監(jiān)控驅(qū)動(dòng)優(yōu)化沒有監(jiān)控就不要談優(yōu)化建立完整的指標(biāo)監(jiān)控體系漸進(jìn)式改進(jìn)每次只改一個(gè)參數(shù)評估效果后再繼續(xù)這個(gè)案例給我們的核心啟示是內(nèi)存問題不只是內(nèi)存問題。在分布式系統(tǒng)中局部故障可能引發(fā)連鎖反應(yīng)。作為工程師我們需要深入理解底層原理建立全面的監(jiān)控體系設(shè)計(jì)彈性的故障處理機(jī)制保持對異常的敏感度記住每一個(gè)異常現(xiàn)象背后都隱藏著一個(gè)等待被發(fā)現(xiàn)的設(shè)計(jì)缺陷或優(yōu)化機(jī)會(huì)。