php-fpm與swoole
php本身是單進程單線程的,那么它是怎么解決並發問題的呢?這就涉及到本文將要提及的php-fpm和swoole
一、php-fpm(FastCGI 進程管理器)
php-fpm的生命周期如圖:
它的工作原理大概為:
php-fpm啟動->生成n個fast-cgi協議處理進程->監聽一個端口等待任務用戶請求->web服務器接收請求->請求轉發給php-fpm->php-fpm交給一個空閑進程處理->進程處理完成->php-fpm返回給web服務器->web服務器接收數據->返回給用戶
nginx+php-fpm 就是用的以上的方法。
優點:
nginx+php-fpm場景下php的單線程並不妨礙處理並發。當並發來了fpm會啟動更多的worker去處理。並沒有發生阻塞。
缺點:
1)這種模型嚴重依賴進程的數量解決並發問題,一個客戶端連接就需要占用一個進程,工作進程的數量有多少,並發處理能力就有多少。操作系統可以創建的進程數量是有限的。
2)啟動大量進程會帶來額外的進程調度消耗。數百個進程時可能進程上下文切換調度消耗占CPU不到1%可以忽略不計,如果啟動數千甚至數萬個進程,消耗就會直線上升。調度消耗可能占到 CPU 的百分之幾十甚至 100%。
另外有一些場景多進程模型無法解決,比如即時聊天程序(IM),一台服務器要同時維持上萬甚至幾十萬上百萬的連接(經典的C10K問題),多進程模型就力不從心了。
還有一種場景也是多進程模型的軟肋。通常Web服務器啟動100個進程,如果一個請求消耗100ms,100個進程可以提供1000qps,這樣的處理能力還是不錯的。但是如果請求內要調用外網Http接口,像QQ、微博登錄,耗時會很長,一個請求需要10s。那一個進程1秒只能處理0.1個請求,100個進程只能達到10qps,這樣的處理能力就太差了。
有沒有一種技術可以在一個進程內處理所有並發IO呢?答案是有,這就是IO復用技術。
二、IO復用
1、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
2、知識拓展一下:Redis 為什么使用單進程單線程方式也這么快
Redis 采用的是基於內存的采用的是單進程單線程模型的 KV 數據庫,由 C 語言編寫。官方提供的數據是可以達到100000+的 qps。這個數據不比采用單進程多線程的同樣基於內存的 KV 數據庫 Memcached 差。
Redis 快的主要原因有:
- 完全基於內存
- 數據結構簡單,對數據操作也簡單
- 使用多路 I/O 復用模型
這里主要圍繞第三點采用多路 I/O 復用技術來展開。
多路 I/O 復用模型
它是利用 select、poll、epoll 可以同時監察多個流的 I/O 事件的能力,在空閑的時候,會把當前線程阻塞掉,當有一個或多個流有 I/O 事件時,就從阻塞態中喚醒,於是程序就會輪詢一遍所有的流(epoll 是只輪詢那些真正發出了事件的流),並且只依次順序的處理就緒的流,這種做法就避免了大量的無用操作。這里“多路”指的是多個網絡連接,“復用”指的是復用同一個線程。采用多路 I/O 復用技術可以讓單個線程高效的處理多個連接請求(盡量減少網絡 IO 的時間消耗),且 Redis 在內存中操作數據的速度非常快(內存內的操作不會成為這里的性能瓶頸),主要以上兩點造就了 Redis 具有很高的吞吐量。
三、Swoole
swoole的進程管理模式與php-fpm並沒有本質上的區別。同樣還是一個master多個worker進程。但是不同的是一個同步阻塞。一個異步非阻塞。
1、swoole的生命周期
如下圖:
分析:
php-fpm在執行代碼的時候遇到IO(數據庫讀寫,緩存讀寫,文件讀寫)會同步阻塞。后面的任務必須等前面的任務執行完了之后才會接着執行。這種模式的性能是不如swoole的異步執行的。同時因為性能不足還引發了一個更重要的問題。當並發來的時候,進程數達到了最大限制,fpm就會進入等待。當一個進程釋放之后才能繼續執行代碼。
而swoole因為單個進程執行速度更快,就能更快的釋放掉,也就比fpm更不容易等待進程。這樣就能比fpm承載更大的QPS。
2、Swoole的進程模型
多進程的實現一般會被設計Master-Worker模式,常見的nginx默認的多進程模式也正是如此,當然swoole默認的也是多進程模型
相比Master-Worker模式,swoole的進程模型可以用Master-Manager-Worker來形容。即在Master-Worker的基礎上又增加了一層Manager進程。
正所謂“存在即合理”,我們來看一下Master\Manager\Worker三種進程各自存在的原因。
Master進程是一個多線程程序。注解:按照我們之前的理解,多個線程是運行在單一進程的上下文中的,其實對於單一進程中的每一個線程,都有它自己的上下文,但是由於共同存在於同一進程,所以它們也共享這個進程,包括它的代碼、數據等等。
再回來繼續說Master進程,Master進程就是我們的主進程,掌管生殺大權,它掛了,那底下的都得玩完。Master進程,包括主線程,多個Reactor線程等。
每一個線程都有自己的用途,比如主線程用於Accept、信號處理等操作,而Reactor線程是處理tcp連接,處理網絡IO,收發數據的線程。
說明兩點:
1)主線程的Accept操作,socket服務端經常用accept阻塞
2)信號處理,信號就相當於一條消息,比如我們經常操作的Ctrl+C其實就是給Master進程的主線程發送一個SIGINT的信號,意思就是你可以終止啦,其實除此之外,信號有很多種。
通常,主線程處理完新的連接后,會將這個連接分配給固定的Reactor線程,並且這個Reactor線程會一直負責監聽此socket(socket即套接字,是用來與另一個進程進行跨網絡通信的文件,文件可讀可寫),換句話就是說當此socket可讀時,會讀取數據,並將該請求分配給worker進程;當此socket可寫時,會把數據發送給tcp客戶端。
用一張圖清晰的梳理下
3、Manager進程的作用
1)疑問
疑問:那swoole為什么不能像Nginx一樣,是Master-Worker進程結構的呢?Manager進程的作用是什么呢?
我們知道,在Master-Worker模型中,Master只有一個,Worker是由父進程Master進程復制出來的,且Worker進程可以有多個。
注解:在linux中,父進程可以通過調用fork函數創建一個新的子進程,子進程是父進程的一個副本,幾乎但不完全相同,二者的最大區別就是都擁有自己獨立的進程ID,即PID。
對於多線程的Master進程而言,想要多Worker進程就必須fork操作,但是fork操作是不安全的,所以,在swoole中,有一個專職的Manager進程,Manager進程就專門負責worker/task進程的fork操作和管理。換句話也就是說,對於worker進程的創建、回收等操作全權有“保姆”Manager進程進行管理。
2)fork操作
關於fork操作不安全,這里擴展一下:
在Linux中,fork的時候只復制當前線程到子進程,在fork(2)-Linux Man Page中有着這樣一段相關的描述:
The child process is created with a single thread--the one that called fork(). The entire virtual address space of the parent is replicated in the child, including the states of mutexes, condition variables, and other pthreads objects; the use of pthread_atfork(3) may be helpful for dealing with problems that this can cause.
也就是說除了調用fork的線程外,其他線程在子進程中“蒸發”了。
這就是多線程中fork所帶來的一切問題的根源所在了。
通常,worker進程被誤殺或者由於程序的原因會異常退出,Manager進程為了保證服務的穩定性,會重新拉起新的worker進程,意思就是Worker進程你發生意外“死”了,沒關系,我自身不“死”,就可以fork千千萬萬個你。
當然,Master進程和Manager進程我們是不怎么關心的,真正實現業務邏輯,是在worker/task進程內完成的。
再來一張圖梳理下Manager進程和Worker/Task進程的關系:
四、協程是什么
協程是一種輕量級的線程,由用戶代碼來調度和管理,而不是由操作系統內核來進行調度,也就是在用戶態進行。可以直接的理解為就是一個非標准的線程實現,但什么時候切換由用戶自己來實現,而不是由操作系統分配 CPU 時間決定。具體來說,Swoole 的每個Worker進程會存在一個協程調度器來調度協程,協程切換的時機就是遇到 I/O 操作或代碼顯性切換時,進程內以單線程的形式運行協程,也就意味着一個進程內同一時間只會有一個協程在運行且切換時機明確,也就無需處理像多線程編程下的各種同步鎖的問題。
協程從底層技術角度看實際上還是異步IO Reactor模型,應用層自行實現了任務調度,借助Reactor切換各個當前執行的用戶態線程,但用戶代碼中完全感知不到Reactor的存在。
作用:節省出進程中因為IO操作而阻塞等待的時間
參考鏈接:
http://rango.swoole.com/
http://www.manks.top/swoole-process-model.html
https://www.jianshu.com/p/64e52f6f4837
https://www.easyswoole.com/