Nginx的核心原理解析

Nginx

反向代理VS正向代理:
在這裏插入圖片描述
在這裏插入圖片描述
反向代理服務器對於客戶端而言它就像是原始服務器,並且客戶端不需要進行任何特別的設置。客戶端向反向代理的命名空間(name-space)中的內容發送普通請求,接着反向代理服務器將判斷向何處(原始服務器)轉交請求,並將獲得的內容返回給客戶端,就像這些內容原本就是它自己的一樣。

工作流程:

  • 用戶通過域名發出訪問Web服務器的請求,該域名被DNS服務器解析爲反向代理服務器的IP地址;
  • 反向代理服務器接受用戶的請求;
  • 反向代理服務器在本地緩存中查找請求的內容,找到後直接把內容發送給用戶;
  • 如果本地緩存裏沒有用戶所請求的信息內容,反向代理服務器會代替用戶向源服務器請求同樣的信息內容,並把信息內容發給用戶,> - [ ] 如果信息內容是非緩存的還會把它保存到緩存中。

作用意義:

  • 保護了真實的web服務器,保證了web服務器的資源安全
  • 節約了有限的IP地址資源
  • 減少WEB服務器壓力,提高響應速度
  • 反向代理就是通常所說的web服務器加速,它是一種通過在繁忙的web服務器和外部網絡之間增加一個高速的web緩衝服務器來降低實際的web服務器的負載的一種技術。反向代理是針對web服務器提高加速功能,作爲代理緩存,它並不是針對瀏覽器用戶,而針對一臺或多臺特定的web服務器,它可以代理外部網絡對內部網絡的訪問請求。
    反向代理服務器會強制將外部網絡對要代理的服務器的訪問經過它,這樣反向代理服務器負責接收客戶端的請求,然後到源服務器上獲取內容,把內容返回給用戶,並把內容保存到本地,以便日後再收到同樣的信息請求時,它會把本地緩存裏的內容直接發給用戶,以減少後端web服務器的壓力,提高響應速度。因此Nginx還具有緩存功能。
  • 請求的統一控制,包括設置權限、過濾規則等;
  • 區分動態和靜態可緩存內容;
  • 實現負載均衡,內部可以採用多臺服務器來組成服務器集羣,外部還是可以採用一個地址訪問;
  • 解決Ajax跨域問題;
  • 作爲真實服務器的緩衝,解決瞬間負載量大的問題;

Nginx模塊:
Nginx有五大優點:模塊化、事件驅動、異步、非阻塞、多進程單線程。由內核和模塊組成的,其中內核完成的工作比較簡單,僅僅通過查找配置文件將客戶端請求映射到一個location block,然後又將這個location block中所配置的每個指令將會啓動不同的模塊去完成相應的工作。

模塊劃分
Nginx的模塊從結構上分爲核心模塊、基礎模塊和第三方模塊:

  • 核心模塊:HTTP模塊、EVENT模塊和MAIL模塊
  • 基礎模塊:HTTP Access模塊、HTTP FastCGI模塊、HTTP Proxy模塊和HTTP Rewrite模塊,
  • 第三方模塊:HTTP Upstream Request Hash模塊、Notice模塊和HTTP Access Key模塊

Nginx的模塊從功能上分爲如下四類:

  • Core(核心模塊):構建nginx基礎服務、管理其他模塊。
  • Handlers(處理器模塊):此類模塊直接處理請求,並進行輸出內容和修改headers信息等操作。
  • Filters (過濾器模塊):此類模塊主要對其他處理器模塊輸出的內容進行修改操作,最後由Nginx輸出。
  • Proxies (代理類模塊):此類模塊是Nginx的HTTP Upstream之類的模塊,這些模塊主要與後端一些服務比如FastCGI等進行交互,實現服務代理和負載均衡等功能。

Nginx的核心模塊主要負責建立nginx服務模型、管理網絡層和應用層協議、以及啓動針對特定應用的一系列候選模塊。其他模塊負責分配給web服務器的實際工作:
(1) 當Nginx發送文件或者轉發請求到其他服務器,由Handlers(處理模塊)或Proxies(代理類模塊)提供服務;
(2) 當需要Nginx把輸出壓縮或者在服務端加一些東西,由Filters(過濾模塊)提供服務

