Nginx 實現高並發原理


Nginx 實現高並發原理

1. 概述

Nginx由內核和模塊組成。
Nginx本身做的工作實際很少,當它接到一個HTTP請求時,它僅僅是通過查找配置文件將此次請求映射到一個location block,而此location中所配置的各個指令則會啟動不同的模塊去完成工作,因此模塊可以看做Nginx真正的勞動工作者。通常一個location中的指令會涉及一個handler模塊和多個filter模塊(當然,多個location可以復用同一個模塊)。handler模塊負責處理請求,完成響應內容的生成,而filter模塊對響應內容進行處理。

Nginx進程模型
Nginx默認采用多進程工作方式,Nginx啟動后,會運行一個master進程和多個worker進程。其中master充當整個進程組與用戶的交互接口,同時對進程進行監護,管理worker進程來實現重啟服務、平滑升級、更換日志文件、配置文件實時生效等功能。worker用來處理基本的網絡事件,worker之間是平等的,他們共同競爭來處理來自客戶端的請求。

Nginx 采用的是多進程(單線程) & 多路IO復用模型。使用了 I/O 多路復用技術的 Nginx,就成了”並發事件驅動“的服務器

2. 驚群現象

主進程(master 進程)首先通過 socket() 來創建一個 sock 文件描述符用來監聽,然后fork生成子進程(workers 進程),子進程將繼承父進程的 sockfd(socket 文件描述符),之后子進程 accept() 后將創建已連接描述符(connected descriptor)),然后通過已連接描述符來與客戶端通信。

那么,由於所有子進程都繼承了父進程的 sockfd,那么當連接進來時,所有子進程都將收到通知並“爭着”與它建立連接,這就叫驚群現象。大量的進程被激活又掛起,只有一個進程可以accept() 到這個連接,這當然會消耗系統資源。

3. Nginx對驚群現象的處理

Nginx 提供了一個 accept_mutex 這個東西,這是一個加在accept上的一把共享鎖。即每個 worker 進程在執行 accept 之前都需要先獲取鎖,獲取不到就放棄執行 accept()。有了這把鎖之后,同一時刻,就只會有一個進程去 accpet(),這樣就不會有驚群問題了。accept_mutex 是一個可控選項,我們可以顯示地關掉,默認是打開的。

4. Nginx進程詳解

nginx的進程模型如圖所示:
1

使用多進程模式,不僅能提高並發率,而且進程之間相互獨立,一個 worker 進程掛了不會影響到其他 worker 進程。

注意: worker 進程數,一般會設置成機器 cpu 核數。因為更多的worker 數,只會導致進程相互競爭 cpu,從而帶來不必要的上下文切換

4.1 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進程發送信號一樣了。

4.2 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_connections。這里的連接池里面保存的其實不是真實的連接,它只是一個worker_connections大小的一個ngx_connection_t結構的數組。並且,nginx會通過一個鏈表free_connections來保存所有的空閑ngx_connection_t,每次獲取一個連接時,就從空閑連接鏈表中獲取一個,用完后,再放回空閑連接鏈表里面。一個nginx能建立的最大連接數,應該是worker_connections * worker_processes。當然,這里說的是最大連接數,對於HTTP請求本地資源來說,能夠支持的最大並發數量是worker_connections * worker_processes,而如果是HTTP作為反向代理來說,最大並發數量應該是worker_connections * worker_processes/2。因為作為反向代理服務器,每個並發會建立與客戶端的連接和與后端服務的連接,會占用兩個連接。

4.3 worker進程工作流程

當一個 worker 進程在 accept() 這個連接之后,就開始讀取請求,解析請求,處理請求,產生數據后,再返回給客戶端,最后才斷開連接,一個完整的請求。一個請求,完全由 worker 進程來處理,而且只能在一個 worker 進程中處理。

5. 這樣做帶來的好處:

  1. 節省鎖帶來的開銷。每個 worker 進程都是獨立的進程,不共享資源,不需要加鎖。同時在編程以及問題查上時,也會方便很多。

  2. 獨立進程,減少風險。采用獨立的進程,可以讓互相之間不會影響,一個進程退出后,其它進程還在工作,服務不會中斷,master 進程則很快重新啟動新的 worker 進程。當然,worker 進程的也能發生意外退出。

6. IO 多路復用

多進程模型每個進程/線程只能處理一路IO,那么 Nginx是如何處理多路IO呢?

如果不使用 IO 多路復用,那么在一個進程中,同時只能處理一個請求,比如執行 accept(),如果沒有連接過來,那么程序會阻塞在這里,直到有一個連接過來,才能繼續向下執行。

