1. 程式人生 > >轉:PHP並發IO編程之路

轉:PHP並發IO編程之路

可能 recv 一個數 線程創建 以及 worker 主動推送 golang 時間

並發IO問題一直是服務器端編程中的技術難題,從最早的同步阻塞直接Fork進程,到Worker進程池/線程池,到現在的異步IO、協程。PHP程序員因為有強大的LAMP框架,對這類底層方面的知識知之甚少,本文目的就是詳細介紹PHP進行並發IO編程的各種嘗試,最後再介紹Swoole的使用,深入淺出全面解析並發IO問題。

多進程/多線程同步阻塞

最早的服務器端程序都是通過多進程、多線程來解決並發IO的問題。進程模型出現的最早,從Unix系統誕生就開始有了進程的概念。最早的服務器端程序一般都是Accept一個客戶端連接就創建一個進程,然後子進程進入循環同步阻塞地與客戶端連接進行交互,收發處理數據。

技術分享

多線程模式出現要晚一些,線程與進程相比更輕量,而且線程之間是共享內存堆棧的,所以不同的線程之間交互非常容易實現。比如聊天室這樣的程序,客戶端連接之間可以交互,比聊天室中的玩家可以任意的其他人發消息。用多線程模式實現非常簡單,線程中可以直接向某一個客戶端連接發送數據。而多進程模式就要用到管道、消息隊列、共享內存,統稱進程間通信(IPC)復雜的技術才能實現。

代碼實例:

技術分享

多進程/線程模型的流程是

  1. 創建一個 socket,綁定服務器端口(bind),監聽端口(listen),在PHP中用stream_socket_server一個函數就能完成上面3個步驟,當然也可以使用更底層的sockets擴展分別實現。
  2. 進入while循環,阻塞在accept操作上,等待客戶端連接進入。此時程序會進入隨眠狀態,直到有新的客戶端發起connect到服務器,操作系統會喚醒此進程。accept函數返回客戶端連接的socket
  3. 主進程在多進程模型下通過fork(php: pcntl_fork)創建子進程,多線程模型下使用pthread_create(php: new Thread)創建子線程。下文如無特殊聲明將使用進程同時表示進程/線程。
  4. 子進程創建成功後進入while循環,阻塞在recv(php: fread)調用上,等待客戶端向服務器發送數據。收到數據後服務器程序進行處理然後使用send(php: fwrite)向客戶端發送響應。長連接的服務會持續與客戶端交互,而短連接服務一般收到響應就會close。
  5. 當客戶端連接關閉時,子進程退出並銷毀所有資源。主進程會回收掉此子進程。

這種模式最大的問題是,進程/線程創建和銷毀的開銷很大。所以上面的模式沒辦法應用於非常繁忙的服務器程序。對應的改進版解決了此問題,這就是經典的Leader-Follower模型。

代碼實例:

技術分享

它的特點是程序啟動後就會創建N個進程。每個子進程進入Accept,等待新的連接進入。當客戶端連接到服務器時,其中一個子進程會被喚醒,開始處理客戶端請求,並且不再接受新的TCP連接。當此連接關閉時,子進程會釋放,重新進入Accept,參與處理新的連接。

這個模型的優勢是完全可以復用進程,沒有額外消耗,性能非常好。很多常見的服務器程序都是基於此模型的,比如Apache、PHP-FPM。

多進程模型也有一些缺點。

  1. 這種模型嚴重依賴進程的數量解決並發問題,一個客戶端連接就需要占用一個進程,工作進程的數量有多少,並發處理能力就有多少。操作系統可以創建的進程數量是有限的。
  2. 啟動大量進程會帶來額外的進程調度消耗。數百個進程時可能進程上下文切換調度消耗占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個核心的操作:

  1. add添加socket監聽到reactor,可以是listen socket也可以使客戶端socket,也可以是管道、eventfd、信號等
  2. set修改事件監聽,可以設置監聽的類型,如可讀、可寫。可讀很好理解,對於listen socket就是有新客戶端連接到來了需要accept。對於客戶端連接就是收到數據,需要recv。可寫事件比較難理解一些。一個SOCKET是有緩存區的,如果要向客戶端連接發送2M的數據,一次性是發不出去的,操作系統默認TCP緩存區只有256K。一次性只能發256K,緩存區滿了之後send就會返回EAGAIN錯誤。這時候就要監聽可寫事件,在純異步的編程中,必須去監聽可寫才能保證send操作是完全非阻塞的。
  3. del從reactor中移除,不再監聽事件
  4. 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的優點:

  1. 第一個是簡單,PHP比其他任何的語言都要簡單,入門的話PHP真的是可以一周就入門。C++有一本書叫做《21天深入學習C++》,其實21天根本不可能學會,甚至可以說C++沒有3-5年不可能深入掌握。但是PHP絕對可以7天入門。所以PHP程序員的數量非常多,招聘比其他語言更容易。
  2. PHP的功能非常強大,因為PHP官方的標準庫和擴展庫裏提供了做服務器編程能用到的99%的東西。PHP的PECL擴展庫裏你想要的任何的功能。

另外PHP有超過20年的歷史,生態圈是非常大的,在Github可以找到很多代碼。

PHP的缺點:

  1. 性能比較差,因為畢竟是動態腳本,不適合做密集運算,如果同樣用PHP寫再用c++寫,PHP版本要比它差一百倍。
  2. 函數命名規範差,這一點大家都是了解的,PHP更講究實用性,沒有一些規範。一些函數的命名是很混亂的,所以每次你必須去翻PHP的手冊。
  3. 提供的數據結構和函數的接口粒度比較粗。PHP只有一個Array數據結構,底層基於HashTable。PHP的Array集合了Map,Set,Vector,Queue,Stack,Heap等數據結構的功能。另外PHP有一個SPL提供了其他數據結構的類封裝。

所以PHP

  1. PHP更適合偏實際應用層面的程序,業務開發、快速實現的利器
  2. PHP不適合開發底層軟件
  3. 使用C/C++、JAVA、Golang等靜態編譯語言作為PHP的補充,動靜結合
  4. 借助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來開發網絡通信的程序。

技術分享技術分享

轉:PHP並發IO編程之路