寫給後端的Nginx初級入門教程:Nginx原理初探

韓數發表於2019-11-11

在上一篇文章寫給後端的Nginx初級入門教程:配置高可用叢集 中,我們使用keepalived實現了我們Nginx伺服器的高可用配置,防止因為Nginx伺服器掛掉而導致整個應用掛掉的這種情況的發生。而Nginx作為當下最受歡迎的web伺服器軟體之一,能做到如今的地位和成就也並不是沒有原因的,優秀的效能表現,可伸縮性,可修改性的設計,同時跨平臺的特性以及非常低的故障率都是Nginx現今如此受歡迎的重要因素,那Nginx整體架構又是如何設計的呢?本篇文章呢(由於要了解Nginx核心技術需要非常深的技術內力,我沒有(大哭)),我們將輕輕的稍微揭開Nginx神祕面紗的一角,去探索一下Nginx內部是如何設計與運作的。

Nginx的特性

Nginx能如此受歡迎,並且企業所廣泛採用,一定是有很多技能點滿了的,經過查閱相關資料,前輩們一共總結出來六點Nginx伺服器相對於其他型別的web伺服器軟體做的更加優秀的地方,也是Nginx設計之初主要關注的地方。它們分別是:

  • 效能
  • 可伸縮性
  • 簡單性
  • 可修改性
  • 可見性
  • 可移植性

效能:

效能我想不必多說,這是Nginx能混到現在的核心資本,即使Nginx在其他方面做的很優秀,如果效能比不上其他的web伺服器,在現在這個大家普遍比較看重效能的時代,Nginx有較大概率會受到冷遇。而Nginx和傳統的程式不一樣的是,其他程式比如遊戲可能會需要計算效能,圖形渲染,網路渲染效能,而Nginx作為一款web伺服器,就只能在網路領域和別人一決雌雄了。

Nginx在網路效能這塊做了大量的工作,包括使用事件驅動架構配合請求的多階段非同步處理,以及Master-workers機制的使用,都保證了在高併發場景下Nginx所展現出來的出色效能表現。

可伸縮性:

可伸縮性也可以理解為可擴充套件性,比如谷歌瀏覽器的外掛,火狐瀏覽器的外掛等等(沒想到什麼合適的例子),Nginx支援新增相關的模組來增強我們的服務,同時優秀的模組化設計允許我們定製或者採用第三方開發的模組來滿足我們額外的業務需求。

簡單性:

簡單性通常指的是元件的簡單程度,每個元件越簡單,就會越容易理解和實現,也更容易被驗證。當然開發Nginx元件不能隨心所欲,同時要遵循Nginx模組開發統一的規範,而Nginx模組介面非常簡單,具有很高的靈活性。

可修改性:

Nginx基於BSD開源,這意味當Nginx某些功能不能滿足我們其他的額外需求時,我們可以修改它的程式碼來達到我們的業務要求。同時Nginx也支援在我們不重啟,停止服務的前提下,修改我們web伺服器的某些配置並使之生效。(平滑重啟)

可見性:

可見性呢,就是我們整個應用對使用者的透明程度,開放程度。Nginx 有 http_stub_status_module 來實現基礎的可見性,可以讓我們瞭解到Nginx 當前一共建立了多少個連結,處理了多少個請求等等,這些監控引數可以讓運維人員更好的瞭解Nginx服務整體執行的狀況,並及時的做出調整。比如當 Reading + Writing 數值比較高的時候,就意味著我們當前的應用併發量還是比較大的。

可移植性:

由於Nginx是基於C語言開發的,這意味著Nginx可以在多個作業系統平臺上執行,同時Nginx重新封裝了日誌,各種資料結構等工具軟體,而且核心程式碼皆採用與作業系統無關程式碼的實現方式,而涉及到與作業系統的互動,Nginx則為不同作業系統提供了各自獨立的實現,這點其實和java虛擬機器有著異曲同工之妙。

說完了這些,Nginx又是如何實現這些騷操作的呢?接下來我們淺入Nginx內部,從模組設計,事件驅動,請求處理,程式管理四個方面來簡單地瞭解Nginx內部是如何設計得如此高效的。

優秀的模組化設計:

Nginx和java一樣,java呢是除了少數基本型別之外,其他一切皆為物件,Nginx也是如此,除了少部分核心程式碼之外,其他的皆為模組,Nginx模組遵循著同樣的設計規範(ngx_module-t),設計規範中只要求了最核心的幾個實現,比如初始化,退出,以及配置等等,這樣做的好處和java介面類一樣,在給了模組設計者充分自由的同時,又有效地避免了模組設計者亂來導致Nginx本身出現問題

同樣的,規範(ngx_module-t)中允許我們自定義服務型別,比如在之前的實戰篇 配置詳解那部分,我們就主要說了Nginx的 全域性塊,events塊,http塊。而這些就屬於我們Nginx的模組型別。比如http模組就只負責相關的http請求的處理,而關於事件的處理則全部交給events模組處理。

同時Nginx也引入了核心模組的概念,目前Nginx一共有六個核心模組,用來處理我們常見的

  • 日誌(ngx_errlog_module)
  • 事件(ngx_events_module)
  • 安全(ngx_openssl_module)
  • 網路(ngx_http_module)
  • 郵件(ngx_mail_module)
  • 核心程式碼(ngx_mail_module)

這樣做有什麼好處呢,這意味著Nginx非模組的程式碼,比如Nginx的核心程式碼,只需要關注怎麼呼叫這六個模組進行相應的處理就可以了,完全不需要管它們是具體怎麼實現的。同樣的,Nginx框架不會約束核心模組的介面和功能。這種簡潔,靈活的設計為Nginx實現動態可擴充套件性,動態可配置性。動態可定製性帶來了極大的便利。這段話怎麼理解呢,這樣理解:

不管黑貓白貓,能抓住耗子的就是好貓。Nginx核心模組不管你是怎麼實現的,只要實現就行。所以核心模組的實現才可以充足的發揮。當然,這一切也是需要遵守相關的規範的,但是規範只是極少的一部分,整體留給核心模組的空間是十分大的。

事件驅動架構:

在瞭解Nginx的事件驅動架構前,我們先看一下傳統的web伺服器是如何工作的,接下來進入小劇場:

報,報tomcat大王,一個請求過來了!

這樣啊,你派一個執行緒跟著,防止它有什麼小動作,記住,等請求結束離開之後,再讓那個執行緒回來。

在傳統的web伺服器中,一個請求往往會分配一個獨立的執行緒或程式去處理,直到該執行緒結束,這當然沒有什麼問題,可是如果該請求請求到一半又想去讀一下檔案,這個時候就會造成IO阻塞,我們執行緒就只能在那乾等著等它處理完,而請求開始到請求結束的這個過程,執行緒都始終佔用著系統資源,直到請求結束執行緒被銷燬才會釋放資源,當然,如果請求刷的一下就處理完了這沒有什麼問題,但是如果請求一下子處理了幾分鐘,幾十分鐘,新的請求到來時只能額外再開新的執行緒,這誰頂得住,併發量稍微高一點執行緒數就達到最大值了。

當然,以上只是舉例,tomcat在7之後就支援NIO非同步IO處理了,tomcat8在linux環境中已經預設開啟NIO模式。

而Nginx不一樣在哪呢,傳統的web伺服器往往是事件消費者獨自佔用一個程式資源,而Nginx的事件消費者只是被事件分發者短期呼叫而已。比如在傳統的web伺服器中,當TCP建立連結的時候發生一個事件,然後連結之後交給一個程式去處理消費,這其中比如讀寫操作什麼的都是這一個程式始終如一地去完成的。

而Nginx的獨特之處就在於:

比如當tcp連線事件來的時候,會首先被我們事件收集者,分發者收到,然後事件分發者將這個事件交給,記住,交給僅僅處理tcp連結的消費者去處理,而tcp讀事件和tcp連線消費者一點關係都沒有,當讀事件來的時候,就分發給只負責讀事件的事件消費者,而每個事件消費者的處理都是刷的一下非常快的就處理完了,所有的事件消費者只是事件分發者程式的短期呼叫而已,這種設計使得網路效能,使用者感知和請求時延都得到了提升,每個使用者的請求都會得到及時的響應,整個伺服器的網路吞吐量都會由於事件的及時響應而增大。

如果200個請求到達傳統的web伺服器,將會分配兩百個執行緒去處理,如果傳統的web伺服器最大隻能申請兩百個執行緒的話,後面的使用者就只有等待前面的請求完成,而Nginx則是兩百個請求發起連結,連線事件消費者只把連線事件處理了,然後剩下的操作交給其他的事件消費者去處理,這樣第201個請求來的時候,由於tcp連線事件消費者已經處理完了或者已經處理了大多數請求的連線,所以第201個請求也可以瞬間得到連線成功的響應。

太牛X了。

當然,這樣也有弊端,就是我們的事件消費者程式不能阻塞和休眠,比如請求來了,你負責連線的事件消費者阻塞了,那我的事件分發者就得一直等你處理完,要不連線不上我也沒法執行讀事件。或者負責tcp連線的事件消費者因為太閒程式睡著了,事件分發者每次呼叫連線事件消費者的時候還得先把它喚醒,這都是不能忍的。所以Nginx的整體實現難度要比傳統的web伺服器高很多。

Nginx事件處理大致圖如下(畫的有點醜):

寫給後端的Nginx初級入門教程:Nginx原理初探

請求的多階段非同步處理:

既然說到了多階段,在Nginx能夠把單個請求分割成多個階段的也只有事件驅動機制了,所以請求的多階段非同步處理實際上就是基於Nginx本身的事件驅動架構實現的。

比如獲取靜態檔案的HTTP請求就可以劃分為以下七個階段:

階段 觸發事件
建立tcp連線 接收到tcp中的SYN包
開始接收使用者請求 接收到TCP中的ACK包表示連線建立成功
接收到使用者請求並分析已經接收到的請求是否完整 接收到使用者的資料包
接收到完整的使用者請求後開始處理使用者請求 接受到使用者的資料包
由目標靜態檔案中讀取部分內容,並直接傳送給使用者 接收到使用者的資料包,或者接收到TCP中的ACK包表示使用者已經接收到上次傳送的資料包,TCP滑動視窗向前滑動。
對於非keep-alive請求,再傳送完靜態檔案之後主動關閉連線。 接收到TCP中的ACK包表示使用者已經收到之前傳送的所有資料包。
由於使用者關閉連線而結束請求 接收到TCP中的FIN包。

當然,對於很多計算機網路基礎較差的同學不是特別明白也沒有關係,我們這篇文章並不是去分析Nginx這些操作是如何具體去實現的,而是去巨集觀的瞭解Nginx具體用了一種什麼樣的思路去設計和實現的。

大家這樣去理解,每個響應的事件都會有對應的專門的事件消費者去處理,由於是單一的任務(比如只處理連線或者關閉),這對於每一個事件消費者來說都是相對容易且處理迅速的,負責tcp連線的事件消費者處理過之後可以馬上投入到下一個tcp連線事件的處理中,這樣可以使得我們每個事件消費者程式都一直在馬不停蹄的全速工作,在高併發的情況下就很少有程式休眠這種情況的發生,因為在高併發的場景下,每個程式要處理的事件是非常多的,哪有功夫去睡覺。而傳統的web伺服器,一旦出現程式休眠,對於使用者的感知就是請求的響應變慢了,而在高併發的場景下,由於一個請求對應一個程式(或執行緒),這個時候,如果程式不夠了,系統就會去建立更多的程式,程式間的切換都會佔用相當多的作業系統的資源,從而導致我們網路效能的下降。

可是如何把一個請求劃分成多個階段的呢?一般是找到請求處理流程中的阻塞方法。

比如在使用send呼叫傳送資料給使用者時,如果使用阻塞socket控制程式碼,當send在向作業系統核心發出資料包之後就必須把當前程式休眠,直到資料成功傳送之後才能醒來。而Nginx根據不同的觸發事件把send這個過程分成兩個階段:

  1. 向作業系統核心發出資料包,不等待結果
  2. send結果返回。

因此就可以使用非阻塞的socket控制程式碼,然後把socket控制程式碼加入到事件中,也就是你發吧,我先幹別的事兒,發完了通過事件告訴我,我再來處理資料包的事兒。

而在大檔案中,也可以把阻塞的方法按照時間分解成多個階段的方法呼叫,比如在沒有開啟非同步IO的情況下,把1000M 的檔案處理成1000份,每份1M,處理完這1M,馬上處理其他的事情,然後再回來接著依次處理剩下的999M,這樣的好處是,每次處理1m,我們能先騰出手來去處理一下其他的事情,而不是一下子處理1000M,乾等著傳送完。

如果實在沒有辦法把阻塞的操作拆分成多個階段處理,Nginx便會派一個新的程式去單獨處理這個阻塞方法,完成之後再傳送完成事件通知。這樣雖然方法是阻塞的,但是由於是額外的程式在處理,對其他的請求處理的影響是相對來說較小的。

管理程式+多工作程式的設計:

Nginx採用master - worker 機制,這樣對於每個worker程式來說,由於是獨立的程式,所以也避免了鎖帶來的額外開銷,如果有多個CPU的情況下,多個worker程式佔用不同的CPU核心來工作,提高了網路效能,降低了請求的平均時延,畢竟再怎麼說,十個程式也要比一個程式處理起來快一點。

而我們master程式並不針對請求做處理,主要是用來管理和監控我們其他的worker程式,所以master並不會佔用特別多的系統資源,同時還能通過程式通訊做到worker之間的負載均衡,比如請求來的時候,優先分配給壓力較小的worker程式去處理。同樣的,比如我們單個worker程式掛了,由於程式之間是獨立的,所以並不會影響到其他worker程式的處理。提高了整個系統的可靠性,降低了由於單個程式掛掉導致整個應用掛掉的風險。

如圖所示:

寫給後端的Nginx初級入門教程:Nginx原理初探

下面開始技術總結:

今天呢,作為寫給後端的Nginx初級入門教程最後一篇,原理篇,我們通過對Nginx架構的設計的簡單探索非常淺顯地瞭解了一下Nginx內部是如何設計和工作的,總的來說,本篇文章內容較為基礎,對程式碼層面上的分析幾乎沒有提到,主要原因第一呢,考慮到這是一篇初級入門教程,所以並沒有在程式碼設計上做很深的分析,更多的是架構設計,實現思路上面的巨集觀解釋,至少讓我們在不瞭解程式碼實現之前可以粗略地知道Nginx是如何運作的,第二個則是Nginx原始碼太過複雜,不是我這樣的菜鳥可以分析透徹的(這個是主要原因)。

最後,非常感謝閱讀本篇文章的小夥伴們,能夠幫助到你們對於我來說是一件非常開心的事兒,如果有什麼疑問或者批評歡迎留言到本篇文章下方,有時間的話我會一一回復。

韓數的學習筆記目前已經悉數開源至github,一定要點個star啊啊啊啊啊啊啊

萬水千山總是情,給個star行不行

韓數的開發筆記

歡迎點贊,關注我,有你好果子吃(滑稽)

附:寫給後端的Nginx初級入門教程所有文章連結:

寫給後端的Nginx初級入門教程:基礎篇

寫給後端的Nginx初級入門教程:實戰篇

寫給後端的Nginx初級入門教程:配置高可用叢集

相關文章