在上一篇文章寫給後端的Nginx初級入門教程:配置高可用集羣 中,咱們使用keepalived實現了咱們Nginx服務器的高可用配置,防止由於Nginx服務器掛掉而致使整個應用掛掉的這種狀況的發生。而Nginx做爲當下最受歡迎的web服務器軟件之一,能作到現在的地位和成就也並非沒有緣由的,優秀的性能表現,可伸縮性,可修改性的設計,同時跨平臺的特性以及很是低的故障率都是Nginx現今如此受歡迎的重要因素,那Nginx總體架構又是如何設計的呢?本篇文章呢(因爲要了解Nginx核心技術須要很是深的技術內力,我沒有(大哭)),咱們將輕輕的稍微揭開Nginx神祕面紗的一角,去探索一下Nginx內部是如何設計與運做的。java
Nginx能如此受歡迎,而且企業所普遍採用,必定是有不少技能點滿了的,通過查閱相關資料,前輩們一共總結出來六點Nginx服務器相對於其餘類型的web服務器軟件作的更加優秀的地方,也是Nginx設計之初主要關注的地方。它們分別是:linux
性能我想沒必要多說,這是Nginx能混到如今的核心資本,即便Nginx在其餘方面作的很優秀,若是性能比不上其餘的web服務器,在如今這個你們廣泛比較看重性能的時代,Nginx有較大機率會受到冷遇。而Nginx和傳統的程序不同的是,其餘程序好比遊戲可能會須要計算性能,圖形渲染,網絡渲染性能,而Nginx做爲一款web服務器,就只能在網絡領域和別人一決雌雄了。git
Nginx在網絡性能這塊作了大量的工做,包括使用事件驅動架構配合請求的多階段異步處理,以及Master-workers機制的使用,都保證了在高併發場景下Nginx所展示出來的出色性能表現。github
可伸縮性也能夠理解爲可擴展性,好比谷歌瀏覽器的插件,火狐瀏覽器的插件等等(沒想到什麼合適的例子),Nginx支持添加相關的模塊來加強咱們的服務,同時優秀的模塊化設計容許咱們定製或者採用第三方開發的模塊來知足咱們額外的業務需求。web
簡單性一般指的是組件的簡單程度,每一個組件越簡單,就會越容易理解和實現,也更容易被驗證。固然開發Nginx組件不能爲所欲爲,同時要遵循Nginx模塊開發統一的規範,而Nginx模塊接口很是簡單,具備很高的靈活性。後端
Nginx基於BSD開源,這意味當Nginx某些功能不能知足咱們其餘的額外需求時,咱們能夠修改它的代碼來達到咱們的業務要求。同時Nginx也支持在咱們不重啓,中止服務的前提下,修改咱們web服務器的某些配置並使之生效。(平滑重啓)瀏覽器
可見性呢,就是咱們整個應用對使用者的透明程度,開放程度。Nginx 有 http_stub_status_module 來實現基礎的可見性,可讓咱們瞭解到Nginx 當前一共創建了多少個連接,處理了多少個請求等等,這些監控參數可讓運維人員更好的瞭解Nginx服務總體運行的情況,並及時的作出調整。好比當 Reading + Writing 數值比較高的時候,就意味着咱們當前的應用併發量仍是比較大的。tomcat
因爲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一共有六個核心模塊,用來處理咱們常見的
這樣作有什麼好處呢,這意味着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自己的事件驅動架構實現的。
好比獲取靜態文件的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這個過程分紅兩個階段:
所以就可使用非阻塞的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源碼太過複雜,不是我這樣的菜鳥能夠分析透徹的(這個是主要緣由)。
最後,很是感謝閱讀本篇文章的小夥伴們,可以幫助到大家對於我來講是一件很是開心的事兒,若是有什麼疑問或者批評歡迎留言到本篇文章下方,有時間的話我會一一回復。
韓數的學習筆記目前已經悉數開源至github,必定要點個star啊啊啊啊啊啊啊
萬水千山老是情,給個star行不行
歡迎點贊,關注我,有你好果子吃(滑稽)
附:寫給後端的Nginx初級入門教程全部文章連接: