併發IO問題一直是服務器端編程中的技術難題,從最先的同步阻塞直接Fork進程,到Worker進程池/線程池,到如今的異步IO、協程。PHP程序員由於有強大的LAMP框架,對這類底層方面的知識知之甚少,本文目的就是詳細介紹PHP進行併發IO編程的各類嘗試,最後再介紹Swoole的使用,深刻淺出全面解析併發IO問題。react
多進程/多線程同步阻塞
最先的服務器端程序都是經過多進程、多線程來解決併發IO的問題。進程模型出現的最先,從Unix系統誕生就開始有了進程的概念。最先的服務器端程序通常都是Accept一個客戶端鏈接就建立一個進程,而後子進程進入循環同步阻塞地與客戶端鏈接進行交互,收發處理數據。c++
多線程模式出現要晚一些,線程與進程相比更輕量,並且線程之間是共享內存堆棧的,因此不一樣的線程之間交互很是容易實現。好比聊天室這樣的程序,客戶端鏈接之間能夠交互,比聊天室中的玩家能夠任意的其餘人發消息。用多線程模式實現很是簡單,線程中能夠直接向某一個客戶端鏈接發送數據。而多進程模式就要用到管道、消息隊列、共享內存,統稱進程間通訊(IPC)複雜的技術才能實現。git
代碼實例:程序員
多進程/線程模型的流程是github
建立一個 socket,綁定服務器端口(bind),監聽端口(listen),在PHP中用stream_socket_server一個函數就能完成上面3個步驟,固然也可使用更底層的sockets擴展分別實現。web
進入while循環,阻塞在accept操做上,等待客戶端鏈接進入。此時程序會進入隨眠狀態,直到有新的客戶端發起connect到服務器,操做系統會喚醒此進程。accept函數返回客戶端鏈接的socket主進程在多進程模型下通fork(php: pcntl_fork)建立子進程,多線程模型下使用pthread_create(php: new Thread)建立子線程。下文如無特殊聲明將使用進程同時表示進程/線程。算法
子進程建立成功後進入while循環,阻塞在recv(php: fread)調用上,等待客戶端向服務器發送數據。收到數據後服務器程序進行處理而後使用send(php: fwrite)向客戶端發送響應。長鏈接的服務會持續與客戶端交互,而短鏈接服務通常收到響應就會close。編程
當客戶端鏈接關閉時,子進程退出並銷燬全部資源。主進程會回收掉此子進程。數組
這種模式最大的問題是,進程/線程建立和銷燬的開銷很大。因此上面的模式沒辦法應用於很是繁忙的服務器程序。對應的改進版解決了此問題,這就是經典的Leader-Follower模型。
代碼實例:
它的特色是程序啓動後就會建立N個進程。每一個子進程進入Accept,等待新的鏈接進入。當客戶端鏈接到服務器時,其中一個子進程會被喚醒,開始處理客戶端請求,而且再也不接受新的TCP鏈接。當此鏈接關閉時,子進程會釋放,從新進入Accept,參與處理新的鏈接。
這個模型的優點是徹底能夠複用進程,沒有額外消耗,性能很是好。不少常見的服務器程序都是基於此模型的,好比Apache、PHP-FPM。
多進程模型也有一些缺點。
這種模型嚴重依賴進程的數量解決併發問題,一個客戶端鏈接就須要佔用一個進程,工做進程的數量有多少,併發處理能力就有多少。操做系統能夠建立的進程數量是有限的。
啓動大量進程會帶來額外的進程調度消耗。數百個進程時可能進程上下文切換調度消耗佔CPU不到1%能夠忽略不接,若是啓動數千甚至數萬個進程,消耗就會直線上升。調度消耗可能佔到CPU的百分之幾十甚至100%。
另外有一些場景多進程模型沒法解決,好比即時聊天程序(IM),一臺服務器要同時維持上萬甚至幾十萬上百萬的鏈接(經典的C10K問題),多進程模型就力不從心了。
還有一種場景也是多進程模型的軟肋。一般Web服務器啓動100個進程,若是一個請求消耗100ms,100個進程能夠提供1000qps,這樣的處理能力仍是不錯的。可是若是請求內要調用外網Http接口,像QQ、微博登陸,耗時會很長,一個請求須要10s。那一個進程1秒只能處理0.1個請求,100個進程只能達到10qps,這樣的處理能力就太差了。
有沒有一種技術能夠在一個進程內處理全部併發IO呢?答案是有,這就是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個核心的操做:
add添加socket監聽到reactor,能夠是listen socket也可使客戶端socket,也能夠是管道、eventfd、信號等
set修改事件監聽,能夠設置監聽的類型,如可讀、可寫。可讀很好理解,對於listen socket就是有新客戶端鏈接到來了須要accept。對於客戶端鏈接就是收到數據,須要recv。可寫事件比較難理解一些。一個SOCKET是有緩存區的,若是要向客戶端鏈接發送2M的數據,一次性是發不出去的,操做系統默認TCP緩存區只有256K。一次性只能發256K,緩存區滿了以後send就會返回EAGAIN錯誤。這時候就要監聽可寫事件,在純異步的編程中,必須去監聽可寫才能保證send操做是徹底非阻塞的。
del從reactor中移除,再也不監聽事件
callback就是事件發生後對應的處理邏輯,通常在add/set時制定。C語言用函數指針實現,JS能夠用匿名函數,PHP能夠用匿名函數、對象方法數組、字符串函數名。
Reactor只是一個事件發生器,實際對socket句柄的操做,如connect/accept、send/recv、close是在callback中完成的。具體編碼可參考下面的僞代碼:
Reactor模型還能夠與多進程、多線程結合起來用,既實現異步非阻塞IO,又利用到多核。目前流行的異步服務器程序都是這樣的方式:如
Nginx:多進程Reactor
Nginx+Lua:多進程Reactor+協程
Golang:單線程Reactor+多線程協程
Swoole:多線程Reactor+多進程Worker
協程是什麼
協程從底層技術角度看實際上仍是異步IO Reactor模型,應用層自行實現了任務調度,藉助Reactor切換各個當前執行的用戶態線程,但用戶代碼中徹底感知不到Reactor的存在。
PHP併發IO編程實踐
PHP相關擴展
Stream:PHP內核提供的socket封裝
Sockets:對底層Socket API的封裝
Libevent:對libevent庫的封裝
Event:基於Libevent更高級的封裝,提供了面向對象接口、定時器、信號處理的支持
Pcntl/Posix:多進程、信號、進程管理的支持
Pthread:多線程、線程管理、鎖的支持
PHP還有共享內存、信號量、消息隊列的相關擴展
PECL:PHP的擴展庫,包括系統底層、數據分析、算法、驅動、科學計算、圖形等都有。若是PHP標準庫中沒有找到,能夠在PECL尋找想要的功能。
PHP語言的優劣勢
PHP的優勢:
第一個是簡單,PHP比其餘任何的語言都要簡單,入門的話PHP真的是能夠一週就入門。C++有一本書叫作《21天深刻學習C++》,其實21天根本不可能學會,甚至能夠說C++沒有3-5年不可能深刻掌握。可是PHP絕對能夠7天入門。因此PHP程序員的數量很是多,招聘比其餘語言更容易。
PHP的功能很是強大,由於PHP官方的標準庫和擴展庫裏提供了作服務器編程能用到的99%的東西。PHP的PECL擴展庫裏你想要的任何的功能。
另外PHP有超過20年的歷史,生態圈是很是大的,在Github能夠找到不少代碼。
PHP的缺點:
性能比較差,由於畢竟是動態腳本,不適合作密集運算,若是一樣用PHP寫再用c++寫,PHP版本要比它差一百倍。
函數命名規範差,這一點你們都是瞭解的,PHP更講究實用性,沒有一些規範。一些函數的命名是很混亂的,因此每次你必須去翻PHP的手冊。
提供的數據結構和函數的接口粒度比較粗。PHP只有一個Array數據結構,底層基於HashTable。PHP的Array集合了Map,Set,Vector,Queue,Stack,Heap等數據結構的功能。另外PHP有一個SPL提供了其餘數據結構的類封裝。
因此PHP
PHP更適合偏實際應用層面的程序,業務開發、快速實現的利器
PHP不適合開發底層軟件
使用C/C++、JAVA、Golang等靜態編譯語言做爲PHP的補充,動靜結合
藉助IDE工具實現自動補全、語法提示
PHP的Swoole擴展
基於上面的擴展使用純PHP就能夠徹底實現異步網絡服務器和客戶端程序。可是想實現一個相似於多IO線程,仍是有不少繁瑣的編程工做要作,包括如何來管理鏈接,如何來保證數據的收發原則性,網絡協議的處理。另外PHP代碼在協議處理部分性能是比較差的,因此我啓動了一個新的開源項目Swoole,使用C語言和PHP結合來完成了這項工做。靈活多變的業務模塊使用PHP開發效率高,基礎的底層和協議處理部分用C語言實現,保證了高性能。它以擴展的方式加載到了PHP中,提供了一個完整的網絡通訊的框架,而後PHP的代碼去寫一些業務。它的模型是基於多線程Reactor+多進程Worker,既支持全異步,也支持半異步半同步。
Swoole的一些特色:
Accept線程,解決Accept性能瓶頸和驚羣問題
多IO線程,能夠更好地利用多核
提供了全異步和半同步半異步2種模式
處理高併發IO的部分用異步模式
複雜的業務邏輯部分用同步模式
底層支持了遍歷全部鏈接、互發數據、自動合併拆分數據包、數據發送原子性。
Swoole的進程/線程模型:
Swoole程序的執行流程:
使用PHP+Swoole擴展實現異步通訊編程
實例代碼在https://github.com/swoole/swoole-src 主頁查看。
TCP服務器與客戶端
異步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
PHP+Swoole協程
異步編程通常使用回調方式,若是遇到很是複雜的邏輯,可能會層層嵌套回調函數。協程就能夠解決此問題,能夠順序編寫代碼,但運行時是異步非阻塞的。騰訊的工程師基於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
PHP和Swoole均可以在ARM平臺上編譯運行,因此在樹莓派系統上也可使用PHP+Swoole來開發網絡通訊的程序。
轉載:韓天峯我的博客