淺析 Jetty 中的線程優(yōu)化思路
一、什么是 Jetty
Jetty 跟 Tomcat 一樣是一種 Web 容器,它的總體架構(gòu)設(shè)計(jì)如下:
Jetty 總體上由一系列 Connector、一系列 Handler 和一個(gè) ThreadPool組成。
Connector 也就是 Jetty 的連接器組件,相比較 Tomcat 的連接器,Jetty 的連接器在設(shè)計(jì)上有自己的特點(diǎn)。
Jetty 的 Connector 支持 NIO 通信模型,NIO 模型中的主角是 Selector,Jetty 在 Java 原生 Selector 的基礎(chǔ)上封裝了自己的 Selector:ManagedSelector。
二、Jetty 中的 Selector 交互
2.1 傳統(tǒng)的 Selector 實(shí)現(xiàn)
常規(guī)的 NIO 編程思路是將 I/O 事件的偵測和請(qǐng)求的處理分別用不同的線程處理。
具體過程是:
- 啟動(dòng)一個(gè)線程;
- 在一個(gè)死循環(huán)里不斷地調(diào)用 select 方法,檢測 Channel 的 I/O 狀態(tài);
- 一旦 I/O 事件到達(dá),就把該 I/O 事件以及一些數(shù)據(jù)包裝成一個(gè) Runnable;
- 將 Runnable 放到新線程中去處理。
這個(gè)過程有兩個(gè)線程在干活:一個(gè)是 I/O 事件檢測線程、一個(gè)是 I/O 事件處理線程。
這兩個(gè)線程是"生產(chǎn)者"和"消費(fèi)者"的關(guān)系。
這樣設(shè)計(jì)的好處:
將兩個(gè)工作用不同的線程處理,好處是它們互不干擾和阻塞對(duì)方。
這樣設(shè)計(jì)的缺陷:
當(dāng) Selector 檢測讀就緒事件時(shí),數(shù)據(jù)已經(jīng)被拷貝到內(nèi)核中的緩存了,同時(shí) CPU 的緩存中也有這些數(shù)據(jù)了。
這時(shí)當(dāng)應(yīng)用程序去讀這些數(shù)據(jù)時(shí),如果用另一個(gè)線程去讀,很有可能這個(gè)讀線程使用另一個(gè) CPU 核,而不是之前那個(gè)檢測數(shù)據(jù)就緒的 CPU 核。
這樣 CPU 緩存中的數(shù)據(jù)就用不上了,并且線程切換也需要開銷。
2.2 Jetty 中的 ManagedSelector 實(shí)現(xiàn)
Jetty 的 Connector 將 I/O 事件的生產(chǎn)和消費(fèi)放到同一個(gè)線程處理。
如果執(zhí)行過程中線程不阻塞,操作系統(tǒng)會(huì)用同一個(gè) CPU 核來執(zhí)行這兩個(gè)任務(wù),這樣既能充分利用 CPU 緩存,又可以減少線程上下文切換的開銷。
ManagedSelector 本質(zhì)上是一個(gè) Selector,負(fù)責(zé) I/O 事件的檢測和分發(fā)。
為了方便使用,Jetty 在 Java 原生 Selector 的基礎(chǔ)上做了一些擴(kuò)展,它的成員變量如下:
public class ManagedSelector extends ContainerLifeCycle implements Dumpable
{
// 原子變量,表明當(dāng)前的ManagedSelector是否已經(jīng)啟動(dòng)
private final AtomicBoolean _started = new AtomicBoolean(false);
// 表明是否阻塞在select調(diào)用上
private boolean _selecting = false;
// 管理器的引用,SelectorManager管理若干ManagedSelector的生命周期
private final SelectorManager _selectorManager;
// ManagedSelector的id
private final int _id;
// 關(guān)鍵的執(zhí)行策略,生產(chǎn)者和消費(fèi)者是否在同一個(gè)線程處理由它決定
private final ExecutionStrategy _strategy;
// Java原生的Selector
private Selector _selector;
// "Selector更新任務(wù)"隊(duì)列
private Deque<SelectorUpdate> _updates = new ArrayDeque<>();
private Deque<SelectorUpdate> _updateable = new ArrayDeque<>();
...
}
2.2.1 SelectorUpdate 接口
為什么需要一個(gè)"Selector更新任務(wù)"隊(duì)列呢?
對(duì)于 Selector 的用戶來說,我們對(duì) Selector 的操作無非是將 Channel 注冊(cè)到 Selector 或者告訴 Selector 我對(duì)什么 I/O 事件感興趣。
這些操作其實(shí)就是對(duì) Selector 狀態(tài)的更新,Jetty 把這些操作抽象成 SelectorUpdate 接口。
/**
* A selector update to be done when the selector has been woken.
*/
public interface SelectorUpdate
{
void update(Selector selector);
}
這意味著不能直接操作 ManagedSelector 中的 Selector,而是需要向 ManagedSelector 提交一個(gè)任務(wù)類。
這個(gè)類需要實(shí)現(xiàn) SelectorUpdate 接口的 update 方法,在 update 方法中定義要對(duì)
ManagedSelector 做的操作。
比如 Connector 中的 Endpoint 組件對(duì)讀就緒事件感興趣。
它就向 ManagedSelector 提交了一個(gè)內(nèi)部任務(wù)類
ManagedSelector.SelectorUpdate:
_selector.submit(_updateKeyAction);
這個(gè) _updateKeyAction 就是一個(gè)
SelectorUpdate 實(shí)例,它的 update 方法實(shí)現(xiàn)如下:
private final ManagedSelector.SelectorUpdate _updateKeyAction = new ManagedSelector.SelectorUpdate()
{
@Override
public void update(Selector selector)
{
// 這里的updateKey其實(shí)就是調(diào)用了SelectionKey.interestOps(OP_READ);
updateKey();
}
};
在 update 方法里,調(diào)用了 SelectionKey 類的 interestOps 方法,傳入的參數(shù)是 OP_READ,意思是我對(duì)這個(gè) Channel 上的讀就緒事件感興趣。
2.2.2 Selectable 接口
上面有了 update 方法,那誰來執(zhí)行這些 update 呢,答案是 ManagedSelector 自己。
它在一個(gè)死循環(huán)里拉取這些 SelectorUpdate 任務(wù)逐個(gè)執(zhí)行。
I/O 事件到達(dá)時(shí),ManagedSelector 通過一個(gè)任務(wù)類接口(Selectable 接口)來確定由哪個(gè)函數(shù)處理這個(gè)事件。
public interface Selectable
{
// 當(dāng)某一個(gè)Channel的I/O事件就緒后,ManagedSelector會(huì)調(diào)用的回調(diào)函數(shù)
Runnable onSelected();
// 當(dāng)所有事件處理完了之后ManagedSelector會(huì)調(diào)的回調(diào)函數(shù)
void updateKey();
}
Selectable 接口的 onSelected() 方法返回一個(gè) Runnable,這個(gè) Runnable 就是 I/O 事件就緒時(shí)相應(yīng)的處理邏輯。
ManagedSelector 在檢測到某個(gè) Channel 上的 I/O 事件就緒時(shí),ManagedSelector 調(diào)用這個(gè) Channel 所綁定的類的 onSelected 方法來拿到一個(gè) Runnable。
然后把 Runnable 扔給線程池去執(zhí)行。
三、Jetty 的線程優(yōu)化思路
3.1 Jetty 中的 ExecutionStrategy 實(shí)現(xiàn)
前面介紹了 ManagedSelector 的使用交互:
- 如何注冊(cè) Channel 以及 I/O 事件
- 提供什么樣的處理類來處理 I/O 事件
那么 ManagedSelector 如何統(tǒng)一管理和維護(hù)用戶注冊(cè)的 Channel 集合呢,答案是
ExecutionStrategy 接口。
這個(gè)接口將具體任務(wù)的生產(chǎn)委托給內(nèi)部接口 Producer,而在自己的 produce 方法里實(shí)現(xiàn)具體執(zhí)行邏輯。
這個(gè) Runnable 的任務(wù)可以由當(dāng)前線程執(zhí)行,也可以放到新線程中執(zhí)行。
public interface ExecutionStrategy
{
// 只在HTTP2中用到的一個(gè)方法,暫時(shí)忽略
public void dispatch();
// 實(shí)現(xiàn)具體執(zhí)行策略,任務(wù)生產(chǎn)出來后可能由當(dāng)前線程執(zhí)行,也可能由新線程來執(zhí)行
public void produce();
// 任務(wù)的生產(chǎn)委托給Producer內(nèi)部接口
public interface Producer
{
// 生產(chǎn)一個(gè)Runnable(任務(wù))
Runnable produce();
}
}
實(shí)現(xiàn) Produce 接口生產(chǎn)任務(wù),一旦任務(wù)生產(chǎn)出來,ExecutionStrategy 會(huì)負(fù)責(zé)執(zhí)行這個(gè)任務(wù)。
private class SelectorProducer implements ExecutionStrategy.Producer
{
private Set<SelectionKey> _keys = Collections.emptySet();
private Iterator<SelectionKey> _cursor = Collections.emptyIterator();
@Override
public Runnable produce()
{
while (true)
{
// 如果Channel集合中有I/O事件就緒,調(diào)用前面提到的Selectable接口獲取Runnable,直接返回給ExecutionStrategy去處理
Runnable task = processSelected();
if (task != null)
return task;
// 如果沒有I/O事件就緒,就干點(diǎn)雜活,看看有沒有客戶提交了更新Selector的任務(wù),就是上面提到的SelectorUpdate任務(wù)類。
processUpdates();
updateKeys();
// 繼續(xù)執(zhí)行select方法,偵測I/O就緒事件
if (!select())
return null;
}
}
}
SelectorProducer 是 ManagedSelector 的內(nèi)部類。
SelectorProducer 實(shí)現(xiàn)了 ExecutionStrategy 中的 Producer 接口中的 produce 方法,需要向 ExecutionStrategy 返回一個(gè) Runnable。
在 produce 方法中 SelectorProducer 主要干了三件事:
- 如果 Channel 集合中有 I/O 事件就緒,調(diào)用前面提到的 Selectable 接口獲取 Runnable,直接返回給
ExecutionStrategy 處理。 - 如果沒有 I/O 事件就緒,就干點(diǎn)雜活,看看有沒有客戶提交了更新 Selector 上事件注冊(cè)的任務(wù),也就是上面提到的
SelectorUpdate 任務(wù)類。 - 干完雜活繼續(xù)執(zhí)行 select 方法,偵測 I/O 就緒事件。
3.2 Jetty 的線程執(zhí)行策略
3.2.1 ProduceConsume(PC) 線程執(zhí)行策略
任務(wù)生產(chǎn)者自己依次生產(chǎn)和執(zhí)行任務(wù),對(duì)應(yīng)到 NIO 通信模型就是用一個(gè)線程來偵測和處理一個(gè) ManagedSelector 上的所有的 I/O 事件。
后面的 I/O 事件要等待前面的 I/O 事件處理完,效率明顯不高。
圖中,綠色代表生產(chǎn)一個(gè)任務(wù),藍(lán)色代表執(zhí)行這個(gè)任務(wù),下同。
3.2.2 ProduceExecuteConsume(PEC) 線程執(zhí)行策略
任務(wù)生產(chǎn)者開啟新線程來執(zhí)行任務(wù),這是典型的 I/O 事件偵測和處理用不同的線程來處理。
缺點(diǎn)是不能利用 CPU 緩存,并且線程切換成本高。
圖中,棕色代表線程切換,下同。
3.2.3 ExecuteProduceConsume(EPC) 線程執(zhí)行策略
任務(wù)生產(chǎn)者自己運(yùn)行任務(wù),這種方式可能會(huì)新建一個(gè)新的線程來繼續(xù)生產(chǎn)和執(zhí)行任務(wù)。
它的優(yōu)點(diǎn)是能利用 CPU 緩存,但是潛在的問題是如果處理 I/O 事件的業(yè)務(wù)代碼執(zhí)行時(shí)間過長,會(huì)導(dǎo)致線程大量阻塞和線程饑餓。
3.2.4 EatWhatYouKill(EWYK) 改良線程執(zhí)行策略
這是 Jetty 對(duì) ExecuteProduceConsume 策略的改良,在線程池線程充足的情況下等同于 ExecuteProduceConsume;
當(dāng)系統(tǒng)比較忙線程不夠時(shí),切換成 ProduceExecuteConsume 策略。
這么做的原因是:
ExecuteProduceConsume 是在同一線程執(zhí)行 I/O 事件的生產(chǎn)和消費(fèi),它使用的線程來自 Jetty 全局的線程池,這些線程有可能被業(yè)務(wù)代碼阻塞,如果阻塞的多了,全局線程池中線程自然就不夠用了,最壞的情況是連 I/O 事件的偵測都沒有線程可用了,會(huì)導(dǎo)致 Connector 拒絕瀏覽器請(qǐng)求。
于是 Jetty 做了一個(gè)優(yōu)化:
在低線程情況下,就執(zhí)行
ProduceExecuteConsume 策略,I/O 偵測用專門的線程處理, I/O 事件的處理扔給線程池處理,其實(shí)就是放到線程池的隊(duì)列里慢慢處理。
四、總結(jié)
本文基于 Jetty-9 介紹了 ManagedSelector 和 ExecutionStrategy 的設(shè)計(jì)實(shí)現(xiàn),介紹了 PC、PEC、EPC 三種線程執(zhí)行策略的差異,從 Jetty 對(duì)線程執(zhí)行策略的改良操作中可以看出,Jetty 的線程執(zhí)行策略會(huì)優(yōu)先使用 EPC 使得生產(chǎn)和消費(fèi)任務(wù)能夠在同一個(gè)線程上運(yùn)行,這樣做可以充分利用熱緩存,避免調(diào)度延遲。
這給我們做性能優(yōu)化也提供了一些思路:
- 在保證不發(fā)生線程饑餓的情況下,盡量使用同一個(gè)線程生產(chǎn)和消費(fèi)可以充分利用 CPU 緩存,并減少線程切換的開銷。
- 根據(jù)實(shí)際場景選擇最適合的執(zhí)行策略,通過組合多個(gè)子策略也可以揚(yáng)長避短達(dá)到1+1>2的效果。
參考文檔: