併發IO問題一直是服務器端編程中的技術難題,從最先的同步阻塞直接Fork進程,到Worker進程池/線程池,到如今的異步IO、協程。PHP程序員由於有強大的LAMP框架,對這類底層方面的知識知之甚少,本文目的就是詳細介紹PHP進行併發IO編程的各類嘗試,最後再介紹Swoole的使用,深刻淺出全面解析併發IO問題。php
最先的服務器端程序都是經過多進程、多線程來解決併發IO的問題。進程模型出現的最先,從Unix系統誕生就開始有了進程的概念。最先的服務器端程序通常都是Accept一個客戶端鏈接就建立一個進程,而後子進程進入循環同步阻塞地與客戶端鏈接進行交互,收發處理數據。react
多線程模式出現要晚一些,線程與進程相比更輕量,並且線程之間是共享內存堆棧的,因此不一樣的線程之間交互很是容易實現。好比聊天室這樣的程序,客戶端鏈接之間能夠交互,比聊天室中的玩家能夠任意的其餘人發消息。用多線程模式實現很是簡單,線程中能夠直接向某一個客戶端鏈接發送數據。而多進程模式就要用到管道、消息隊列、共享內存,統稱進程間通訊(IPC)複雜的技術才能實現。c++
代碼實例:git
多進程/線程模型的流程是程序員
這種模式最大的問題是,進程/線程建立和銷燬的開銷很大。因此上面的模式沒辦法應用於很是繁忙的服務器程序。對應的改進版解決了此問題,這就是經典的Leader-Follower模型。github
代碼實例:web
它的特色是程序啓動後就會建立N個進程。每一個子進程進入Accept,等待新的鏈接進入。當客戶端鏈接到服務器時,其中一個子進程會被喚醒,開始處理客戶端請求,而且再也不接受新的TCP鏈接。當此鏈接關閉時,子進程會釋放,從新進入Accept,參與處理新的鏈接。算法
這個模型的優點是徹底能夠複用進程,沒有額外消耗,性能很是好。不少常見的服務器程序都是基於此模型的,好比Apache、PHP-FPM。編程
多進程模型也有一些缺點。數組
另外有一些場景多進程模型沒法解決,好比即時聊天程序(IM),一臺服務器要同時維持上萬甚至幾十萬上百萬的鏈接(經典的C10K問題),多進程模型就力不從心了。
還有一種場景也是多進程模型的軟肋。一般Web服務器啓動100個進程,若是一個請求消耗100ms,100個進程能夠提供1000qps,這樣的處理能力仍是不錯的。可是若是請求內要調用外網Http接口,像QQ、微博登陸,耗時會很長,一個請求須要10s。那一個進程1秒只能處理0.1個請求,100個進程只能達到10qps,這樣的處理能力就太差了。
有沒有一種技術能夠在一個進程內處理全部併發IO呢?答案是有,這就是IO複用技術。
其實IO複用的歷史和多進程同樣長,Linux很早就提供了select系統調用,能夠在一個進程內維持1024個鏈接。後來又加入了poll系統調用,poll作了一些改進,解決了1024限制的問題,能夠維持任意數量的鏈接。但select/poll還有一個問題就是,它須要循環檢測鏈接是否有事件。這樣問題就來了,若是服務器有100萬個鏈接,在某一時間只有一個鏈接向服務器發送了數據,select/poll須要作循環100萬次,其中只有1次是命中的,剩下的99萬9999次都是無效的,白白浪費了CPU資源。
直到Linux 2.6內核提供了新的epoll系統調用,能夠維持無限數量的鏈接,並且無需輪詢,這才真正解決了C10K問題。如今各類高併發異步IO的服務器程序都是基於epoll實現的,好比Nginx、Node.js、Erlang、Golang。像Node.js這樣單進程單線程的程序,均可以維持超過1百萬TCP鏈接,所有歸功於epoll技術。
IO複用異步非阻塞程序使用經典的Reactor模型,Reactor顧名思義就是反應堆的意思,它自己不處理任何數據收發。只是能夠監視一個socket句柄的事件變化。
Reactor有4個核心的操做:
Reactor只是一個事件發生器,實際對socket句柄的操做,如connect/accept、send/recv、close是在callback中完成的。具體編碼可參考下面的僞代碼:
Reactor模型還能夠與多進程、多線程結合起來用,既實現異步非阻塞IO,又利用到多核。目前流行的異步服務器程序都是這樣的方式:如
協程從底層技術角度看實際上仍是異步IO Reactor模型,應用層自行實現了任務調度,藉助Reactor切換各個當前執行的用戶態線程,但用戶代碼中徹底感知不到Reactor的存在。
PHP的優勢:
另外PHP有超過20年的歷史,生態圈是很是大的,在Github能夠找到不少代碼。
PHP的缺點:
因此PHP
基於上面的擴展使用純PHP就能夠徹底實現異步網絡服務器和客戶端程序。可是想實現一個相似於多IO線程,仍是有不少繁瑣的編程工做要作,包括如何來管理鏈接,如何來保證數據的收發原則性,網絡協議的處理。另外PHP代碼在協議處理部分性能是比較差的,因此我啓動了一個新的開源項目Swoole,使用C語言和PHP結合來完成了這項工做。靈活多變的業務模塊使用PHP開發效率高,基礎的底層和協議處理部分用C語言實現,保證了高性能。它以擴展的方式加載到了PHP中,提供了一個完整的網絡通訊的框架,而後PHP的代碼去寫一些業務。它的模型是基於多線程Reactor+多進程Worker,既支持全異步,也支持半異步半同步。
實例代碼在https://github.com/swoole/swoole-src 主頁查看。
異步TCP服務器:
在這裏new swoole_server對象,而後參數傳入監聽的HOST和PORT,而後設置了3個回調函數,分別是onConnect有新的鏈接進入、onReceive收到了某一個客戶端的數據、onClose某個客戶端關閉了鏈接。最後調用start啓動服務器程序。swoole底層會根據當前機器有多少CPU核數,啓動對應數量的Reactor線程和Worker進程。
異步客戶端:
客戶端的使用方法和服務器相似只是回調事件有4個,onConnect成功鏈接到服務器,這時能夠去發送數據到服務器。onError鏈接服務器失敗。onReceive服務器向客戶端鏈接發送了數據。onClose鏈接關閉。
設置完事件回調後,發起connect到服務器,參數是服務器的IP,PORT和超時時間。
同步客戶端:
同步客戶端不須要設置任何事件回調,它沒有Reactor監聽,是阻塞串行的。等待IO完成纔會進入下一步。
異步任務:
異步任務功能用於在一個純異步的Server程序中去執行一個耗時的或者阻塞的函數。底層實現使用進程池,任務完成後會觸發onFinish,程序中能夠獲得任務處理的結果。好比一個IM須要廣播,若是直接在異步代碼中廣播可能會影響其餘事件的處理。另外文件讀寫也可使用異步任務實現,由於文件句柄沒辦法像socket同樣使用Reactor監聽。由於文件句柄老是可讀的,直接讀取文件可能會使服務器程序阻塞,使用異步任務是很是好的選擇。
異步毫秒定時器
這2個接口實現了相似JS的setInterval、setTimeout函數功能,能夠設置在n毫秒間隔實現一個函數或 n毫秒後執行一個函數。
異步MySQL客戶端
swoole還提供一個內置鏈接池的MySQL異步客戶端,能夠設定最大使用MySQL鏈接數。併發SQL請求能夠複用這些鏈接,而不是重複建立,這樣能夠保護MySQL避免鏈接資源被耗盡。
異步Redis客戶端
異步的Web程序
程序的邏輯是從Redis中讀取一個數據,而後顯示HTML頁面。使用ab壓測性能以下:
一樣的邏輯在php-fpm下的性能測試結果以下:
WebSocket程序
swoole內置了websocket服務器,能夠基於此實現Web頁面主動推送的功能,好比WebIM。有一個開源項目能夠做爲參考。https://github.com/matyhtf/php-webim
異步編程通常使用回調方式,若是遇到很是複雜的邏輯,可能會層層嵌套回調函數。協程就能夠解決此問題,能夠順序編寫代碼,但運行時是異步非阻塞的。騰訊的工程師基於Swoole擴展和PHP5.5的Yield/Generator語法實現相似於Golang的協程,項目名稱爲TSF(Tencent Server Framework),開源項目地址:https://github.com/tencent-php/tsf。目前在騰訊公司的企業QQ、QQ公衆號項目以及車輪忽略的查違章項目有大規模應用 。
TSF使用也很是簡單,下面調用了3個IO操做,徹底是串行的寫法。但其實是異步非阻塞執行的。TSF底層調度器接管了程序的執行,在對應的IO完成後纔會向下繼續執行。
PHP和Swoole均可以在ARM平臺上編譯運行,因此在樹莓派系統上也可使用PHP+Swoole來開發網絡通訊的程序。