使用過nginx的朋友都知道nginx的性能很高,而其緣由可能少有人知。首先,nginx的架構就奠基了其高性能的基礎。那麼就先來看看nginx的基礎架構吧,以下圖所示:(不能徹底理清楚全部內容也不要緊,由於本小節講述的主要內容是Nginx的進程模型)html
本小節先來講說Nginx基礎架構中的進程模型:linux
所謂進程模型,即Nginx響應請求或服務時程序運行(機器執行指令集)的方式,通常在nginx服務啓動後,在Unix系統中會以daemon的方式在後臺運行,後臺進程包含一個master進程以及多個worker進程。nginx
而在咱們進行調試時,能夠手動關閉後臺模式以及設置nginx取消master進程,使其以單進程方式運行,可是生產環境中確定不容許這樣上線服務,主流的運行方式仍是默認使用多進程。c++
這裏插一下關於進程和程序的區別:shell
其實這二者徹底不是一個概念,程序只是一堆等待執行的代碼和部分待處理的數據,只有被加到內存中,由CPU處理執行代碼,才能夠發揮其做用,從而造成一個真正的「活的」動態的進程。因此兩者的典型區別不言而喻,程序未被執行時是靜態的,只有運行時才變成動態的進程,而且進程善始善終(進程的「生老病死」)。編程
言歸正傳,仍是回來講nginx的多進程模式。nginx啓動後,會有如上圖所示的一個master進程和多個worker進程。vim
master進程:負責管理worker進程,接收、發送信號,監控worker進程狀態;操做控制Nginx只須要經過與master進程通訊就能夠了。服務器
worker進程:負責處理基本網絡事件,每一個worker進程都是平等且獨立的(記住獨立兩個字)。通常worker進程數會設置爲機器(服務器)的CPU核心數(緣由下文會講到)。網絡
咱們從nginx啓動時的工做流程以及啓動後所響應外部的操做來理解nginx處理一個鏈接的工做原理及過程。數據結構
Nginx啓動時,會先解析配置文件,獲取須要監聽的ip地址與端口號(涉及網絡編程以及TCP/IP理論),而後在master進程中,首先初始化好這個監控的socket,而後fork多個子進程,子進程競爭(例如互斥鎖機制)accept新的鏈接。此時,Nginx以及啓動好,等待客戶端來鏈接本身。
fork——屬於系統編程內容,原意爲叉子,可能老外使用的是叉子,而決定使用這個單詞來比喻fork的做用,fork函數的功能就是建立(有時候理解爲派生)出多個子進程
客戶機發起鏈接請求,經過TCP三次握手與Nginx創建一個鏈接,此時競爭成功的一個worker進程獲取到這個創建好鏈接的socket,開始建立nginx對鏈接的封裝(其實說白了就是結構體封裝)。隨之執行讀寫事件(本文所述的事件理解爲網絡事件便可)函數、添加讀寫事件來與客戶端進行數據交互。最後,其中一方主動斷開鏈接(四次揮手),到此,一個鏈接也就壽終正寢了(該worker進程被宣告退休)。
當咱們執行命令kill -HUP pid時,master進程是如何響應的呢?
首先,master接收到kill(不要簡單理解爲殺死,不少人都保持着這樣的理解,可是這樣不許確,在linux系統中,kill表示的是用於向進程發送信號的,可使用kill -l查看能夠攜帶的信號)的信號時,首先會重載配置文件,而後啓動新的worker進程,而且向舊的worker進程發送信號,提示他們作完當前事件以後就能夠光榮退休了。新的worker進程啓動後,開始接收新的請求。這種方式就是直接給mater進程發送信號。
而在新的版本中,可使用其餘方式例如:./nginx -s reload就是重啓服務,./nginx -s stop就是中止服務,執行./nginx -s reload時會啓動一個新的進程(nginx),該進程解析到reload的時候,就控制nginx重載配置文件,向master進程發送信號了,隨後就和上面舊版本的方式同樣的過程了。
以上就是nginx內部工做(內部究竟幹了啥活)了,可是此時考慮一個問題:worker進程如何處理請求的呢?
既然worker進程是由master進程fork出來的,而且每一個worker進程都是對等的,那麼當一個請求過來時,任何一個worker進程都有可能處理它,這個時候怎麼辦呢?
爲了保證只有一個進程處理該鏈接請求,全部的worker進程會進行競爭,觸發鎖機制,通常是互斥鎖,搶到鎖的進程得到權利來處理這個請求(讀事件開始調用accept接受該鏈接),進行讀取、解析、處理請求,將結果(數據信息)返回給客戶端,最後斷開鏈接,這個請求完整走完它的人生。所以,一個請求徹底是由也僅僅由一個worker進程處理。
這個問題其實不是特別容易說,沒有進程線程的理論基礎可能也沒法理解原理,這邊給出其具有的」獨立「特徵所帶來的優點吧。
」獨立「的進程,意味着不須要加鎖,節省開銷,而且多個worker進程之間互不影響,某一個出現bug後,會有新的worker進程開始工做,從而下降風險,也不會中斷其餘服務,同時也簡化了編程和檢查問題。
那麼,問題又來了,使用多進程模型,每一個worker進程中也只有一個主線程,如何能夠處理高併發呢?
且看下節。
舉個例子:阻塞就比如下課你去食堂吃飯,但去的時候人太多了,你就傻傻地在原地排隊等。
非阻塞就比如是你去食堂吃飯,人依舊不少,可是你能夠先去上個洗手間,看會兒資訊,不影響你幹這些事情。充分利用時間,這個時間就比如是CPU的使用率,非阻塞的存在能夠避免浪費CPU資源。
上面說的非阻塞,在nginx應用時,雖然不阻塞了,但你得不時地過來檢查一下事件的狀態,你能夠作更多的事情了,但帶來的開銷也是不小的。因此,纔會採起異步方式。
舉個例子:同步就是你有不懂的問題問同事,他給你開始講解解決思路或方案,你一直在主動聽取他的內容,異步就是一樣你問同事問題,他可能說我先考慮考慮,想出來了主動來告訴你。
異步方式+非阻塞處理請求能夠避免浪費CPU資源,同時提升響應速度,工做效率。其實本質上就是說worker進程,在循環執行異步請求(事件),從而處理高併發。
所以,設置worker的個數爲cpu的核數,在這裏就很容易理解了,更多的worker數,只會致使進程來競爭cpu資源了,從而帶來沒必要要的資源浪費。
結尾給出Nginx源碼目錄介紹吧。
解壓nginx軟件,進入其目錄就能夠看到它的目錄結構以下:
[root@localhost nginx-1.16.1]# tree -d . ├── auto #自動編譯安裝相關目錄 │ ├── cc #針對各類編譯器進行相應的編譯配置目錄,包括gcc、ccc等 │ ├── lib #程序依賴的各類庫,包括openssl、pcre 、perl等 │ │ ├── geoip │ │ ├── google-perftools │ │ ├── libatomic │ │ ├── libgd │ │ ├── libxslt │ │ ├── openssl │ │ ├── pcre │ │ ├── perl │ │ └── zlib │ ├── os #針對不一樣的操做系統所作的編譯配置目錄 │ └── types #與數據類型相關的一些輔助腳本 ├── conf #存放默認配置文件,在make install後,會拷貝到安裝目錄中去 ├── contrib #存放一些實用工具,如geo配置生成工具(geo2nginx.pl │ ├── unicode2nginx # │ └── vim # │ ├── ftdetect # │ ├── ftplugin # │ ├── indent # │ └── syntax # ├── html #存放默認的網頁文件,在make install後,會拷貝到安裝目錄中去 ├── man #手冊 └── src #存放nginx的源代碼 ├── core #nginx的核心源代碼,包括經常使用數據結構的定義,以及nginx初始化運行的核心代碼如main函數 ├── event #對系統事件處理機制的封裝,以及定時器的實現相關代碼 │ └── modules #不一樣事件處理方式的模塊化,如select、poll、epoll、kqueue等 ├── http #nginx做爲http服務器相關的代碼 │ ├── modules #包含http的各類功能模塊 │ │ └── perl # │ └── v2 # ├── mail #nginx做爲郵件代理服務器相關的代碼 ├── misc #一些輔助代碼,測試c++頭的兼容性,以及對google_perftools的支持 ├── os #主要是對各類不一樣體系統結構所提供的系統函數的封裝,對外提供統一的系統調用接口 │ └── unix # └── stream #實現四層協議的轉發、代理或者負載均衡等第三方模塊
對於使用者而言最關鍵的是conf目錄,html目錄,對於開發者而言可能須要看其源碼文件:目錄爲src,這就涉及到nginx的核心部分,包括模塊、模塊對應的功能等。
參考連接:tengine.taobao.org