對於伺服器的併發處理能力,我們需要的是:每一毫秒伺服器都能及時處理這一毫秒內收到的數百個不同tcp連線上的報文,與此同時,可能伺服器上還有數以十萬計的最近幾秒沒有收發任何報文的相對不活躍連線。同時處理多個並行發生事件的連線,簡稱為併發;同時處理萬計、十萬計的連線,則是高併發。伺服器的併發程式設計所追求的就是處理的併發連線數目無限大,同時維持著高效率使用cpu等資源,直至物理資源首先耗盡。
併發程式設計有很多種實現模型,最簡單的就是與「執行緒」**,1個執行緒處理1個連線的全部生命週期。優點:這個模型足夠簡單,它可以實現複雜的業務場景,同時,執行緒個數是可以遠大於cpu個數的。然而,執行緒個數又不是可以無限增大的,為什麼呢?因為執行緒什麼時候執行是由作業系統核心排程演算法決定的,排程演算法並不會考慮某個執行緒可能只是為了乙個連線服務的,它會做大一統的玩法:時間片到了就執行一下,哪怕這個執行緒一執行就會不得不繼續睡眠。這樣來回的喚醒、睡眠執行緒在次數不多的情況下,是廉價的,但如果作業系統的執行緒總數很多時,它就是昂貴的(被放大了),因為這種技術性的排程損耗會影響到執行緒上執行的業務**的時間。舉個例子,這時大部分擁有不活躍連線的執行緒就像我們的國企,它們執行效率太低了,它總是喚醒就睡眠在做無用功,而它喚醒爭到cpu資源的同時,就意味著處理活躍連線的民企執行緒減少獲得了cpu的機會,cpu是核心競爭力,它的無效率進而影響了gdp總吞吐量。我們所追求的是併發處理數十萬連線,當幾千個執行緒出現時,系統的執行效率就已經無法滿足高併發了。
對高併發程式設計,目前只有一種模型,也是本質上唯一有效的玩法。
這就是io多路復用了。多路復用就是處理等待訊息準備好這件事的,但它可以同時處理多個連線!它也可能「等待」,所以它也會導致執行緒睡眠,然而這不要緊,因為它一對多、它可以監控所有連線。這樣,當我們的執行緒被喚醒執行時,就一定是有一些連線準備好被我們的**執行了,這是有效率的!沒有那麼多個執行緒都在爭搶處理「等待訊息準備好」階段,整個世界終於清淨了!
多路復用有很多種實現,在linux上,2.4核心前主要是select和poll,現在主流是epoll,它們的使用方法似乎很不同,但本質是一樣的。
效率卻也不同,這也是epoll完全替代了select的原因。
簡單的談下epoll為何會替代select。
前面提到過,高併發的核心解決方案是1個執行緒處理所有連線的「等待訊息準備好」,這一點上epoll和select是無爭議的。但select預估錯誤了一件事,就像我們開篇所說,當數十萬併發連線存在時,可能每一毫秒只有數百個活躍的連線,同時其餘數十萬連線在這一毫秒是非活躍的。select的使用方法是這樣的:
返回的活躍連線 ==select(全部待監控的連線)
什麼時候會呼叫select方法呢?在你認為需要找出有報文到達的活躍連線時,就應該呼叫。所以,呼叫select在高併發時是會被頻繁呼叫的。這樣,這個頻繁呼叫的方法就很有必要看看它是否有效率,因為,它的輕微效率損失都會被「頻繁」二字所放大。它有效率損失嗎?顯而易見,全部待監控連線是數以十萬計的,返回的只是數百個活躍連線,這本身就是無效率的表現。被放大後就會發現,處理併發上萬個連線時,select就完全力不從心了。
看幾個圖。當併發連線為一千以下,select的執行次數不算頻繁,與epoll似乎並無多少差距:
然而,併發數一旦上去,select的缺點被「執行頻繁」無限放大了,且併發數越多越明顯:
再來說說epoll是如何解決的。它很聰明的用了3個方法來實現select方法要做的事:
新建的epoll描述符==epoll_create()
epoll_ctrl(epoll描述符,新增或者刪除所有待監控的連線)
返回的活躍連線 ==epoll_wait( epoll描述符)
這麼做的好處主要是:分清了頻繁呼叫和不頻繁呼叫的操作。例如,epoll_ctrl是不太頻繁呼叫的,而epoll_wait是非常頻繁呼叫的。這時,epoll_wait卻幾乎沒有入參,這比select的效率高出一大截,而且,它也不會隨著併發連線的增加使得入參越發多起來,導致核心執行效率下降。
epoll是怎麼實現的呢?其實很簡單,從這3個方法就可以看出,它比select聰明的避免了每次頻繁呼叫「哪些連線已經處在訊息準備好階段」的 epoll_wait時,是不需要把所有待監控連線傳入的。這意味著,它在核心態維護了乙個資料結構儲存著所有待監控的連線。這個資料結構就是一棵紅黑樹,它的結點的增加、減少是通過epoll_ctrl來完成的。用我在《深入理解nginx》第8章中所畫的圖來看,它是非常簡單的:
圖中左下方的紅黑樹由所有待監控的連線構成。左上方的鍊錶,同是目前所有活躍的連線。於是,epoll_wait執行時只是檢查左上方的鍊錶,並返回左上方鍊錶中的連線給使用者。這樣,epoll_wait的執行效率能不高嗎?
最後,再看看epoll提供的2種玩法et和lt,即翻譯過來的邊緣觸發和水平觸發。其實這兩個中文名字倒也有些貼切。這2種使用方式針對的仍然是效率問題,只不過變成了epoll_wait返回的連線如何能夠更準確些。
例如,我們需要監控乙個連線的寫緩衝區是否空閒,滿足「可寫」時我們就可以從使用者態將響應呼叫write傳送給客戶端 。但是,或者連線可寫時,我們的「響應」內容還在磁碟上呢,此時若是磁碟讀取還未完成呢?肯定不能使執行緒阻塞的,那麼就不傳送響應了。但是,下一次epoll_wait時可能又把這個連線返回給你了,你還得檢查下是否要處理。可能,我們的程式有另乙個模組專門處理磁碟io,它會在磁碟io完成時再傳送響應。那麼,每次epoll_wait都返回這個「可寫」的、卻無法立刻處理的連線,是否符合使用者預期呢?
於是,et和lt模式就應運而生了。lt是每次滿足期待狀態的連線,都得在epoll_wait中返回,所以它一視同仁,都在一條水平線上。et則不然,它傾向更精確的返回連線。在上面的例子中,連線第一次變為可寫後,若是程式未向連線上寫入任何資料,那麼下一次epoll_wait是不會返回這個連線的。et叫做 邊緣觸發,就是指,只有連線從乙個狀態轉到另乙個狀態時,才會觸發epoll_wait返回它。可見,et的程式設計要複雜不少,至少應用程式要小心的防止epoll_wait的返回的連線出現:可寫時未寫資料後卻期待下一次「可寫」、可讀時未讀盡資料卻期待下一次「可讀」。
當然,從一般應用場景上它們效能是不會有什麼大的差距的,et可能的優點是,epoll_wait的呼叫次數會減少一些,某些場景下連線在不必要喚醒時不會被喚醒(此喚醒指epoll_wait返回)。但如果像我上面舉例所說的,有時它不單純是乙個網路問題,跟應用場景相關。當然,大部分開源框架都是基於et寫的,框架嘛,它追求的是純技術問題,當然力求盡善盡美。
最後拉下票哈:
高效能網路程式設計5 IO復用與併發程式設計
epoll 網路程式設計 高效能邊緣觸發et 對於伺服器的併發處理能力,我們需要的是 每一毫秒伺服器都能及時處理這一毫秒內收到的數百個不同tcp連線上的報文,與此同時,可能伺服器上還有數以十萬計的最近幾秒沒有收發任何報文的相對不活躍連線。同時處理多個並行發生事件的連線,簡稱為併發 同時處理萬計 十萬...
高效能網路程式設計5 IO復用與併發程式設計
對於伺服器的併發處理能力,我們需要的是 每一毫秒伺服器都能及時處理這一毫秒內收到的數百個不同tcp連線上的報文,與此同時,可能伺服器上還有數以十萬計的最近幾秒沒有收發任何報文的相對不活躍連線。同時處理多個並行發生事件的連線,簡稱為併發 同時處理萬計 十萬計的連線,則是高併發。伺服器的併發程式設計所追...
高效能網路程式設計5 IO復用與併發程式設計
對於伺服器的併發處理能力,我們需要的是 每一毫秒伺服器都能及時處理這一毫秒內收到的數百個不同tcp連線上的報文,與此同時,可能伺服器上還有數以十萬計的最近幾秒沒有收發任何報文的相對不活躍連線。同時處理多個並行發生事件的連線,簡稱為併發 同時處理萬計 十萬計的連線,則是高併發。伺服器的併發程式設計所追...