Nginx 工作模式和程式模型

13sai發表於2019-12-20

工作模式

  1. Nginx啟動後,會產生一個master主程式,主程式執行一系列的工作後會產生一個或者多個工作程式worker
  2. 在客戶端請求動態站點的過程中,Nginx伺服器還涉及和後端伺服器的通訊。Nginx將接收到的Web請求通過代理轉發到後端伺服器,由後端伺服器進行資料處理和組織;
  3. Nginx為了提高對請求的響應效率,降低網路壓力,採用了快取機制,將歷史應答資料快取到本地。保障對快取檔案的快速訪問

程式模型

nginx的程式模型,可以由下圖來表示:

image

master程式

主要用來管理 worker 程式,master程式會接收來自外界發來的訊號,再根據訊號做不同的事情。所以我們要控制nginx,只需要通過kill向master程式傳送訊號就行了。

具體包括以下主要功能:

  • 接收來自外界的訊號
  • 向各worker程式傳送訊號
  • 監控worker程式的執行狀態,當worker程式退出後(異常情況下),會自動重新啟動新的worker程式

重啟說明(示例)

比如kill -HUP pid,則是告訴nginx,重啟nginx,早期版本可以用這個訊號來重啟nginx,因為是從容地重啟,因此服務是不中斷的。(現在一般使用nginx -s reload

master程式在接收到HUP訊號後,會先重新載入配置檔案,然後再啟動新的worker程式,並向所有老的worker程式傳送訊號,告訴他們可以光榮退休了。新的worker在啟動後,就開始接收新的請求,而老的worker在收到來自master的訊號後,就不再接收新的請求,並且在當前程式中的所有未處理完的請求處理完成後,再退出。

(master不需要處理網路事件,不負責業務的執行)

worker程式

主要任務是完成具體的任務邏輯。其主要關注點是與客戶端或後端真實伺服器(此時 worker 作為中間代理)之間的資料可讀/可寫等I/O互動事件。具體包括以下主要功能:

  • 接收客戶端請求;
  • 將請求一次送入各個功能模組進行過濾處理;
  • 與後端伺服器通訊,接收後端伺服器處理結果;
  • 資料快取proxy_cache模組
  • 響應客戶端請求

(一個請求,完全由worker程式來處理,而且只在一個worker程式中處理。)

worker程式是如何處理請求的?

首先,worker程式之間是平等的,每個worker程式都是從master程式fork過來,在master程式裡面,先建立好需要listen的socket(listenfd)之後,然後再fork出多個worker程式。每個worker程式,處理請求的機會也是一樣的。當一個連線請求過來,每個程式都有可能處理這個連線,怎麼做的呢?

所有worker程式的listenfd會在新連線到來時變得可讀,為保證只有一個程式處理該連線,所有worker程式在註冊listenfd讀事件前搶accept_mutex,搶到互斥鎖的那個worker程式註冊listenfd讀事件,在讀事件裡呼叫accept接受該連線。

當一個worker程式在accept這個連線之後,就開始讀取請求,解析請求,處理請求,產生資料後,再返回給客戶端,最後斷開連線,這樣就是一個完整的請求就是這樣的了。

我們可以瞭解到一個請求,完全由worker程式來處理,且只在一個worker程式中處理。

Nginx採用的IO多路複用模型

IO多路複用是指核心一旦發現程式指定的一個或者多個IO條件準備讀取,它就通知該程式,目前支援I/O多路複用的系統呼叫有 select , poll , epoll ,I/O多路複用就是通過一種機制,一個程式可以監視多個描述符(socket),一旦某個描述符就緒(一般是讀就緒或者寫就緒),能夠通知程式進行相應的讀 寫操作。

select
基本原理

select 函式監視的檔案描述符分3類,分別是writefds、readfds、和exceptfds。呼叫後select函式會阻塞,直到有描述符就緒(有資料 可讀、可寫、或者有except),或者超時(timeout指定等待時間,如果立即返回設為null即可),函式返回。當select函式返回後,可以通過遍歷fdset,來找到就緒的描述符。

優點
  • 目前幾乎在所有的平臺上支援
缺點

select本質上是通過設定或者檢查存放fd標誌位的資料結構來進行下一步處理。這樣所帶來的缺點是:

  • select最大的缺陷就是單個程式所開啟的FD是有一定限制的,它由FD_SETSIZE設定,預設值是1024。(一般來說這個數目和系統記憶體關係很大,具體數目可以cat /proc/sys/fs/file-max檢視。32位機預設是1024個。64位機預設是2048)
  • 對socket進行掃描時是線性掃描,即採用輪詢的方法,效率較低。(當套接字比較多的時候,每次select()都要通過遍歷FD_SETSIZE個Socket來完成排程,不管哪個Socket是活躍的,都遍歷一遍。這會浪費很多CPU時間。如果能給套接字註冊某個回撥函式,當他們活躍時,自動完成相關操作,那就避免了輪詢,這正是epoll與kqueue做的)
  • 需要維護一個用來存放大量fd的資料結構,這樣會使得使用者空間和核心空間在傳遞該結構時複製開銷大。
poll
基本原理

poll本質上和select沒有區別,它將使用者傳入的陣列拷貝到核心空間,然後查詢每個fd對應的裝置狀態,如果裝置就緒則在裝置等待佇列中加入一項並繼續遍歷,如果遍歷完所有fd後沒有發現就緒裝置,則掛起當前程式,直到裝置就緒或者主動超時,被喚醒後它又要再次遍歷fd。這個過程經歷了多次無謂的遍歷。

優點
  • 它沒有最大連線數的限制,原因是它是基於連結串列來儲存的。
缺點
  • 大量的fd的陣列被整體複製於使用者態和核心地址空間之間,而不管這樣的複製是不是有意義。
  • poll還有一個特點是“水平觸發”,如果報告了fd後,沒有被處理,那麼下次poll時會再次報告該fd。

注意:從上面看,select和poll都需要在返回後,通過遍歷檔案描述符來獲取已經就緒的socket。事實上,同時連線的大量客戶端在一時刻可能只有很少的處於就緒狀態,因此隨著監視的描述符數量的增長,其效率也會線性下降。

epoll

epoll是在2.6核心中提出的,是之前的select和poll的增強版本。相對於select和poll來說,epoll更加靈活,沒有描述符限制。epoll使用一個檔案描述符管理多個描述符,將使用者關係的檔案描述符的事件存放到核心的一個事件表中,這樣在使用者空間和核心空間的copy只需一次。

基本原理

epoll支援水平觸發和邊緣觸發,最大的特點在於邊緣觸發,它只告訴程式哪些fd剛剛變為就緒態,並且只會通知一次。還有一個特點是,epoll使用“事件”的就緒通知方式,通過epoll_ctl註冊fd,一旦該fd就緒,核心就會採用類似callback的回撥機制來啟用該fd,epoll_wait便可以收到通知。

epoll對檔案描述符的操作有兩種模式

LT(level trigger)和ET(edge trigger)。LT模式是預設模式,兩者區別如下:

  • LT模式:當epoll_wait檢測到描述符事件發生並將此事件通知應用程式,應用程式可以不立即處理該事件。下次呼叫epoll_wait時,會再次響應應用程式並通知此事件。
  • ET模式:當epoll_wait檢測到描述符事件發生並將此事件通知應用程式,應用程式必須立即處理該事件。如果不處理,下次呼叫epoll_wait時,不會再次響應應用程式並通知此事件。
優點
  • 沒有最大併發連線的限制,能開啟的FD的上限遠大於1024(1G的記憶體上能監聽約10萬個埠)。
  • 效率提升,不是輪詢的方式,不會隨著FD數目的增加效率下降。
      只有活躍可用的FD才會呼叫callback函式;即Epoll最大的優點就在於它只管你“活躍”的連線,而跟連線總數無關,因此在實際的網路環境中,Epoll的效率就會遠遠高於select和poll。
  • 記憶體拷貝,利用mmap()檔案對映記憶體加速與核心空間的訊息傳遞;即epoll使用mmap減少複製開銷。
kqueue

kqueue與epoll非常相似,最初是2000年Jonathan Lemon在FreeBSD系統上開發的一個高效能的事件通知介面。註冊一批socket描述符到 kqueue 以後,當其中的描述符狀態發生變化時,kqueue 將一次性通知應用程式哪些描述符可讀、可寫或出錯了。只是適應平臺不多。

參考:

這應該是Nginx系列最後一篇文章了,如果你有疑問,歡迎交流,水平有限,錯誤歡迎指正。

Nginx 相關文章:


技術文章也釋出在自己的公眾號【愛好歷史的程式設計師】,歡迎掃碼關注,謝謝!

愛好歷史的程式設計師

分享開發知識,歡迎交流。

相關文章