當服務器啓動,每個handlers(處理模塊)都有機會映射到配置文件中定義的特定位置(location);如果有多個handlers(處理模塊)映射到特定位置時,只有一個會“贏”(說明配置文件有衝突項,應該避免發生)。
處理模塊以三種形式返回:
OK
ERROR
或者放棄處理這個請求而讓默認處理模塊來處理(主要是用來處理一些靜態文件,事實上如果是位置正確而真實的靜態文件,默認的處理模塊會搶先處理)

如果handlers(處理模塊)把請求反向代理到後端的服務器,就變成另外一類的模塊:load-balancers(負載均衡模塊)。負載均衡模塊的配置中有一組後端服務器,當一個HTTP請求過來時,它決定哪臺服務器應當獲得這個請求。

如果handlers(處理模塊)沒有產生錯誤,filters(過濾模塊)將被調用。多個filters(過濾模塊)能映射到每個位置,所以(比如)每個請求都可以被壓縮成塊。它們的執行順序在編譯時決定。
filters(過濾模塊)是經典的“接力鏈表(CHAIN OF RESPONSIBILITY)”模型其實類似責任鏈設計模式思想:一個filters(過濾模塊)被調用,完成其工作,然後調用下一個filters(過濾模塊),直到最後一個filters(過濾模塊)。

過濾模塊鏈的特別之處在於:
每個filters(過濾模塊)不會等上一個filters(過濾模塊)全部完成;
它能把前一個過濾模塊的輸出作爲其處理內容;有點像Unix中的流水線;

過濾模塊能以buffer(緩衝區)爲單位進行操作,這些buffer一般都是一頁(4K)大小,當然你也可以在nginx.conf文件中進行配置。這意味着,比如,模塊可以壓縮來自後端服務器的響應,然後像流一樣的到達客戶端,直到整個響應發送完成。
總之,過濾模塊鏈以流水線的方式高效率地向客戶端發送響應信息。

小結:
客戶端發送HTTP請求 –>
Nginx基於配置文件中的位置選擇一個合適的處理模塊 ->
(如果有)負載均衡模塊選擇一臺後端服務器 –>
處理模塊進行處理並把輸出緩衝放到第一個過濾模塊上 –>
第一個過濾模塊處理後輸出給第二個過濾模塊 –>
然後第二個過濾模塊又到第三個 –>
依此類推 –> 最後把響應發給客戶端。
Nginx本身做的工作實際很少,當它接到一個HTTP請求時,它僅僅是通過查找配置文件將此次請求映射到一個location block,而此location中所配置的各個指令則會啓動不同的模塊去完成工作,因此模塊可以看做Nginx真正的勞動工作者。通常一個location中的指令會涉及一個handler模塊和多個filter模塊(當然,多個location可以複用同一個模塊)。handler模塊負責處理請求,完成響應內容的生成,而filter模塊對響應內容進行處理。

Nginx請求處理
Nginx在啓動時會以daemon形式在後臺運行,採用多進程+異步非阻塞IO事件模型來處理各種連接請求。多進程模型包括一個master進程,多個worker進程,一般worker進程個數是根據服務器CPU核數來決定的。master進程負責管理Nginx本身和其他worker進程。
在這裏插入圖片描述
在這裏插入圖片描述
從上圖中可以很明顯地看到,4個worker進程的父進程都是master進程,表明worker進程都是從父進程fork出來的,並且父進程的ppid爲1,表示其爲daemon進程。
需要說明的是,在nginx多進程中,每個worker都是平等的,因此每個進程處理外部請求的機會權重都是一致的。
Master進程的作用是?
讀取並驗證配置文件nginx.conf;管理worker進程;
Worker進程的作用是?
每一個Worker進程都維護一個線程(避免線程切換),處理連接和請求;注意Worker進程的個數由配置文件決定,一般和CPU個數相關(有利於進程切換),配置幾個就有幾個Worker進程。

Nginx如何做到熱部署?
所謂熱部署,就是配置文件nginx.conf修改後,不需要stop Nginx,不需要中斷請求,就能讓配置文件生效!(nginx -s reload 重新加載/nginx -t檢查配置/nginx -s stop)
通過上文我們已經知道worker進程負責處理具體的請求,那麼如果想達到熱部署的效果,可以想象:
方案一:
修改配置文件nginx.conf後,主進程master負責推送給woker進程更新配置信息,woker進程收到信息後,更新進程內部的線程信息。
方案二:
修改配置文件nginx.conf後,重新生成新的worker進程,當然會以新的配置進行處理請求,而且新的請求必須都交給新的worker進程,至於老的worker進程,等把那些以前的請求處理完畢後,kill掉即可。
Nginx採用的就是方案二來達到熱部署的!

