Tomcat出現(xiàn)假死原因及解決方法
1. 問(wèn)題背景
線上環(huán)境因?yàn)橛袀€(gè)接口內(nèi)部在錯(cuò)誤的參數(shù)下,會(huì)不斷生成字符串,導(dǎo)致OOM,在OOM之后服務(wù)還能正常運(yùn)行,但是發(fā)送的Api請(qǐng)求已經(jīng)沒(méi)有辦法響應(yīng)了。
2. 問(wèn)題復(fù)現(xiàn)
模擬線上問(wèn)題,在測(cè)試環(huán)境上進(jìn)行復(fù)現(xiàn),一段時(shí)間后服務(wù)會(huì)爆出OOM,但是不是每次都會(huì)導(dǎo)致Tomcat假死,有些情況下Tomcat還能正常訪問(wèn)。
情況一:核心線程丟失
OOM之前Tomcat線程情況
ID | 線程名稱 | Group |
---|---|---|
125 | http-nio-9989-Acceptor-0 | main |
126 | http-nio-9989-AsyncTimeout | main |
123 | http-nio-9989-ClientPoller-0 | main |
124 | http-nio-9989-ClientPoller-1 | main |
113 | http-nio-9989-exec-1 | main |
OOM之后Tomcat線程情況
ID | 線程名稱 | Group |
---|---|---|
123 | http-nio-9989-ClientPoller-0 | main |
1431 | http-nio-9989-exec-103 | main |
情況二:服務(wù)重啟
日志打印java.lang.OutOfMemoryError: Java heap space
后,服務(wù)重啟。
情況三:Tomcat后臺(tái)線程丟失
只有后臺(tái)線程丟失,但是Acceptor線程和Poller線程還存在
3. 假死情況
從Tomcat的NIO模型得知有幾個(gè)組件,Acceptor、Poller、業(yè)務(wù)線程池。這三個(gè)組件情況如下:
- Acceptor線程:該線程主要是監(jiān)聽(tīng)連接(socket.accept()),如果該線程掛掉,那么及時(shí)操作系統(tǒng)層面TCP3次握手成功,但是業(yè)務(wù)上也辦法獲取到這個(gè)連接。默認(rèn)情況下,只有1個(gè)Acceptor線程,可以通過(guò)acceptorThreadCount參數(shù)設(shè)置。
- Poller線程:Acceptor獲取到連接之后,會(huì)輪詢從Poller列表中取一個(gè)Poller進(jìn)行處理。如果Poller線程掛掉了,那么就沒(méi)法處理讀請(qǐng)求了。默認(rèn)情況下,會(huì)有min(2,cpu核數(shù))個(gè)Poller線程,可以通過(guò)pollerThreadCount參數(shù)設(shè)置。
- 業(yè)務(wù)線程:Poller線程將讀請(qǐng)求放到業(yè)務(wù)線程處理,如果業(yè)務(wù)線程阻塞(比如被某個(gè)網(wǎng)絡(luò)IO阻塞),那么此刻的讀請(qǐng)求還在業(yè)務(wù)線程池的隊(duì)列中,沒(méi)有被處理。默認(rèn)情況下,最小線程為10,可以通過(guò)minSpareThreads參數(shù)設(shè)置,最大線程為200,可以通過(guò)maxThreads參數(shù)設(shè)置。
此時(shí)分析再結(jié)合復(fù)現(xiàn)的情況,如果核心線程掛掉,那確實(shí)存在假死情況。但是從事發(fā)現(xiàn)場(chǎng)來(lái)看,并沒(méi)有發(fā)現(xiàn)Tomcat的Acceptor、Poller線程打印出OutofMemoryError的異常,及時(shí)將org.apache.tomcat和org.apache.catalina設(shè)置成Debug級(jí)別。因此需要深入源碼分析。
4. 異常處理分析
4.1 Acceptor異常處理分析
Acceptor邏輯如下,就是在循環(huán)內(nèi)不斷地監(jiān)聽(tīng)accept(),查看是否有新連接。
//NioEndpoint$Acceptor#run protected class Acceptor extends AbstractEndpoint.Acceptor { @Override public void run() { int errorDelay = 0; // Loop until we receive a shutdown command while (running) { //...忽略一些代碼 state = AcceptorState.RUNNING; try { //if we have reached max connections, wait countUpOrAwaitConnection(); SocketChannel socket = null; try { socket = serverSock.accept(); } catch (IOException ioe) { } // Successful accept, reset the error delay errorDelay = 0; // Configure the socket if (running && !paused) { // setSocketOptions() will hand the socket off to // an appropriate processor if successful if (!setSocketOptions(socket)) { closeSocket(socket); } } else { closeSocket(socket); } } catch (Throwable t) { ExceptionUtils.handleThrowable(t); log.error(sm.getString("endpoint.accept.fail"), t); } } state = AcceptorState.ENDED; } //...忽略一些代碼 }
其中setSocketOptions是往Poller中調(diào)用register方法,把這個(gè)Socket傳遞過(guò)去。getPoller0()方法會(huì)以輪詢的策略獲取一個(gè)Poller
//NioEndpoint#setSocketOptions protected boolean setSocketOptions(SocketChannel socket) { // Process the connection try { //disable blocking, APR style, we are gonna be polling it socket.configureBlocking(false); Socket sock = socket.socket(); socketProperties.setProperties(sock); NioChannel channel = nioChannels.pop(); if (channel == null) { SocketBufferHandler bufhandler = new SocketBufferHandler( socketProperties.getAppReadBufSize(), socketProperties.getAppWriteBufSize(), socketProperties.getDirectBuffer()); if (isSSLEnabled()) { channel = new SecureNioChannel(socket, bufhandler, selectorPool, this); } else { channel = new NioChannel(socket, bufhandler); } } else { channel.setIOChannel(socket); channel.reset(); } getPoller0().register(channel); } catch (Throwable t) { ExceptionUtils.handleThrowable(t); try { log.error("",t); } catch (Throwable tt) { ExceptionUtils.handleThrowable(tt); } // Tell to close the socket return false; } return true; }
此處重點(diǎn)看一下ExceptionUtils.handleThrowable方法的邏輯,因?yàn)镺utofMemoryError是VirtualMachineError的子類,所以這里會(huì)被直接拋出異常,。而OutofMemeoryError屬于 uncheck exception,拋出uncheck exception就會(huì)導(dǎo)致線程終止,并且主線程和其他線程無(wú)法感知這個(gè)線程拋出的異常。如果線程代碼(run方法之外)之外來(lái)捕獲這個(gè)異常的話,可以通過(guò)Thread的setUncaughtExceptionHandler處理。
//ExceptionUtils#handleThrowable public static void handleThrowable(Throwable t) { if (t instanceof ThreadDeath) { throw (ThreadDeath) t; } if (t instanceof StackOverflowError) { // Swallow silently - it should be recoverable return; } if (t instanceof VirtualMachineError) { throw (VirtualMachineError) t; } // All other instances of Throwable will be silently swallowed }
再看啟動(dòng)的時(shí)候,線程是否會(huì)設(shè)置uncaughtExceptionHandler,發(fā)現(xiàn)并沒(méi)有設(shè)置,所以異常沒(méi)法被正常打印到日志中。
//AbstractEndpoint#startAcceptorThreads protected final void startAcceptorThreads() { int count = getAcceptorThreadCount(); acceptors = new Acceptor[count]; for (int i = 0; i < count; i++) { acceptors[i] = createAcceptor(); String threadName = getName() + "-Acceptor-" + i; acceptors[i].setThreadName(threadName); Thread t = new Thread(acceptors[i], threadName); t.setPriority(getAcceptorThreadPriority()); t.setDaemon(getDaemon()); t.start(); } }
小結(jié):OutofMemoryError被捕獲了,然后重新拋出,但是因?yàn)镺utofMemoryError是uncheck exception,而線程沒(méi)有設(shè)置uncaughtExceptionHandler,所以沒(méi)法被打印。
4.2 增加全局異常捕獲
在啟動(dòng)的時(shí)候,設(shè)置全局線程nncaughtException處理器。這里簡(jiǎn)單打印線程名稱,并且拋出異常。
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { @Override public void uncaughtException(Thread t, Throwable e) { logger.error("[Global Handler]thread-name:{},happen exp,", t.getName(), e); } });
重新復(fù)現(xiàn)問(wèn)題,發(fā)現(xiàn)Acceptor線程有打印異常情況。
不過(guò),同時(shí)也發(fā)現(xiàn),Poller線程有打印錯(cuò)誤日志,但并不是全局處理器打印的。
下圖為Arthas截圖,發(fā)現(xiàn)仍然Poller線程仍然存在。因此再分析Poller的異常處理。
4.3 Poller異常處理
從上面的異常日志倆看,Poller線程是在處理PollerEvent中處理REGISTER事件時(shí)的拋出異常,查看相關(guān)代碼。發(fā)現(xiàn)此處捕獲的是Exception,而OutofMemoryError屬于Error,所以此處不會(huì)被捕獲到,并且會(huì)往上拋出。
//NioEndpoint$Poller#events public void run() { if (interestOps == OP_REGISTER) { try { socket.getIOChannel().register( socket.getPoller().getSelector(), SelectionKey.OP_READ, socketWrapper); } catch (Exception x) { log.error(sm.getString("endpoint.nio.registerFail"), x); } } //... }
在Poller的events方法中,會(huì)循環(huán)調(diào)用PollerEvent的run方法,這里內(nèi)部有捕獲一個(gè)Throwable,而Error是繼承Throwable。所以O(shè)utofMemoryError會(huì)在這里被捕獲,而且會(huì)打印日志,并且線程不會(huì)掛掉。
//NioEndpoint$Poller#events public boolean events() { boolean result = false; PollerEvent pe = null; for (int i = 0, size = events.size(); i < size && (pe = events.poll()) != null; i++ ) { result = true; try { pe.run(); pe.reset(); if (running && !paused) { eventCache.push(pe); } } catch ( Throwable x ) { log.error("",x); } } return result; }
而在Poller的循環(huán)中,發(fā)現(xiàn)也有ExceptionUtils.handleThrowable處理,如果在這里出現(xiàn)OutofMemoryError異常的話,那么Poller線程將會(huì)被終止。
//NioEndpoint$Poller#run public class Poller implements Runnable { public void run() { // Loop until destroy() is called while (true) { Boolean hasEvents = false; try { if (!close) { hasEvents = events(); //.... } }catch (Throwable x) { ExceptionUtils.handleThrowable(x); log.error("",x); continue; } //... } } }
小結(jié):Poller內(nèi)部實(shí)現(xiàn)中,對(duì)于異常處理不同,有些地方能捕獲異常并且Poller線程正常處理,有些地方?jīng)]有捕獲異常,可能會(huì)因?yàn)镺utofMemoryError導(dǎo)致線程終止
5. 結(jié)論
當(dāng)應(yīng)用程序出現(xiàn)OOM的時(shí)候,Tomcat核心線程有可能會(huì)掛掉,導(dǎo)致接口接口無(wú)法正常訪問(wèn),因此要盡量避免業(yè)務(wù)上出現(xiàn)OOM。此外,當(dāng)出現(xiàn)OOM后應(yīng)用無(wú)法訪問(wèn)時(shí),可以試著排查一下,是不是tomcat的核心線程掛掉導(dǎo)致。
以上就是Tomcat出現(xiàn)假死原因及解決方法的詳細(xì)內(nèi)容,更多關(guān)于Tomcat假死的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Tomcat部署項(xiàng)目局域網(wǎng)使用IP地址實(shí)現(xiàn)直接訪問(wèn)
這篇文章主要介紹了Tomcat部署項(xiàng)目局域網(wǎng)使用IP地址實(shí)現(xiàn)直接訪問(wèn),具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-06-06優(yōu)化Tomcat配置(內(nèi)存、并發(fā)、緩存等方面)方法詳解
這篇文章主要介紹了優(yōu)化Tomcat配置(內(nèi)存、并發(fā)、緩存等方面)方法詳解,具有一定參考價(jià)值,需要的朋友可以了解下。2017-10-10tomcat 開(kāi)啟遠(yuǎn)程debug模式的方法步驟
在部署和使用Apache Tomcat時(shí),可能需要根據(jù)具體需求修改其啟動(dòng)參數(shù)和環(huán)境變量,以優(yōu)化性能或適應(yīng)特定的運(yùn)行環(huán)境,本文就來(lái)介紹一下tomcat 開(kāi)啟遠(yuǎn)程debug模式的方法步驟,感興趣的可以了解一下2024-11-11詳解springboot-修改內(nèi)置tomcat版本
這篇文章主要介紹了springboot-修改內(nèi)置tomcat版本的相關(guān)資料,希望通過(guò)本文大家能掌握這樣的方法,需要的朋友可以參考下2017-08-08Tomcat正常訪問(wèn)localhost報(bào)404問(wèn)題解決
這篇文章主要介紹了Tomcat正常訪問(wèn)localhost報(bào)404問(wèn)題解決,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-03-03解決Tomcat重新部署后圖片等資源被自動(dòng)刪除的問(wèn)題
這篇文章主要介紹了解決Tomcat重新部署后圖片等資源被自動(dòng)刪除的問(wèn)題,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-11-11使用tomcat設(shè)定shared lib共享同樣的jar
這篇文章主要介紹了使用tomcat設(shè)定shared lib共享同樣的jar操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-07-07idea配置Tomcat Deployment添加時(shí)沒(méi)有Artifact的問(wèn)題及解決
這篇文章主要介紹了idea配置Tomcat Deployment添加時(shí)沒(méi)有Artifact的問(wèn)題及解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-07-07