tomcat7鏈接數異常致使超時問題的排查

 

1.摘要java

 

前幾天線上tomcat出了一次詭異的超時問題,在此記錄下來。每次問題的排查都很痛苦,而定位緣由以後再回想整個過程又經常有「當時若是看了那個其實早就能知道了」之類的懊惱,看來問題排查流程的標準化還有很長的路要走。nginx

 

2.現象和處理正則表達式

 

某天某個跑在tomcat上的java服務的全部接口都忽然開始出現偶發超時,響應時間從幾十毫秒增加到幾秒,甚至幾十秒。後端

比較靈異的一個現象tomcat處理日誌和業務日誌中都沒有發現超時,日誌裏打出來的請求的響應時間都在幾十毫秒,而且對線程數的監控也沒有發現波動。tomcat

懷疑是負載均衡的問題,查看nginx日誌,發現nginx訪問後端時有不少慢請求。負載均衡

查看tomcat的gc狀況,比較正常。less

在tomcat本機調用接口,發現一樣存在超時問題,排除nginx的嫌疑。socket

感受問題基本出在tomcat上,決定先重啓服務,果真重啓後響應時間恢復。tcp

3.緣由排查oop

 

重啓的時候從集羣中摘除了一臺節點保留現場,由於服務已經兩週沒有上過線,因此懷疑跟某種資源堆積有關。

嘗試復現問題:

 

直接調用摘除的節點沒有發現問題。

嘗試使用ab壓測,沒有復現。

嘗試使用tcpcopy引流,在引流單臺5倍流量的狀況下依然沒有出現。

把節點從新加回到線上集羣,問題神奇的重現了。

jstack查看出問題時的線程棧,沒有發現死鎖或者大量線程阻塞。

 

jmap作heap dump,分析了一下內存,沒發現特別明顯的內存泄露或者大對象。

 

strace查看系統調用,發現poll()系統調用偶發會超時,超時時間和connectionTimeout設定的時間一致。

 

繼續追查了一下poll()超時的問題,發現請求超時和poll()超時之間沒有什麼必然聯繫,在沒有poll()超時的時間段裏也會出現偶發的請求超時,這條路彷佛走不通。

 

同時使用strace和tcpdump,在特定機器上調用一個特殊的uri,uri會打印在strace獲得的read()的返回值裏,從而得能夠獲得fd,並找出相應的accept()和write(),並結合對指定ip的tcpdump,能夠得出這個請求的處理時間軸大概是:

 

0s 發出請求,tcp握手

11s accept()

11s read()

11s 業務日誌

11s write()

11s tomcat access日誌

基本能夠定位是tcp握手到accept()之間出了什麼問題,查看rx_queue,沒有發現堆積,排查方向轉到tomcat調用accept()部分。

 

查看tomcat源碼,線上使用的是tomcat7.0.24,bio connector。在JioEndpoint的代碼裏發現了比較可疑的地方:

 

 

try {

//if we have reached max connections, wait

countUpOrAwaitConnection(); // 感受這句比較可疑

Socket socket = null;

try {

// Accept the next incoming connection from the server

// socket

socket = serverSocketFactory.acceptSocket(serverSocket);

} catch (IOException ioe) {

1

2

3

4

5

6

7

8

9

try {

//if we have reached max connections, wait

countUpOrAwaitConnection(); // 感受這句比較可疑

Socket socket = null;

try {

// Accept the next incoming connection from the server

// socket

socket = serverSocketFactory.acceptSocket(serverSocket);

} catch (IOException ioe) {

內部是一個LimitLatch:

 

 

protected void countUpOrAwaitConnection() throws InterruptedException {

if (maxConnections==-1) return;

LimitLatch latch = connectionLimitLatch;

if (latch!=null) latch.countUpOrAwait();

}

1

2

3

4

5

protected void countUpOrAwaitConnection() throws InterruptedException {

if (maxConnections==-1) return;

LimitLatch latch = connectionLimitLatch;

if (latch!=null) latch.countUpOrAwait();

}

簡單閱讀了一下源碼,使用bio的connector時,線程分爲:

 

接收請求並建立client socket的accetpor線程(默認1個),在acceptor中維護了connectionCount,就是上面的connectionLimitLatch(最大鏈接數,bio中的默認最大值與maxThreadCount相同)

實際處理請求的exec線程(線程池中線程的最大數量等於maxThreadCount),執行exec的線程池中也有一個線程數threadCount(最大值爲maxThreadCount)

網上隨便找了張圖,bio的處理流程:

 

bio 圖片來自http://www.infoq.com/cn/articles/zh-tomcat-http-request-1

 

經過剛纔拿到的heap dump查看connectionLimitLatch的值爲800,跟配置的maxThreadCount相同,而當時實際tomcat的工做線程只有不到100個,因此猜想是connectionLimitLatch這個計數值異常致使新請求的accept被阻塞了。

 

繼續追查代碼,在實際處理的線程中對這個計數的減操做是這麼處理的:

 

 

if ((state != SocketState.CLOSED)) {

if (status == null) {

state = handler.process(socket, SocketStatus.OPEN);

} else {

state = handler.process(socket,status);            }

}

if (state == SocketState.CLOSED) {

// Close socket

if (log.isTraceEnabled()) {

log.trace("Closing socket:"+socket);

}

countDownConnection();

try {

socket.getSocket().close();

} catch (IOException e) {

// Ignore

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

if ((state != SocketState.CLOSED)) {

if (status == null) {

state = handler.process(socket, SocketStatus.OPEN);

} else {

state = handler.process(socket,status);            }

}

if (state == SocketState.CLOSED) {

// Close socket

if (log.isTraceEnabled()) {

log.trace("Closing socket:"+socket);

}

countDownConnection();

try {

socket.getSocket().close();

} catch (IOException e) {

// Ignore

}

彷佛對Exception沒有作什麼處理,進一步查看handler的代碼:

 

 

try {

blablabla...

}catch(java.net.SocketException e) {

// SocketExceptions are normal

getLog().debug(sm.getString("abstractConnectionHandler.socketexception.debug"), e);

} catch (java.io.IOException e) {

// IOExceptions are normal

getLog().debug(sm.getString("abstractConnectionHandler.ioexception.debug"), e);

}

// Future developers: if you discover any other

// rare-but-nonfatal exceptions, catch them here, and log as

// above.

catch (Throwable e) {

ExceptionUtils.handleThrowable(e);

// any other exception or error is odd. Here we log it

// with "ERROR" level, so it will show up even on

// less-than-verbose logs.

getLog().error(sm.getString("abstractConnectionHandler.error"), e);

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

try {

blablabla...

}catch(java.net.SocketException e) {

// SocketExceptions are normal

getLog().debug(sm.getString("abstractConnectionHandler.socketexception.debug"), e);

} catch (java.io.IOException e) {

// IOExceptions are normal

getLog().debug(sm.getString("abstractConnectionHandler.ioexception.debug"), e);

}

// Future developers: if you discover any other

// rare-but-nonfatal exceptions, catch them here, and log as

// above.

catch (Throwable e) {

ExceptionUtils.handleThrowable(e);

// any other exception or error is odd. Here we log it

// with "ERROR" level, so it will show up even on

// less-than-verbose logs.

getLog().error(sm.getString("abstractConnectionHandler.error"), e);

}

進一步查看ExceptionUtils.handleThrowable(e):

 

 

public static void handleThrowable(Throwable t) {

if (t instanceof ThreadDeath) {

throw (ThreadDeath) t;

}

if (t instanceof VirtualMachineError) {

throw (VirtualMachineError) t;

}

// All other instances of Throwable will be silently swallowed

}

1

2

3

4

5

6

7

8

9

public static void handleThrowable(Throwable t) {

if (t instanceof ThreadDeath) {

throw (ThreadDeath) t;

}

if (t instanceof VirtualMachineError) {

throw (VirtualMachineError) t;

}

// All other instances of Throwable will be silently swallowed

}

到這裏能夠肯定若是線程拋出了ThreadDeath或者VirtualMachineError,那麼這個計數應該會有問題。

 

進一步搜索出錯以前的日誌,在catalina.out裏發現有偶發的StackOverflowError:

 

 

Exception in thread "catalina-exec-109" java.lang.StackOverflowError

at java.util.regex.Pattern$5.isSatisfiedBy(Pattern.java:5151)

at java.util.regex.Pattern$5.isSatisfiedBy(Pattern.java:5151)

at java.util.regex.Pattern$5.isSatisfiedBy(Pattern.java:5151)

at java.util.regex.Pattern$5.isSatisfiedBy(Pattern.java:5151)

at java.util.regex.Pattern$5.isSatisfiedBy(Pattern.java:5151)

at java.util.regex.Pattern$5.isSatisfiedBy(Pattern.java:5151)

at java.util.regex.Pattern$5.isSatisfiedBy(Pattern.java:5151)

at java.util.regex.Pattern$5.isSatisfiedBy(Pattern.java:5151)

at java.util.regex.Pattern$5.isSatisfiedBy(Pattern.java:5151)

at java.util.regex.Pattern$5.isSatisfiedBy(Pattern.java:5151)

at java.util.regex.Pattern$5.isSatisfiedBy(Pattern.java:5151)

at java.util.regex.Pattern$5.isSatisfiedBy(Pattern.java:5151)

at java.util.regex.Pattern$CharProperty.match(Pattern.java:3694)

at java.util.regex.Pattern$GroupHead.match(Pattern.java:4556)

at java.util.regex.Pattern$Branch.match(Pattern.java:4502)

at java.util.regex.Pattern$GroupHead.match(Pattern.java:4556)

at java.util.regex.Pattern$Loop.match(Pattern.java:4683)

at java.util.regex.Pattern$GroupTail.match(Pattern.java:4615)

at java.util.regex.Pattern$BranchConn.match(Pattern.java:4466)

at java.util.regex.Pattern$GroupTail.match(Pattern.java:4615)

at java.util.regex.Pattern$CharProperty.match(Pattern.java:3694)

at java.util.regex.Pattern$GroupHead.match(Pattern.java:4556)

at java.util.regex.Pattern$Branch.match(Pattern.java:4502)

at java.util.regex.Pattern$GroupHead.match(Pattern.java:4556)

at java.util.regex.Pattern$Loop.match(Pattern.java:4683)

at java.util.regex.Pattern$GroupTail.match(Pattern.java:4615)

at java.util.regex.Pattern$BranchConn.match(Pattern.java:4466)

...

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

Exception in thread "catalina-exec-109" java.lang.StackOverflowError

at java.util.regex.Pattern$5.isSatisfiedBy(Pattern.java:5151)

at java.util.regex.Pattern$5.isSatisfiedBy(Pattern.java:5151)

at java.util.regex.Pattern$5.isSatisfiedBy(Pattern.java:5151)

at java.util.regex.Pattern$5.isSatisfiedBy(Pattern.java:5151)

at java.util.regex.Pattern$5.isSatisfiedBy(Pattern.java:5151)

at java.util.regex.Pattern$5.isSatisfiedBy(Pattern.java:5151)

at java.util.regex.Pattern$5.isSatisfiedBy(Pattern.java:5151)

at java.util.regex.Pattern$5.isSatisfiedBy(Pattern.java:5151)

at java.util.regex.Pattern$5.isSatisfiedBy(Pattern.java:5151)

at java.util.regex.Pattern$5.isSatisfiedBy(Pattern.java:5151)

at java.util.regex.Pattern$5.isSatisfiedBy(Pattern.java:5151)

at java.util.regex.Pattern$5.isSatisfiedBy(Pattern.java:5151)

at java.util.regex.Pattern$CharProperty.match(Pattern.java:3694)

at java.util.regex.Pattern$GroupHead.match(Pattern.java:4556)

at java.util.regex.Pattern$Branch.match(Pattern.java:4502)

at java.util.regex.Pattern$GroupHead.match(Pattern.java:4556)

at java.util.regex.Pattern$Loop.match(Pattern.java:4683)

at java.util.regex.Pattern$GroupTail.match(Pattern.java:4615)

at java.util.regex.Pattern$BranchConn.match(Pattern.java:4466)

at java.util.regex.Pattern$GroupTail.match(Pattern.java:4615)

at java.util.regex.Pattern$CharProperty.match(Pattern.java:3694)

at java.util.regex.Pattern$GroupHead.match(Pattern.java:4556)

at java.util.regex.Pattern$Branch.match(Pattern.java:4502)

at java.util.regex.Pattern$GroupHead.match(Pattern.java:4556)

at java.util.regex.Pattern$Loop.match(Pattern.java:4683)

at java.util.regex.Pattern$GroupTail.match(Pattern.java:4615)

at java.util.regex.Pattern$BranchConn.match(Pattern.java:4466)

...

根據線程棧定位到異常的緣由是進行正則表達式匹配時棧溢出。

 

4.解決問題

 

嘗試最小代碼重現問題,把tomcat的maxThreadCount調成1,直接在Servlet裏throw new StackOverflowError(),調用普通接口沒有問題,調用一次錯誤接口後,再普通接口發現請求hang住了,與線上表現一致。

把tomcat的connector改成nio,問題解決。

升級tomcat到7.0.61,並改回bio,問題解決。進一步查看代碼發現tomcat7.0.61已經修復了這個bug,單獨判斷了StackOverflow的狀況。

相關文章
相關標籤/搜索