一、Nginx核心原理
?????本節為大家介紹Nginx的核心原理,包含Reactor模型
、Nginx的模塊化設計、Nginx的請求處理階段.
(本文源自微博客,且已獲得授權)
1.1、Reactor模型
?????Nginx對高并發IO的處理使用了Reactor事件驅動模型
。Reactor模型的基本組件包含時間收集器、事件發送器、事件處理器3個基本單元,其核心實現四將所有要處理的I/O事件注冊到一個中心I/O多路復用器上,同時主線程/進程阻塞在多路復用器上,一旦有I/O時間到來或者準備就緒(文件描述符或Socket可讀、寫),多路復用器返回并將事先注冊的響應I/O事件分發到對應的處理器中。
?????在Reactor模式中,時間收集器、事件發送器、事件處理器則3個基本單元的職責分別如下:
- 時間收集器:負責收集Worker進程的各種I/O請求
- 事件發送器:負責將I/O時間發送到事件處理器
- 事件處理器:負責各種時間的響應工作
Nginx的Reactor模型的設計大致如下:
?????時間收集器將各個連接通道的IO事件放入一個待處理時間列,通過事件發送器發送給對應的事件處理器來處理。而時間收集器之所能夠同時管理上百萬連接通道的事件,是基于操作系統提供的“多路IO復用”
技術,常見的包括select、epoll兩種模型。
?????正是由于Nginx使用了高性能的Reactor模式,因此是目前并發能力很高的Web服務器之一,成為迄今為止使用廣泛的工業級Web服務器。當然,Nginx也解決了著名的網絡讀寫是C10K
問題。什么是C10K問題呢?網絡服務在處理數以萬計的客戶端連接時,往往出現效率低下甚至完全癱瘓,這類問題就被成為C10K問題。
1.2、Nginx的兩類進程
?????一般來說,Nginx在啟動以后會與daemon
方式在后臺運行,其后臺進程有兩種: 一類稱為Master進程
(相當于管理進程);另一類稱為Worker進程
(工作進程)。Nginx的進程結構大致如下:
?????Nginx啟動方式有兩種:
- 單進程啟動:此時系統中僅有一個進程,該進程既充當Master管理進程角色,又充當Worker工作進程角色
- 多進程啟動:此時系統有且僅有一個Master管理進程,至少有一個Worker工作進程。
(有點類似于RocketMQ的模式)
?????一般來說,單進程模式用于調試。在生產環境中,一般會配置成多進程模式,并且Worker工作進程的數量和機器CPU核心數配置不一樣多。
?????了解Worker工作進程之前,首先了解一下Master管理進程的主要工作,主要有以下兩點:
- Master管理進程主要負責調度Worker工作進程,比如加載配置、啟動工作進程、接收來自外界的信號、向各Worker進程發送信號、監控Worker進程的運行狀態等。所以Nginx啟動以后,我們能看到至少兩個Nginx進程
- Master負責創建監聽套接口,交由Worker進程進行連接監聽。
?????接下來介紹Nginx的Worker進程。Worker進程主要用來處理網絡事件。當一個Worker進程在接受一條連接通道之后,就開始讀取請求、解析請求、處理請求,處理完成產生數據以后,再返回給客戶端,最后斷開連接通道。
?????各個Worker進程之間是對等且相互獨立的,它們同等競爭來自客戶端的請求,一個請求只可能在一個Worker進程中處理。則都是典型的Reactor模型中Worker進程(或者線程)的職能。
?????如果啟動了多個Worker進程,那么每個Worker子進程獨自嘗試接受已連接的Socket監聽通道,accept操作默認會上鎖,優先使用操作系統的共享內存原子鎖,如果操作系統不支持,就是用文件上鎖。
?????經過配置,Worker進程的接受操作也可以不適用鎖,在多個進程同時接受時,當一個連接進來的時候多個工作進程同時被喚起,則會導致驚群問題
。而在上鎖的場景下,只會有一個Worker阻塞在accept上,其他的進程會因為不能獲取鎖而阻塞,所以上鎖的場景不存在驚群問題。
1.3、Nginx模塊化設計
?????Nginx服務器被分解為多個模塊,模塊之間嚴格遵循“高內聚,低耦合”的原則,每個模塊都聚焦于一個功能。高度模塊化的設計是Nginx的架構基礎。
?????什么是Nginx模塊呢?在Nginx的實現中,一個模塊包含一系列命令(cmd)和這些命令相對應的處理函數(cmd→handler)。Nginx的Worker進程在執行過程中會通過配置文件的配置指令定位到對應的功能模塊的某個命令(cmd),然后調用命令對應的處理函數來完成相應的處理。
?????Nginx的Worker進程首先會調用Nginx的Core核心模塊。大家知道,在Reactor模型中會維護一個運行循環(Run-Loop),主要包括事件收集、事件分發、事件處理,這個工作在Nginx中由Core核心模塊負責。Core模塊負責執行網絡請求處理的基礎操作,比如網絡讀寫、存儲讀寫、內容傳輸、外出過濾以及將請求發往上游服務器等。
?????Nginx的Core模塊是啟動時一定會加載的,其他的模塊只有在解析配置時遇到了這個模塊的命令才會加載對應的模塊。Core模塊為其他模塊構建了基本的運行時環境,并成為其他各模塊的協作基礎。
?????除了Core模塊外,Nginx還有Event、Conf、HTTP、Mail等一系列模塊,并且可以在編譯時加入第三方模塊。Nginx的模塊結構如圖:
?????這里對Nginx的主要模塊說明如下:
- Core核心模塊:核心模塊是Nginx服務器正常運行必不可少的模塊,提供錯誤日志記錄、配置文件解析、Reactor事件驅動機制、進程管理等核心功能。
- 標準HTTP模塊:標準HTTP模塊提供HTTP協議解析相關的功能,比如端口配置、網頁編碼設置、HTTP響應頭設置等。
- 可選HTTP模塊:可選HTTP模塊主要用于擴展標準的HTTP功能,讓Nginx能處理一些特殊的服務,比如Flash多媒體傳輸、網絡傳輸壓縮、安全協議SSL的支持等。
- 郵件服務模塊:郵件服務模塊主要用于支持Nginx的郵件服務,包括對POP3協議、IMAP協議和SMTP協議的支持。
- 第三方模塊:第三方模塊是為了擴展Nginx服務器的功能,定制開發者自定義功能,比如JSON支持、Lua支持等。
?????Nginx的非核心模塊可以在編譯時按需加入,這里不再贅述。
?????總之,Nginx通過模塊化設計使得大家可以根據需要對功能模塊進行適當的選擇和修改,編譯成具有特定功能的服務器。
1.4、Nginx配置文件上下文結構
?????前面介紹到,一個Nginx的功能模塊包含一系列的命令(cmd)以及與命令對應的處理函數(cmd→handler)。而Nginx根據配置文件中的配置指令就知道對應到哪個模塊的哪個命令,然后調用命令對應的處理函數來處理。
?????一個Nginx配置文件包含若干配置項,每個配置項由配置指令和指令參數兩部分組成,可以簡單認為配置項是一個鍵-值對。圖中有3個簡單的Nginx配置項:
?????Nginx配置文件中的配置指令如果包含空格,就需要用單引號或雙引號引起來。指令參數如果是由簡單的字符串構成的,簡單配置項就需要以分號結束;指令參數如果是復雜的多行字符串,配置項就需要用花括號“{}”括起來。
?????Nginx配置項的具體功能與其所處的作用域(上下文、配置塊)是強相關的。Nginx指令的作用域配置塊大致有5種,它們之間的層次關系如圖:
一個標準的Nginx配置文件的上下文結構如下:
nginx
#main全局配置塊,例如工作進程數
events { #events事件處理模式配置塊,例如IO讀寫模式、連接數等}
http #HTTP協議配置塊
{#HTTP協議的全局配置塊server #server虛擬服務器配置塊一{#server全局塊location [PATTERN] #location路由規則配置塊一{}location [PATTERN] #location路由規則配置塊二{}}server #server虛擬服務器配置塊二{}
#其他HTTP協議的全局配置塊
}
mail #mail服務配置塊
{#email相關協議,如SMTP/IMAP/POP3的處理配置
}
對以上作用域(上下文、配置塊)說明如下:
1.4.1、main全局配置塊
?????配置影響Nginx全局的指令,一般有運行Nginx服務器的用戶組、Nginx進程PID存放路徑、日志存放路徑、配置文件引入、允許生成的Worker進程數等。
1.4.2、events事件處理模式配置塊
?????配置Nginx服務器的IO多路復用模型、客戶端的最大連接數限制等。Nginx支持多種IO多路復用模型,可以使用use指令在配置文件中設置IO讀寫模型。
1.4.3、HTTP協議配置塊
?????可以配置與HTTP協議處理相關的參數,比如keepalive長連接參數、GZIP壓縮參數、日志輸出參數、mime-type參數、連接超時參數等。
1.4.4、server虛擬服務器配置塊
?????配置虛擬主機的相關參數,如主機名稱、端口等。一個HTTP協議配置塊中可以有多個server虛擬服務器配置塊。
1.4.5、location路由規則塊
?????配置客戶端請求的路由匹配規則以及請求過程中的處理流程。一個server虛擬服務器配置塊中一般會有多個location路由規則塊。
1.4.6、mail服務配置塊
?????Nginx為email相關協議(如SMTP/IMAP/POP3)提供反向代理時,mail服務配置塊負責配置一些相關的配置項。
?????提示:以上介紹的Nginx配置塊主要針對的是Nginx基本應用程序配置文件,包括基本配置文件在內,Nginx的常用配置文件大致有下面這些:
- nginx.conf:應用程序基本配置文件
- mime.types:與MIME類型關聯的擴展配置文件
- fastcgi.conf:與FastCGI相關的配置文件
- proxy.conf:與Proxy相關的配置文件
- sites.conf:單獨配置Nginx提供的虛擬機主機
1.5、Nginx的請求處理流程
?????Nginx中HTTP請求的處理流程可以分為4步:
- 讀取解析請求行
- 讀取解析請求頭
- 多階段處理,也就是執行handler處理器列表
- 將結果返回給客戶端
Nginx中HTTP請求的處理流程如圖:
?????多階段處理是Nginx的HTTP處理流程中非常重要的一步。Nginx把請求處理劃分成了11個階段,在完成第一步讀取請求行和第二步讀取請求頭之后,Nginx將整個請求封裝到一個請求結構體ngx_http_request_t實例中(相當于Java中的一個請求對象),然后進入第三步多階段處理,也就是執行handler處理器列表。列表中的每個handler處理器都會對請求對象進行處理,例如重寫URI、權限控制、路徑查找、生成內容以及記錄日志等。
?????Nginx將HTTP請求處理流程分成了11個階段,每個階段都涉及一些handler處理器。HTTP請求到來時,這些組裝在一個列表的handler處理器會按組裝的先后次序執行。這一點和Netty的處理流水線pipeline在原理上是類同的。
?????在Nginx進行多階段處理時,handler處理器的執行次序除了和配置文件中對應指令的配置順序相關外,還和指令所處的階段先后次序相關。
?????Nginx請求處理的11個階段以及階段與階段之間的執行次序如圖:
?????對HTTP請求進行多階段處理是Nginx模塊化非常關鍵和重要的功能,第三方模塊的處理器都在不同的處理階段注冊,例如:
- 用Memcache進行頁面緩存的第三方模塊
- 用Redis集群進行頁面緩存的第三方模塊
- 執行Lua腳本的第三方模塊
1.6、HTTP請求處理的11個階段
?????Nginx請求處理的11個階段介紹如下
1.6.1、post-read階段
?????在完成第一步讀取請求行和第二步讀取請求頭之后就進入多處理階段,首當其沖的就是post-read階段。注冊在post-read階段的處理器不多,標準模塊的ngx_realip處理器就注冊在這個階段。ngx_realip處理器模塊的用途是改寫請求的來源地址。
?????為何要改寫請求的來源地址呢?
?????當Nginx處理的請求經過了某個正向代理服務器(Nginx、CDN)的轉發后,請求中的IP地址($remote_addr)可能就不是客戶端的真實IP了,變成了下游代理服務器的IP。如何獲取用戶請求的真實IP地址呢?解決辦法之一:在下游的正向代理服務器把請求的原始來源地址編碼成某個特殊的HTTP請求頭,在Nginx中把這個請求頭中編碼的地址恢復出來,然后傳給Nginx自己后頭的上游服務器。ngx_realip模塊正是用來處理這個需求的。
?????下面有一個簡單的例子,假定前頭的正向代理服務器能將客戶端IP編碼成某個特殊的HTTP請求頭(如X-My-IP),Nginx就可以通過ngx_realip模塊的real_ip_header指令將X-My-IP請求頭的IP取出,作為請求中的IP地址($remote_addr)。
server {listen 8080;set_real_ip_from 192.168.0.100;real_ip_header X-My-IP;location /test {echo "from: $remote_addr ";}
}
?????這里的配置是讓Nginx把來自正向代理服務器192.168.0.100的所有請求的IP來源地址都改寫為請求頭X-My-IP所指定的值,放在$remote_addr內置標準變量中。
1.6.2、server-rewrite階段
?????server-rewrite階段,簡單地翻譯就是server塊中的請求地址重寫階段。在進行請求URI與location路由規則匹配之前可以修改請求的URI地址。
?????大部分直接配置在server配置塊中的配置項都運行在server-rewrite階段。
server {listen 8080;set $a hello; #server-rewrite階段運行location /test {set $b "$a, world";echo $b;}set $b hello; #server-rewrite階段運行
}
?????其中,兩個變量賦值的配置項set a h e l l o 和 s e t a hello和set ahello和setb hello直接寫在server配置塊中,因此它們就運行在server-rewrite階段。
1.6.3、find-config
?????緊接在server-rewrite階段后面的是find-config階段,也叫配置查找階段,主要功能是根據請求URL地址去匹配location路由表達式。
?????find-config階段由Nginx HTTP Core(ngx_http_core_module)模塊全部負責,完成當前請求URL與location配置塊之間的配對工作。這個階段不支持Nginx模塊注冊處理程序。
?????在find-config階段之前,客戶端請求并沒有與任何location配置塊相關聯。因此,對于運行在此之前的post-read和server-rewrite階段來說,只有server配置塊以及更外層作用域中的配置項才會起作用,location配置塊中的配置項不起作用。
1.6.4、rewrite
?????由于Nginx已經在find-config階段完成了當前請求與location的匹配,因此從rewrite階段開始,location配置塊中的指令就可以產生作用。
?????rewrite階段也叫請求地址重寫階段,注冊在rewrite階段的指令首先是ngx_rewrite模塊的指令,比如break、if、return、rewrite、set等。其次,第三方ngx_lua模塊中的set_by_lua指令和rewrite_by_lua指令也能在此階段注冊。
1.6.5、post-rewrite
?????請求地址URI重寫提交(Post)階段,防止遞歸修改URI造成死循環(一個請求執行10次就會被Nginx認定為死循環),該階段只能由NginxHTTP Core(ngx_http_core_module)模塊實現。
1.6.6、preaccess
?????訪問權限檢查準備階段,控制訪問頻率的ngx_limit_req模塊和限制并發度的ngx_limit_zone模塊的相關指令就注冊在此階段。
1.6.7、access
?????在訪問權限檢查階段,配置指令多是執行訪問控制類型的任務,比如檢查用戶的訪問權限、檢查用戶的來源IP地址是否合法等。在此階段能注冊的指令有:HTTP標準模塊ngx_http_access_module的指令、第三方ngx_auth_request模塊的指令、第三方ngx_lua模塊的access_by_lua指令等。
?????比如,deny和allow指令屬于ngx_http_access_module模塊,它的
使用示例如下:
server {#拒絕全部location = /denyall {deny all;}#允許來源IP屬于192.168.0.0/24網段或127.0.0.1的請求#其他來源IP全部拒絕location = /allowsome {allow 192.168.0.0/24;allow 127.0.0.1;deny all;echo "you are ok";}
}
?????如果同一個location塊配置了多個allow/deny配置項,access階段的配置項之間是按配置的先后順序匹配的,匹配成功一個便跳出。上面的例子中,如果客戶端源IP是127.0.0.1,則匹配到“allow127.0.0.1;”配置項后就不再匹配后面的“deny all;”,也就是說該請求不會被拒絕。如果這些配置項的指令來自不同的模塊,則每個模塊會執行一個訪問控制類型的指令。
?????特別提醒:echo指令用于返回內容,在location上下文中,該指令注冊在content生產階段。由于echo指令不是注冊在access階段,因此在access階段不執行該指令的配置項。
1.6.8、post-access
?????訪問權限檢查提交階段。如果請求不被允許訪問Nginx服務器,該階段負責就向用戶返回錯誤響應。在access階段可能存在多個訪問控制模塊的指令注冊,post-access階段的satisfy配置指令可以用于控制它們彼此之間的協作方式。下面有一個例子:
#satisfy指令進行協調
location = /satisfy-demo {satisfy any;access_by_lua "ngx.exit(ngx.OK)";deny all;echo "hello";
}
?????在上面的例子中,deny指令屬于HTTP標準模塊的ngx_http_access_module訪問控制模塊,而access_by_lua指令屬于第三方ngx_lua模塊,兩個模塊都有自己的計算結果,需要經過最終的結果統一。
?????不同訪問控制模塊的計算結果統一工作,這里由satisfy指令負責,有兩種統一的方式:
- 邏輯或操作:具體的配置項為“satisfy any;”,表示訪問控制模塊A、B、C或更多,只要其中任意一個通過驗證就算通過。
- 邏輯與操作:具體的配置項為“satisfy all;”,表示訪問控制模塊A、B、C或更多,全部模塊都通過驗證才能最終通過。
1.6.9、try-files
?????如果HTTP請求訪問靜態文件資源,那么try-files配置項可以使這個請求按順序訪問多個靜態文件資源,直到某個靜態文件資源符合選取條件。這個階段只有一個標準配置指令try-files,并不支持Nginx模塊注冊處理程序。
?????try-files指令接收兩個以上任意數量的參數,每個參數都指定了一個URI,Nginx會在try-files階段依次把前N-1個參數映射為文件系統上的對象(文件或者目錄),然后檢查這些對象是否存在。若Nginx發現某個文件系統對象存在,則查找成功,進而在try-files階段把當前請求的URI改寫為該對象所對應的參數URI(但不會包含末尾的斜杠字符,也不會發生“內部跳轉”)。如果前N-1個參數所對應的文件系統對象都不存在,try-files階段就會立即發起“內部跳轉”,跳轉到最
后一個參數(第N個參數)所指定的URI。
?????下面是一個簡單的實例:
root /var/www/; #root指令把“查找文件的根目錄”配置為 /var/www/
location = /try_files-demo {try_files /foo /bar /last;
}
#對應到前面try_files的最后一個URI
location /last {echo "uri: $uri ";
}
?????這里try-files會在文件系統查找前兩個參數對應的文件/var/www/foo和/var/www/bar所對應的文件是否存在。如果不存在,此時Nginx就會在try-files階段發起到最后一個參數所指定的URI(/last)的內部跳轉,如圖:
1.6.10、content
?????大部分HTTP模塊會介入內容產生階段,是所有請求處理階段中重要的階段。Nginx的echo指令、第三方ngx_lua模塊的content_by_lua指令都注冊在此階段。
?????這里要注意的是,每一個location只能有一個“內容處理程序”,因此,當在location中同時使用多個模塊的content階段指令時,只有一個模塊能成功注冊成為“內容處理器”。例如echo和content_by_lua同時注冊,最終只會有一個生效,但具體是哪一個生效,結果是不穩定的。
1.6.11、log
?????日志模塊處理階段記錄日志。
?????最后,總結一下:
- Nginx將一個HTTP請求分為11個處理階段,這樣做讓每個HTTP模塊可以只專注于完成一個獨立、簡單的功能。而一個請求的完整處理過程由多個HTTP模塊共同合作完成,可以極大地提高多個模塊合作的協同性、可測試性和可擴展性。
- Nginx請求處理的11個階段中,有些階段是必備的,有些階段是可選的,各個階段可以允許多個模塊的指令同時注冊。但是,find-config、post-rewrite、post-access、try-files四個階段是不允許其他模塊的處理指令注冊的,它們僅注冊了HTTP框架自身實現的幾個固定的方法。
- 同一個階段內的指令,Nginx會按照各個指令的上下文順序執行對應的handler處理器方法。