文章來源:http://blog.seclibs.com/nginx學習-架構/php
Nginx程序架構圖以下html
後面就先按照這個圖所展現出來的內容對Nginx的架構進行一次梳理,文中所涉及到的內容,主要都是針對Linux系統的。python
最上面的Master進程是管理員直接控制的,也只有Master進行接受管理員信號,一個Master用戶能夠fork多個Worker進程,一個Worker進程能夠響應多個用戶請求。linux
每一個Worker都是由核心模塊core和多個模塊modules組成的,好比有http協議的ht_core模塊,爲了功能完善還有不少其它模塊,如實現負載均衡的ht_upstream模塊,ht_proxy反代模塊,ht_fastcgi模塊,memcache模塊等。nginx
由於Nginx是高度模塊化的,咱們在用到哪一個模塊的時候,便將哪一個模塊編譯或者載入就能夠了,好比基於ht_core能夠與web通訊, 基於ht_fastcgi模塊可與php通訊,基於memcache模塊可與mamcache通訊。web
這裏來解釋一下什麼是FastCGI,CGI全稱」通用網關接口」(Common Gateway Interface),用於HTTP服務器與其它機器上的程序服務通訊交流的一種工具,CGI程序須運行在網絡服務器上;可是因爲CGI的性能和安全性都比較差,處理高併發幾乎是不可用的,因此就誕生了FastCGI,FastCGI是一個可伸縮地、高速地在HTTP服務器和動態腳本語言間通訊的接口(FastCGI接口在Linux下是socket(能夠是文件socket,也能夠是ip socket)),主要優勢是把動態語言和HTTP服務器分離開來。多數流行的HTTP服務器都支持FastCGI,包括Apache、Nginx和lightpd,同時它也被許多腳本語言所支持,包括PHP等。shell
Nginx不支持對外部動態程序的直接調用或者解析,全部的外部程序(包括PHP)必須經過FastCGI接口來調用。FastCGI接口在Linux下是socket(能夠是文件socket,也能夠是ip socket)。爲了調用CGI程序,還須要一個FastCGI的wrapper,這個wrapper綁定在某個固定socket上,如端口或者文件socket。當Nginx將CGI請求發送給這個socket的時候,經過FastCGI接口,wrapper接收到請求,而後派生出一個新的線程,這個線程調用解釋器或者外部程序處理腳本並讀取返回數據;接着,wrapper再將返回的數據經過FastCGI接口,沿着固定的socket傳遞給Nginx;最後,Nginx將返回的數據發送給客戶端,這就是Nginx+FastCGI的整個運做過程。數據庫
FastCGI的主要優勢是把動態語言和HTTP服務器分離開來,是Nginx專注處理靜態請求和向後轉發動態請求,而PHP/PHP-FPM服務器專注解析PHP動態請求。後端
說完Nginx+FastCGI後,繼續說前面的模塊,memcache是一個分佈式的高速緩存系統,經常使用來作緩存服務器、將從數據庫查詢的數據緩存起來,減小數據庫查詢、加快查詢速度。緩存
而後說圖中用戶與Nginx交互的部分,在與用戶請求進行交互的時候,經過kevent、epoll和select來實現多路複用,實現處理併發用戶請求,這裏是由於Nginx採用的是多進程(單線程)的模式,採用多進程能夠提升併發效率,而且各進程之間相互獨立,一個Worker進程掛掉以後不會影響其餘進程的運行。
到這裏爲止上面圖中上半部分就說完了,主要就是nginx與用戶和後端程序之間的關係,此外Nginx還提供了緩存機制,支持高級I/O機制、sendfile機制、AIO機制(異步非阻塞I/O)、mmap機制等。
咱們先來講sendfile機制,在網上查到的全部資料裏都提到sendfile機制能夠提升文件傳輸的性能。在傳統的文件傳輸中,使用的是read/write方式來進行文件與socket的傳輸,所須要通過的流程是這樣的
總結一下就是硬盤—>內核buf—>用戶buf—>socket相關緩衝區—>協議引擎,用一張圖來表示,就是這個樣子的
而在引入sendfile機制之後,數據的流程變爲了這樣
能夠發現,引入sendfile機制之後,省去了拷貝到用戶buf的過程,流程就變成了下圖的樣子
nginx在支持了sendfile系統調用後,避免了內核層與用戶層的上線文切換(content swith)工做,大大減小了系統性能的開銷。
前面說了這麼多,都沒有繞開內核緩衝區和用戶緩衝區,那它們分別又是什麼東西?
這裏首先先區分一下緩衝區buffer和緩存cache是兩個徹底不一樣的東西,buffer是減小調用次數,集中調用,提升系統性能的,而cache是將讀取過的數據保留下來,若是從新讀取的時候發現已經讀取過,就不須要再去讀硬盤數據了。
上圖是一個計算機系統運行時的簡化模型,在說用戶進程和系統進程以前還須要再說一下內核態(kernel mode)和用戶態(user mode),內核態能夠訪問系統資源,好比CPU、IO設備、進程管理、內存、進程間通訊IPC、網絡通訊等,這些資源在用戶進程中是不能直接訪問的,須要通過操做系統才能夠,這些有操做系統提供的功能也叫作系統調用。
下圖是用戶經過shell來對文件進行操做的示例圖,它們都是通過內核來進行操做的,而提供這些限制的基礎就是CPU提供的內核態和用戶態。
前面說了用戶進程在訪問系統資源的時候,須要先切換到內核態,在這以前有不少的堆棧、內存環境等須要提早準備好,在調用結束之後,必須恢復到用戶態,這其中的堆棧等又必須回到用戶進程的上下文,這其中的切換就會消耗大量的資源。因此用戶緩衝區就是在讀取文件的時候申請的一塊內存空間,也就是buffer,而後程序都是從buffer中獲取數據的,只有在內存空間使用完後纔會進行下一次調用來填充buffer,這樣就減小了系統調用的次數,減小了在用戶態和內核態之間切換的消耗時間。
固然內核也有它本身的緩衝區,在用戶進程要從磁盤讀取數據的時候,內核通常不會去讀磁盤,而是將內核緩衝區中的數據複製給用戶進程緩衝區,若是內核緩衝區沒有數據的話,內核會將請求加入到請求隊列中,而後將進程掛起,去處理其餘的進程,直到內核緩衝區讀取到數據之後,纔會將內核緩衝區中的數據複製給用戶進程緩衝區,而後通知進程,固然不一樣的io模型,在調度方式上也是有一些差別的;因此內核緩衝區就在OS級別,提升了磁盤的IO效率。
到這裏sendfile機制也就說完了,接着用剛剛講的內核和用戶進程的知識來講一下AIO機制(異步非阻塞I/O),咱們先來看阻塞,先上圖,看一下阻塞和非阻塞的區別
在兩個圖的對比當中,能夠看到,在阻塞IO中,若是數據沒有準備好你就只能等着,直到數據準備好之後才能夠再繼續執行,這對於Nginx的Worker來講明顯是很不適用的,而非阻塞的IO,當數據沒有準備好時,我能夠返回先去作其餘的事情,過一會再來問一下,若是沒有準備好,我再去處理其餘的事情,直到你準備好,我過來開始拷貝數據,在這期間明顯能夠處理不少的事情,對於大量訪問的時候也是很是好的,可是這樣還有一個問題,雖然非阻塞,可是每隔一段時間就須要請求一下,也是很是浪費資源的,因此也就有了異步,也就是提供一種機制(select/poll/epoll/kquene這樣的系統調用),讓你能夠同時監控多個事件,調用他們是阻塞的,可是能夠設置超時時間,在超時時間以內,若是有事件準備好了就返回。
這樣對於大量的併發就很是的友好了,這裏的併發請求,是指未處理完的請求,線程只有一個,同時處理的請求只有一個,只是在請求間不斷切換,切換是由於異步事件未準備好,主動讓出的。這裏的切換沒有什麼代價,能夠理解爲在循環處理多個準備好的事件;與多線程相比,不需建立線程,每一個請求佔用的內存也不多,沒有上下文切換,事件處理很是輕量級,沒有上下文切換的開銷,更多併發,只會佔更多的內存,這也是如今的網絡服務器基本都使用的方式。
最後還有一個mmap機制,mmap機制也就是內存映射,傳統的web服務器進行頁面輸入的時候,都是將硬盤的頁面先輸入到內核緩衝區,再有內核緩衝區複製一份到web服務器上,mmap機制就是讓內核緩衝區與磁盤進行映射,web服務器直接複製頁面內容便可,省去了從硬盤複製到內核緩衝區這一過程。
上面就是把Nginx所涉及到的功能都說了一遍,從總體角度來看Nginx的功能是這樣的。
咱們啓動Nginx的時候首先會啓動一個Master進程,Master進程會根據配置文件的要求fork相應個數的Worker進程(推薦設置worker數與cpu的核數一致,由於更多的worker,會致使進程競爭cpu資源,從而帶來沒必要要的上下文切換,設置爲auto即爲與cpu一致),當Worker進程接收客戶端請求時,若是使用緩存功能,會從緩存中加載數據直接返回給客戶端,若是客戶端請求的內容內存中沒有,就會將請求代理到後端服務器取資源,若是後端服務器是HTTP就使用http模塊,若是是php就是用FastCGI模塊,而後取出數據後又會將數據在本地緩存下來以提升性能,緩存時基於key-value結構,檢索性能時O(1)恆定不變,把key緩存在內存中,檢索起來也是很是迅速的。
在Worker接受請求這裏還有一些操做,這裏來補充一下。
在Nginx啓動後,Master進程fork出的多個Worker進程,Master能監控Worker進程的運行狀態,若是有Worker異常退出後,會自動啓動新的Worker進程,在Nginx0.8以前,咱們是直接給Master進程發信號的,在重啓或者從新加載配置的時候,Master進程在接收到信號以後,會先從新加載配置文件,而後再啓動新的Worker進程,並向全部老的Worker進程發送再也不接受新請求的信號,而且在處理完全部未處理完的請求後退出,新的Worker在啓動後,就開始接受新的請求了;在Nginx0.8以後,咱們不會直接對Master發送信號了,好比在執行**./nginx -s reload**的時候,會啓動一個新的Nginx進程,該進程解析到reload參數後,知道要從新加載配置文件,它就會向Master進程發送信號,以後的處理與以前直接給Master進程發信號同樣了。
還有一點就是所謂的「驚羣現象」,在啓動後,Master進程首先經過socket()來建立一個sock文件描述符來監聽,而後fork出的Worker進程會繼承父進程Master的socket文件描述符sockfd,以後Worker進程accept()後將建立已鏈接描述符(connected descriptor),而後經過已鏈接描述符來與客戶端通訊,因爲每個Worker進程都擁有Master的sockfd,那當連接進來的時候,全部的Worker都會收到通知,而且爭着去創建連接,這就是「驚羣現象」,這時大量的進程被激活又掛起,只有一個進程能夠accept()到這個鏈接,就消耗了大量的系統資源。在Nginx中提供了一個accept_mutex的東西,這是在accept上加了一把共享鎖,即每一個Worker進程在執行accept以前都須要先獲取鎖,獲取不到就放棄執行accept(),只有有了這把鎖纔會去accept(),同一時刻就只有一個Worker進程去接收請求了,這樣就不會出現驚羣問題了。
可是我在查資料的時候發現了另一個狀況
> 在對啓動了20個worker的nginx進行壓力測試的時候發現:若是把配置文件中event配置塊中的accept_mutex開關打開(1.11.3版本以前默認開),就會出現worker壓力不均,少許的worker的cpu利用率達到了98%,大部分的worker的壓力只有1%左右;若是把accept_mutex關掉,全部的worker的壓力差異就不大,並且QPS會有大幅提高; > > 引用自博客園-sxhlinux
根據他的測試,在請求屬於大量短連接的時候,打開accept_mutex選項是一個比較好的選擇,避免了Worker爭奪資源而形成的上下文切換以及try_lock的鎖的開銷,可是對於傳輸大量數據的TCP長連接來講,打開accept_mutex將會致使壓力集中在某個Worker進程上,特別是將worker_connection值設置過大的時候,影響更加明顯,具體的設置,須要根據實際狀況來進行斷定。並且目前新版的Linux內核中增長了EPOLLEXCLUSIVE選項,nginx從1.11.3版本以後也增長了對NGX_EXCLUSIVE_EVENT選項的支持,這樣就能夠避免多worker的epoll出現的驚羣效應,今後以後accept_mutex從默認的on變成了默認off。
很明顯全部的文件配置都與具體的實際狀況有關,要更好的配置好相關內容必須對全部的內容都要有一個詳細的瞭解才能夠。
參考文檔
Zero Copy I: User-Mode Perspective
文章首發公衆號和我的博客
公衆號:無意的夢囈(wuxinmengyi)