讀《構建高性能Web站點》服務器併發處理能力 - 2

系統調用

進程有兩種運行模式:用戶態和內核態。進程通常在用戶態,這時可以使用CPU和內存,而當進程需要對硬件外設進行操作的時候(如讀取磁盤文件、發送網絡數據),就必須切換到內核態,當在內核態的任務完成後,進程又切回到用戶態。

由於系統調用涉及進程從用戶態到內核態的切換,導致一定的內存空間交換,這也是一定程度上的上下文切換,所以系統調用的開銷通常是比較昂貴的。

減少不必要的系統調用,也是Web服務器性能優化的一個方面。

我們使用 strace 來跟蹤 Nginx 的一個子進程,獲得某次請求處理的一系列系統調用,如下所示:

1_20150424083613.jpg

內存分配

Apache在運行時的內存使用量是非常驚人的,這主要歸咎於它的多進程模型,該模型使得Apache在運行開始便一次性申請大片的內存作爲內存池。而Nginx的內存分配策略,它使用多線程來處理請求,這使得多線程之間可以共享內存資源,從而令它的內存總體使用大大減少,Nginx維持10000個非活躍HTTP持久連接只需要2.5MB內存。

  • 持久連接

持久連接(Keep-Alive)也稱爲長連接。HTTP/1.1對長連接有了完整的定義,HTTP請求數據投中包含關於長連接的生命:

Connection : Keep-Alive


長連接的有效使用可以減少大量重新建立連接的開銷,有效的加速性能。對於Apache這樣的多進程模型來說,如果長連接超時時間過長,那麼即便是瀏覽器沒有任何請求,而Apache仍然維持着連接的子進程,一旦併發用戶數較多,那麼Apache將維持着大量空閒進程,嚴重影響了服務器性能。

I/O模型

有人說,比特天生就是用來別複製的,數據的生命意義便在於輸入輸出。

事實上,如何讓高速的CPU和慢速的I/O設備更好的協調工作,這是從現代計算機誕生到現在一直探索的話題。

PIO與DMA

很早以前,磁盤和內存之間的數據傳輸是需要CPU控制的,也就是說如何讀取磁盤文件到內存,數據要經過CPU存儲轉發,這種方式稱爲PIO。

後來,DMA(直接內存訪問,Direct Memory Access)取代了PIO,它可以不經過CPU而直接進行磁盤和內存的數據交換。在DMA模式下,CPU只需要向DMA下達指令,由DMA來處理數據的傳送即可,DMA通過系統總線來傳輸數據,傳送完畢通知CPU,這樣降低了CPU佔有率。

同步阻塞I/O1_20150204174710.jpg

阻塞是指當前發起I/O操作的進程被阻塞,而不是CPU被阻塞。

舉個例子,比如你去逛街,餓了,你看到小吃城,就在一家麪館買了一碗麪,交了錢,可麪條做起來需要時間,你知不知道什麼時候可以做好,只好坐在那裏等,等麪條做好吃完再繼續逛街。—— 這裏吃麪條便是I/O操作。

同步非阻塞I/O1_20150204180739.jpg

在同步阻塞I/O中,進程實際上等待的時間包括兩部分,一個是等待數據的就緒,另一個是等待數據的複製(copy data from kenrel to user)。

同步非阻塞I/O的調用不會等待數據的就緒,如果數據不可讀或者不可寫,它會立刻告訴進程。

回到買面的故事,假如你不甘心等麪條做好就想去逛街,可又擔心麪條做好了沒有及時領取,所以你逛一會便跑回去看看麪條是否做好,往返了很多次,最後雖然即使吃上了麪條,但是卻累得氣喘吁吁。

多路I/O就緒通知1_20150205125946.jpg

多路I/O就緒通知的出現,提供了對大量文件描述符就緒檢查的高性能方案,它允許進程通過一種方法來同時監視所有文件描述符,並可以快速獲得所喲就緒的文件描述符,然後只針對這些文件描述符進行數據訪問。

回到買面的故事,加入你不止買了一份面,還在其他小吃店買了餃子、粥、餡餅等,這些東西都需要時間來製作。在同步非阻塞I/O模型中,你要輪流不停地去各個小吃店詢問進度。現在引入多路I/O就緒通知後,小吃城在大廳裏安裝了一塊電子屏幕,以後所有小吃店的食物做好後,都會顯示在屏幕上,這樣你只需要間隔性地看看大屏幕就可以了也許你還可以同時逛逛附近的商店。

需要注意的是,I/O就緒通知只是幫助我們快速獲取就緒的文件描述符,當得知就緒後,就訪問數據本身而言,讓然需要選擇阻塞或非阻塞的方式,一般我麼你選擇非阻塞方式。

多路I/O就緒有很多不同的實現:

select

select 最早於1983年出現在4.3BSD中,它通過一個select()系統調用來監視包含多個文件描述符的數組,當select()放回後,該數組中就緒的文件描述符便會被內核修改標誌位,使得進程可以獲得這些文件描述符從而進行後續的讀寫操作。

select目前在所有平臺上都支持,但select的一個缺點在於單個進程能夠監視文件描述符數量存在最大限制,在Linux上一般爲1024,不過可以通過修改宏定義甚至重新編譯內核的方式提升這一限制。

另外,select()所維護大量文件描述符的數據結構,隨着文件描述符數量的增大,其複製的開銷也線性增長。同時,由於網絡響應時間的延遲使得大量TCP連接處於非活躍狀態,但是調用select()會對所有socket進行一次線性掃描,所以這也會浪費一定的開銷。

poll

poll在1986年誕生於System V Release3(UNIX),它和select本質上沒有多大差異,除了沒有監視文件數量的限制,select 缺點同樣適用於 poll。

另外,select()和poll()將就緒的文件描述符告訴進程後,如果進程沒有對其進行I/O操作,那麼下次調用的時候將再次報告這些文件描述符,所以一般不會丟失就緒通知的消息,這種方式稱爲水平觸發(Level Triggered)。

SIGIO

Linxu2.4提供SIGIO,它通過實時信號(Real Time Signal)來實現select/poll的通知方式,但是它們的不同在於,select/poll告訴我們哪些文件描述符是就緒的,一直到我們讀寫之前,每次select/poll都會告訴我們;而SIGIO則是告訴我們哪些文件描述符剛剛變爲就緒狀態,它只說一遍,如果我們沒有採取行動,那麼它就不會再告訴我們,這種方式稱爲邊緣觸發(Edge Triggered)。

/dev/poll

Sun在Solaris中提供了新的實現,它使用虛擬的/dev/poll設備,你可以將要監視的文件描述符數組寫入這個設備,然後通過ioctl()來等待時間通知,當ioctl()放回就緒的文件描述符後,你可以從/dev/poll中讀取所有就緒的文件描述符數組,這點類似於SIGIO。

在Linux下有很多方法可以實現類似/dev/poll的設備,但是都沒有 提供直接的內核支持,這些方法在服務器負載較大時性能不穩定。

/dev/epoll

隨後,名爲/dev/epoll的設備以不定的形式出現在Linux2.4上,它提供了類似/dev/poll的功能,而且增加了內存映射(mmap)技術,在一定程度上提高了性能。

但是,/dev/epoll仍然只是一個補丁,Linux2.4並沒有將它的實現加入內核。

epoll

直到Linux2.6纔出現了有內核直接支持的實現方法,那就是epoll,它被公認爲Linxu2.6下性能最好的多路I/O就緒通知方法。

epoll可以同時支持水平觸發和邊緣出發,在默認情況下,epoll採用水平觸發,如果要使用邊緣出發,需要在事件註冊時增加EPOLLET選項。

在Nginx的epoll模型代買(src/event/modules/ngx_epoll_module.c)中,可以看到它採用了邊緣觸發:

ee.events = EPOLLIN | EPOLLOUT | EPOLLET;

另外一個本質的改進在於epoll採用基於事件的就緒通知方法。在select/poll中,進程只有在調用一定的方法後,內核纔對所有監視的文件描述符進行掃描,而epoll事先通知epoll_ctl()來註冊每一個文件描述符,一旦某個文件描述符就緒時,內核會採用類似callback的回調機制,迅速激活這個文件描述符,當進程調用epoll_wait()時便得到通知。

回到買面的故事,雖然有了電子屏幕,但是顯示的內容是所有食品的狀態,包括正在製作和已經做好的,這顯然給你造成閱讀上的麻煩,就好像select/poll每次返回所有監視的文件描述符一樣,如果能夠只顯示做好的食品,隨後小吃城進行了改進,就像/dev/poll一樣只告知就緒的文件描述符。在顯示做好的食品時,如果只顯示一次,而不管你有沒有看到,這就相當於邊緣出發,而如果在你領取之前,每次都顯示,就相當於水平觸發。

但儘管如此,一旦你走遠了,還得回到小吃城去看電子屏幕,能不能讓你更加輕鬆地獲得通知呢?小吃城採取了手機短信通知的方式,你只需要到小吃城管理處註冊後, 便可以在餐點就緒時及時收到短信通知,這類似於epoll的事件機制。

內存映射

Linux內核提供一種訪問磁盤文件的特殊方式,它可以將內存中某塊地址空間和指定的磁盤文件相關聯,從而把這塊內存的訪問轉換爲對磁盤文件的訪問,這種技術稱爲內存映射(Memory Mapping)。

使用內存映射可以提高磁盤I/O性能,它無需使用read()或write()等系統調用來訪問文件,而是通過mmap()系統調用來建立內存和磁盤文件的關聯,然後想訪問內存一樣訪問磁盤。

直接I/O

在Linux2.6中,內存映射和直接訪問磁盤文件沒有本質上差異,因爲數據從進程用戶態內存空間到磁盤都要經過兩次複製,即"磁盤-->內核緩衝區"和"內核緩衝區-->用戶態內存空間"。

引入內核緩衝區的目的在於提高磁盤文件的訪問性能,因爲當進程需要寫磁盤文件時,實際上只是到了內核緩衝區便告訴進程已經成功。然而,對於一些複雜應用,如數據庫服務器,它們爲了充分提高性能,希望繞過內核緩衝區,由自己在用戶態空間實現並管理I/O緩衝區。

Linxu提供了對這種需求的支持,即在open()系統調用中增加了參數選項O_DIRECT,用它打開的文件便可以繞過內核緩衝區的直接訪問,這樣便避免了CPU和內存的多餘開銷。

在MySQL中,對於Innodb存儲引擎,其自身可以進行數據和索引的緩存管理,所以對於內核緩衝區的依賴不是那麼重要。

sendfile

在向Web服務器請求靜態文件的過程中,磁盤文件的數據要先經過內核緩衝區,然後到用戶態內存空間,因爲是不需要處理的靜態數據,所以它們又被送到網卡對應的內核緩衝區,接着在被送入網卡進行發送。

數據從內核出去,又回到內核,沒有任何變化。在Linux2.4的內核中,引入了一個稱爲khttpd的內核級Web服務器程序,它只處理靜態文件的請求。引入的目的便在於內核希望請求的處理儘量在內核完成,減少內核態的切換以及用戶態數據複製的開銷。

Linux通過系統調用將這種機制提供給開發者,那就是sendfile()系統調用。它可以將磁盤文件的特定部分直接傳送到代表客戶端的socket描述符。

異步I/O1_20150210191402.jpg

阻塞和非阻塞是指當進程訪問的數據如果尚未就緒,進程是否需要等待。

同步和異步是指訪問數據的機制,同步指請求並等待I/O操作完畢方式,當數據就緒後在讀寫的時候必須阻塞;異步是指請求數據後便可以繼續處理其他任務,隨後等待I/O操作完畢的通知,這使進程在數據讀寫時不發生阻塞。

參考文章:高性能 IO 模型淺析


—————————— 本文同步發佈於 ZHANGSR 我的個人博客  ——————————

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