服務器編程之路:進無止境(上)

首先不好意思,盜用了福特汽車的廣告語,呵呵。


今天想在這裏探討一下高性能服務器(server)編程的一些通用技術(或者說是思想)。編程技術發展至今,高性能服務器編程領域仍然是C語言的菜。而C語言在服務器編程中的技術,也不斷在實踐中提高,正暗含我們的題目。


有基礎的初學者寫的第一個基於TCP的服務器程序,想必大概是這樣的:


while (1)

{

         listen();                      // TCP套接字監聽

         fd = accept();          // 接受遠端連接

         create_thread(fd);        // 把這個連接扔到一個新開的線程裏進行業務處理

}


現在我們知道,這樣的服務器性能是有問題的。每接受一個連接,就要開一個線程進行處理,假設這個服務器有1000個併發,就需要同時有1000條線程。而OS調度進程或線程是有代價的,調度1000個線程,OS是很累的,平白無故耗了很多CPU在線程切換上,非常不划算。


雖然性能上不理想,但上面這第一版服務器程序結構上還是合理的。一個線程服務一個連接,線程相當於一個沙盒,裏面怎麼處理,都不會影響其他連接。所以對於性能要求不高的場景,用這種邏輯的程序簡單方便。實際中,apachehttp服務就是按照這種邏輯實現的。


但碼農們永遠不會滿足於這點性能的,這也不符合高性能服務器的要求。現在的服務器往往要求上萬的併發度(意味着同時有上萬條連接),這就不能靠無限擴張線程數來實現了。所以問題轉化爲:如何使用有限的線程數,服務無限的連接數?


主流的,並早已被大家接受的方案是這樣的:

1.        使用IO複用機制(如select/epoll),把所有已連接的套接字監聽起來。

2.        當某個套接字發生網絡事件(比如有數據到來),回調給使用者。

3.        回調所在的線程,是從一個有限大小的線程池中取出來的。

這即所謂的“事件模型”。所有處理不是順序的,而是由事件觸發,在回調中處理。


開源代碼中,比較有名的庫libevent,乾的就是這個事情。在我們公司內,主要用在設備的netframework,軟件線libdsl裏面的DEngine組件,互聯網團隊使用的litepi庫中的IOEngine,都是這個思路。


看起來很美是不是?至少性能上解決了“使用有限的線程數,服務無限的連接數”這個命題。但實際上,這個命題隱含了兩個預設條件,如果使用者不能很好地理解這種模式的運行原理,就免不了要踩雷。


預設條件1:根據抽屜原理,一旦連接數大於線程數,必然存在一條線程服務多個連接的情況。因此,連接不再有一個乾淨的運行沙盒。如果在處理一個連接時造成阻塞,就會阻塞這個線程,從而把該線程上的其他連接也阻塞了。


預設條件2:事件模型下,事件有可能被不同的線程回調。而正常情況下,一個連接的多個事件必然共享這個連接的數據,因此這些數據就會存在多線程競爭問題。使用者必須意識到同一個連接的處理,實際是在多線程中的,這往往與直覺相悖。


關於上面兩個預設條件,可以多說兩句:


關於條件1,直觀的打個比方:好比打地鼠遊戲,冒出一個地鼠你就得打。但萬一你把一隻地鼠打壞了(卡在洞口縮不進去了),那這整臺遊戲機就壞了。。。


關於條件2,如果能夠將同一個連接的所有事件都固定在一個線程中回調,就能避免多線程競爭的問題了。這種模式稱爲“非對稱”處理,相對的,前一種稱爲“對稱”處理。非對稱處理的缺陷在於無法均衡使用各條線程。但是對於大併發(併發上萬)的場景,數量一大,就能達到統計平均,這種缺陷也就不存在了。所以這也是一個取捨的問題,netframeworkDEngine是對稱處理,IOEngine就是非對稱處理,它大大簡化了使用者的編程。


好了,我們給出第二版的服務器程序,採用了“事件模型”,它是不是完美了呢?性能上,也許是;但在使用上,絕對不是。最根本的一個問題,它違反了人類做事的直覺。因爲一般人,就算是水平很高的碼農,幹事情也喜歡一件事情順序幹到底(愚蠢的人類啊)。正常人蓋房子肯定是:我要搬磚,我要砌牆,我要粉刷,完工!但是事件模型卻要求:來來來,你去搬磚;來來來,你去砌牆;來來來,你去粉刷;好了,房子蓋好了。人無法控制程序,反而被程序驅使,算不算悲哀呢?


至此,我們拋出第二個命題:有沒有滿足命題一(使用有限線程服務無限連接)的同時,又能讓我們順序地、同步地寫程序?


答案當然是:有的!


(未完待續)


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章