Nginx如何做到高併發下的高效處理?
Nginx採用了Linux的epoll模型,epoll模型基於事件驅動機制,它可以監控多個事件是否準備完畢,如果OK,那麼放入epoll隊列中,這個過程是異步的。worker只需要從epoll隊列循環處理即可。

Nginx掛了怎麼辦?
Nginx既然作爲入口網關,很重要,如果出現單點問題,顯然是不可接受的。
答案是:Keepalived+Nginx實現高可用。
Keepalived是一個高可用解決方案,主要是用來防止服務器單點發生故障,可以通過和Nginx配合來實現Web服務的高可用。(其實,Keepalived不僅僅可以和Nginx配合,還可以和很多其他服務配合)
Keepalived+Nginx實現高可用的思路:
第一:請求不要直接打到Nginx上,應該先通過Keepalived(這就是所謂虛擬IP,VIP)
第二:Keepalived應該能監控Nginx的生命狀態(提供一個用戶自定義的腳本,定期檢查Nginx進程狀態,進行權重變化,,從而實現Nginx故障切換)
在這裏插入圖片描述
Nginx架構及工作流程圖:
在這裏插入圖片描述
Nginx真正處理請求業務的是Worker之下的線程。worker進程中有一個ngx_worker_process_cycle()函數,執行無限循環,不斷處理收到的來自客戶端的請求,並進行處理,直到整個Nginx服務被停止。
worker 進程中,ngx_worker_process_cycle()函數就是這個無限循環的處理函數。在這個函數中,一個請求的簡單處理流程如下:
在這裏插入圖片描述

多進程處理模型
首先,master進程一開始就會根據我們的配置,來建立需要listen的網絡socket fd,然後fork出多個worker進程。
其次,根據進程的特性,新建立的worker進程,也會和master進程一樣,具有相同的設置。因此,其也會去監聽相同ip端口的套接字socket fd。
然後,這個時候有多個worker進程都在監聽同樣設置的socket fd,意味着當有一個請求進來的時候,所有的worker都會感知到。這樣就會產生所謂的==“驚羣現象”==。爲了保證只會有一個進程成功註冊到listenfd的讀事件,nginx中實現了一個“accept_mutex”類似互斥鎖,只有獲取到這個鎖的進程,纔可以去註冊讀事件。其他進程全部accept 失敗。
最後,監聽成功的worker進程,讀取請求,解析處理,響應數據返回給客戶端,斷開連接,結束。因此,一個request請求,只需要worker進程就可以完成。

進程模型的處理方式帶來的一些好處就是:進程之間是獨立的,也就是一個worker進程出現異常退出,其他worker進程是不會受到影響的;此外,獨立進程也會避免一些不需要的鎖操作,這樣子會提高處理效率,並且開發調試也更容易。
如前文所述,多進程模型+異步非阻塞模型纔是勝出的方案。單純的多進程模型會導致連接併發數量的降低,而採用異步非阻塞IO模型很好的解決了這個問題;並且還因此避免的多線程的上下文切換導致的性能損失。

worker進程會競爭監聽客戶端的連接請求:這種方式可能會帶來一個問題,就是可能所有的請求都被一個worker進程給競爭獲取了,導致其他進程都比較空閒,而某一個進程會處於忙碌的狀態,這種狀態可能還會導致無法及時響應連接而丟棄discard掉本有能力處理的請求。這種不公平的現象,是需要避免的,尤其是在高可靠web服務器環境下。

針對這種現象,Nginx採用了一個是否打開accept_mutex選項的值,ngx_accept_disabled標識控制一個worker進程是否需要去競爭獲取accept_mutex選項,進而獲取accept事件。
ngx_accept_disabled值:nginx單進程的所有連接總數的八分之一,減去剩下的空閒連接數量,得到的這個ngx_accept_disabled。
當ngx_accept_disabled大於0時,不會去嘗試獲取accept_mutex鎖,並且將ngx_accept_disabled減1,於是,每次執行到此處時,都會去減1,直到小於0。不去獲取accept_mutex鎖,就是等於讓出獲取連接的機會,很顯然可以看出,當空閒連接越少時,ngx_accept_disable越大,於是讓出的機會就越多,這樣其它進程獲取鎖的機會也就越大。不去accept,自己的連接就控制下來了,其它進程的連接池就會得到利用,這樣,nginx就控制了多進程間連接的平衡了。

一個簡單的HTTP請求
從 Nginx 的內部來看,一個 HTTP Request 的處理過程涉及到以下幾個階段:
初始化 HTTP Request(讀取來自客戶端的數據,生成 HTTP Request 對象,該對象含有該請求所有的信息)。
處理請求頭。
處理請求體。
如果有的話,調用與此請求(URL 或者 Location)關聯的 handler。
依次調用各 phase handler 進行處理。
在建立連接過程中,對於nginx監聽到的每個客戶端連接,都會將它的讀事件的handler設置爲ngx_http_init_request函數,這個函數就是請求處理的入口。在處理請求時,主要就是要解析http請求,比如:uri,請求行等,然後再根據請求生成響應。下面看一下nginx處理的具體過程:在這裏插入圖片描述

keepalive 長連接
當然,在nginx中,對於http1.0與http1.1也是支持長連接的。
什麼是長連接呢?我們知道,http請求是基於TCP協議之上的,那麼,當客戶端在發起請求前,需要先與服務端建立TCP連接,而每一次的TCP連接是需要三次握手來確定的,如果客戶端與服務端之間網絡差一點,這三次交互消費的時間會比較多,而且三次交互也會帶來網絡流量。當然,當連接斷開後,也會有四次的交互,當然對用戶體驗來說就不重要了。而http請求是請求應答式的,如果我們能知道每個請求頭與響應體的長度,那麼我們是可以在一個連接上面執行多個請求的,這就是所謂的長連接,但前提條件是我們先得確定請求頭與響應體的長度。
對於請求來說,如果當前請求需要有body,如POST請求,那麼nginx就需要客戶端在請求頭中指定content-length來表明body的大小,否則返回400錯誤。也就是說,請求體的長度是確定的,那麼響應體的長度呢?先來看看http協議中關於響應body長度的確定
對於http1.0協議來說,如果響應頭中有content-length頭,則以content-length的長度就可以知道body的長度了,客戶端在接收body時,就可以依照這個長度來接收數據,接收完後,就表示這個請求完成了。而如果沒有content-length頭,則客戶端會一直接收數據,直到服務端主動斷開連接,才表示body接收完了。
而對於http1.1協議來說,如果響應頭中的Transfer-encoding爲chunked傳輸,則表示body是流式輸出,body會被分成多個塊,每塊的開始會標識出當前塊的長度,此時,body不需要通過長度來指定。如果是非chunked傳輸,而且有content-length,則按照content-length來接收數據。否則,如果是非chunked,並且沒有content-length,則客戶端接收數據,直到服務端主動斷開連接。
從上面,我們可以看到,除了http1.0不帶content-length以及http1.1非chunked不帶content-length外,body的長度是可知的。此時,當服務端在輸出完body之後,會可以考慮使用長連接。能否使用長連接,也是有條件限制的。如果客戶端的請求頭中的connection爲close,則表示客戶端需要關掉長連接,如果爲keep-alive,則客戶端需要打開長連接,如果客戶端的請求中沒有connection這個頭,那麼根據協議,如果是http1.0,則默認爲close,如果是http1.1,則默認爲keep-alive。如果結果爲keepalive,那麼,nginx在輸出完響應體後,會設置當前連接的keepalive屬性,然後等待客戶端下一次請求。
當然,nginx不可能一直等待下去,如果客戶端一直不發數據過來,豈不是一直佔用這個連接?所以當nginx設置了keepalive等待下一次的請求時,同時也會設置一個最大等待時間,這個時間是通過選項keepalive_timeout來配置的,如果配置爲0,則表示關掉keepalive,此時,http版本無論是1.1還是1.0,客戶端的connection不管是close還是keepalive,都會強制爲close。
如果服務端最後的決定是keepalive打開,那麼在響應的http頭裏面,也會包含有connection頭域,其值是”Keep-Alive”,否則就是”Close”。如果connection值爲close,那麼在nginx響應完數據後,會主動關掉連接。所以,對於請求量比較大的nginx來說,關掉keepalive最後會產生比較多的time-wait狀態的socket。一般來說,當客戶端的一次訪問,需要多次訪問同一個server時,打開keepalive的優勢非常大,比如圖片服務器,通常一個網頁會包含很多個圖片。打開keepalive也會大量減少time-wait的數量。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章