首頁  >  文章  >  運維  >  nginx效能為什麼好

nginx效能為什麼好

藏色散人
藏色散人原創
2019-06-12 10:11:342748瀏覽

nginx效能為什麼好

nginx在啟動後,在unix系統中會以daemon的方式在背景執行,後台程式包含一個master行程和多個worker行程。我們也可以手動地關掉後台模式,讓nginx在前台運行,並且透過配置讓nginx取消master進程,從而可以使nginx以單一進程方式運行。

很顯然,生產環境下我們肯定不會這麼做,所以關閉後台模式,一般是用來調試用的,在後面的章節裡面,我們會詳細地講解如何調試nginx。

所以,我們可以看到,nginx是以多進程的方式來工作的,當然nginx也是支援多執行緒的方式的,只是我們主流的方式還是多進程的方式,也是nginx的預設方式。 nginx採用多進程的方式有諸多好處,所以我就主要講解nginx的多進程模式吧。

剛剛講到,nginx在啟動後,會有一個master進程和多個worker進程。 master進程主要用來管理worker進程,包含:接收來自外界的訊號,向各worker進程發送訊號,監控worker進程的運作狀態,當worker進程退出後(異常情況下),會自動重新啟動新的worker進程。

而基本的網路事件,則是放在worker進程中來處理了。多個worker進程之間是對等的,他們同等競爭來自客戶端的請求,各進程相互之間是獨立的。一個請求,只可能在一個worker進程中處理,一個worker進程,不可能處理其它進程的請求。 worker進程的數量是可以設定的,一般我們會設定與機器cpu核數一致,這裡面的原因與nginx的進程模型以及事件處理模型是分不開的。

在nginx啟動後,如果我們要操作nginx,要怎麼做呢?

從上文我們可以看到,master來管理worker進程,所以我們只需要與master進程通訊就行了。 master進程會接收來自外界發來的訊號,再根據訊號做不同的事情。所以我們要控制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進程發送訊號一樣了。

現在,我們知道了當我們在操作nginx的時候,nginx內部做了些什麼事情,那麼,worker進程又是如何處理請求的呢?我們前面有提到,worker進程之間是平等的,每個進程,處理請求的機會也是一樣的。當我們提供80埠的http服務時,一個連接請求過來,每個行程都有可能處理這個連接,怎麼做到的呢?

首先,每個worker行程都是從master行程fork過來,在master行程裡面,先建立好需要listen的socket(listenfd)之後,再fork出多個worker行程。所有worker進程的listenfd會在新連接到來時變得可讀,為確保只有一個進程處理該連接,所有worker進程在註冊listenfd讀取事件前搶accept_mutex,搶到互斥鎖的那個進程註冊listenfd讀取事件,在讀取事件裡呼叫accept接受該連線。

當一個worker進程在accept這個連接之後,就開始讀取請求,解析請求,處理請求,產生資料後,再返回給客戶端,最後才斷開連接,這樣一個完整的請求就是這樣的了。我們可以看到,一個請求,完全由worker進程來處理,而且只在一個worker進程中處理。

多執行緒模型 VS 多進程模型,這是個問題!

那麼,nginx採用這種進程模型有什麼好處呢?當然,好處一定會很多了。首先,對於每個worker進程來說,獨立的進程,不需要加鎖,所以省掉了鎖帶來的開銷,同時在程式設計以及問題查找時,也會方便很多。其次,採用獨立的進程,可以讓互相之間不會影響,一個進程退出後,其它進程還在工作,服務不會中斷,master進程則很快啟動新的worker進程。當然,worker進程的異常退出,肯定是程式有bug了,異常退出,會導致目前worker上的所有請求失敗,不過不會影響到所有請求,所以降低了風險。當然,好處還有很多,大家可以慢慢體會。

上面講了很多關於nginx的進程模型,接下來,我們來看看nginx是如何處理事件的。

有人可能要問了,nginx採用多worker的方式來處理請求,每個worker裡面只有一個主線程,那能夠處理的並發數很有限啊,多少個worker就能處理多少個並發,何來高並發呢?非也,這就是nginx的高明之處,nginx採用了非同步非阻塞的方式來處理請求,也就是說,nginx是可​​以同時處理成千上萬個請求的。

想想apache的常用工作方式(apache也有異步非阻塞版本,但因其與自帶某些模組衝突,所以不常用),每個請求會獨佔一個工作線程,當並發數上到幾千時,就同時有幾千的線程在處理請求了。這對作業系統來說,是個不小的挑戰,執行緒帶來的記憶體佔用非常大,執行緒的上下文切換帶來的cpu開銷很大,自然效能就上不去了,而這些開銷完全是沒有意義的。

同步阻塞 VS 非同步非阻塞

為什麼nginx可以採用非同步非阻塞的方式來處理呢,或是非同步非阻塞到底是怎麼回事呢?我們先回到原點,看看一個請求的完整過程。首先,請求過來,要建立連接,然後再接收數據,接收數據後,再發送數據。具體到系統底層,就是讀寫事件,而當讀寫事件沒有準備好時,必然不可操作,如果不用非阻塞的方式來調用,那就得阻塞調用了,事件沒有準備好,那就只能等了,等事件準備好了,你再繼續吧。阻塞呼叫會進入核心等待,cpu就會讓出去給別人用了,對單線程的worker來說,顯然不合適,當網路事件越多時,大家都在等待呢,cpu空閒下來沒人用,cpu利用率自然上不去了,更別談高並發了。

好吧,你說加進程數,這跟apache的執行緒模型有什麼差別,注意,別增加無謂的上下文切換。所以,在nginx裡面,最忌諱阻塞的系統呼叫了。不要阻塞,那就非阻塞嘍。非阻塞就是,事件還沒準備好,馬上回到EAGAIN,告訴你,事件還沒準備好呢,你慌什麼,過會再來吧。好吧,你過一會,再來檢查一下事件,直到事件準備好了為止,在這期間,你就可以先去做其它事情,然後再來看看事件好了沒。雖然不阻塞了,但你得不時地過來檢查一下事件的狀態,你可以做更多的事情了,但帶來的開銷也是不小的。所以,才會有了非同步非阻塞的事件處理機制,具體到系統呼叫就是像select/poll/epoll/kqueue這樣的系統呼叫。

它們提供了一種機制,讓你可以同時監控多個事件,調用他們是阻塞的,但可以設定超時時間,在超時時間之內,如果有事件準備好了,就返回。這個機制正好解決了我們上面的兩個問題,拿epoll為例(在後面的例子中,我們多以epoll為例子,以代表這一類函數),當事件沒準備好時,放到epoll裡面,事件準備好了,我們就去讀寫,當讀寫返回EAGAIN時,我們將它再次加入到epoll裡面。這樣,只要有事件準備好了,我們就去處理它,只有當所有事件都沒準備好時,才在epoll裡面等著。這樣,我們就可以並發處理大量的並發了,當然,這裡的並發請求,是指未處理完的請求,線程只有一個,所以同時能處理的請求當然只有一個了,只是在請求間進行不斷地切換而已,切換也是因為非同步事件未準備好,而主動讓出的。這裡的切換是沒有任何代價,你可以理解為循環處理多個準備好的事件,事實上就是這樣的。

與多線程相比,這種事件處理方式是有很大的優勢的,不需要創建線程,每個請求佔用的內存也很少,沒有上下文切換,事件處理非常的輕量級級。並發數再多也不會導致無謂的資源浪費(上下文切換)。更多的並發數,只是會佔用更多的記憶體而已。我之前有對連線數進行過測試,在24G記憶體的機器上,處理的並發請求數達到200萬。現在的網頁伺服器基本上都採用這種方式,這也是nginx效能高效的主要原因。

我們之前說過,推薦設定worker的個數為cpu的核數,在這裡就很容易理解了,更多的worker數,只會導致進程來競爭cpu資源了,從而帶來不必要的上下文切換。

而且,nginx為了更好的利用多核心特性,提供了cpu親緣性的綁定選項,我們可以將某一個進程綁定在某一個核上,這樣就不會因為進程的切換帶來cache的失效。像這種小的優化在nginx中非常常見,同時也說明了nginx作者的苦心孤詣。例如,nginx在做4個位元組的字串比較時,會將4個字元轉換成一個int型,再作比較,以減少cpu的指令數等等。

現在,知道了nginx為什麼會選擇這樣的行程模型與事件模型了。對於一個基本的web伺服器來說,事件通常有三種類型,網路事件、訊號、定時器。從上面的講解中知道,網路事件透過非同步非阻塞可以很好的解決掉。如何處理訊號與定時器?

首先,訊號的處理。

對nginx來說,有一些特定的訊號,代表著特定的意義。訊號會中斷掉程式目前的運行,改變狀態後,繼續執行。如果是系統調用,則可能會導致系統調用的失敗,需要重入。關於訊號的處理,大家可以學習一些專業書籍,這裡不多說。對於nginx來說,如果nginx正在等待事件(epoll_wait時),如果程式收到訊號,在訊號處理函數處理完後,epoll_wait會回傳錯誤,然後程式可再次進入epoll_wait呼叫。

另外,再來看看定時器。由於epoll_wait等函數在呼叫的時候是可以設定一個超時時間的,所以nginx借助這個超時時間來實現定時器。 nginx裡面的定時器事件是放在一顆維護定時器的紅黑樹裡面,每次在進入epoll_wait前,先從該紅黑樹裡面拿到所有定時器事件的最小時間,在計算出epoll_wait的超時時間後進入epoll_wait。

所以,當沒有事件產生,也沒有中斷訊號時,epoll_wait會超時,也就是說,計時器事件到了。這時,nginx會檢查所有的逾時事件,將他們的狀態設為超時,然後再去處理網路事件。由此可以看出,當我們寫nginx程式碼時,在處理網路事件的回呼函數時,通常做的第一個事情就是判斷超時,然後再去處理網路事件。

更多Nginx相關知識,訪問Nginx使用教學欄位! 

以上是nginx效能為什麼好的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn