首先我們要知道,Nginx 採用的是多進程(單執行緒) & 多路IO複用模型。使用了 I/O 多路復用技術的 Nginx,就成了」並發事件驅動「的伺服器。
(推薦教學:nginx教學)
多行程的工作模式
Nginx 啟動後,會有一個master 進程和多個相互獨立的worker 進程。 master 接收來自外界的訊號,向各 worker 行程發送訊號,每個行程都有可能來處理這個連線。 master 進程能監控 worker 進程的運作狀態,當 worker 進程退出後(異常情況下),會自動啟動新的 worker 進程。
注意 worker 進程數,一般會設定成機器 cpu 核數。因為更多的 worker 數,只會導致進程相互競爭 cpu ,從而帶來不必要的上下文切換。
使用多進程模式,不僅能提高並發率,而且進程之間相互獨立,一個 worker 進程掛了不會影響到其他 worker 進程。
驚群現象
主流程(master 行程)先透過socket() 來建立sock 檔案描述子用來監聽,然後fork產生子行程(workers進程),子進程將繼承父進程的sockfd(socket 檔案描述符),之後子進程accept() 後將建立已連接描述符(connected descriptor),然後透過已連接描述符來與客戶端通訊。
那麼,由於所有子進程都繼承了父進程的sockfd,那麼當連接進來時,所有子進程都將收到通知並「爭著」與它建立連接,這就叫「驚群現象」。大量的進程被啟動又掛起,只有一個行程可以accept() 到這個連接,當然會消耗系統資源。
Nginx對驚群現象的處理
Nginx 提供了一個 accept_mutex 這個東西,這是一個加在accept上的一把共享鎖定。即每個 worker 程序在執行 accept 之前都需要先取得鎖,取得不到就放棄執行 accept()。有了這把鎖之後,同一時刻,就只會有一個進程去 accpet(),這樣就不會有驚群問題了。 accept_mutex 是一個可控選項,我們可以顯示地關掉,預設是打開的。
Nginx程式詳解
Nginx在啟動後,會有一個master程式和多個worker程式。
master進程
主要用來管理worker進程,包含:
接收來自外界的訊號向各worker進程發送訊號監控worker進程的運行狀態,當worker進程退出後(異常情況下),會自動重新啟動新的worker進程
master進程充當整個進程組與用戶的交互接口,同時對進程進行監護。它不需要處理網路事件,不負責業務的執行,只會透過管理worker進程來實現重啟服務、平滑升級、更換日誌檔案、設定檔即時生效等功能。
我們要控制nginx,只需要透過 kill 向master程序發送訊號就行了。例如kill -HUP pid 是告訴nginx從容地重啟nginx。我們一般都會用這個訊號來重啟nginx,或重新載入配置,因為是從容地重啟,因此服務是不中斷的。 master行程在接收HUP訊號後是怎麼做的呢?
首先master進程在接到訊號後,會先重新載入設定檔然後再啟動新的worker進程並向所有舊的worker進程發送訊號,告訴他們可以光榮退休了新的worker在啟動後,就開始接收新的請求,
老的worker在收到來自master的信號後,就不再接收新的請求,並且在當前進程中的所有未處理完的請求處理完成後,再退出。
當然,直接給master進程發送訊號,這是比較老的操作方式,nginx在0.8版本之後,引入了一系列命令列參數,來方便我們管理。例如 ./nginx -s reload 就是來重啟nginx,./nginx -s stop 就是來停止nginx的運作。如何做到的呢?我們還是拿reload 來說,我們看到,執行指令時,我們是啟動一個新的nginx進程,而新的nginx進程在解析到reload參數後,就知道我們的目的是控制nginx來重新載入設定檔了,它會向master進程發送訊號,然後接下來的動作,就跟我們直接向master進程發送訊號一樣了。
worker流程
而基本的網路事件,則是放在worker進程中來處理了。多個worker進程之間是對等的,他們同等競爭來自客戶端的請求,各進程相互之間是獨立的。一個請求只可能在一個worker進程中處理,一個worker進程不可能處理其它進程的請求。 worker進程的數量是可以設定的,一般我們會設定與機器cpu核數一致,這裡面的原因與nginx的進程模型以及事件處理模型是分不開的。
worker進程之間是平等的,每個進程處理請求的機會也是一樣的。當我們提供80埠的http服務時,一個連接請求過來,每個行程都有可能處理這個連接,怎麼做到的呢?首先,每個worker行程都是從master流程fork過來,在master行程裡面,先建立好需要listen的socket(listenfd)之後,再fork出多個worker行程。所有worker進程的listenfd會在新連接到來時變得可讀,為確保只有一個進程處理該連接,所有worker進程在註冊listenfd讀取事件前搶accept_mutex,搶到互斥鎖的那個進程註冊listenfd讀取事件,在讀取事件裡呼叫accept接受該連線。當一個worker進程在accept這個連接之後,就開始讀取請求,解析請求,處理請求,產生資料後,再返回給客戶端,最後才斷開連接,這樣一個完整的請求就是這樣的了。我們可以看到,一個請求,完全由worker進程來處理,而且只在一個worker進程中處理。
worker進程工作流程
當一個worker 進程在accept() 這個連線之後,就開始讀取請求,解析請求,處理請求,產生資料後,再返回給客戶端,最後才斷開連接,一個完整的請求。一個請求完全由 worker 程序來處理,而且只能在一個 worker 進程中處理。
這樣做帶來的好處:
節省鎖定帶來的開銷。每個 worker 進程都是獨立的進程,不共享資源,不需要加鎖。同時在程式設計以及問題查上時,也會方便很多。獨立進程,減少風險。採用獨立的進程,可以讓彼此之間不會影響,一個進程退出後,其它進程還在工作,服務不會中斷,master 進程則很快重新啟動新的 worker 進程。當然,worker 進程的也能發生意外退出。
多進程模型每個進程/執行緒只能處理一路IO,那麼 Nginx是如何處理多路IO呢?
如果不使用IO 多路復用,那麼在一個進程中,同時只能處理一個請求,例如執行accept(),如果沒有連接過來,那麼程式會阻塞在這裡,直到有一個連接過來,才能繼續往下執行。
而多路復用,允許我們只在事件發生時才將控制權返回給程序,而其他時候核心都掛起進程,隨時待命。
核心:Nginx採用的IO多路復用模型epoll
#範例: Nginx 會註冊一個事件:「如果來自一個新客戶端的連線請求到來了,再通知我”,此後只有連接請求到來,伺服器才會執行accept() 來接收請求。又例如向上游伺服器(例如PHP-FPM)轉發請求,並等待請求返回時,這個處理的worker 不會在這阻塞,它會在發送完請求後,註冊一個事件:「如果緩衝區接收到資料了,告訴我一聲,我再將它讀進來”,於是進程就空閒下來等待事件發生。
以上是為什麼nginx很快?的詳細內容。更多資訊請關注PHP中文網其他相關文章!