曹工說Tomcat:200個http-nio-8080-exec執行緒全都被第三方服務拖住了,這可如何是好(上:執行緒模型解析)
阿新 • • 發佈:2020-09-27
# 前言
這兩年,tomcat慢慢在新專案裡不怎麼接觸了,因為都被spring boot之類的框架封裝進了內部,成了內建server,不用像過去那樣打個war包,再放到tomcat裡部署了。
但是,內部的機制我們還是有必要了解的,尤其是執行緒模型和classloader,這篇我們會聚焦執行緒模型。
其實我本打算將一個問題,即大家知道,我們平時最終寫的controller、service那些業務程式碼,最終是由什麼執行緒來執行的呢?
大家都是debug過的人,肯定知道,執行緒名稱大概如下:
```java
http-nio-8080-exec-2@5076
```
這個執行緒是tomcat的執行緒,假設,我們在這個執行緒裡,sleep個1分鐘,模擬呼叫第三方服務時,第三方服務異常卡住不返回的情況,此時客戶端每秒100個請求過來,此時整個程式會出現什麼情況?
但是我發現,這個問題,一篇還是講不太清楚,因此,本篇只講一下執行緒模型。
# 主要執行緒模型簡介
大家可以思考下,一個服務端程式,有哪些是肯定需要的?
我們肯定需要開啟監聽對吧,大家看看下面的bio程式:
![](https://img2020.cnblogs.com/blog/519126/202009/519126-20200927163100236-1692072383.png)
這個就是個執行緒,在while(true)死迴圈裡,一直accept客戶端連線。
ok,這個執行緒肯定是需要的。接下來,再看看還是否需要其他的執行緒。
如果一切從簡,我們只用這1個執行緒也足夠了,就像redis一樣,redis都是記憶體操作,做啥都很快,還避免了執行緒切換的開銷;
但是我們的java後端,一般都要操作資料庫的,這個是比較慢,自然是希望把這部分工作能夠交給單獨的執行緒去做,在tomcat裡,確實是這樣的,交給了一個執行緒池,執行緒池裡的執行緒,就是我們平時看到的,名稱類似http-nio-8080-exec-2@5076這樣的,一般預設配置,最大200個執行緒。
但如果這樣的話,1個acceptor + 一個業務執行緒池,會導致一個問題,就是,該acceptor既要負責新連線的接入,還要負責已接入連線的socket的io讀寫。假設我們維護了10萬個連線,這10萬個連線都在不斷地給我們的服務端發資料,我們服務端也在不停地給客戶端返回資料,那這個工作還是很繁重的,可能會壓垮這個唯一的acceptor執行緒。
因此,理想情況下,我們會在單獨弄幾個執行緒出來,負責已經接入的連線的io讀寫。
大體流程:
```java
acceptor--->poller執行緒(負責已接入連線的io讀寫)-->業務執行緒池(http-nio-8080-exec-2@5076)
```
這個大概就是tomcat中的流程了。
在netty中,其實是類似的:
```java
boss eventloop--->worker eventloop-->一般在解碼完成後的最後一個handler,交給自定義業務執行緒池
```
# tomcat如何接入新連線
大家可以看看下圖,這裡面有幾個橙色的方塊,這幾個代表了執行緒,從左到右,分別就是acceptor、nio執行緒池、poller執行緒。
![](https://img2020.cnblogs.com/blog/519126/202009/519126-20200927164259122-958427416.png)
* 1處,acceptor執行緒內部維護了一個endpoint物件,這個物件呢,就代表了1個服務端端點;該物件有幾個實現類,如下:
![](https://img2020.cnblogs.com/blog/519126/202009/519126-20200927164733590-674653526.png)
我們spring boot程式裡,預設是用的NioEndpoint。
* 2處,將新連線交給NioEndpoint處理
```java
@Override
protected boolean setSocketOptions(SocketChannel socket) {
// Process the connection
try {
// Disable blocking, polling will be used
socket.configureBlocking(false);
Socket sock = socket.socket();
socketProperties.setProperties(sock);
// 進行一些socket的引數設定
NioSocketWrapper socketWrapper = new NioSocketWrapper(channel, this);
channel.setSocketWrapper(socketWrapper);
socketWrapper.setReadTimeout(getConnectionTimeout());
socketWrapper.setWriteTimeout(getConnectionTimeout());
//3 交給poller處理
poller.register(channel, socketWrapper);
return true;
}
...
// Tell to close the socket
return false;
}
```
* 3處,就是交給NioEndpoint內部的poller物件去進行處理。
```java
public void register(final NioChannel socket, final NioSocketWrapper socketWrapper) {
socketWrapper.interestOps(SelectionKey.OP_READ);//this is what OP_REGISTER turns into.
PollerEvent r = null;
// 丟到poller的佇列裡,poller執行緒會輪旋該佇列
r = new PollerEvent(socket, OP_REGISTER);
// 丟到佇列裡
addEvent(r);
}
```
上面的addEvent值得一看。
```java
private final SynchronizedQueue events =
new SynchronizedQueue<>();
private void addEvent(PollerEvent event) {
// 丟到佇列裡
events.offer(event);
// 喚醒poller裡的selector,及時將該socket註冊到selector中
if (wakeupCounter.incrementAndGet() == 0) {
selector.wakeup();
}
}
```
到這裡,acceptor執行緒的邏輯就結束了,一個非同步放佇列,完美收工。接下來,就是poller執行緒的工作了。
poller執行緒,要負責將該socket註冊到selector裡面去,然後還要負責該socket的io讀寫事件處理。
* poller執行緒邏輯
```java
public class Poller implements Runnable {
private Selector selector;
private final SynchronizedQueue events =
new SynchronizedQueue<>();
```
可以看到,poller內部維護了一個selector,和一個佇列,佇列裡也說了,主要是要新註冊到selector的新socket。
既然丟到隊列了,那我們看看什麼時候去佇列取的呢?
```java
@Override
public void run() {
// Loop until destroy() is called
while (true) {
boolean hasEvents = false;
// 檢查events
hasEvents = events();
}
}
```
這裡我們跟一下events()。
```java
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;
pe.run();
...
}
return result;
}
```
這裡的
```java
pe = events.poll()
```
就是去佇列拉取事件,拉取到了之後,就會賦值給pe,然後下面就呼叫了pe.run方法。
pe的型別是PollerEvent,我們看看其run方法會幹啥?
```java
@Override
public void run() {
if (interestOps == OP_REGISTER) {
try { socket.getIOChannel().register(socket.getSocketWrapper().getPoller().getSelector(), SelectionKey.OP_READ, socket.getSocketWrapper());
} catch (Exception x) {
log.error(sm.getString("endpoint.nio.registerFail"), x);
}
}
}
```
這個方法難理解嗎,看著有點嚇人,其實就是把這個新的連線,向selector註冊,感興趣的io事件為OP_READ。後續呢,這個連線的io讀寫,就全由本poller的selector包了。
# tomcat如何處理客戶端讀事件
我們說了,poller是個執行緒,在其runnable實現裡,除了要處理上面的新連線註冊到selector這個事,還要負責io讀寫,這部分邏輯就是在:
```java
Iterator iterator=selector.selectedKeys().iterator();
while (iterator != null && iterator.hasNext()) {
SelectionKey sk = iterator.next();
NioSocketWrapper socketWrapper = sk.attachment();
processKey(sk, socketWrapper);
}
```
最後一行的processKey,會呼叫如下邏輯,將工作甩鍋給http-nio-8080-exec-2@5076這類打雜的執行緒。
```java
public boolean processSocket(SocketWrapperBase socketWrapper,SocketEvent event, boolean dispatch) {
Executor executor = getExecutor();
executor.execute(sc);
return true;
}
```
給個圖的話,大概就是如下的紅線流程部分了:
![](https://img2020.cnblogs.com/blog/519126/202009/519126-20200927171210112-1445709580.png)
# 小結
好了,到了課後思考時間了,我們也說了,最終會交給http-nio-8080-exec-2@5076這類執行緒所在的執行緒池,那假設這些執行緒全都在sleep,會發生什麼呢?
下一篇,我們