PHP併發IO程式設計之路

發表於2016-03-22

併發IO問題一直是後端程式設計中的技術挑戰,從最早的同步阻塞Fork程式,到多程式/多執行緒,到現在的非同步IO、協程。PHP程式設計師因為有強大的LAMP框架,對底層方面的知識知之甚少,本文目的就是詳細介紹PHP進行併發IO程式設計的各種嘗試,最後再介紹Swoole的使用,深入淺出全面理解併發IO問題。

多程式/多執行緒同步阻塞

最早的伺服器端程式都是通過多程式、多執行緒來解決併發IO的問題。程式模型出現的最早,從Unix系統誕生就開始有了程式的概念。最早的伺服器端程式一般都是Accept一個客戶端連線就建立一個程式,然後子程式進入迴圈同步阻塞地與客戶端連線進行互動,收發處理資料。

4

多執行緒模式出現要晚一些,執行緒與程式相比更輕量,而且執行緒之間是共享記憶體堆疊的,所以不同的執行緒之間互動非常容易實現。比如聊天室這樣的程式,客戶端連線之間可以互動,比聊天室中的玩家可以任意的其他人發訊息。用多執行緒模式實現非常簡單,執行緒中可以直接讀寫某一個客戶端連線。而多程式模式就要用到管道、訊息佇列、共享記憶體實現資料互動,統稱程式間通訊(IPC)複雜的技術才能實現。

程式碼例項:

1

多程式/執行緒模型的流程是:

  1. 建立一個 socket,繫結伺服器埠(bind),監聽埠(listen),在PHP中用stream_socket_server一個函式就能完成上面3個步驟,當然也可以使用php 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模型。

程式碼例項:
2

它的特點是程式啟動後就會建立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控制程式碼的事件變化。

5

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中完成的。具體編碼可參考下面的虛擬碼:

6

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語言的優劣勢

3

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物件,然後引數傳入監聽的HOSTPORT,然後設定了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個介面實現了類似JSsetIntervalsetTimeout函式功能,可以設定在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.5Yield/Generator語法實現類似於Golang的協程,專案名稱為TSFTencent Server Framework),開源專案地址:https://github.com/tencent-php/tsf。目前在騰訊公司的企業QQQQ公眾號專案以及車輪忽略的查違章專案有大規模應用 。

TSF使用也非常簡單,下面呼叫了3IO操作,完全是序列的寫法。但實際上是非同步非阻塞執行的。TSF底層排程器接管了程式的執行,在對應的IO完成後才會向下繼續執行。

在樹莓派上使用PHP+Swoole

PHPSwoole都可以在ARM平臺上編譯執行,所以在樹莓派系統上也可以使用PHP+Swoole來開發網路通訊的程式。

相關文章