I/0模型

在高性能服務器中,一般採用非阻塞網絡IO,單進程事件驅動的架構。這種架構的核心是事件驅動機制。目前Linux常用select,poll和epoll系統調用來完成事件驅動。select和poll是傳統的unix事件驅動機制,但它們有很大的缺點:在大量的併發連接中,如果冷連接較多,select和poll的性能會因爲併發數的線性上升而成平方速度的下降,這是因爲調用者在每次select和poll返回時都要檢測每個連接是否有事件發生,當連接數很大時,系統開銷會非常大。另外select和poll每次返回時都要從內核向用戶空間複製大量的數據,這樣的開銷也很大。所以,select和poll並不是處理網絡IO的最好方案。

於是從Linux2.5開始,出現了一個新的系統調用epoll,它不僅可以完成select/epoll的功能,而且性能更高,功能更強。epoll的優點:1,每次只返回有事件發生的文件描述符信息,這樣調用者不用遍歷整個文件描述符隊列,而且系統不用從內核向用戶空間複製大量無用的數據;2,epoll可以設置不同的事件觸發方式:邊緣觸發和電平觸發,爲用戶使用epoll提供了靈活性。epoll也有缺點:1,在冷連接很少的情況下,性能與select和poll相比並沒有什麼優勢;2,目前的實現還不完整,不支持EPOLLHUP事件,在邊緣觸發情況下,無法處理客戶端網絡連接主動斷開;3,使用比較麻煩,如何邊緣觸發方式必須要小心處理,否則程序可能無法完成功能,在採用電平觸發方式時,調用者要避免epoll_wait發生空轉。

一, 磁盤IO

Linux磁盤IO有同步模式和異步模式。同步模式就是常見read/write系統調用,這兩個系統調用都被認爲是低速系統調用,執行時可能會使進程阻塞,在單進程的服務器中,這會使系統性能下降。所以Linux提供了異步磁盤IO。目前Linux中主要有兩種異步IO解決方案:1,由glibc 實現的Posix AIO(aio_read,aio_write,….),採用線程或實時信號來通知IO完成,用戶也可以採用輪詢方式來檢測IO是否完成;2,由kernel實現的Linux AIO(io_setup, io_getevents,…),事件的通知機制(io_getevents)是採用類似select的語義。它們的共同點都是採用了多線程的方式來處理阻塞的磁盤IO,使其看起來像異步的。這兩種方式主要區別是:1,Posix AIO是在用戶態下實現的而Linux AIO是在內核中實現的;2,事件的通知機制不同,前面已說明。Posix AIO和Linux AIO性能的比較還沒有做過。Linux的異步磁盤IO機制的缺點是:無論Posix AIO還是Linux AIO在處理新的IO請求時都會做創建線程的操作,這樣比較的費時。

Linux磁盤IO還可以用內存映射的方式,優點是:1,可以避免在用戶空間和內核空間之間複製數據;2,如果把小文件合成大文件,對大文件進行內存映射,可以減少open系統調用的次數。但內存映射的缺點也很明顯:1,由於3G以上的大文件無法映射進內存,使用有侷限性;2,如果所訪問的頁面不在內存中,需要進行換頁操作,進程會阻塞。雖然合理地使用mincore可以避免進程阻塞,但編程實現難度較高。

如果是從磁盤向網絡傳送數據的話還可以使用sendfile,sendfile可以在內核空間中直接從磁盤向網絡傳送數據,避免了內存複製

二, 同時處理網絡IO和磁盤IO

數據服務器要同時處理大量的併發網絡連接,而且處理這些網絡連接需要進行大量的磁盤IO,如讀取文件。在這樣情況下,我們提出了兩種模型:

1,分離網絡IO和磁盤IO。這種模型是指用一個進程(RelayServer)來處理大量併發的網絡連接,另一個進程(DataServer)處理磁盤IO,兩個進程通過一個TCP連接交換信息,當客戶(Client)請求到達,RelayServer將其轉發給DataServer,DataServer去磁盤上讀數據,讀好數據後,將數據回傳給RelayServer,RelayServer再轉發給Client。

2,不分離網絡IO和磁盤IO。這種模型是指只用一個進程(DataServer)來同時處理磁盤IO和網絡IO。

下面討論這兩種模型的實現方案:

無論是採用哪種模型,都涉及到對大量磁盤IO的處理。根據前面對Linux磁盤IO的討論,我們試驗了以下幾種磁盤IO的實現方式:

1,Posix AIO,實時信號通知。由於磁盤IO通常會在較短的時間內完成,進程頻繁地被信號中斷,系統調用頻繁自動重啓動,造成整個性能的下降。另外,因爲無法向信號處理函數傳參數,信號處理函數很難實現較爲複雜的功能。

2,Posix AIO,線程通知。每完成一個IO請求,系統就會創建線程,開銷較大。

3,Posix AIO,輪詢。很難跟DataServer的epoll邏輯結合,只有不斷輪詢,性能較低。
Linux AIO未作測試,性能未知。內存映射無法處理很大的文件,也未採用。

從上面看來,Linux提供的異步IO機制無法滿足我們的需求,於是我們自己在用戶層實現了異步IO。我們的異步IO主要框架是採用預先創建線程,形成線程池。處理流程:用戶發IO請求,線程池中的工作線程被喚醒,工作線程採用一般的read/write系統調用處理IO(爲了保證磁盤IO能夠完成,我們用的是readn和writen),工作線程做完IO後通過一個管道向epoll通知,並把用戶IO的請求掛到完成隊列上,epoll收到通知後,從完成隊列中取下,再做相應的操作。根據我們的性能比較,這種方式比前面幾種實現性能都要高,而且實現起來比較容易。所以我們對磁盤IO的處理主要採用這種方式,但針對不分離模型和分離模型的不同細節,又有所變化。

針對不分離模型,我們系統使用epoll管理socket描述符和一個管道描述符(磁盤IO用管道來通知epoll)。epoll只處理socket的讀事件,負責完整收到Client一個請求,然後把請求交給磁盤IO。磁盤IO使用線程池,跟前面的區別在於,工作線程在讀數據時使用sendfile,直接將磁盤數據發給對應的網絡套接字,如果sendfile返回EAGAIN錯誤或者沒發完數據,就更新請求,然後把請求重新放回隊列。

分離的模型中DataServer也是用epoll管理socket描述符和與磁盤IO通信的管道描述符。epoll需要處理socket的讀事件和寫事件,負責從RelayServer收請求和向RelayServer發送數據。磁盤IO與前面的模型一致。

不分離模型和分離模型的對比結論:

1,不分離模型和分離模型在面對200左右併發連接數時性能上基本一致。測試表明:當請求讀文件的大小較大時(超過256KB),無論OS對文件的緩存命中率大小,兩種模型所能達到的網絡吞吐量都差不多(20-30MB/S)。並且網絡吞吐量隨着請求文件大小的增大而增大,在這種情況下,磁盤IO嚴重滯後於網絡IO和CPU的速度,所以網絡吞吐量由磁盤IO的速度所決定。因爲當讀文件較大時,根據磁盤IO特點,磁盤吞吐量會較高,所以請求文件越大,網絡吞吐量越好。當請求文件的大小較小時(如16KB和64KB),如果OS文件緩存的命中率較高,磁盤IO在整個系統中幾乎沒有影響,網絡的吞吐量基本可以達到網卡的物理上限。但是,當OS文件緩存命中率很小時,由於磁盤IO很難高效地處理對小塊數據的讀,整個系統性能受制於磁盤IO,並且網絡吞吐量和磁盤吞吐量都很低(4MB/S)。總得來看,無論使用哪種模型,磁盤IO都是瓶頸。

2,不分離模型的優缺點。優點:1,不分離模型的實現較爲容易,可以使用sendfile等高效的系統調用。2,由於對每個併發連接不需要分配大量的內存緩衝區來緩存從磁盤讀到的數據,對併發連接數的支持可以不受內存大小的限制,具有較好的伸縮性。缺點:可擴展性不足,比如對於客戶端要求寫文件就很難處理,而且如果加上覆雜的協議分析,性能可能會大幅度下降。

3,分離模型的優缺點。優點:擴展性好,可以專門對磁盤IO進行優化,而不必考慮其對同時處理大量併發網絡連接的影響;針對網絡IO也可以添加複雜協議的分析,不必考慮對同時處理磁盤IO的影響。缺點:實現較爲複雜,實現方案直接影響系統性能;用戶接收數據的速率不平滑,有時會較長時間沒有數據到達,而有時會瞬間到達很多數據,這樣對於流媒體的應用,用戶體驗會很差。

三, 結論

針對我們的需求,決定使用分離模型。因爲分離模型的擴展性很好,並且通過優化預計可以達到較高的效率。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章