程序員圈子的快速發展使得 Web 應用開發人員大多數狀況下面對的是一個 Web Framework 如 Python 的 Django、tornado, PHP 的 Laravel 等,可是在這些 framework 以前的 Server 如 Nginx,Apache 的原理卻顯有了解。本文就網絡 Server 模型的原理與演進展開描述,這裏的「網絡 Server 模型」指的是具備高阻塞、低佔用特色的一類應用,不只僅 HTTP 服務,其餘的如 ftp 服務,SQL 數據庫連接服務等也都在此列。網絡 Server 的發展前後經歷了 Process(進程模型),Thread(線程模型),Prefork(進程池),ThreadPool,Event Driven(事件模型)等,本文一一介紹程序員
網絡編程剛剛興起的時候尚未人考慮併發這個問題,當作傳統的應用來編寫的 Server 是阻塞而且沒有使用任何模型的。只是簡單的監聽某個端口 accept,接收數據 recv,而後返回 send。邏輯很是簡單,易於實現。可是缺點顯而易見,阻塞佔據了大多數 CPU 時間,併發數只有 1,也就是說某個端口上的應用在服務於某個用戶的時候其餘用戶都要等待。redis
上面「沒有模型」的設計顯然是低效率的,結合操做系統多進程的概念提出了一個主進程監聽端口,對於每一個鏈接都使用一個獨立的 worker 子進程去處理,鏈接的讀寫數據操做所有阻塞在這個子進程中,這樣對 server 的併發能力有了很大的提高。可是進程在操做系統中是個相對比較重的概念,進程的建立、銷燬、切換都是很是大的開銷,同時隨着線程的興起,在 server 端使用多個子線程的 worker 處理不一樣鏈接的方式進一步提高了單機併發性能。數據庫
面對進程的建立、銷燬、切換成本開銷很是大的問題,除了使用線程替代進程處理不一樣鏈接外,有人想出了一個很秒的方法,那就是預先建立數個進程在內存中,不一樣鏈接到來時將請求分發到內存中不一樣的進程裏去處理,也就是預先開闢進程池,這樣避免了頻繁重複建立銷燬進程的問題,從而大大提高了 server 性能。與進程池相對應的即是線程池的概念,線程池的使用也大大提升的線程模型的效率。編程
固然進程模型與線程模型有各自的優缺點,並不存在一方佔據絕對優點的狀況。好比在穩定性方面若是一個進程掛了對另外的進程沒有影響,而線程模型中一個線程掛了那麼全部程序都掛了;可是線程間共享內存空間因此線程間數據共享要比進程之間更容易。在這個基礎上咱們再考慮 memcached 使用的單進程多線程模型就更好理解了,memcached 進程啓動後,全部鏈接過來寫的數據所有存儲在一個進程空間中,不一樣的線程能夠無障礙訪問,即知足高併發的性能要求又不至於去編寫不一樣進程之間 IPC 的複雜邏輯。同時 redis 在平時工做時也是單進程多線程模型,但在涉及到諸如持久化的耗時操做時使用多進程的方式來組織。網絡
進程、線程的優缺點對比:多線程
多進程 | 多線程 | 總結 | |
---|---|---|---|
CPU,內存消耗 | 內存佔用多,CPU 利用率低 | 內存佔用少,CPU 利用率高 | 線程佔優 |
建立、銷燬、切換 | 比較複雜 | 比較簡單 | 線程佔優 |
數據同步與共享 | 數據間相互隔離,同步簡單,共享複雜須要 IPC | 數據存在於同一個內存空間,共享簡單,可是同步涉及到加鎖等問題 | 各有優點 |
調試複雜度 | 簡單 | 複雜 | 進程佔優 |
可靠性 | 進程間不相互影響 | 一個線程會致使該進程下全部線程掛掉 | 進程佔優 |
擴展性 | 多核心、多設備擴展 | 是適用於多核心擴展 | 進程佔優 |
上面提到的進程、線程或者進程池、線程池模型都是阻塞模式的。那麼在阻塞的時候鏈接仍然以進程或者線程的形式佔據耗費着系統資源。而在事件驅動模型中只有在阻塞事件就緒時纔會分配相應的系統資源,天然大大提升了系統併發處理性能。併發
得益於操做系統的快速發展,從操做系統層面提供了 select,poll,epoll(Linux),kqueue(BSD)等基於事件的 IO 多路複用機制。一旦某個文件描述符轉變爲可讀或者可寫的狀態,就通知相應的程序進行操做。但他們本質上都是同步 IO,由於在收到讀或者寫事件後程序須要本身負責進行讀或者寫操做,也就是說這個讀寫過程仍是阻塞的。而異步 IO 則無需程序本身負責進行讀寫操做而是操做系統內核直接把數據存儲到用戶空間提供使用。咱們先來看看同步 IO 的 select, poll, 和 epoll。異步
上圖是 select() 調用過程,select 有 3 大缺點:async
poll 方式相對於 select 方式只是文件描述符的結構由 fd_set 變成了 pollfd,並無從本質上解決 select 的問題。memcached
epoll 是對 select、poll 方式的改進,那麼 epoll 是怎樣解決上面 3 個問題的呢。首先從暴露出的 API 上來看,select 和 poll 只有一個同名函數,而 epoll 提供了 epoll_create,epoll_ctl和epoll_wait 三個函數,分別爲了建立一個 epoll 句柄,註冊要監聽的事件類型,等待事件的產生。對於缺點1,每次註冊新事件到 epoll 句柄中時(在 epoll_ctl 中指定 EPOLL_CTL_ADD)會把 fd 拷貝到內核中,而不是在 epoll_wait 時重複拷貝,這樣保證了每一個 fd 在整個過程當中只會被拷貝一次。對於缺點2,epoll 不像 select 和 poll 每次都把 fd 掛進阻塞隊列,而是隻在 epoll_ctl 時掛一次並同時給相應 fd 註冊一個回調函數,當相應設備被喚醒執行這個回調函數的時候實際上就是把這個 fd 放入就緒隊列,而後 epoll_wait 的時候就查看就緒隊列中有沒有內容並返回便可。對於缺點3,epoll 沒有這個限制,epoll 支持的 fd 數量是系統最大 fd 數量,一般 cat /proc/sys/fs/file-max 查看,跟設備內存有很大關係。
上面事件驅動的 select,poll,epoll 機制即便很大的提高了性能,可是在數據的讀寫操做上仍是同步的。而異步 IO 的出現進一步提高了 Server 的處理能力,應用程序發起一個異步讀寫操做,並提供相關參數(如用於存放數據的緩衝區、讀寫數據的大小、以及請求完成後的回調函數等),操做系統在自身的內核線程中執行實際的讀或者寫操做,並將結果存入程序制定的緩衝區中,而後把事件和緩衝區回調給應用程序。目前有不少語言已經封裝了各自的異步 IO 庫,如 Python 的 asyncio。
在沒有新的模型提出來以前,咱們能作的就是結合實際的應用場景和上面模型的優缺點組合出最高效的解決方案,好比 epoll + ThreadPool 就是 muduo 這個高效 C++ 網絡庫採起的方案。
在當前 C10K 問題的主流背景下,epoll 和異步 IO 這種事件驅動模型正逐漸變爲人們的首選方案,這也是 Nginx 能不斷從 Apache 中搶佔市場的一個重要緣由。然而從 Linux 社區來看對填平 aio(異步 IO)這個大坑並無太大興趣,那麼爲了異步 IO 的統一隻能從應用層進行兼容,免不了屢次內核態與用戶態的交互,這對程序性能天然會有損失。固然隨着時間的發展單機併發性能的解決辦法愈來愈高效,可是對應的程序開發複雜度也愈來愈高,咱們要作的就是在這二者之間作出最優權衡。