同步與異步
同步與異步的重點(diǎn)是在消息通知的方式上俊鱼,也就是調(diào)用結(jié)果通知的方式上蝶棋。同步方式是當(dāng)一個同步調(diào)用發(fā)出后,調(diào)用者要一直等待調(diào)用結(jié)果的通知后叫榕,才能進(jìn)行后續(xù)的執(zhí)行。異步方式是當(dāng)一個異步調(diào)用發(fā)出后姊舵,調(diào)用者不能立即得到調(diào)用結(jié)果的返回晰绎。異步調(diào)用要想獲得結(jié)果一般有兩種方式:主動輪詢異步調(diào)用的結(jié)果、被調(diào)用方通過回調(diào)callback
來通知調(diào)用方調(diào)用結(jié)果括丁。
阻塞與非阻塞
阻塞與非阻塞的重點(diǎn)在于進(jìn)程或線程等待消息時的行為荞下,也就是在等待消息的時候,當(dāng)前進(jìn)程或線程是掛起狀態(tài)還是非掛起狀態(tài)。阻塞方式的阻塞調(diào)用在發(fā)出后尖昏,在消息返回之前仰税,當(dāng)前線程或進(jìn)程是會被掛起的,直到有消息返回抽诉,當(dāng)前進(jìn)程或線程才會被激活陨簇。非阻塞方式的非阻塞消息在發(fā)出后,不會阻塞當(dāng)前進(jìn)程或線程迹淌,而會立即返回河绽。
簡單來說,同步與異步的重點(diǎn)在于消息通知的方式唉窃,阻塞與非阻塞的重點(diǎn)在于等待消息時候的行為耙饰。因此也就有了4種組合:同步阻塞、同步非阻塞纹份、異步阻塞苟跪、異步非阻塞。
阻塞IO與非阻塞IO
一個IO操作實(shí)際上是分成兩個步驟:
- 發(fā)起IO請求
阻塞IO和非阻塞IO的區(qū)別在于第一步矮嫉,發(fā)起IO請求是否會被阻塞削咆,如果阻塞直到完成,那就是傳統(tǒng)的阻塞IO蠢笋,否則就是非阻塞IO拨齐。 - 實(shí)際的IO操作
同步IO與異步IO的區(qū)別在于第二步是否阻塞,如果實(shí)際的IO讀寫阻塞請求進(jìn)程昨寞,那么就是同步IO瞻惋,因此阻塞IO、非阻塞IO援岩、IO復(fù)用歼狼、信號驅(qū)動IO都是同步IO。如果不阻塞享怀,而是操作系統(tǒng)幫助做完IO操作再將結(jié)果返回羽峰,那它就是異步IO。
多進(jìn)程和多線程同步阻塞
最早的服務(wù)器程序都是通過多進(jìn)程或多線程來解決并發(fā)IO的問題添瓷,進(jìn)程模型出現(xiàn)的最早梅屉,從UNIX系統(tǒng)誕生之初就有了進(jìn)程的概念。最早的服務(wù)器端程序一般都是Accept
一個客戶端連接就創(chuàng)建一個進(jìn)程鳞贷,然后子進(jìn)程進(jìn)入循環(huán)同步阻塞地與客戶端連接交互坯汤,收發(fā)處理數(shù)據(jù)。
多線程模式出現(xiàn)要晚一些搀愧,線程與進(jìn)程相比更輕量惰聂,而且線程之間是共享內(nèi)存堆棧的疆偿,所以不同的線程之間交互非常容易實(shí)現(xiàn)。比如搓幌,聊天室程序中客戶端連接之間可以交互杆故,玩家可以任意的向其他人發(fā)消息。用多線程模型實(shí)現(xiàn)非常簡單鼻种,線程中可以直接向某個客戶端連接發(fā)送數(shù)據(jù)反番。如果使用多進(jìn)程模式就需要使用到管道、消息隊(duì)列叉钥、共享內(nèi)存等進(jìn)程間通信IPC的復(fù)雜技術(shù)才能實(shí)現(xiàn)罢缸。
多進(jìn)程和多線程模型的操作流程
<?php
$address = "tcp://0.0.0.0:8000";
$svr = stream_socket_server($address, $errno, $errstr) or die("create server failed");
while(true)
{
$connect = stream_socket_accept($svr);
if(pcntl_fork() == 0)
{
$request = fread($connect);
fwrite($response);
fclose($connect);
exit(0);
}
}
- 創(chuàng)建一個
socket
并綁定服務(wù)器端口,然后監(jiān)聽端口投队。 - 進(jìn)入
while
循環(huán)枫疆,阻塞在accept
操作上,等待客戶端連接進(jìn)入敷鸦。此時程序會進(jìn)入休眠狀態(tài)息楔,直到有新客戶端發(fā)起connect
連接到服務(wù)器,操作系統(tǒng)才會喚醒此進(jìn)程扒披。 - 主進(jìn)程在多進(jìn)程模型下通過
fork
創(chuàng)建子進(jìn)程值依,多線程模型下可以使用pthread_create
創(chuàng)建子線程。 - 子進(jìn)程創(chuàng)建成功后進(jìn)入
while
循環(huán)碟案,阻塞在recv
調(diào)用上愿险,等待客戶端向服務(wù)器發(fā)送數(shù)據(jù)。當(dāng)服務(wù)器收到數(shù)據(jù)后服務(wù)器程序進(jìn)行處理价说,然后send
向客戶端發(fā)送響應(yīng)辆亏。長連接的服務(wù)會持續(xù)與客戶端交互,而短連接服務(wù)一般在收到響應(yīng)后就會close
鳖目。 - 當(dāng)客戶端連接關(guān)閉時扮叨,子進(jìn)程退出并銷毀所有資源,主進(jìn)程會回收掉此子進(jìn)程领迈。
多進(jìn)程和多進(jìn)程模型的最大問題在于彻磁,進(jìn)程和線程的創(chuàng)建和銷毀開銷很大,因此美喲u辦法應(yīng)用在非常繁忙的服務(wù)器程序上狸捅,對應(yīng)的改進(jìn)版也就是經(jīng)典的Leader-Follower
模型衷蜓。
Leader-Follower
模型
Leader-Follower
模型的特點(diǎn)是程序啟動后會創(chuàng)建n個進(jìn)程,每個子進(jìn)程進(jìn)入Accept
薪贫,等待新的連接的進(jìn)入。當(dāng)客戶端連接到服務(wù)器時刻恭,其中一個子進(jìn)程會被喚醒瞧省,開始處理客戶端請求扯夭,并且不再接收新的TCP連接。當(dāng)此連接關(guān)閉時鞍匾,子進(jìn)程會釋放并重新進(jìn)入Accept并參與處理新的連接交洗。
<?php
$address = "tcp://0.0.0.0:8000";
$svr = stream_socket_server($address, $errno, $errmsg) or die("create server failed");
for($i=0; $i<32; $i++)
{
if(pcntl_fork() == 0)
{
while(true)
{
$connect = stream_socket_accept($svr);
if($connect == false)
{
continue;
}
$request = fread($connect);
fwrite($response);
fclose($connect);
}
exit(0);
}
}
Leader-Follower
模型的優(yōu)勢在于完全可以復(fù)用進(jìn)程,沒有額外消耗橡淑,性能非常好构拳。很多常見的服務(wù)器程序都是基于此模型的,如Apache梁棠、PHP-FPM置森。
當(dāng)然,多進(jìn)程模型也是存在缺陷的:
- 多線程模型嚴(yán)重依賴進(jìn)程的數(shù)量解決并發(fā)問題符糊,一個客戶端連接就需要占用一個進(jìn)程凫海,工作進(jìn)程的數(shù)量有多少,并發(fā)處理能力就有多少男娄,但是操作系統(tǒng)可以創(chuàng)建的進(jìn)程數(shù)量是有限的行贪。
- 多進(jìn)程模型啟動的大量進(jìn)程會帶來額外的進(jìn)程調(diào)度消耗,數(shù)百個進(jìn)程時可能進(jìn)程上下文切換調(diào)度消耗占CPU不到1%可以忽略不計(jì)模闲,如果啟動數(shù)千甚至數(shù)萬個進(jìn)程建瘫,消耗會直線上升。調(diào)度消耗可能占到CPU的100%尸折。
- 在即時通訊程序中啰脚,單臺服務(wù)器要同時維持上萬、數(shù)十萬翁授、上百萬的鏈接時拣播,多進(jìn)程模型就無法勝任了。
- 在Web服務(wù)器啟動100個進(jìn)程收擦,如果一個請求消耗100毫秒,100個進(jìn)程可以提供1000QPS塞赂,這樣的處理能力還可以。但是如果請求內(nèi)要調(diào)用外網(wǎng)HTTP接口宴猾,如QQ、微信仇哆、微博登錄時沦辙,耗時會很長,一個請求如果需要10秒油讯,那么一個進(jìn)程1秒就只能處理0.1個請求,100個進(jìn)程只能達(dá)到10QPS陌兑,這樣的處理能力就太差了沈跨。
那么,有沒有一種技術(shù)可以在一個進(jìn)程內(nèi)處理所有并發(fā)IO呢兔综?答案是有饿凛,也就是IO復(fù)用技術(shù)软驰。
IO復(fù)用
IO復(fù)用的歷史和多進(jìn)程一樣長,Linux很早就提供了select
系統(tǒng)調(diào)用杀狡,可以在一個進(jìn)程內(nèi)維護(hù)1024個連接贰镣,后來加入poll
系統(tǒng)調(diào)用,poll
做了一系列改進(jìn)后解決了1024個連接的限制問題碑隆,可以維持任意數(shù)量的連接。但是select
和poll
存在一個問題是休玩,它們需要循環(huán)檢測連接是否有事件劫狠。這樣問題就來了,如果服務(wù)器有100w個連接独泞,在某一時間只有一個連接是向服務(wù)器發(fā)送了數(shù)據(jù),select/poll
就需要做100w次循環(huán)蜒犯,而其中只會有1次命中荞膘,剩下99w9999次都是無效的,白白浪費(fèi)CPU時間片資源淘菩。
直到Linux2.6內(nèi)核開始提供新的epoll
系統(tǒng)調(diào)用屠升,可以維持無限數(shù)量的連接潮改,而且無需輪詢费奸,這才真正解決了C10K問題〗福現(xiàn)在各種高并發(fā)異步IO的服務(wù)器程序都是基于epoll
實(shí)現(xiàn)的微服,如Nginx、Node.js以蕴、Erlang、Golnag丛肮。像Node.js這樣單進(jìn)程單線程的程序赡磅,都可以維持超過100wTCP連接宝与,這全部都要?dú)w功于epoll
技術(shù)。
IO復(fù)用異步非阻塞
IO復(fù)用異步非阻塞使用經(jīng)典的Reactor反應(yīng)堆模型习劫,它本身不處理任何數(shù)據(jù)收發(fā)诽里,只是監(jiān)視一個socket
句柄的事件變化。
Reactor模型可以與多進(jìn)程谤狡、多線程結(jié)合使用,即可以實(shí)現(xiàn)異步非阻塞IO焰宣,又可以利用到多核拒贱。目前流程的異步服務(wù)器程序都是使用這種方式:
- Nginx:多進(jìn)程Reactor
- Nginx+Lua:多進(jìn)程Reactor+協(xié)程
- Golang:單線程Rector+多線程協(xié)程
- Swoole:多線程Reactor+多進(jìn)程Worker
協(xié)程是什么
協(xié)程從底層技術(shù)角度看實(shí)際上還是異步IO Reactor模型,應(yīng)用層自行實(shí)現(xiàn)了任務(wù)調(diào)度闸天,借助于Reactor切換各個當(dāng)前執(zhí)行的用戶態(tài)線程斜做,但用戶代碼中完全感知不到Reactor的存在。
Apache面對高并發(fā)為什么很無力
Apache處理一個請求是同步阻塞的模式笼吟,每到達(dá)一個請求,Apache都會去fork
一個子進(jìn)程去處理這個請求贷帮,直到這個請求處理完畢。面對低并發(fā)民晒,這種模式?jīng)]有什么缺點(diǎn)锄禽。但對于高并發(fā)就是這種模式的缺陷了。
因?yàn)橐粋€客戶端占用一個進(jìn)程磁滚,也就是說進(jìn)程數(shù)量有多少并發(fā)能力就有多少宵晚,但操作系統(tǒng)可以創(chuàng)建的進(jìn)程數(shù)量是有限的。其次搜贤,多進(jìn)程存在進(jìn)程間的切換問題钝凶,進(jìn)程間的切換調(diào)度勢必造成CPU的額外消耗。當(dāng)進(jìn)程數(shù)量達(dá)到成千上萬的時候耕陷,進(jìn)程間的切換就會占用CPU大部分的時間片哟沫,而真正進(jìn)程的執(zhí)行反而占用了CPU的一小部分,這就得不償失了嗜诀。
例如:在即時通訊場景中,單臺服務(wù)器可能要維持?jǐn)?shù)十萬的連接发皿,那么就要啟動數(shù)十萬的進(jìn)程來維持拂蝎,這顯然時不可能的。另外玄货,在調(diào)用外部HTTP接口時,假設(shè)Apache啟動100個進(jìn)程來處理請求松捉,每個請求消耗100毫秒,那么這100個進(jìn)程能提供1000QPS隘世。但是,在調(diào)用HTTP接口時,如QQ登錄蔓钟、微博登錄卵贱,一般耗時較長,假設(shè)一個請求消耗10秒键俱,也就是1個進(jìn)程1秒處理0.1個請求,那么100個進(jìn)程只能達(dá)到10QPS缀辩,這樣的處理能力就未免太差了踪央。
綜上所述,由于Apache采用的是同步阻塞的多進(jìn)程模式健无,在面對高并發(fā)場景時是無能為力的液斜。
Nginx是如何處理高并發(fā)的
傳統(tǒng)的服務(wù)器模型就是這樣,因?yàn)橥阶枞亩噙M(jìn)程模型無法面對高并發(fā)臼膏,那么有沒有一種方式可以在一個進(jìn)程中處理所有的并發(fā)I/O呢示损?當(dāng)然是有的,這也就是I/O復(fù)用技術(shù)夺溢。
所謂的I/O復(fù)用也就是多個I/O可以復(fù)用一個進(jìn)程,由于同步阻塞的方式不適合處理高并發(fā)嘉汰,如果是非阻塞的方式呢状勤?采用非阻塞的模式,當(dāng)一個連接過來的時候密似,由于不阻塞住所以一個進(jìn)程可以同時處理多個連接葫盼。
例如:一個進(jìn)程接收1w個鏈接,這個進(jìn)程每次從頭到尾的詢問每個連接:“你有I/O事件沒贫导?有的話就交給我來處理,沒有的話我一會兒再來問你”闺金。然后進(jìn)程就一直從頭到尾的問這1w個連接峰档。如果這1w個連接都沒有I/O事件,就會造成CPU的空轉(zhuǎn)哎壳,如此效率很低尚卫。
那么能不能引入一個代理,這個代理可以同時觀察許多I/O流事件呢刹泄?當(dāng)沒有I/O事件的時候怎爵,這個進(jìn)程處于阻塞狀態(tài),當(dāng)有I/O事件的時候姆蘸,這個代理就去通知進(jìn)程醒來呢?于是狂秦,早期就出現(xiàn)了兩個代理:select
和poll
推捐。
select
和poll
代理的原理是:當(dāng)連接有I/O流事件產(chǎn)生的時候,就會去喚醒進(jìn)程去處理堪簿。select
和poll
的區(qū)別在于select
只能觀察1024個鏈接皮壁,而poll
可以觀察無限個連接。
但是進(jìn)程并不知道是哪個連接產(chǎn)生的I/O流事件蛾魄,于是進(jìn)程就挨個去問:“請問畏腕,是你有事情要處理嗎茉稠?”,當(dāng)問了9999遍后發(fā)現(xiàn)原來是第1w個進(jìn)程有事情要處理铭污。那么前面這9999次就白問了膀篮,白白浪費(fèi)了寶貴的CPU時間片。由于select
和poll
不知道哪個連接有I/O流事件要處理磅网,所以性能也不是很好筷屡。
如果存在一個代理,每次都能夠知道哪個連接有I/O流事件燎潮,也就可以避免CPU無意義的空轉(zhuǎn)了扼倘。于是,epoll
出現(xiàn)了爪喘。epoll
代理的原理是:當(dāng)連接有I/O流事件產(chǎn)生的時候,epoll
就會去告訴進(jìn)程哪個連接有I/O
流事件產(chǎn)生泛啸,然后進(jìn)程就去處理這個進(jìn)程秃症。有了epoll
,理論上一個進(jìn)程就可以無限數(shù)量的連接种柑,而且無需輪詢聚请,真正解決了C10K的問題。
Nginx是基于epoll
的異步非阻塞的服務(wù)器程序驶赏,可以輕松的處理百萬級并發(fā)連接煤傍。