而多路復用,允許我們只在事件發生時才將控制返回給程序,而其他時候內核都掛起進程,隨時待命。

核心:Nginx采用的 IO多路復用模型epoll

epoll通過在Linux內核中申請一個簡易的文件系統(文件系統一般用什么數據結構實現?B+樹),其工作流程分為三部分:

  • 調用 int epoll_create(int size)建立一個epoll對象,內核會創建一個eventpoll結構體,用於存放通過epoll_ctl()向epoll對象中添加進來的事件,這些事件都會掛載在紅黑樹中。
  • 調用 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event) 在 epoll 對象中為 fd 注冊事件,所有添加到epoll中的件都會與設備驅動程序建立回調關系,也就是說,當相應的事件發生時會調用這個sockfd的回調方法,將sockfd添加到eventpoll 中的雙鏈表
  • 調用 int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout) 來等待事件的發生,timeout 為 -1 時,該調用會阻塞知道有事件發生

這樣,注冊好事件之后,只要有 fd 上事件發生,epoll_wait() 就能檢測到並返回給用戶,用戶就能”非阻塞“地進行 I/O 了。

epoll() 中內核則維護一個鏈表,epoll_wait 直接檢查鏈表是不是空就知道是否有文件描述符准備好了。(epoll 與 select 相比最大的優點是不會隨着 sockfd 數目增長而降低效率,使用 select() 時,內核采用輪訓的方法來查看是否有fd 准備好,其中的保存 sockfd 的是類似數組的數據結構 fd_set,key 為 fd,value 為 0 或者 1。)

能達到這種效果,是因為在內核實現中 epoll 是根據每個 sockfd 上面的與設備驅動程序建立起來的回調函數實現的。那么,某個 sockfd 上的事件發生時,與它對應的回調函數就會被調用,來把這個 sockfd 加入鏈表,其他處於“空閑的”狀態的則不會。在這點上,epoll 實現了一個”偽”AIO。但是如果絕大部分的 I/O 都是“活躍的”,每個 socket 使用率很高的話,epoll效率不一定比 select 高(可能是要維護隊列復雜)。

可以看出,因為一個進程里只有一個線程,所以一個進程同時只能做一件事,但是可以通過不斷地切換來“同時”處理多個請求。

例子:

  • Nginx 會注冊一個事件:“如果來自一個新客戶端的連接請求到來了,再通知我”,此后只有連接請求到來,服務器才會執行 accept() 來接收請求。
  • 又比如向上游服務器(比如 PHP-FPM)轉發請求,並等待請求返回時,這個處理的 worker 不會在這阻塞,它會在發送完請求后,注冊一個事件:“如果緩沖區接收到數據了,告訴我一聲,我再將它讀進來”,於是進程就空閑下來等待事件發生。

這樣,基於 多進程+epoll, Nginx 便能實現高並發。

Nginx 與 多進程模式 Apache 的比較:

  1. 對於Apache,每個請求都會獨占一個工作線程,當並發數到達幾千時,就同時有幾千的線程在處理請求了。這對於操作系統來說,占用的內存非常大,線程的上下文切換帶來的cpu開銷也很大,性能就難以上去,同時這些開銷是完全沒有意義的。
    2

    • web服務器進程(web server process)在監聽套接字上,監聽新的連接(客戶端發起的新比賽)。
    • 一局新的比賽發起后,進程就開始工作,每一步棋下完后都進入阻塞狀態,等待客戶端走下一步棋。
    • 一旦比賽結束,web服務器進程會看看客戶是否想開始新的比賽(這相當於一個存活的連接)。如果連接被關閉(客戶端離開或者超時),web服務器進程會回到監聽狀態,等待全新的比賽
  2. 對於Nginx來講,一個進程只有一個主線程,通過異步非阻塞的事件處理機制,實現了循環處理多個准備好的事件,從而實現輕量級和高並發。
    3

    • 工作進程在監聽套接字和連接套接字上等待事件。
    • 事件發生在套接字上,工作進程會處理這些事件。
      • 監聽套接字上的事件意味着:客戶端開始了一局新的游戲。工作進程創建了一個新的連接套接字。
      • 連接套接字上的事件意味着:客戶端移動了棋子。工作進程會迅速響應。

NGINX的規模可以很好地支持每個工作進程上數以萬計的連接。每個新連接都會創建另一個文件描述符,並消耗工作進程中少量的額外內存。每一個連接的額外消耗都很少。NGINX進程可以保持固定的CPU占用率。當沒有工作時,上下文切換也較少。
APACHE 的阻塞式的、一個連接/一個進程的模式中,每個連接需要大量的額外資源和開銷,並且上下文切換(從一個進程到另一個進程)非常頻繁。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM