Web APP編程模型和IO策略
現代大型高性能網站諸如淘寶,京東,微博,FB,知乎等等,網站架構涉及很多知識。像業務分層,軟件分割模塊化,分布式部署,集群服務器,負載均衡等技術可以幫助架構師將一個大的復雜的問題切分成小的簡單的問題。這篇文章著眼于解決這些切好的小問題上,單機上有哪些編程實踐或者模型可以很好的做到高并發。本人web開發小白一枚,寫文章是想梳理自己的思路,求得大牛斧正,希望各位多多批判。文章的內容大多來自網上的閱讀加上些自己的理解,文末附上參考閱讀的文章。
一個極簡高并發模型
因為有數年的嵌入式領域的經驗,先說一下我認為的比較高效的處理模型。
- 硬件環境:單機30core, 1G Hz。
- 軟件環境:6Wind fastpath,每個core上都是run-to-complete的endless loop.沒有操作系統。
- 功能:一個超級簡單的reverse proxy,具有load balance的簡單功能。
衡量并發性能,我們看一下一個IP包從網口緩沖區收上來處理到發出去大約需要多長時間呢?
收+處理+發大概是500+1000+500=2000 cycles,時間也就是2us。單機1s內可以支持30*(1s/2us)=15,000,000 request/s的并發。屌炸天的并發能力了吧!原因有兩個:
- 沒有操作系統overhead。
- 包處理簡單,IP層的處理,直接c函數調用,總共1000 cycle。
當然這是從嵌入式得來的經驗,web開發中不可能這樣,沒有Nginx,沒有web框架,沒有lib沒有各種open source,甚至沒有Linux?;氐皆忌鐣斐鲲w機大炮來,這不把web開發者逼瘋了。軟件也是一個社會化協作的過程,os,framework,lib,opensource給開發者帶來極大方便的同時,也伴隨著性能的開銷。如何在性能和可擴展性、維護性等其他指標找到一個平衡點,如何選擇合適的編程模型,合適的第三方模塊達到最小的overhead,這是成長為高手的開發者都會不斷思考的問題。
High Performance architecture,這篇文章總結了四個性能殺手:
- 數據復制
- 上下文切換
- 動態內存分配
- 鎖競爭
上面的編程模型之所以高效,就是將CPU用到極致,盡量避免這4種情況發生。心中有這么一個極簡的高效模型,后面學習其他模式的時候可以暗做對比看一下到底會有哪些額外的開銷。
常用的server端Linux高并發編程模型
Nginx Vs Apache
大名鼎鼎的Nginx使用了多進程模型,主進程啟動時初始化,bind,監聽一組sockets,然后fork一堆child processes(workers),workers共享socket descriptor。workers競爭accept_mutex,獲勝的worker通過IO multiplex(select/poll/epoll/kqueue/…)來處理成千上萬的并發請求。為了獲得高性能,Nginx還大量使用了異步,事件驅動,non-blocking IO等技術?!盬hat resulted is a modular, event-driven, asynchronous, single-threaded, non-blocking architecture which became the foundation of nginx code.”

Nginx 架構
對比著看一下Apache的兩種常用運行模式,詳見 Apache Modules
- 1. Apache MPM prefork模式
主進程通過進程池維護一定數量(可配置)的worker進程,每個worker進程負責一個connection。worker進程之間通過競爭mpm-accept mutex實現并發和鏈接處理隔離。 由于進程內存開銷和切換開銷,該模式相對來說是比較低效的并發。

- 2. Apache MPM worker模式
由于進程開銷較大,MPM worker模式做了改進,處理每個connection的實體改為thread。主進程啟動可配數量的子進程,每個進程啟動可配數量的server threads和listen thread。listen threads通過競爭mpm-accept mutex獲取到新進的connection request通過queue傳遞給自己進程所在的server threads處理。由于調度的實體變成了開銷較小的thread,worker模式相對prefork具有更好的并發性能。
小結兩種webserver,可以發現Nginx使用了更高效的編程模型,worker進程一般跟CPU的core數量相當,每個worker駐留在一個core上,合理編程可以做到最小程度的進程切換,而且內存的使用也比較經濟,基本上沒有浪費在進程狀態的存儲上。而Apache的模式是每個connection對應一個進程/線程,進程/線程間的切換開銷,大量進程/線程的內存開銷,cache miss的概率增大,都限制了系統所能支持的并發數。
IO策略
由于IO的處理速度要遠遠低于CPU的速度,運行在CPU上的程序不得不考慮IO在準備暑假的過程中該干點什么,讓出CPU給別人還是自己去干點別的有意義的事情,這就涉及到了采用什么樣的IO策略。一般IO策略的選用跟進程線程編程模型要同時考慮,兩者是有聯系的。
同步阻塞IO

同步阻塞IO是比較常見的IO模型,網絡編程中如果創建的socket的描述符屬性設置為阻塞的,當socket對應的用戶空間緩沖區內尚無可讀數據時,該進程/線程在系統調用read/recv socket時,會將自己掛起阻塞等待socket ready。
同步非阻塞IO和非阻塞IO同步復用
對比著同步阻塞IO,如果socket數據沒有ready,系統調用read/recv會直接返回,進程可以繼續執行不會掛起讓出CPU。當然這樣做對單個socket來說沒有多大的意義,如果要支持大量socket的并發就很有用了,也就是IO復用。select/poll/epoll就是這樣的應用,IO的read是非阻塞式調用,select是阻塞式的,同步發生在select上。程序通過select調用同時監控一組sockets,任何一個socket發生注冊過的事件時,select由阻塞變為ready,函數調用返回后程序可以讀取IO了。前面提到的Nginx(使用epoll)和apache(使用select)都有使用這一IO策略。select/epoll這種IO策略還有另外一個名字叫Reactor,具體他們之間的細節區別再另開一文。
異步非阻塞IO

對比同步非阻塞IO,異步非阻塞IO也有個名字—Proactor。這種策略是真正的異步,使用注冊callback/hook函數來實現異步。程序注冊自己感興趣的socket 事件時,同時將處理各種事件的handler也就是對應的函數也注冊給內核,不會有任何阻塞式調用。事件發生后內核之間調用對應的handler完成處理。這里暫且理解為內核做了event的調度和handler調用,具體到底是異步IO庫如何做的,如何跟內核通信的,后續繼續研究。
原創作者招募ing
歡迎各位朋友加入馬幫傳道者寫作團,秀出你的好身材,秀出你的好文章,讓大家發現你的美,美在文章中,美在不斷的自我挑戰中!
馬哥Linux運維微信公眾號致力于發送好文章,也會不斷發現身邊的好文章,發現優秀的你,惠及更多的運維朋友們!
歡迎一起交流成長,一起愉快的玩耍!
原創投稿聯系magedu-小助手QQ:152260971