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