天天看點

34 | Nginx:高性能的Web伺服器

經過前面幾大子產品的學習,你已經完全掌握了 HTTP 的所有知識,那麼接下來請收拾一下行囊,整理一下裝備,跟我一起去探索 HTTP 之外的廣闊天地。

現在的網際網路非常發達,使用者越來越多,網速越來越快,HTTPS 的安全加密、HTTP/2 的多路複用等特性都對 Web 伺服器提出了非常高的要求。一個好的 Web 伺服器必須要具備穩定、快速、易擴充、易維護等特性,才能夠讓網站“立于不敗之地”。

那麼,在搭建網站的時候,應該選擇什麼樣的伺服器軟體呢?

在開頭的幾講裡我也提到過,Web 伺服器就那麼幾款,目前市面上主流的隻有兩個:Apache 和 Nginx,兩者合計占據了近 90% 的市場佔有率。

今天我要說的就是其中的 Nginx,它是 Web 伺服器的“後起之秀”,雖然比 Apache 小了 10 歲,但增長速度十分迅猛,已經達到了與 Apache“平起平坐”的地位,而在“Top Million”網站中更是超過了 Apache,擁有超過 50% 的使用者。

34 | Nginx:高性能的Web伺服器

在這裡必須要說一下 Nginx 的正确發音,它應該讀成“Engine X”,但我個人感覺“X”念起來太“拗口”,還是比較傾向于讀做“Engine ks”,這也與 UNIX、Linux 的發音一緻。

作為一個 Web 伺服器,Nginx 的功能非常完善,完美支援 HTTP/1、HTTPS 和 HTTP/2,而且還在不斷進步。目前的主線版本已經發展到了 1.17,正在進行 HTTP/3 的研發,或許一年之後就能在 Nginx 上跑 HTTP/3 了。

Nginx 也是我個人的主要研究領域,我也寫過相關的書,按理來說今天的課程應該是“手拿把攥”,但真正動筆的時候還是有些猶豫的:很多要點都已經在書裡寫過了,這次的專欄如果再重複相同的内容就不免有“騙稿費”的嫌疑,應該有些“不一樣的東西”。

是以我決定抛開書本,換個角度,結合 HTTP 協定來講 Nginx,帶你窺視一下 HTTP 處理的内幕,看看 Web 伺服器的工作原理。

程序池

你也許聽說過,Nginx 是個“輕量級”的 Web 伺服器,那麼這個所謂的“輕量級”是什麼意思呢?

“輕量級”是相對于“重量級”而言的。“重量級”就是指伺服器程序很“重”,占用很多資源,當處理 HTTP 請求時會消耗大量的 CPU 和記憶體,受到這些資源的限制很難提高性能。

而 Nginx 作為“輕量級”的伺服器,它的 CPU、記憶體占用都非常少,同樣的資源配置下就能夠為更多的使用者提供服務,其奧秘在于它獨特的工作模式。

34 | Nginx:高性能的Web伺服器

在 Nginx 之前,Web 伺服器的工作模式大多是“Per-Process”或者“Per-Thread”,對每一個請求使用單獨的程序或者線程處理。這就存在建立程序或線程的成本,還會有程序、線程“上下文切換”的額外開銷。如果請求數量很多,CPU 就會在多個程序、線程之間切換時“疲于奔命”,平白地浪費了計算時間。

Nginx 則完全不同,“一反慣例”地沒有使用多線程,而是使用了“程序池 + 單線程”的工作模式。

Nginx 在啟動的時候會預先建立好固定數量的 worker 程序,在之後的運作過程中不會再 fork 出新程序,這就是程序池,而且可以自動把程序“綁定”到獨立的 CPU 上,這樣就完全消除了程序建立和切換的成本,能夠充分利用多核 CPU 的計算能力。

在程序池之上,還有一個“master”程序,專門用來管理程序池。它的作用有點像是 supervisor(一個用 Python 編寫的程序管理工具),用來監控程序,自動恢複發生異常的 worker,保持程序池的穩定和服務能力。

不過 master 程序完全是 Nginx 自行用 C 語言實作的,這就擺脫了外部的依賴,簡化了 Nginx 的部署和配置。

I/O 多路複用

如果你用 Java、C 等語言寫過程式,一定很熟悉“多線程”的概念,使用多線程能夠很容易實作并發處理。

但多線程也有一些缺點,除了剛才說到的“上下文切換”成本,還有程式設計模型複雜、資料競争、同步等問題,寫出正确、快速的多線程程式并不是一件容易的事情。

是以 Nginx 就選擇了單線程的方式,帶來的好處就是開發簡單,沒有互斥鎖的成本,減少系統消耗。

那麼,疑問也就産生了:為什麼單線程的 Nginx,處理能力卻能夠超越其他多線程的伺服器呢?

這要歸功于 Nginx 利用了 Linux 核心裡的一件“神兵利器”,I/O 多路複用接口,“大名鼎鼎”的 epoll。

“多路複用”這個詞我們已經在之前的 HTTP/2、HTTP/3 裡遇到過好幾次,如果你了解了那裡的“多路複用”,那麼面對 Nginx 的 epoll“多路複用”也就好辦了。

Web 伺服器從根本上來說是“I/O 密集型”而不是“CPU 密集型”,處理能力的關鍵在于網絡收發而不是 CPU 計算(這裡暫時不考慮 HTTPS 的加解密),而網絡 I/O 會因為各式各樣的原因不得不等待,比如資料還沒到達、對端沒有響應、緩沖區滿發不出去等等。

這種情形就有點像是 HTTP 裡的“隊頭阻塞”。對于一般的單線程來說 CPU 就會“停下來”,造成浪費。而多線程的解決思路有點類似“并發連接配接”,雖然有的線程可能阻塞,但由于多個線程并行,總體上看阻塞的情況就不會太嚴重了。

Nginx 裡使用的 epoll,就好像是 HTTP/2 裡的“多路複用”技術,它把多個 HTTP 請求處理打散成碎片,都“複用”到一個單線程裡,不按照先來後到的順序處理,而是隻當連接配接上真正可讀、可寫的時候才處理,如果可能發生阻塞就立刻切換出去,處理其他的請求。

通過這種方式,Nginx 就完全消除了 I/O 阻塞,把 CPU 利用得“滿滿當當”,又因為網絡收發并不會消耗太多 CPU 計算能力,也不需要切換程序、線程,是以整體的 CPU 負載是相當低的。

這裡我畫了一張 Nginx“I/O 多路複用”的示意圖,你可以看到,它的形式與 HTTP/2 的流非常相似,每個請求處理單獨來看是分散、阻塞的,但因為都複用到了一個線程裡,是以資源的使用率非常高。

34 | Nginx:高性能的Web伺服器

epoll 還有一個特點,大量的連接配接管理工作都是在作業系統核心裡做的,這就減輕了應用程式的負擔,是以 Nginx 可以為每個連接配接隻配置設定很小的記憶體維護狀态,即使有幾萬、幾十萬的并發連接配接也隻會消耗幾百 M 記憶體,而其他的 Web 伺服器這個時候早就“Memory not enough”了。

多階段處理

有了“程序池”和“I/O 多路複用”,Nginx 是如何處理 HTTP 請求的呢?

Nginx 在内部也采用的是“化整為零”的思路,把整個 Web 伺服器分解成了多個“功能子產品”,就好像是樂高積木,可以在配置檔案裡任意拼接搭建,進而實作了高度的靈活性和擴充性。

Nginx 的 HTTP 處理有四大類子產品:

  1. handler 子產品:直接處理 HTTP 請求;
  2. filter 子產品:不直接處理請求,而是加工過濾響應封包;
  3. upstream 子產品:實作反向代理功能,轉發請求到其他伺服器;
  4. balance 子產品:實作反向代理時的負載均衡算法。

因為 upstream 子產品和 balance 子產品實作的是代理功能,Nginx 作為“中間人”,運作機制比較複雜,是以我今天隻講 handler 子產品和 filter 子產品。

不知道你有沒有了解過“設計模式”這方面的知識,其中有一個非常有用的模式叫做“職責鍊”。它就好像是工廠裡的流水線,原料從一頭流入,線上有許多勞工會進行各種加工處理,最後從另一頭出來的就是完整的産品。

Nginx 裡的 handler 子產品和 filter 子產品就是按照“職責鍊”模式設計群組織的,HTTP 請求封包就是“原材料”,各種子產品就是工廠裡的勞工,走完子產品構成的“流水線”,出來的就是處理完成的響應封包。

下面的這張圖顯示了 Nginx 的“流水線”,在 Nginx 裡的術語叫“階段式處理”(Phases),一共有 11 個階段,每個階段裡又有許多各司其職的子產品。

34 | Nginx:高性能的Web伺服器
  • charset 子產品實作了字元集編碼轉換;
  • chunked 子產品實作了響應資料的分塊傳輸;
  • range 子產品實作了範圍請求,隻傳回資料的一部分;
  • rewrite 子產品實作了重定向和跳轉,還可以使用内置變量自定義跳轉的 URI;
  • not_modified 子產品檢查頭字段“if-Modified-Since”和“If-None-Match”,處理條件請求;
  • realip 子產品處理“X-Real-IP”“X-Forwarded-For”等字段,擷取用戶端的真實 IP 位址;
  • ssl 子產品實作了 SSL/TLS 協定支援,讀取磁盤上的證書和私鑰,實作 TLS 握手和 SNI、ALPN 等擴充功能;
  • http_v2 子產品實作了完整的 HTTP/2 協定。

小結

  1. Nginx 是一個高性能的 Web 伺服器,它非常的輕量級,消耗的 CPU、記憶體很少;
  2. Nginx 采用“master/workers”程序池架構,不使用多線程,消除了程序、線程切換的成本;
  3. Nginx 基于 epoll 實作了“I/O 多路複用”,不會阻塞,是以性能很高;
  4. Nginx 使用了“職責鍊”模式,多個子產品分工合作,自由組合,以流水線的方式處理 HTTP 請求。