效能追擊:萬字長文30+圖揭祕8大主流伺服器程式執行緒模型 | Node.js,Apache,Nginx,Netty,Redis,Tomcat,MySQL,Zuul
阿新 • • 發佈:2021-03-10
> 本文為《[高效能網路程式設計遊記](https://www.itzhai.com/articles/decrypt-the-threading-model-of-common-server-programs.html)》的第六篇“效能追擊:萬字長文30+圖揭祕8大主流伺服器程式執行緒模型”。
![image-20210306172815543](https://cdn.itzhai.com/image-20210306172815597-a.png-itzhai)
最近拍的照片比較少,不知道配什麼圖好,於是自己畫了一個,湊合著用,讓大家見笑了。
本文我們來探索一下主流的各種應用伺服器的網路處理模型,看看大家都是怎麼設計網路程式的。在本文中,我會從Node.js、Apache Server、Nginx、Netty、Redis、Tomcat、MySQL、Zuul等常用的伺服器程式,給大家逐一分析,分析各種伺服器程式的效能,心中有數,才能手中有術,從此效能是熟客。
![image-20210306130751209](https://cdn.itzhai.com/image-20210306130751209-a.png-itzhai)
雖然涉及到很多底層知識,各種框架的原理,但是我都會盡量配上直白易懂的圖文,方便大家理解。
更多優質文章,讓我們相約在`IT宅`(itzhai.com)的`Java架構雜談`公眾號。
首先,我們從什麼都能寫的Javascript說起,來看看Node.js伺服器的併發內幕。
# 1、Node.js
![image-20210306130146642](https://cdn.itzhai.com/image-20210306130146642-a.png-itzhai)
上篇文章中,我們介紹了JavaScript在瀏覽器端的執行模式,接下來,我們繼續講講Node.js的執行模式,揭開它高效能背後的實現機制。
## 1.1、Node.js執行模式
以下是一個流傳度很廣的Node.js系統檢視,來源於 Totally Radical Richard[^1]
![image-20201206181302381](https://cdn.itzhai.com/image-20201206181302381-a.png-itzhai)
Node.js是單執行緒的Event Loop[^2]:
* V8引擎解析JS指令碼,呼叫Node API;
* libuv庫執行Node API,會先將API請求**封裝成事件**,放入事件佇列,**Event Loop執行緒處於空閒狀態時,就開始遍歷處理事件佇列中的事件**,對事件進行處理:
* 如果不是阻塞任務,直接處理得到結果,通過回撥函式返回給V8;
* 如果是阻塞任務,則從Worker執行緒池中取出一個執行緒,交給執行緒處理,最終執行緒把處理結果設定到事件的結果屬性中,**把事件放回事件佇列**,等待Event Loop執行緒執行回撥返回給V8引擎。
其中請求的任務會被封裝成如下的結構:[^3]
```javascript
varevent= createEvent({
params:request.params, // 傳遞請求引數
result:null, // 存放請求結果
callback:function(){} // 指定回撥函式
});
```
更多精彩討論,可以閱讀這個帖子:[How the single threaded non blocking IO model works in Node.js](https://stackoverflow.com/questions/14795145/how-the-single-threaded-non-blocking-io-model-works-in-node-js)[^10],以下是來源於這個帖子的圖片:
![image-20210125001051141](https://cdn.itzhai.com/image-20210125001051141-a.png-itzhai)
> 當然了,在客戶端請求到Node.js伺服器的時候,肯定會有一個建立已連線套接字的過程,然後把這個已連線套接字描述符與具體的執行程式碼關聯起來,這樣再非同步處理完成之後,才知道要響應給哪個客戶端。
## 1.2、Node.js非同步案例
以上的執行模式說明還是需要結合例子來說明比較好理解。
如果沒有通過回撥函式進行非同步處理,我們可能會寫出如下程式碼:
```javascript
var result = db.query("select * from t_user");
// do something with result here...
console.log("do something else...");
```
這個程式碼在執行查詢result的時候,查詢速度可能很慢,等待查詢出結果後,才可以執行後面的console.log操作,因為這是在一個執行緒上執行的。
但是Node.js不是這麼玩的,Node.js的執行模式下,只有一個Event Loop執行緒,如果這個執行緒被阻塞,這將導致無法接收新的請求。為了避免這種情況,我們按照Node.js的回撥方式重寫程式碼:
```javascript
db.query("select * from t_user", function(rows) {
var result = rows;
// do something with result here...
});
console.log("do something else...");
```
現在,Node.js可以非同步處理查詢請求了,並且把**查詢請求委託給Worker Thread,等待Worker Thread得到查詢結果之後,再把結果連同回撥匿名函式封裝成事件釋出到事件佇列,等待Event Loop執行緒執行該回調函式**。這樣console.log程式碼就可以立刻得到執行,而不會因為查詢請求導致被阻塞住了。
## 1.3、Node.js併發模型優缺點
從以上分析可知,Node.js通過事件驅動,把阻塞的IO任務丟到執行緒池中進行非同步處理,也就是說,**Node.js適合I/O密集型任務**。
但是,如果碰到CPU密集型任務的時候,Node.js中的EventLoop執行緒就會自己處理任務,這樣會導致在事件佇列中的CPU密集型任務沒有處理完,那麼後面的任務就不會被執行到了,從而導致後續的請求響應變慢。
如下圖,本來socket2和socket3很快就可以處理完的,但是由於socket1的任務一直佔用著CPU時間,導致socket2和socket3都不能及時得到處理,從表現上看,就是響應變慢了。
![image-20210107235531275](https://cdn.itzhai.com/image-20210107235531275-a.png-itzhai)
如果CPU是單核的還好,充分的利用了CPU核心,但是如果CPU是多核的,這種情況就會導致其他記憶體處於閒置狀態,造成資源浪費。
所以,**Node.js不適合CPU密集型任務**。
> Node.js適合請求和響應內容小,無需大量計算邏輯的場景,這能夠充分發揮Node.js執行模式的優勢。類似的場景有聊天程式。
不過,從Node.js可以通過提供cluster、child_process API建立子程序的方式來充分利用多核的能力,但是多程序也就意味著犧牲了共享記憶體,並且通訊必須使用json進行傳輸。[^4]
Node.js V10.5.0開始,提供了worker_threads,讓Node.js擁有了多工作執行緒:Event Loop執行緒 + 自己啟動的執行緒,多工作執行緒對於CPU密集型的JavaScript操作非常有用。
需要注意的是,worker_threads可以作為Node.js中CPU密集型問題的解決策略之一,對於IO,Node.js原生執行緒(Event Loop執行緒)已經做了很好的支援,無需自己啟動一個執行緒去做此類工作(參考本節第一張圖片的介紹)。
好了,前端知識不能聊深了,聊深就出破綻了,畢竟也是有前端大佬在關注IT宅的`Java架構雜談`公眾號,在偷偷學學Java技術的。
![image-20210306130819628](https://cdn.itzhai.com/image-20210306130819628-a.png-itzhai)
接下來,我們從一根羽毛的故事說起。
# 2、Apache
![image-20210306114520845](https://cdn.itzhai.com/image-20210306114520845-a.png-itzhai)
Apache於1995年首次釋出,並迅速佔領了市場,成為世界上最受歡迎的Web伺服器。配合世界上最好的語言——PHP搭建網站,在那個年代可謂是打遍天下無敵手。
![image-20210304001604262](https://cdn.itzhai.com/image-20210304001604262-a.png-itzhai)
這裡我們來探討下Apache Web伺服器使用的兩個工作模型:
* Apache MPM Prefork:用於實現多程序模型;
* Apache MPM Worker:用於實現多執行緒模型;
> Apache使用到了**Multi Processing Module**模組(MPM)來實現多程序或者多執行緒處理器。
## 2.1、Apache MPM Prefork
一句話總結:**Prefork是一個非執行緒型的、預派生的MPM**。
Prefork預派生出多個程序,每個程序在某個確定的時間只單獨處理一個連線,效率高,但記憶體使用比較大。
這種模型是**每個請求一個程序**的模型,由一個父程序建立了許多子程序,這些子程序等待請求的到達並且進行處理,每個請求均由單獨的程序進行處理。
![image-20210306110133134](https://cdn.itzhai.com/image-20210306110133134-a.png-itzhai)
需要注意的是,每個程序都會使用RAM和CPU等系統資源,使用的RAM數量都是相等的。
如果同時有很多請求,那麼apache會產生很多子程序,這將導致大量的資源利用率。
## 2.2、Apache MPM Worker
一句話總結:**Worker是支援混合的多執行緒多程序的MPM**。如下圖:
![image-20210306112335983](https://cdn.itzhai.com/image-20210306112335983-a.png-itzhai)
子程序藉助內部固定數量的執行緒來處理請求,該數量由配置檔案中的引數“`ThreadsPerChild`指定。
該模型一般使用多個子程序,每個子程序有多個執行緒,每個執行緒在某個確定的時間只處理一個連線,消耗記憶體較少。這種Apache模型可以用較少的系統資源來滿足大量請求,因為這種模型下,有限數量的程序將為許多請求提供服務。
> **PHP攻城獅提問題:為啥mod_php中不能使用MPM Worker?**
>
> 由於一些mod_php模組問題,導致該模組不能和MPM Worker一起使用,PMP Worker一般都是跟apache的mod_fcgid 搭配,而PHP則是安裝php-cgi來執行。
作為一個Java攻城獅,這裡我就不展開繼續講了,畢竟世界上最好的語言相信大家會主動去了解的。
即使是一個請求用一個執行緒,Apache在高併發場景下,執行效率也是很差的。因為,如果一個請求需要資料庫中的一些資料以及磁碟中的檔案等涉及到IO操作的處理,則該執行緒將進入等待。因此,**Apache中的某些執行緒(Worker模式)或者程序(Prefork模式)只是停下來下來等待某些任務完成,這些執行緒或者程序吃掉了系統資源。**
而接下來我們介紹對併發場景處理更高效的主角:Nginx,**從根本上說,Apache和Nginx差別很大。Nginx的誕生是為了解決Apache中的c10k問題。**
想象以下,從豬圈裡衝出一群豬,Apache Server能夠抵擋得住嗎,也許不行,但是,Nginx,一定可以。這就是Nginx的強大之處。
![concurrency01](https://cdn.itzhai.com/image-20210306143031189-a.png-itzhai)
# 3、Nginx
![image-20210306115912298](https://cdn.itzhai.com/image-20210306115912298-a.png-itzhai)
Nginx是一種開源Web伺服器,自從最初作為Web伺服器獲得成功以來,現在還用作反向代理,HTTP快取和負載均衡器。
Nginx旨在提供**低記憶體使用率**和高併發性。Nginx不會為每個Web請求建立新的流程,而是使用非同步的,事件驅動的方法,在單個執行緒中處理請求。
接下來我們就來詳細介紹下。
## 3.1、Nginx的程序模型
### 3.1.1、Nginx的程序數
我們在作業系統各種啟動Nginx之後,一般會發現幾個Nginx程序,如下圖:
![image-20210115232628709](https://cdn.itzhai.com/image-20210115232628709-a.png-itzhai)
這裡有一個master程序,3個workder程序。
為什麼啟動Nginx會有3個worker程序呢,這是因為我們在配置檔案中指定了工作程序數:
```
worker_processes 3;
```
## 3.2、程序模型
Nginx是多程序模型,在啟動Nginx之後,以daemon的方式在後臺執行,後臺程序包含一個Master程序和多個worker程序,模型如下:
![image-20210117114210610](https://cdn.itzhai.com/image-20210117114210610-a.png-itzhai)
> CM: Cache Manager, CL Cache Loader
Master程序主要用於管理Worker程序,主要負責如下功能:
* 接收外接訊號;
* 向Worker程序傳送訊號;
* 監控Worker程序執行狀態;
* Workder程序異常退出,會自動建立新的Worker程序。
Worker程序主要用於處理網路事件,我們一般設定的Worker程序數為機器的CPU核數,以最有效的利用硬體資源。為此,可以進行如下配置:
```
worker_processes auto;
```
**通過使用共享快取來實現子程序的快取,會話永續性,限流,會話日誌等。**
## 3.3、工作原理
大致來說,Master程序執行以下步驟:
```c
socket();
bind();
listen();
fork();
```
fork出若干個Worker程序,Worker進執行以下步驟:
```c
accept(); // accept_mutex鎖
register IO handler;
epoll() or kqueue();
handle_events();
...
```
> accept_mutex鎖作用:保證同一時刻只有一個Worker程序在accept連線,從而解決驚群問題。當客戶連線到達時候,只有成功獲取到了鎖的程序才會執行accept。[^6]
>
> 驚群問題[^5]:一個程式派生出N個子程序,它們各自呼叫accept並因此而被投入核心睡眠。當第一個客戶連線到達的時候,所有N個子程序均被喚醒,這是因為所有子程序所用的監聽描述符指向了同一個socket結構。儘管有N個子程序被喚醒,但是隻有最先執行的子程序獲得那個客戶連線,其餘的N-1個子程序繼續恢復睡眠。
當Nginx伺服器處於活動狀態的時候,僅Worker程序處於繁忙狀態,每個Worker程序以非阻塞方式處理多個連線。
每個Worker程序都是單執行緒的,並且獨立執行。當Worker程序獲取到連線之後就進行處理,程序可以使用`共享記憶體`進行通訊以及共享快取資料、會話永續性資料和其他共享資源。
我們重點來看看Worker程序的工作原理。
### 3.3.1、Worker程序工作原理
每個Worker程序都是運行於非阻塞、事件驅動的Reactor模型。
一個客戶端請求在服務端的大致處理流程如下圖所示:
![image-20210118233000336](https://cdn.itzhai.com/image-20210118233000336-a.png-itzhai)
我們在上一篇文章中詳細介紹了單執行緒版本的Reactor模型:
![image-20210118235826656](https://cdn.itzhai.com/image-20210118235826656-a.png-itzhai)
而Worker程序中基本的處理邏輯則如上圖所示:
* 當一個請求到達Workder程序的之後,Accept接收新連線,把新連線IO讀寫事件註冊到同步事件多路解複用器;
* 執行dispatch呼叫多路解複用器阻塞等待IO事件;
* 分發事件到特定的Handler中處理;
> 可是,這裡有個問題:**compute環節一般是要執行upstream的,這裡又會跟backend建立一個新的連線,進行請求互動,Worker程序豈不是會阻塞在這裡?**如果你來設計一個類似的事件驅動的程式,這種場景你會如何處理呢?
>
> 很顯然,為了發起與backend的連線請求導致程序被阻塞,這裡也是需要進行非同步操作的,可以把與backend的fd連線套接字與客戶端請求進來的fd連線套接字的關係維護起來。
### 3.3.2、如何處理繁重的工作?
與Node.js類似,Nginx中也會有一些繁重的工作。比如第三方模組中使用了阻塞呼叫,有時候該模組開發人員都沒有意識到這個阻塞呼叫的缺點,如果直接在Worker程序中執行,就會導致整個事件處理週期都被阻塞了,必須等待操作完成才可以繼續處理後續的事先。顯然,這不是我們期望的效果。
為了解決該問題,Nginx 1.7.11版本中實現了執行緒池機制。
#### 使用執行緒池機制解決繁重工作或者第三方阻塞操作效能問題
以下操作可能導致Nginx進入阻塞狀態:
* 處理冗長佔用大量CPU的處理;
* 阻塞訪問資源,如硬碟資源,互斥量,或者系統呼叫等,或者以同步方式從資料庫獲取資料
* ...
以上這些情況都需要執行比較長的時間,遇到這種情況,Nginx會將需要執行很長時間的任務放入執行緒池處理佇列中,通過執行緒池非同步處理這些任務:
![image-20210122003115359](https://cdn.itzhai.com/image-20210122003115359-a.png-itzhai)
通過引入執行緒池,從而消除了對Worker程序的阻塞,將Nginx的效能提升到了新的高度。更加重要的是,以前那些與Nginx不相容的第三方類庫,都可以相對容易的使用,並且不影響Nginx的效能。
## 3.4、優雅更新配置[^8]
我們在更新完Nginx的配置之後,一般執行以下命令即可:
```
nginx -s reload
```
這行命令會檢查磁碟上的配置,並向主程序傳送SIGNUP訊號。
主程序收到SIGNUP訊號時,會執行如下操作:
* 重新載入配置,並派生一組新的工作程序,這組新的工作程序立即開始接受連線並處理流量;
* 指示舊的工作程序正常退出,工作程序停止接收新連線,當前的每個請求處理完成之後,舊的工作程序就會關掉,一旦所有的連線關閉,工作程序就將退出。
![image-20210123112243048](https://cdn.itzhai.com/image-20210123112243048-a.png-itzhai)
這種重新載入配置過程可能導致CPU的記憶體使用量小幅度提升,但是這個效能犧牲是值得的。
## 3.5、優雅的升級
Nginx的二進位制升級過程也實現了不停服的效果。
![image-20210123113340085](https://cdn.itzhai.com/image-20210123113340085-a.png-itzhai)
升級過程與政策重新載入配置的方法型別,新的Nginx主程序與原始主程序並行執行,他們共享監聽套接字,兩個程序都處於活動狀態,他們各自的工作程序都在處理流量,然後可以可以指示舊的Master和Worker程序正常退出。
## 3.5、Nginx的優勢
在每個請求一個程序,阻塞式的連線方法中,每個連線都需要大量額外的資源開銷,並且會導致頻繁的上下文切換。
而Nginx的單程序模型,可以儘可能消耗少的記憶體,每個連線幾乎沒有額外的開銷,Nginx程序數可以設定為CPU核心數,上下文切換相對較少。
那麼問題來了,我們自己寫網路程式的時候,有沒有可以幫助我們提高網路效能的程式框架呢?有,那就是大名鼎鼎的Netty,接下來就來說他。
# 4、Netty
![image-20210306130935405](https://cdn.itzhai.com/image-20210306130935405-a.png-itzhai)
## 4.1、Netty主從Reactor模式
Netty也不例外,是基於Reactor模型設計和開發的。
Netty採用了主從Reactor模式,主Reactor只負責建立連線,獲取已連線套接字,然後把已連線套接字的IO事件轉給從Reactor執行緒進行處理。
我們再來回顧一下主從模式的圖示,更詳細的說明參考我的部落格 `IT宅(itzhai.com)` 或者功眾號 `Java架構雜談(itread)`中的文章更新 [網路程式設計正規化:高效能伺服器就這麼回事 | C10K,Event Loop,Reactor,Proactor](https://www.itzhai.com/articles/high-performance-network-programming-paradigm.html)[^9]
![image-20201217230206165](https://cdn.itzhai.com/image-20201217230206165-a.png-itzhai)
我們先來大致講講Netty中的幾個與Reactor有關的抽象概念:
* Selector:可以理解為一個Reactor執行緒,內部會通過IO多路複用感知事件的發生,然後把事件交代給Channel進行處理;
* Channel:註冊到Selector中的物件,代表Selector監聽的事件,如套接字讀寫事件;
具體上,Netty抽象出了以下模型進行實現Reactor主從模式:
![image-20210124150436889](https://cdn.itzhai.com/image-20210124150436889-a.png-itzhai)
* Boss Group,即主Reactor,服務於監聽套接字,其中的NioEventLoop運行於主Reactor執行緒,其中:
* Selector為IO多路解複用器,用於感知監聽套接字的Accept事件;
* ServerSocketChannel對應監聽套接字,這裡綁定了OP_ACCEPT已連線事件;
* Acceptor為已連線事件的處理器,接收到已連線事件之後,會通過Acceptor進行處理。
* Worker Group,即從Reactor,服務於已連線套接字,Worker Group中可以開啟多個NioEventLoop,每個NioEventLoop運行於一個從Reactor執行緒,其中:
* Selector為IO多路解複用器,用於感知已連線套接字的IO讀寫事件;
* SocketChannel對應已連線套接字,監聽套接字獲取到已連線套接字之後,會包裝成SocketChannel,註冊到Worker Group的NioEventLoop的Selector中進行監聽;
* Handler為IO讀寫事件的處理器,由API傳入,自定義業務處理邏輯,最終的IO事件會通過這個Handler進行處理。
> Netty基於Pipeline管道的模式來處理Channel事件,從Netty的使用API中也可以瞭解到。
## 4.2、Netty主從Reactor+Worker執行緒池模式
為了降低具體業務邏輯對從Reactor的影響,我們可以單獨把業務邏輯處理放到一個執行緒池中處理,這樣無論是對於監聽套接字的事件處理,還是對於已連線套接字事件的處理,都不會因為業務處理程式而導致阻塞了,如下圖所示,更詳細的說明參考我的部落格 `IT宅(itzhai.com)` 或者功眾號 `Java架構雜談(itread)`中的文章更新 [網路程式設計正規化:高效能伺服器就這麼回事 | C10K,Event Loop,Reactor,Proactor](https://www.itzhai.com/articles/high-performance-network-programming-paradigm.html)[^9]:
![image-20201218000305993](https://cdn.itzhai.com/image-20201218000305993-a.png-itzhai)
**我們可以通過建立一個 DefaultEventExecutorGroup 執行緒池來處理業務邏輯。**
大致程式框架如下圖所示:
```java
// 宣告一個bossGroup作為主Reactor,本質是一個執行緒池,每個執行緒是一個EventLoop
EventLoopGroup bossGroup = new NioEventLoopGroup();
// 宣告一個workerGroup作為從Reactor,本質是一個執行緒池,每個執行緒是一個EventLoop
EventLoopGroup workerGroup = new NioEventLoopGroup();
// 構建業務處理Group
DefaultEventExecutorGroup defaultEventExecutorGroup =
new DefaultEventExecutorGroup(10,
new ThreadFactory() {
private AtomicInteger threadIndex = new AtomicInteger(0);
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "BusinessThread-" + this.threadIndex.incrementAndGet());
}
});
try {
// 建立服務端啟動類
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
...
.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// 在pipeline中新增自定義的handler
ch.pipeline().addLast(defaultEventExecutorGroup, new BizHandler());
}
});
ChannelFuture future = bootstrap.bind(port).sync();
future.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
```
**Netty是基於NIO的,而Java中的NIO是JDK1.4開始支援,內部是基於IO多路複用實現的,具體的實現思路不再詳細說,底層都是IO複用技術,通過Channel藉助於Buffer處理感知到的IO事件**。
> **有了NIO為啥還要有Netty?**
>
> 這是兩個不同層次的東西,NIO只是一個IO類庫,實現同步非阻塞IO,而Netty是基於NIO實現的高效能網路框架,基於主從Reactor設計的。
>
> NIO類庫API複雜,需要處理多執行緒程式設計,自己寫Reactor模式,並且客戶端斷線重連、半包讀寫,失敗快取、網路阻塞和異常碼流等問題處理起來難度很大。而Netty對於NIO遇到的這些問題都做了很好的封裝,主要優點體現在:
>
> * API使用簡單;
> * 封裝度高,功能強大,提供多種編碼解碼器,解決了TCP拆包粘包問題;
> * 基於Reactor模式實現,效能高,無需在自己實現Reactor了;
> * 商用專案多,經歷過很多考驗,社群活躍...
# 5、Redis
![image-20210306131120047](https://cdn.itzhai.com/image-20210306131120047-a.png-itzhai)
相信大家已經聽過無數遍“Redis是單執行緒的”這句話了,**Redis真的是單執行緒的嗎,又是如何支撐那麼大的併發量**,並且運用到了這麼多的網際網路應用中的呢?
其實,Redis的單執行緒指的是Redis內部會有一個主處理執行緒,充分利用了非阻塞、IO多路複用模型,實現的一個Reactor架構。但是在某些情況下,Redis會生成執行緒或者子程序來執行某些比較繁重的任務。
Redis包含了一個稱為`AE事件模型`的強大的非同步事件庫來包裝不同作業系統的IO複用技術,如epoll、kqueue、select等。
## 5.1、Redis執行緒模型
還是那個Reactor模型,只不過我們再次踏入了不同的國界,於是又出現了一種新的的表述方式。
Redis基於Reactor模型開發了網路事件處理器,這個處理器被稱為檔案事件處理器。不過叫什麼不重要,重要的是原理都是一樣的。以下是Redis的執行緒模型:
![image-20210125231811481](https://cdn.itzhai.com/image-20210125231811481-a.png-itzhai)
這個圖基本上涵蓋了Redis程序處理的主要事情:
* 客戶端A發起請求建立連線,監聽套接字Server Socket建立連線之後,產生一個AE_READABLE事件;
* 該事件被IO多路複用處理,放入事件佇列,最終被檔案事件分派器分派給了連線應答處理器進行處理:
* 連線應答處理器處理新連線,將FD1套接字的AE_READABLE事件與命令請求處理器關聯起來。
* 客戶端A最終在客戶端生成一個已連線套接字FD1;
* 客戶端A傳送命令請求,產生一個AE_READABLE事件,該事件被IO多路複用處理,放入事件佇列,最終被檔案事件分派器分派給了命令請求處理器進行處理:
* 命令請求處理器執行客戶端FD1套接字命令操作,得到結果,將結果寫入到套接字的回覆緩衝區中,準備好響應給客戶端;
* 同時將FD1套接字的AE_WRITABLE事件與命令回覆處理器關聯;
* 當FD1套接字準備好寫的時候,會產生一個AE_WRITABLE事件,該事件被IO多路複用處理,放入事件佇列,最終被事件分派器分派給了命令回覆處理器進行處理:
* 命令回覆處理器把結果輸出響應給客戶端的FD1已連線套接字;
* 然後將FD1套接字的AE_WRITABLE事件跟命令回覆處理器解除關聯。
大致一個互動流程就這樣完成了,是不是很簡單呢。
思路還是那個思路,但是實現卻百花齊放。就好像如果有人覺得我的文章寫得好,都會表示支援,但是支援的方式卻各有不同,有的人會偷偷的收藏起來白嫖,有的人會點個贊,有的人會點個在看,也有的人會積極分享,點選Java架構雜談進行關注星標,訪問IT宅itzhai.com進一步閱讀。
這個Reactor模型沒有哪個伺服器程式實現是最好的一說,但是對於我來說,你的點贊,關注,評論,轉發就是最好的支援。
## 5.2、為啥Redis單執行緒也這麼高效?
前面已經講了這麼多Reactor模式的好處,詳細大家心裡也有個底了,大致總結下:
* Redis是存記憶體操作的,所以處理速度非常快,這同時也跟Redis高效的資料結構有關,不過本文重點是講網路相關的,資料結構不展開講;
* Redis的瓶頸不在CPU,而是在記憶體和網路。而基於Reactor模型,實現了非阻塞的IO多路複用,儘可能高效的利用了CPU,避免不必要的阻塞;
* 單執行緒反而避免了上下文切換的開銷。
對於開發人員來說,最關注的一點就是:單執行緒降低了開發的複雜度,再也不需要處理各種競態條件了,就連Hash的惰性Rehash,Lpush等執行緒不安全的命令都可以進行無鎖程式設計了。
頭髮都可以少掉幾根,難怪Redis的作者跟我說,Redis更強了,而我卻沒有禿,反而更帥了[^11]。
## 5.3、Redis真的是單執行緒的嗎?
我再問一句大家,Redis真的是單執行緒的嗎,從Reactor模型上來說,單執行緒肯定會存在瓶頸的,還不清楚的盆友可以回去翻看我IT宅(itzhai.com)上面的相關文章,或者Java架構雜談上面的文章。
比如 UNLINK、FLUSHALL ASYNC、FLUSHDB ASYNC等非阻塞的刪除操作,如果要釋放的記憶體空間比較大,就需要耗費比較多的時間進行處理,這些操作就會阻塞住待處理的執行緒。而如果是單執行緒模型,可想而知,整個Redis服務的請求都會被阻塞住了;
為此,Redis引入了多執行緒機制。
### Redis 4.0初步引入多執行緒
在Redis 4.0中,Redis開始使具有更多執行緒。這個版本僅限於在後臺刪除物件,其中包括非阻塞的刪除操作。
> UNLINK操作,只會將鍵從元資料中刪除,並不會立刻刪除資料,真正的刪除操作會在一個後臺執行緒非同步執行。
### Redis 6.0真正引入多執行緒
雖然基於Reactor模型,單執行緒也可以支援很大的併發量,但是要是IO讀寫多了,待處理的已連線套接字多了,需要執行的命令也多了,那麼,單執行緒依舊是瓶頸,這個時候我們就要引入主從Reactor模型,甚至主從Reactor模型+Worker執行緒池了。
再次強調下,這塊知識點落下的同學,可以閱讀我的部落格 `IT宅(itzhai.com)` 或者功眾號 `Java架構雜談(itread)`中的文章更新 [網路程式設計正規化:高效能伺服器就這麼回事 | C10K,Event Loop,Reactor,Proactor](https://www.itzhai.com/articles/high-performance-network-programming-paradigm.html)[^9]。
在Redis 6.0中,如果要開啟多執行緒,可以進行設定:
```
io-threads 執行緒數
io-threads-do-reads yes // 預設IO執行緒只會用於寫操作,如果要在讀操作和協議解析的時候啟用IO執行緒,則可以設定該選項為yes,但是Redis團隊聲稱它並沒有多大幫忙
```
不過呢,**Redis為了避免產生執行緒併發安全的問題,在執行命令階段仍然是單執行緒順序執行的,只是在網路資料讀寫和協議解析階段才用到了多執行緒。**
為了進一步瞭解這個特性,我們可以閱讀以下 redis.conf[^12] 配置檔案的說明。在這裡,這個特性被命名為:`THREADED I/O`,下面是翻譯整理自裡面的一些說明。
#### **THREADED I/O**
Redis大多是單執行緒的,但是有一些執行緒操作,例如UNLINK,執行緩慢的I/O訪問等是在後臺執行緒上執行的操作。
但是現在,可以在不同的I/O執行緒中處理Redis客戶端套接字的讀寫。 由於特別慢的寫入速度,因此Redis使用者通常使用pipelining流水線以加快每個核心的Redis效能,並生成多個例項以擴大規模。 而使用I/O執行緒可以輕鬆地將Redis寫入速度提升2倍。
IO執行緒預設是禁用的,建議在至少有4個核心的機器上啟用,至少保留一個備用核心。也就是說,如果您的CPU是4核的,請嘗試使用2~3個IO執行緒。
```
io-threads 4
```
**將io-threads設定為1只會像傳統一樣只啟用單執行緒。**
使用8個以上的執行緒不會有太大幫助,並且建議實際存在效能問題的時候才使用IO執行緒,否則就沒有必要使用了。
啟用IO執行緒後,我們僅將IO執行緒用於寫操作,即對write(2)系統呼叫進行執行緒化並將客戶端緩衝區傳輸到套接字。 但是,也可以使用以下配置指令通過以下方式啟用讀取執行緒和協議解析:
> io-threads-do-reads yes
**通常,執行緒讀取沒有太大幫助。**
**Redis用的是類似單執行緒版的Reactor + IO執行緒池(Worker執行緒池)**,不過與我們前面提到的單執行緒Reactor + Worker執行緒池模式有所不同,再回顧下Reactor + Worker執行緒池模式:
![image-20201218000146145](https://cdn.itzhai.com/image-20201218000146145-a.png-itzhai)
Redis是在所謂的Reactor執行緒(主執行緒)中把IO讀事件一批一批的交給IO執行緒池進行讀取,讀取完畢之後,統一執行所有請求的命令,然後才是一次性把所有請求的響應寫到socket,如下圖所示:
![image-20210131111642013](https://cdn.itzhai.com/image-20210131111642013-a.png-itzhai)
等待佇列中的待處理時間平均分給每個IO執行緒,IO執行緒池只是負責IO讀寫和解析資料,IO執行緒池充分利用了CPU多核處理的能力,提高了IO讀寫速度。所以,我們再來強調一次重點:**Redis為了避免產生執行緒併發安全的問題,在執行命令階段仍然是單執行緒順序執行的,只是在網路資料讀寫和協議解析階段才用到了多執行緒。**
#### Redis 6.0真的是單執行緒的嗎?
可以發現,Redis也不是簡單粗暴的引入多執行緒機制,而是基於避免引入併發操作的複雜度的前提下,進行的合理改良設計實現的,這樣才能保護頭髮,避免頭髮脫落的太快呀,程式設計師都需要重點關注這個...
如果真有人要跟你追究Redis是不是單執行緒的,**你要記住的是,即使是Redis 6.0,執行命令的過程仍然是單執行緒的。**
# 6、Tomcat
![image-20210306131154782](https://cdn.itzhai.com/image-20210306131154782-a.png-itzhai)
作為一個Java程式設計師,怎麼能不認識Tomcat呢,Tomcat的執行緒模型又是怎樣的?不用往下看,我們都能猜出Tomcat肯定會利用Reactor模式來優化網路處理,不過這個優化過程卻是跟隨者技術的發展慢慢演變的。
## 6.1、Tomcat整體架構[^13]
Tomcat是HTTP伺服器,同時還是一個Servlet容器,可以執行Java Servlet,並將JavaServer Pages(JSP)和JavaServerFaces(JSF)轉換為Java Servlet。
我們先來看看Tomcat各個元件的整體架構。Tomcat採用了分層和模組化的體系結構,如下所示,這個結構有點像套娃,一層套一層的,這也同時是Tomcat server.xml配置檔案的層級結構:
![image-20210128235313492](https://cdn.itzhai.com/image-20210128235313492-a.png-itzhai)
Server是頂層元件,代表著一個Tomcat例項,在配置檔案中一般如下:
```xml
......
```
Server下面可以包含多個Service,每個服務都有自己的Container和Connector。
> 注意,port為該伺服器等待關閉命令的TCP/IP埠號。設定為-1可禁用關閉埠。
**Connector和Container是Service的兩個主要元件。**
整個Tomcat的生命週期由Server控制。
### 6.1.1、Container
Container用於管理各種Servlet,處理Connector傳過來的Request請求。
大家可以看到,Container內部若隱若現的好像還有內幕..是的,上圖中我把內幕隱藏起來了,介面Container內部,我們可以看到這樣的結構:
![image-20210129000017905](https://cdn.itzhai.com/image-20210129000017905-a.png-itzhai)
* Container最頂層是Engine,一個Service只能有一個Engine,用來管理多個站點;
* Host代表一個站點,一個Engine下可以有多個Host;
* Context代表一個應用,一個Host下面可以有多個應用;
* Wrapper封裝Servlet,每個應用都有很多Servlet,這個是大家最熟悉的了。
### 6.1.2、Connector
Connector用於處理請求,處理Socket套接字,把原始的網路資料包裝成Request物件給Container進行處理,並封裝Response物件用於響應套接字輸出。
如上圖,一個Service可以有多個Connector,**每個Connector實現不同的連線協議,通過不同的埠提供服務。**
**這裡已經看到我們要關注的重點了,是的,Connector就是處理網路的關鍵模組,這個模組的效率直接決定了Tomcat的效能!!!**
接下來,我們開啟Connector潘多拉的盒子,看看裡面究竟有什麼不可告人的祕密。
話不多說,我直接上圖,這麼爽快不斷附圖片的部落格還真不多,IT宅(itzhai.com)的`Java架構雜談`算一個,重點來了,這裡我們先列出傳統的BIO執行模型的元件圖:
![image-20210201005248288](https://cdn.itzhai.com/image-20210201005248288-a.png-itzhai)
如上圖,Connector主要由三個元件:
* `ProtocolHandler`,Connector使用ProtocolHandler來接收請求並按照協議進行解析,不同的協議有不同的ProtocolHandler實現:
* `Http11Protocol`:阻塞式IO實現的HTTP/1.1協議處理器,採用傳統的IO進行操作,每個請求都會建立一個執行緒;
* `Http11NioProtocol`:同步非阻塞IO實現的HTTP/1.1協議處理器,Tomcat 8預設採用該模式;
* `Http11Nio2Protocol`:非同步IO實現的HTTP/1.1協議處理器,Tomcat 8之後開始支援;
* `Http11AprProtocol`:apr(Apache Portable Runtime/Apache可移植執行時),是一個高度可移植的庫,它是Apache HTTP Server 2.x的核心。APR有許多用途,包括訪問高階IO功能(例如sendfile,epoll和OpenSSL),作業系統級別的功能(生成隨機數,系統狀態等)和本機程序處理(共享記憶體,NT管道和Unix套接字)。
* `Adapter`:Adapter最終將Request物件交給Container容器程序具體的處理;
* `Mapper`:使用Mapper,可以通過請求地址找到對應的servlet;
其中`ProtocolHandler`中主要的元件有:
* `EndPoint`:直接負責對接Socket套接字API,處理套接字連線,往套接字讀寫資料,也就是負責處理TCP層相關工作;
* `Processor`:按照協議封裝TCP資料,比如,使用的是HTTP協議,則將EndPoint接收到的請求對應的IO資料以HTTP協議的規範,封裝成Request物件;
不過既然知道EndPoint是直接負責對接套接字Api的,那我們就知道了核心的網路程式設計效能關鍵就在EndPoint這個元件裡面,在這裡可以使用各種IO程式設計正規化來進行網路效能優化。EndPoint裡面又有幾個抽象概念:
* `Acceptor`:用於處理監聽套接字,負責建立連線,並監聽請求;
* `Handler`:用於處理接收到的套接字請求;
* `AsyncTimeout`:用於檢測非同步Request的超時。
既然EndPoint元件是網路處理關鍵的效能所在,我們就重點來看看這塊的設計吧。
## 6.2、Tomcat聯結器效能分析
首先來看看傳統的BIO執行緒模型。
### 6.2.1、Tomcat之BIO執行緒模型
BIO執行緒模型即傳統的以多執行緒處理請求的方式獲取到一個新的已連線套接字之後,都丟到執行緒池裡面,交給一個執行緒處理,從讀取IO資料,處理業務,到響應IO資料都是在同一個執行緒中處理。如下圖,我只把相關的元件給畫出來:
![image-20210201003029439](https://cdn.itzhai.com/image-20210201003029439-a.png-itzhai)
如上圖,Acceptor執行緒獲取到新的已連線套接字之後,直接把新的已連線套接字交給Executor執行緒池進行處理。
這種模式,受能夠建立執行緒數的限制,導致不能支撐很大併發,並且越多的因IO導致阻塞的執行緒,會導致越多的執行緒上下文切換,浪費了系統資源。
接下來我們看看NIO執行緒模型,該模型基於`主從Reactor + Worker執行緒池`網路程式設計模型。
### 6.2.2、Tomcat之NIO執行緒模型
對應的實現類為:`Http11NioProtocol`,同步非阻塞IO實現的HTTP/1.1協議處理器,Tomcat 8預設採用該模式。
以下是該模型的元件架構圖:
![image-20210221172859867](https://cdn.itzhai.com/image-20210221172859867-a.png-itzhai)
其中Poller執行緒中維護了一個Selector物件,用來實現基於NIO網路事件處理。
大致工作原理如下:
* `Acceptor執行緒`接收已連線套接字,這裡使用傳統的serverSocket.accept()方式,獲取到一個`SocketChannel`物件,然後把該物件封裝到`NioChannel`物件中,進一步的把`NioChannel`物件封裝成`PollerEvent`物件,並把PollerEvent物件push到事件佇列,最後喚醒`Poller中的selector`,以便有時機把套接字讀事件註冊到Poller中的Selector中(相關原始碼:`org.apache.tomcat.util.net.NioEndpoint.Poller.addEvent(PollerEvent event)`);
* `PollerEvent執行緒`執行run方法,把PollerEvent中的已連線套接字的channel的`SelectionKey.OP_READ`讀事件註冊到Poller的Selector選擇器中(相關原始碼:`org.apache.tomcat.util.net.NioEndpoint.Poller.run()`);
* `Poller執行緒`執行Selector.select()方法,感知IO讀事件,一旦感知到IO讀事件,就通過NioEndPoint把socket封裝成SocketProcessor,交給Worker執行緒進行處理;
* Worker執行緒執行SocketProcessor的doRun方法,最終交給HTTP1NioProcessor進行後續處理。
基於NIO的Tomcat,避免了由於IO導致的阻塞,減少了執行緒開銷,以及執行緒上下文切換開銷,能夠支撐更大的併發量。
與此同時,Tomcat支援非同步IO的網路讀寫,對應的實現類為:Http11Nio2Protocol。
### 6.2.3、Tomcat之NIO2執行緒模型
`Http11Nio2Protocol`:非同步IO實現的HTTP/1.1協議處理器,Tomcat 8之後開始支援,基於Java的AIO API實現的非同步IO。
> 這裡需要注意的一點是,Java中的Nio2,也就是AIO,底層是不是真正的非同步IO,還跟具體的作業系統優化,在Windows平臺,nio2是基於IOCP實現的非同步IO,而Linux還是在使用者空間基於epoll多路複用模擬實現的非同步IO,只是在程式設計介面上體現為了非同步IO,更易於編寫。
相關元件架構圖如下:
![image-20210222232455069](https://cdn.itzhai.com/image-20210222232455069-a.png-itzhai)
對應的非同步IO處理類是Nio2EndPoint,獲取已連線套接字的類為Nio2Acceptor。
由於IO非同步化了,所以Nio中的Poller類也就沒有了存在的必要。不管是accept獲取已連線套接字還是IO讀寫,都改為了非同步處理,當可以做IO操作的時候,會由Java非同步IO框架呼叫對應IO操作的`CompletionHandler`類進行後續處理。
這裡的SocketProcessor實現了Runnable介面,其中的run方法即是原本丟給Worker執行緒處理的,包括IO讀寫。但是現在,SocketProcessor再也不需要多一次IO操作的系統呼叫開銷了。
### 6.2.4、Tomcat之APR執行緒模型
我們再簡要介紹下,APR,對應的實現為`Http11AprProtocol`:apr(Apache Portable Runtime/Apache可移植執行時),是一個高度可移植的庫,它是Apache HTTP Server 2.x的核心。
在Tomcat中使用APR庫,其實就是在Tomcat中使用JNI的方式來讀取檔案以及進行網路傳輸,可以大大提升Tomcat對靜態檔案的處理效能。如果服務開啟了HTTPS的話,也可以提升SSL的處理效能。
# 7、MySQL
![image-20210306131237282](https://cdn.itzhai.com/image-20210306131237282-a.png-itzhai)
我們前面介紹過MySQL儲存架構 [洞悉MySQL底層架構:遊走在緩衝與磁碟之間](https://www.itzhai.com/articles/insight-into-the-underlying-architecture-of-mysql-buffer-and-disk.html),但是並沒有介紹MySQL執行的執行緒模型,這裡我就給大家**免費**補上這節課。講得好請給我個贊,否則也給我個贊。
![image-20210224001857604](https://cdn.itzhai.com/image-20210224001857604-a.png-itzhai)
MySQL的執行緒模型又是怎樣的呢,還是要請專業人士解說下,更有權威性。
在聽了MySQL資料庫的開發和維護人員我的好朋友 Geir Hoydalsvik[^14] 的講解後,我也來總結下(雖然他可能不認識我)。
## 7.1、MySQL執行緒模型
首先我們來看一個引數:`thread_handling`[^15],一個控制MySQL連線執行緒的引數,它有以下三個取值:
* `no-threads`: 表示MySQL使用主執行緒處理連線請求,不額外建立執行緒;
* `one-thread-per-connection`: 表示MySQL為每個客戶端連線請求建立一個執行緒;
* `loaded-dynamically`: 當初始化好了thread pool plugin的時候,由該外掛進行設定,即通過執行緒池的方式處處理連線請求。
看起來,MySQL並沒有使用Reactor或者Proactor優化網路IO效率。
那麼我們就來看看傳統的一個請求建立一個執行緒的模型下,MySQL內部是如何工作的吧,如下是該執行緒模型工作圖示:
![image-20210225083338171](https://cdn.itzhai.com/image-20210225083338171-a.png-itzhai)
* **連線請求:** 客戶端請求MySQL伺服器,預設的 ,由MySQL伺服器的TCP 3306介面進行接收訊息,傳入的連線請求被排隊;
* **Receiver Thread(接收執行緒):** 接受執行緒負責處理排隊的連線請求,accept到請求之後,建立一個使用者執行緒,讓使用者執行緒進一步處理後續邏輯;
* **執行緒快取:** 如果線上程快取中能夠找到接收執行緒,則可以重用執行緒快取中的執行緒,否則,新建執行緒。如果建立OS執行緒的成本很高,那麼執行緒快取對於連線速度起到很大的幫助作用。如今,建立OS執行緒開銷相對小,所以此優化幫助不是很大。如果連線數很小,嘗試增加執行緒快取,讓執行緒儘可能複用是有意義的;
* **使用者執行緒:** 使用者執行緒負責處理連線階段和命令階段的工作。[連線階段](https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_connection_phase.html)[^16]:連線協議,分配THD,進行功能協商,以及身份驗證,使用者憑證也是儲存在THD中,如果在連線階段沒有問題,則將進入[命令階段](https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_command_phase.html)[^17];
* **THD:** thread/connection descriptor,對於每個客戶端連線,我們使建立一個單獨的執行緒,並且為該執行緒提供[THD](https://dev.mysql.com/doc/dev/mysql-server/8.0.11/classTHD.html)資料結構,作為執行緒/連線描述符,每個連線執行緒對應一個THD。THD在連線建立的時候建立,在連線斷開的時候刪除。THD是一個大型的資料結構,用於跟蹤執行狀態的各種資訊,在查詢執行期間,THD記憶體將顯著增長。為了進行記憶體規劃,Geir Hoydalsvik 建議每個連線平均規劃月10MB記憶體。
## 7.2、限制MySQL併發效率的因素
限制MySQL併發效率的主要因素主要有互斥鎖、資料庫鎖或IO。
* `互斥鎖`:一般為了保護共享內部資料結構的時候,都會建立一個互斥鎖,確保任何時間只有一個執行緒在操作內部資料結構,但是互斥鎖卻導致了其他執行緒必須排隊等待。為了減少互斥鎖對併發的影響,**可以通過無鎖演算法,將受保護的資源分解為更細粒度的資源**,以確保不同的執行緒使用了不同的互斥量,從而減少對全域性資源的爭用;
* `資料庫鎖`:資料庫鎖,從某種程度上來說,資料庫鎖和資料庫語義相關,一次更難避免(而InnoDB具有多版本併發控制,所以它在避免鎖方面比較擅長)。資料庫大致分為:
* 由SQL DML引起的資料鎖,如行鎖通常保護一個執行緒正在更新的資料不被另一個執行緒讀取或者寫入;
* 由SQL DDL引起的元資料鎖,用於保護資料庫結構免遭併發的不相容的更新。為了維護更加重要的資料庫語義,不得不對效能做出妥協
* `磁碟和網路IO`:由於MySQL資料庫是儲存在硬碟的,執行SQL的過程中,不可避免的從磁碟載入資料頁,此時執行緒會進入等待狀態。執行緒併發性將受到IO容量的限制。
**為什麼MySQL沒有使用Reactor模式優化IO?**
關於這個問題,我想主要有以下原因:MySQL的架構設計,就決定了在通過索引查詢資料的過程中,需要不斷的載入資料頁,採用Reactor模式,編碼複雜度將更高。更重要的是,MySQL併發性很大程度上取決於使用者負載,資料庫死鎖、索引不合理導致的全表掃描、效能較差的SQL語句,如select *, 大表limit a, b,大表關聯查詢等,都有可能導致影響MySQL的併發性,在這種情況下,瓶頸反而不線上程上,最差的情況下,使用者連線數甚至可能低於CPU核心數。正是因為這樣,所以才要大家精通MySQL調優呀,針對業務優化好SQL語句,效能可能就差幾十倍了。
說到這裡,大家是不是瞭解到了MySQL其實是很脆弱的呢,要寫好SQL真不容易,索引設計要跟隨業務合理設計,一旦上線就很難調整不說,各種分庫分表中介軟體也要用上,分庫分表了,卻又引發了分散式事務的問題需要解決。為了解決MySQL併發性低的問題,我們引入了快取來抗併發,但是快取與資料庫資料一致性問題又來了...
魯迅說:真的不考慮以下其他的資料庫嗎?
![image-20210226004312166](https://cdn.itzhai.com/image-20210226004312166-a.png-itzhai)
## 7.3、InnoDB對併發流量的守衛戰
基於以上提及的MySQL效能問題,InnoDB儲存引擎做了一些防守:在有助於最大程度地減少執行緒之間的上下文切換的情況下,`InnoDB`可以使用多種技術來限制併發執行緒數。當`InnoDB`從使用者會話接收到新請求時,如果同時執行的執行緒數已超預定義限制,則新請求將休眠一小段時間,然後再次嘗試。睡眠後無法重新安排的請求被放入先進/先出佇列,並最終得到處理。等待鎖的執行緒不計入同時執行的執行緒數。
涉及的引數:
* innodb_thread_concurrency:InnDB儲存引擎最大併發執行緒數,如果為0,則表示不限制;
* innodb_thread_sleep_delay:超過最大併發執行緒數,請求執行緒需要等待innodb_thread_sleep_delay毫秒後才可以再次重試。
* innodb_concurrency_tickets:一旦請求執行緒進入到了InnoDB,會獲取到innodb_concurrency_tickets次通行證,代表該執行緒可以直接進入InnoDB而不需要檢查的次數。
通過這種設計,**儘可能的讓一次查詢請求儘快的完成(如一次join查詢操作,可能包含多個InnoDB查詢請求),而不會導致頻繁的InnoDB執行緒上下文切換開銷**。
> 本文首次發表於: [效能追擊:萬字長文30+圖揭祕8大主流伺服器程式執行緒模型](https://www.itzhai.com/articles/decrypt-the-threading-model-of-common-server-programs.html) 以及公眾號 Java架構雜談,未經許可,不得轉載。
# 8、Zuul
![image-20210306131458081](https://cdn.itzhai.com/image-20210306131458081-a.png-itzhai)
這一節我們來聊聊Zuul的效能。
既然是Netflix開源的微服務閘道器,那麼我們還是從Netflix技術部落格[^18]裡面瞭解一些Zuul效能相關的細節吧。
先來看看Zuul 1的效能情況。如下圖,來自Zuul技術部落格[^18]
![image-20210303234332993](https://cdn.itzhai.com/image-20210303234332993-a.png-itzhai)
(本圖片來源於: Zuul技術部落格[^18])
這是一個多執行緒的系統架構。Zuul 1是基於Servlet構建的,特點是阻塞IO呼叫和多執行緒,每個連線請求都會使用一個執行緒來處理。IO操作是通過從執行緒池中獲取一個執行緒來執行IO來完成的,在執行IO操作的過程中,請求執行緒被阻塞。
當後端延遲增加或者由於錯誤而導致請求重試,活動的連結和請求執行緒數就會增加,這種情況下可能會導致服務負載激增,為了抵禦這些風險,於是便有了Hystrix熔斷器,用於提供過載保護。
為了優化Zuul 1阻塞IO呼叫的問題,我們來看看Zuul 2的架構設計,如下圖,同樣的,圖片來自Zuul技術部落格[^18]:
![image-20210303234239719](https://cdn.itzhai.com/image-20210303234239719-a.png-itzhai)
(本圖片來源於: Zuul技術部落格[^18])
Zuul 2內部也是用到了事件迴圈。在非同步的執行方式下,通常每個CPU核心對應一個執行緒,用於處理所有的請求和響應,請求和響應通過事件和回撥進行處理。
因為每個連線不用建立新的執行緒,只需要付出檔案描述符和監聽器的成本,所以連線的成本很低。而阻塞模型中,連線成本是開啟一個新執行緒,並且產生大量的記憶體和系統開銷。
在非同步模式下,佇列中的連線和事件的增加成本遠低於執行緒堆積的成本。但是假設後端處理不過來,響應時間還是會不可避免的增加。
**非同步模型優缺點嗎?**當然有,阻塞呼叫的系統易於除錯,執行緒堆疊是請求執行過程的準確快照。而非同步是基於回撥,並且由事件迴圈驅動的,這種情況下,事件迴圈的堆疊跟蹤是沒有意義的。這將導致很難追蹤請求。
---
本文就講到這裡了,覺得意猶未盡的朋友們可以收藏IT宅(itzhai.com),關注星標`Java架構雜談`,最新干貨不錯過。
> 對了,不要太較真,標題裡面的 30+圖是包含了幾張表情圖片,但字數肯定是超過1了萬...
>
> **英**:By the way, don't be too serious. The 30+ pictures in the title contain several emoticons, but the number of words must be more than 10,000...
>
> **客**:30+圖片可能系車大炮,一萬過字就真真有影
>
> **粵**:話時話,唔好太較真,標題入面嘅30+圖係包含咗幾表情圖,但字數肯定係超過咗1萬...
>
> **Java**:java.lang.OutOfMemoryError
整體看來,目前主流的伺服器程式,實現高併發的套路都是那些,我們前面幾篇文章都把底層基礎的知識給講透了,還不太瞭解的朋友,可以重新點進去看看。
**《高效能網路程式設計遊記》**
* [高效能網路程式設計遊記開篇雜談](https://www.itzhai.com/articles/the-beginning-of-high-performance-network-programming-travel-notes.html)
* [網路程式設計必備知識:圖解Socket核心內幕以及五大IO模型](https://www.itzhai.com/articles/necessary-knowledge-of-network-programming-graphic-socket-core-insider-and-five-io-models.html)
* [三分鐘短文快速瞭解訊號驅動式IO,似乎沒那麼完美](https://www.itzhai.com/articles/it-seems-not-so-perfect-signal-driven-io.html)
* [徹底弄懂IO複用:IO處理殺手鐗,帶您深入瞭解select,poll,epoll](https://www.itzhai.com/articles/thoroughly-understand-io-reuse-take-you-in-depth-understanding-of-select-poll-epoll.html)
* [非同步IO:新時代的IO處理利器](https://www.itzhai.com/articles/asynchronous-programming-a-new-era-of-io-processing-weapon.html)
* [網路程式設計正規化:高效能伺服器就這麼回事 | C10K,Event Loop,Reactor,Proactor](https://www.itzhai.com/articles/high-performance-network-programming-paradigm.html)
* 效能追擊:萬字長文30+圖揭祕8大主流伺服器程式執行緒模型
最關鍵的是,這些內容在IT宅的`Java架構雜談`公眾號裡面都是完全**免費的**!看完別忘記點個贊~
![image-20210306143257678](https://cdn.itzhai.com/image-20210306143257678-a.png-itzhai)
# References
[^1]: [NodeJS System diagram. Retrieved https://twitter.com/TotesRadRichard/status/494959181871316992](https://twitter.com/TotesRadRichard/status/494959181871316992)
[^2]: [JavaScript 執行機制詳解:再談Event Loop. Retrieved from http://www.ruanyifeng.com/blog/2014/10/event-loop.html](http://www.ruanyifeng.com/blog/2014/10/event-loop.html)
[^3]: [Node.js 事件迴圈機制. Retrieved from https://www.cnblogs.com/onepixel/p/7143769.html](https://www.cnblogs.com/onepixel/p/7143769.html)
[^4]: [理解Node.js中的多執行緒. Retrieved from https://zhuanlan.zhihu.com/p/74879045](https://zhuanlan.zhihu.com/p/74879045)
[^5]: UNIX網路程式設計 卷1:套接字聯網API(第三版). 人民郵電出版社. P657
[^6]: UNIX網路程式設計 卷1:套接字聯網API(第三版). 人民郵電出版社. P659
[^7]: [Thread Pools in NGINX Boost Performance 9x!. Retrieved from https://www.nginx.com/blog/thread-pools-boost-performance-9x/](https://www.nginx.com/blog/thread-pools-boost-performance-9x/)
[^8]: [Inside NGINX: How We Designed for Performance & Scale. Retrieved from https://www.nginx.com/blog/inside-nginx-how-we-designed-for-performance-scale/](https://www.nginx.com/blog/inside-nginx-how-we-designed-for-performance-scale/)
[^9]: [網路程式設計正規化:高效能伺服器就這麼回事 | C10K,Event Loop,Reactor,Proactor. Retrieved from https://www.itzhai.com/articles/high-performance-network-programming-paradigm.html](https://www.itzhai.com/articles/high-performance-network-programming-paradigm.html)
[^10]: [How the single threaded non blocking IO model works in Node.js. Retrieved from https://stackoverflow.com/questions/14795145/how-the-single-threaded-non-blocking-io-model-works-in-node-js](https://stackoverflow.com/questions/14795145/how-the-single-threaded-non-blocking-io-model-works-in-node-js)
[^11]: [RedisConf17 - Redis Community Updates - Salvatore Sanfilippo. Retrieved from https://www.youtube.com/watch?v=U7J33pd3hLU](https://www.youtube.com/watch?v=U7J33pd3hLU)
[^12]: [redis.conf. Retrieved from https://github.com/redis/redis/blob/unstable/redis.conf](https://github.com/redis/redis/blob/unstable/redis.conf)
[^13]: 劉光瑞. Tomcat架構解析. 人民郵電出版社.
[^14]: [MySQL Connection Handling and Scaling. Retrieved from https://mysqlserverteam.com/mysql-connection-handling-and-scaling/](https://mysqlserverteam.com/mysql-connection-handling-and-scaling/)
[^15]: [https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_thread_handling. Retrieved from https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html](https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html)
[^16]: [連線階段. Retrieved from https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_connection_phase.html](https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_connection_phase.html)
[^17]: [命令階段. Retrieved from https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_command_phase.html](https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_command_phase.html)
[^18]: [Zuul 2 : The Netflix Journey to Asynchronous, Non-Blocking Systems. Retrieved from https://netflixtechblog.com/zuul-2-the-netflix-journey-to-asynchronous-non-blocking-systems-45947377fb5c](https://netflixtechblog.com/zuul-2-the-netflix-journey-to-asynchronous-non-blocking-systems-45947377fb5c)
---
> 本文同步發表於我的部落格IT宅(itzhai.com)和公眾號(Java架構雜談)
>
> 作者:arthinking | 公眾號:Java架構雜談
>
> 部落格連結:https://www.itzhai.com/articles/decrypt-the-threading-model-of-common-server-programs.html
>
> 版權宣告: 版權歸作者所有,未經許可不得轉載,侵權必究!聯絡作者請加公眾號。