1. 程式人生 > >Jetty9原始碼剖析 - Connector元件 - EPC(ExecuteProduceConsume)

Jetty9原始碼剖析 - Connector元件 - EPC(ExecuteProduceConsume)

轉載自ph0ly:http://www.ph0ly.com

一、概念

對於常規的IO操作來說,通常我們會拿一個執行緒作為生產者,阻塞在select呼叫上面,等待新的IO事件,當IO到來時,生產者將該事件以及一些資料包裝起來放到佇列,另一個執行緒去消費這些事件,然後根據事件型別處理,這樣解耦了事件的生產與消費,讓IO事件生產和IO事件消費互不阻塞,這種模式在Jetty任務執行模型裡面稱為ProduceExecuteConsume(簡稱PEC)。這樣確實帶來了很多好處,不過對於大量的IO操作來說,例如讀事件,這時候喚醒select,說明核心態已經處理完成了,CPU的某個核的暫存器可能還存在熱快取(Hot Cache),使用者態去讀取資料時,如果放到另一個執行緒,很大概率並不一定是這個核來執行,而且還存線上程排程的時間開銷,因此從底層來看,Jetty做了一個很大的優化,那就是讓IO事件生產和消費放到同一個執行緒,這樣很大概率仍然可以在同一個核執行並能利用暫存器熱快取。於是Jetty就搞了個新的任務執行模型ExecuteProduceConsume(簡稱EPC,又稱為EatWhatYouKill)出來,讓同一個執行緒去執行生產和消費,經過Jetty官方測試,這種模型比PEC模型效能高出了近10倍,如下圖

Jetty-EPC效能圖

二、繼承體系

繼承體系

繼承圖還是比較簡潔,ExecuteProduceConsume繼承自ExecutingExecutionStrategy,實現了ExecutionStrategy策略,而ExecutionStrategy內部定義了Producer介面和Factory執行工廠,比較簡單,這裡從簡,重點關注EPC的實現

三、總體架構

總體架構

正如第一節提到的,EPC是一個任務執行模型,它執行傳入的生產者來生產任務,並在同一個執行緒中執行生產的任務,通常Jetty就拿來執行IO或其他動作,如上圖,EPC利用ManagedSelector提供的SelectorProducer.produce方法來生產任務,通常這些任務是IO事件或一些動作(連線、EndPoint建立或銷燬等等),如圖SelectorProducer.produce主要包含的幾個步驟:processSelected處理IO事件、runActions獲取動作任務、updateKeys動態調整SelectionKey的讀寫模式、select等待IO事件,這些方法中processSelected、runActions會返回一個任務,update、select僅是執行自身邏輯,拿到任務(通常就是IO讀寫、動作)後就在當前的執行緒執行

四、原始碼剖析

1. 建構函式

建構函式

建構函式比較簡單,沒啥說的

2. 啟動

由於EPC不是具有生命週期的,因此也就不存在doStart的說法,它的啟動是由ManagedSelector的run方法觸發(不清楚的讀者,請回到上一篇文章瞭解詳情),其實就是EPC的execute方法

執行

execute方法首先拿鎖,保證狀態改得是原子的,不解釋。如果當前EPC是空閒狀態,那就改成生產中狀態,如果是生產中,那下面的produceConsume方法就能執行,也就是可以執行生產消費,否則當前EPC處於非生產中狀態,那就不會執行生產消費

produceConsume

produceConsume首先會判斷當前執行緒池是否處於低執行緒狀態(也就是執行緒幾乎被打滿了),那這個時候就執行produceExecuteConsume(PEC,也就是概念裡面提到的傳統任務執行模型,生產者和消費者在不同執行緒執行),如果不是低執行緒狀態,那就執行EPC

讀者肯定會問,這個低執行緒模式下為什麼要執行PEC?
原因是EPC本身是在同一執行緒執行生產和消費,它的執行仍然是利用qtp來執行,那如果這個執行緒被業務程式碼阻塞,那如果阻塞的多了,執行緒自然就不夠用了,到最後qtp直接被打滿,打滿了那自然連基礎的生產都不行(IO事件沒法處理了),這就是執行緒飢餓狀態,那這個時候Jetty做了一個優化,低執行緒模式下,就執行PEC,如下圖,我們來分析下PEC

produceExecuteConsume

executeProduct

PEC會一直判斷當前是否是低執行緒模式,如果是那就會一直佔住當前這個執行緒,並執行生產操作,有任務後,執行executeProduct,其實就是放到了Executor來執行,那其實就是生產和消費在不同的執行緒執行了,這樣就保證了在低執行緒的情況下,Jetty仍然能夠完成生產任務,這樣執行緒飢餓的場景,Jetty也能應付

executeProduceConsume-1

executeProduceConsume-2

接下來分析EPC真正的邏輯
Runnable task = _producer.produce()這行就是調前面的SelectorProducer來生產任務,生產完了改狀態_producing為false,如果拿到了任務,如果當前的EPC處於生產狀態,那就會將dispatch置位true,也就是當前EPC已經在幹活了,執行緒已經被佔住了,於是會進入下面if(dispatch),execute(this),執行自己,EPC本身就是一個Runnable,所以這裡在把自己扔到執行緒池,執行run方法,開啟下一個EPC任務(注意這裡是生產完成後就會開下一個,並不是等這個生產後的任務執行完了才開下一個EPC執行緒),開了新的EPC後,會執行拿到的任務,task.run(),之後改狀態。下面再來看下EPC的run在幹什麼

run

run其實就是改狀態,然後執行produceConsume,其實和execute邏輯基本一致,這裡就不再多說

五、 總結

EPC其實就是Jetty對任務執行的優化,傳統的ProduceExecuteConsume(PEC)模型,IO事件監聽放到一些執行緒,而IO事件處理則放到另一些執行緒,這其實不能很好利用暫存器快取,Jetty作為高效能Web容器自然要做大量優化,因此就有了EPC的誕生,EPC其實算是一個綜合的產物,並不是所有情況全部在同一執行緒執行任務,而是結合PEC在低執行緒模式下,仍能夠完成生產消費,這是Jetty考慮的非常周全的地方,點贊。所以要做好高效能還得考慮底層的硬體層面的操作,這樣才能做到更高的效能。這篇文章就寫到這裡,看起來還是很複雜,希望對這塊有興趣的讀者,仔細閱讀,相信你能收穫一些東西。接下來的文章我將會深入HTTP的邏輯連線、資料解析、資料的響應,歡迎大家繼續關注~

六、參考資料

  1. Eat What You Kill
  2. Thread Starvation with Eat What You Kill
  3. Mechanical Sympathy