為什麼nginx很快?

2020-10-20 21:00:47

首先我們要知道,Nginx 採用的是多程序(單執行緒) & 多路IO複用模型。使用了 I/O 多路複用技術的 Nginx,就成了」並行事件驅動「的伺服器。

(推薦教學:)

9760708dc923c69d89cb9b8e486bd84.png

多程序的工作模式

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很快?的詳細內容,更多請關注TW511.COM其它相關文章!