Nginx 學習筆記


零、寫在前面


1、背景

nginx 用了起碼有六七年了(最早期做 Java Web,用的是 apache tomcat),一直也沒能專門整理一下,有時候遇到問題搜索引擎查找到就粘貼上去,也沒好好深入的研究機理。

本文參考 《 NGINX —— A PRACTICAL GUIDE TO HIGH PERFORMANCE 》 (英文版) 和 《實戰Nginx —— 取代Apache的高性能Web服務器》。

前者為 Preview Edition(預覽版,只有1-5章),網上搜了一大圈都沒搜到完整版,估計是作者還沒出,不過即使這樣,內容仍然尤其棒(后者就不推薦了)。

2、環境

  • CentOS 7.6.1810 (Core)
  • Nginx 1.17.10

一、入門


1、介紹

(1)歷史

Igor Sysoev 於2000年代創建了 Nginx,並於2004年將其開源。在2015年,他創立了Nginx 同名公司,后來被 F5 Networks 以6.7億美元收購。

(2)介紹

nginx 屬於 HTTP 服務器。(也可以叫 Web 服務器

官網:

文檔:
https://docs.nginx.com/

(3)原理與優缺點

原理:

  • nginx 是事件驅動的服務器。
  • nginx 采用的是多進程,(每個進程都是)單線程
  • nginx 采用 IO 多路復用的事件模型。在Linux操作系統下,Nginx使用epoll事件模型(得益於此,Nginx在Linux操作系統下效率相當高)。在OpenBSD或FreeBSD操作系統上采用類似於epoll的事件模型kqueue。

    Nginx 支持 select、poll、kqueue、epoll、rtsig 和 /dev/poll。當然,你不用特別指定事件模型,Nginx 會自動選擇最佳。

優點:

  • 高性能
  • 資源消耗低
  • 模塊化設計
  • 模塊編寫簡單
  • 配置文件簡潔

缺點:( nginx 性能提升的代價是降低了其他方面)

  • 穩定性,不如 apache 和 Windows Server 下的 Nginx
  • 沒有像 Apache 使用.htaccess 那樣的訪問設置
  • 添加模塊復雜(在版本 1.9.11 中增加了動態模塊加載。但模塊需要與 Nginx 同時編譯)
(4)競品
  • Nginx:由俄國人開發,輕量級,適合高並發。
  • Apache:重量級,速度、性能沒有 nginx 快。
  • Lighttpd:由德國人開發,也是輕量級,是 Nginx 的競爭對手之一。
  • Tomcat:基於 Java ,是運行 servlet 的 JSP Web 服務器。
  • IIS:基於 C# ,只在 windows 操作系統上,是運行 .NET 的 ASP Web 服務器。

2021-04-07-00-14-16

2、安裝

(1)依賴
  • PCRE (for the HTTP Rewrite module)
  • Zlib (for the HTTP Gzip module)
  • OpenSSL (for HTTPS protocol support)
(2)源代碼安裝

缺點:安裝步驟繁瑣
優點:安裝時可以自定義所需模塊

步驟1:下載源碼

下載地址:http://nginx.org/en/download.html

版本分為:

  • Mainline version(本文寫於 2020-05-27,最新為 v1.19.0)
  • Stable version【更早更穩定,推薦生產環境使用】(本文寫於 2020-05-27,最新為 v1.18.0)

步驟2:裝依賴

apt-get install libpcre3-dev zlib1g-dev libssl-dev

步驟3:安裝

$ tar -zxvf nginx-1.18.0.tar.gz
$ cd nginx-1.18.0
$ ./configure
$ make
$ sudo make install

步驟4:安裝成功

默認執行文件路徑:/usr/sbin/nginx

(3)包管理工具安裝

缺點:版本過時。安裝時不能自定義模塊。
優點:安裝步驟簡單。自動跟 systemctl 集成,方便管理。

sudo yum install epel-release

sudo yum install nginx

sudo systemctl enable nginx

3、查看版本

(1)nginx -v 簡略
nginx version: nginx/1.17.0
(2)nginx -V 詳細(+配置選項)
nginx version: nginx/1.17.0
built by gcc 4.8.5 20150623 (Red Hat 4.8.5-36) (GCC)
built with OpenSSL 1.0.2k-fips  26 Jan 2017
TLS SNI support enabled
configure arguments: --prefix=/etc/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib64/nginx/modules --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --pid-path=/var/run/nginx.pid --lock-path=/var/run/nginx.lock --http-client-body-temp-path=/var/cache/nginx/client_temp --http-proxy-temp-path=/var/cache/nginx/proxy_temp --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp --http-scgi-temp-path=/var/cache/nginx/scgi_temp --user=nginx --group=nginx --with-compat --with-file-aio --with-threads --with-http_addition_module --with-http_auth_request_module --with-http_dav_module --with-http_flv_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_mp4_module --with-http_random_index_module --with-http_realip_module --with-http_secure_link_module --with-http_slice_module --with-http_ssl_module --with-http_stub_status_module --with-http_sub_module --with-http_v2_module --with-mail --with-mail_ssl_module --with-stream --with-stream_realip_module --with-stream_ssl_module --with-stream_ssl_preread_module --with-cc-opt='-O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong --param=ssp-buffer-size=4 -grecord-gcc-switches -m64 -mtune=generic -fPIC' --with-ld-opt='-Wl,-z,relro -Wl,-z,now -pie'

4、模塊

(1)查看模塊支持

查看(此版本) nginx 源碼支持安裝哪些模塊:

./configure --help

  • --without 開頭,后面標記 disable 的,代表是 nginx 默認安裝的模塊(可卸載)
  • --with 開頭,后面標記 enable 的,代表是 nginx 支持的模塊(可安裝)
(2)安裝模塊

可以通過 ./configure 結合(上面提到的)--without/--with 來按需 安裝/移除 模塊。

① 首次安裝 nginx

$ ./configure --with-http_ssl_module \
 --with-http_spdy_module \
 --with-http_realip_module \
 --with-http_stub_status_modul 

$ make
$ make install

注:如果是包管理工具安裝,那這種方式不支持。


② 已經安裝過 nginx

因為 nginx 是不支持動態的安裝模塊的,所以必須得重新構建 nginx。

跟(上面的)首次安裝 nginx 一樣的操作,只不過最后的 make install 最好不要加,否則就是覆蓋安裝(不光是 nginx 執行文件,配置文件什么的都會被覆蓋),推薦先備份原有的 nginx 執行文件,然后再手動把新構建生成的 nginx 執行文件覆蓋過去。

注:如果是包管理工具安裝,原理一樣,但得確保下載相同版本的nginx源碼包。

(3)查看安裝了哪些模塊

nginx -V 2>&1 | tr -- - '\n' | grep module

(4)官方模塊 vs 第三方模塊

上面介紹的都是官方模塊,第三方模塊待寫。

第三方模塊list:https://www.nginx.com/resources/wiki/modules/index.html

二、管理


1、check nginx 是否啟動正常

查看 nginx 進程:ps aux | grep nginx

2、signal


nginx 跑起來后,支持下面的 Signal:

Signal Description
TERM INT Quick shutdown
QUIT Graceful shutdown
KILL Halts a stubborn process
HUP Configuration reload
USR1 Reopen the log €les (useful for log rotation)
USR2 Upgrade executable on the fly
WINCH Gracefully shutdown worker processes

常用命令的對應關系:

Signal systemctl systemctl
kill -QUIT cat /var/run/nginx.pid nginx -s quit
kill -TERM cat /var/run/nginx.pid nginx -s stop systemctl stop nginx.service
kill -KILL cat /var/run/nginx.pid ---- systemctl kill nginx.service
kill -HUP cat /var/run/nginx.pid nginx -s reload systemctl reload nginx.service

注1:QUIT 跟 TERM / KILL 的區別在於,是否在 Nginx 退出前完成已經接受的連接請求。

注2:nginx 的 reload ,即熱部署(此后新生成的 worker 進程,會使用新的配置,至於老的 worker 進程,要等他們把手上的請求處理完畢后再退出。)

三、基本配置


1、配置文件 —— nginx.conf 文件

(1)nginx -t

nginx -t:查找 nginx.conf 配置文件在哪 + 檢測配置文件是否合法。

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

用途:改了配置文件,但是不想 reload nginx ,就先用這個試試,免得影響正式環境。

(2)啟動時指定配置文件

nginx -c

2、配置結構

events {
}

http {
 server {
 }
}

3、指令

以這段簡單的配置為例:

http {
    server {
        listen *:80;
        server_name "";
        root /usr/share/nginx/html;
    }
}
(1)指令分類

簡單指令 Simple Directives

listen *:80;,是由 名稱(listen)-參數(*:80)-分號(;) 構成。


上下文指令 Context Directives

server {
    
}

注:簡單指令只能包含在上下文指令中。(最高層的簡單指令可以理解成在一個隱藏的上下文指令中。)

(2)指令繼承 Directive Inheritance

指令總是向下繼承,除非有內部重名的會覆蓋外層。

這跟編程語言變量作用域的規則一樣。

4、具體指令介紹

(1)user

user nginx;

執行 ps aux | grep nginx 命令可以看到:

root      5791  0.0  0.4  47184  2108 ?        Ss   18:22   0:00 nginx: master process /usr/sbin/nginx -c /etc/nginx/nginx.conf
nginx     5807  0.0  0.4  47188  2248 ?        S    18:22   0:00 nginx: worker process

Nginx 主進程(master process)會先以 root 權限運行,之后主進程讀取 nginx.conf 文件中的 user 模塊的配置,使用此用戶啟動 工作進程(worker process)。

問:為什么 主進程 需要使用 root 用戶運行?

答:因為主進程需要很多權限,例如:

  • 監聽小於1024的端口
  • 讀取和寫入日志文件,
  • 存儲臨時數據
  • ……
(2)worker_processes

worker_processes 1;

指定工作進程數。推薦 auto,它會自動檢測邏輯CPU內核數。

如果 cpu 使用了超線程技術,例如雙核4線程,可以設置為4。

拓展:

  • 物理cpu數:主板上實際插入的cpu數量。
  • cpu核數:單塊CPU上面能處理數據的芯片組的數量,如雙核、四核等。
  • 邏輯cpu數:一般情況下,邏輯cpu=物理CPU數×cpu核數。
(3)Events 上下文指令

Events 這個上下文指令只能在配置文件里出現一次。

events {
 worker_connections 512;
}

① worker_connections

單個工作進程可以允許同時建立連接的數量。(此為調優的重要參數)

注:如果往上調整太多,記得有可能需要同時調整操作系統的最大打開文件數限制(ulimit)。

(4)HTTP 上下文指令

① sendfile + tcp_nopush / tcp_nodelay

1、sendfile

sendfile on;

sendfile 是 Linux 2.0+ 推出的一個系統調用方法。

  • 原來:硬盤 >> kernel buffer >> user buffer >> kernel socket buffer >> 協議棧(傳遞數據需要從用戶空間經過)
  • 現在:硬盤 >> kernel buffer >> kernel socket buffer >> 協議棧(傳遞數據直接在內核空間進行,更快)

所以,sendfile 是個比 read 和 write 更高性能的方法。

適用范圍:

  • 當 Nginx 是作為一個靜態文件服務器來使用的時候,開啟 sendfile 能大大提高性能。
  • 當 Nginx 是作為一個反向代理服務器來使用的時候,開啟 sendfile 沒什么用。(因為 sendfile 方法只支持文件句柄,不支持 socket)
  • 當想開啟 tcp_nopush 的時候,sendfile 必須打開(下面會介紹)

建議:一般我們會把 nginx 即當靜態文件也當反向代理,所以建議開着。

2、tcp_nopush / tcp_nodelay

在我之前一篇文章 朝花夕拾——《網絡是怎樣連接的》 中的 TCP 章節,我提到了為了解決 糊塗窗口綜合症 SWS 而采用的兩種算法,它們分別對應:

  • Nagle 算法 —— tcp_nodelay
  • Cork 算法 —— tcp_nopush

它們分別的適用場景也請看我的那篇文章。

# tcp_nodelay on; # 默認 on ,即 Nagle 算法關閉
# tcp_nopush on; # 默認 off ,即 Cork 算法關閉(注意:tcp_nopush 開啟的前提是 sendfile 也得開啟,原因未知,待寫)

5、虛擬主機

虛擬主機是一種在單一主機或主機群上,實現多網域服務的方法,可以運行多個網站或服務的技術。

  • 方法1、 基於IP地址。前提是你的主機得有多個 IP 地址,這個成本較大。
  • 方法2、 基於端口號。但一般網址都是默認 80/443,弄別的端口體驗會很差。
  • 方法3、 基於主機名。HTTP1.1 新增 Host 頭,用來填寫主機名。
(1)方法1

通過 listen 對應 IP。

server {
 listen 172.19.205.200:80; 
}
server {
 listen 172.19.205.201:80; 
} 

如果不知道 本機 IP ,可以通過 ip addr 查看。

更多操作:

# 監聽所有

server {
 listen *:80;  
}

server {
 listen 0.0.0.0:80;  
}
 
server {
 listen 80;  
}

如果本機只有一個IP,那 監聽那一個IP = 監聽所有。

(2)方法2

通過 listen 對應端口。

server {
 listen 80; 
}
server {
 listen 81; 
} 
(3)方法3

通過 server_name,對應 HTTP 的 Host 頭。

注1:雖然 HTTP 對 Host 頭的定義是 域名:端口(可選),但是 nginx 匹配的時候不會管 Host 的端口。
注2:若不傳 Host 頭,nginx 會返回 400 Bad Request。

server {
 listen 80; 
 server_name  test1.xjnotxj.com;
}
server {
 listen 80; 
 server_name  test2.xjnotxj.com;
} 

高級操作:

# 支持多個值
server_name example.com www.example.com;

# 支持通配符 
server_name example.com *.example.com www.example.*;
server_name ~^www[0-9]\.example\.com$;

問:網址上不是有域名嗎?為什么還要多引入一個 Host 頭來存域名?

網址上的域名會被(例如瀏覽器)先通過 DNS 解析成 IP 地址,然后發送 HTTP 報文的時候,報文頭部是不包含任何域名的,只有域名后的路徑(例如 http://example.com/api/shop 的報文頭部第一行是 GET /api/shop HTTP/1.0 ),所以 HTTP/1.1 引入了 Host 頭部,用來填寫域名(瀏覽器默認行為就是填入網址的域名),可以實現虛擬主機功能。

Host 頭部也可以隨便填寫,未必非要是網址的域名

(4)3 種方法的匹配規則

① default_server

先介紹下 default_server 的寫法(下面會提有什么用)

server {
 listen 80 default_server;
 server_name foobar.com;
}

② 匹配規則

1、先匹配 listen

如果這都匹配不到,則根本就不會有響應,因為 HTTP 連接沒法建立

2、然后匹配 server_name

  • 准確值優先級大於通配符
  • 如果沒找到,有 default_server 則匹配
  • 如果沒有 default_server,就找第一個出現的 server

建議在負責兜底的匹配規則里:

  • 首先有 default_server
  • 然后 server_name 寫成 _(只是慣例,你寫成 @#$ 也沒事,反正不會被匹配上),
  • 返回的話,一、寫上 return 444,這是 nginx 自己獨有的 HTTP 響應碼,含義是服務器不向客戶端返回任何信息,並關閉連接(有助於惡意軟件的威脅)。二、可以重定向到你定制的錯誤頁面或者官網首頁。

6、Location Block

location 即 url 的匹配規則。

(1)修飾符

2021-04-07-00-14-21


先是最普通的寫法:

location /api {} # 前綴匹配

然后支持上面表的修飾符

location = /api {} # 精確匹配

location ~ \.php$ {} # 正則匹配(區分大小寫)

location ~* \.php$ {} # 正則匹配(不分大小寫)

location ^~ /api {} # 非正則的前綴匹配


(2)匹配

匹配規則(按順序依次匹配):

  • 精確匹配 =
  • 非正則的前綴匹配 ^~
  • 正則匹配 ~~*
  • 前綴匹配

注1:

  • 一個網址中,域名不分大小寫(例如 chrome 地址欄中輸入大寫,會自動變小寫),但是后面的路徑分大小寫, nginx 也分。
  • 注意有兩種前綴匹配,且在這兩種前綴匹配下,匹配越長優先級越高,與 location 所在位置的順序無關
  • 正則匹配如果匹配了多個,會選擇出現順序的首個,所以建議越精細的放的越靠前。
  • 最終還是沒找到,nginx 響應 404。

注2:關於網址斜杠的問題

  • 一個域名的最后一定要有一個斜杠,這是 RFC1738 規定的,如 https://www.baidu.com/ 是對的,而 https://www.baidu.com 不對,但日常大家都習慣不加,是因為如瀏覽器等都會幫我們自動加上。(對應 nginx 的精確匹配就是 location = / {}
  • 一個網址(域名+路徑)的最后的斜杠可有可沒有。沒有代表一個文件,有代表一個目錄
  • 域名不看,nginx 只會把路徑當成待比較的值,去參與上面介紹的匹配規則。(如 https://www.baidu.com/api/abc ,只會用 /api/abc 去參與匹配)
  • nginx 用來匹配的路徑,會忽略 ? 及后面。(如 https://www.baidu.com/api?param1&param2,只會用 /api 去參與匹配)
(3)Named Location Blocks

Named Location 不能直接用於常規請求處理,並且永遠不會與請求URI匹配。相反,它們只適用於內部重定向,搭配 try_files 指令。

try_files 指令檢查是否存在所提供的文件名(按順序檢查,從左到右),try_files 的最后一個參數便是 Named Location。

try_files 是 nginx 在 0.7 以后的版本中加入的指令(放置在 server 或 location 塊中),配合 Named Location,可以部分替代原本常用的 rewrite 。(rewrite 下面會詳細介紹)

使用:

location / {
 try_files maintenance.html index.html @foobar;
}
location @foobar {
 ...
}

拓展:try_files 不一定非要搭配 location,直接返回響應碼也是可以的:

location / {
 try_files maintenance.html index.html =404
}
(4)Location Block 內的指令

① 指定文件系統路徑 —— root / alias

1、root 指令:

location /foobar/resource_root { 
    root /etc/nginx/static/;
} 

http://example.org/foobar/resource_root/b.jpg 的請求將解析為文件系統路徑 /etc/nginx/static/foobar/resource_root/b.jpg。

原理:文件系統目錄 = root + url_path(即 /etc/nginx/static/ + /foobar/resource_root/b.jpg = /etc/nginx/static//foobar/resource_root/b.jpg )

注:上面的結果 /etc/nginx/static//foobar/resource_root/b.jpg 注意中間是有兩個斜杠。root 路徑最后的斜杠可帶可不帶,因為 url_path 的開頭肯定是有斜杠的,最終都會合法。即 /etc/nginx/static/; 也可以,/etc/nginx/static; 也可以。


2、alias 指令:

location /foobar/resource_alias {  
    alias /etc/nginx/static/;
} 

https://xjnotxj.com/foobar/resource_alias/img/a.jpg 的請求將解析為文件系統路徑 /etc/nginx/static/img/a.jpg。

原理:文件系統目錄 = alias + (url_path - location)(即 /etc/nginx/static/ + /img/a.jpg = /etc/nginx/static//img/a.jpg )

注:上面的結果 /etc/nginx/static//img/a.jpg 注意中間是有兩個斜杠。alias 路徑最后的斜杠帶不帶要仔細考量,因為 (url_path - location) 的開頭不一定有斜杠,最終結果不一定合法。


注:root 和 alias 還有一個區別,即:alias 只能位於 location 塊中,而 root 可以在 location 塊也可以在 server 塊。(原因很簡單,從它們兩者的原理就可以看出,因為 alias 計算文件系統路徑需要 location 的參與,而 root 不需要。)


② 指定網站默認頁 —— index 指令

默認值:index index.html

原理:內部重定向【重點】

內部重定向即再一次搜索同一個 server 下的 location

最早的時候這個文件被叫作“主頁” (home page),意思就是當省略文件名時訪問的那個默認的頁面。隨着 Web 的普及,這個詞的意義似乎並沒有被正確理解,現在不光是默認頁面,似乎隨便什么網頁都可以被叫作主頁了。

7、Configuring SSL

(1)SSL 證書

① 證書介紹

SSL證書需要國際公認,得從證書認證機構(簡稱CA,Certificate Authority)申請。

證書有3種類型:

  • 域名型SSL證書(DV SSL):一般用於自建網站
  • 企業型SSL證書(OV SSL):一般用於普通企業
  • 增強型SSL證書(EV SSL):一般用於銀行證券等金融機構。

這幾種證書的區別:

  • 審核嚴格程度
  • 是否支持多域名(單域名 / 通配符域名(子域名)/ 多域名)
  • 證書公信等級
  • 加密強度
  • 瀏覽器及兼容性
  • 是否支持安全保險

② 證書購買

以在阿里雲買為例:

因為我搭的是個人站點,所以選擇最便宜的證書,即:Digicert 這家的免費版 DV 證書(單域名 + 一年有效期)

注:需要備案。


③ 證書下載

包含兩個文件:

  • .pem:證書文件。
  • .key:證書的密鑰文件。
(2)配置 HTTPS

① 上傳證書

在 Nginx 的配置文件所在目錄(/etc/nginx)下創建cert目錄,並將上面下載的證書文件和密鑰文件拷貝進去。


② 修改配置文件

  server {
  listen 443;
  server_name localhost;
  ssl on;
  ssl_certificate cert/3921616_xjnotxj.com.pem;
  ssl_certificate_key cert/3921616_xjnotxj.com.key;
  ssl_session_timeout 5m;
  ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
  ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP;
  ssl_prefer_server_ciphers on;
 

注1、開啟 ssl 的兩種方式:

① 推薦
listen 443 ssl;

② deprecated,不建議用
listen 443;
ssl on;

注2、ssl_certificate 和 ssl_certificate_key 可以使用相對路徑 or 絕對路徑。

記得 reload nignx。


(3)設置 HTTP 自動跳轉到 HTTPS(nook)
server {
 listen 80; 
 rewrite ^(.*)$ https://$host$1 permanent; 
} 

8、日志

(1)基本用法

① 訪問日志 access_log

需要設置 format:

access_log  /var/log/nginx/access.log  main;

log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for"';


② 錯誤日志 error_log

需要設置 級別:

error_log  /var/log/nginx/error.log warn; 

級別按嚴重程度分:debug,info,notice,warn,error,crit,alert 和 emerg,只有等於或高於此級別才會寫入錯誤日志中。

(2)日志切割

待寫。

9、配置范例

https://www.nginx.com/resources/wiki/start/topics/examples/full/

四、動態程序


上面介紹的是 nginx 處理靜態文件,nginx 也可以運行程序,然后將程序輸出的數據返回給客戶端,即動態頁面

1、CGI、FastCGI

實現步驟:

  • 一般通過 url 路徑中的文件擴展名來進行判斷是靜態還是動態頁面,例如將 .cgi、.php 等擴展名設置為動態頁面。
  • 接着通過配置 CGI / FastCGI 來運行程序,返回結果。

    與 Apache 不同,nginx 不能嵌入編程語言解釋器進入 Web 服務器,就像 Apache 使用 mod_php 一樣。只能通過 CGI / FastCGI,這讓 nginx 輕量很多。

詳細介紹請參考我的舊文:CGI + FastCGI(PHP-FPM)聯系與區別 【圖解 + 注釋】

2、反向代理

其他大多數語言(Ruby,Node,Go等),則不用 CGI / FastCGI 之流,而是使用反向代理(下面會介紹)。

五、反向代理


1、用處

  • 實現動態Web應用程序
  • 負載均衡

    下面會專門介紹

2、用法

以 nodejs 版本為例:

http {

    upstream node_app {
        server 127.0.0.1:3000;  # focus here
    }

    server {
            listen *:80;
            root /path/to/application/public;
            location / {
                try_files $uri $uri/index.html @node;
            }
            location @node {
                proxy_pass http://node_app;   # focus here
            }
    }
    
}

注1:這里的 upstream 中的 server 只有一個,如果是多個的話,就起到負載均衡的功能了。

注2:server 支持三種形式:

upstream backend {
    # IP Address with Port
    server 192.0.2.10:443;
    # Hostname
    server app1.example.com;
    # Unix Socket(下面會專門介紹)
    server unix:/u/apps/my_app/current/tmp/unicorn.sock
}
[拓展] nginx 中關於 url 的變量

如果 url 是 http://example.com/stat.php?id=1585378&web_id=1585378 ,則:

  • $request_uri: /stat.php?id=1585378&web_id=1585378
  • $uri: /stat.php
  • $document_uri: /stat.php (=$uri)

3、Upstream 使用域名(Hostname)

當 nginx 初始化並讀取配置文件,它會使用 DNS 查找來解析域名,獲取 ip 地址:

  • 如果域名跟 ip 的綁定是靜態的,沒關系。
  • 如果域名跟 ip 的綁定是動態的,那 nginx 就會報錯(example.com could not be resolved.)

解決方案:使用 resolve

http {
 resolver 8.8.8.8; # 指定 DNS 地址
 # resolver 8.8.8.8 valid=30s; # valid 可選,默認使用響應的TTL值。
 upstream backend {
    server loadbalancer.east.elb.amazonaws.com resolve; # focus here
 }
}

4、Upstream 使用 Unix Domain Sockets

如果 upstream 指定的 server 在同一台機器上,可以使用 Unix Domain Sockets(UDS)

UDS 屬於進程間通信IPC,Inter-Process Communication)的一種,僅在操作系統內核中完成,不需要經過網絡協議棧(免去打包拆包等操作,因此性能更好),使用文件系統作為名稱空間(不需要IP和Port)。

① 應用程序(以 Node.js 為例):

var http = require('http');

http.createServer(function(req, res) {
  console.log('received request');
  res.end('received request\n');
}).listen('/tmp/node_app.socket');    // focus here
// }).listen(80);

② nginx 配置:

upstream node_app {
    server unix:/tmp/node_app.socket;  # focus here
}

注意 /tmp/node_app.socket 的權限問題。

5、upstream 常見的 headers 轉發

(1)默認行為

默認情況下,nginx 在代理請求時,會對 headers :

  • 重新定義 “Host” 和 “Connection” 標頭
  • remove 值為空字符串的標頭
  • 剩下的標頭會繼承
(2)如何修改 headers

通過 proxy_set_header

(3)常見的修改

① Host【推薦】

proxy_set_header Host $host;

因為默認的 nginx 會修改 host 值為自身,但作為一個好的代理,最好改寫 host 為源值。

例如,在一些 SaaS 應用程序中通過 host 標識帳戶很常見。


② X-Forwarded-XXX

這些字段是代理特有的字段,分別表示自身的 Host、IP 地址、協議類型(http/https)、端口。

注意,除了 X-Forwarded-For 是多個值(格式為 <client>, <proxy1>, <proxy2>, ……),其余的都是單個值。

  • X-Forwarded-Host
  • X-Forwarded-For(最常用,簡稱 XFF
  • X-Forwarded-Proto
  • X-Forwarded-Port

具體修改(主要是 X-Forwarded-For),下面會介紹。

補充:X-Forwarded-For 是一個 HTTP 擴展頭部。HTTP/1.1(RFC 2616)協議並沒有對它的定義,它最開始是由 Squid 這個緩存代理軟件引入,用來表示 HTTP 請求端真實 IP。如今它已經成為事實上的標准,被各大 HTTP 代理、負載均衡等轉發服務廣泛使用,並被寫入 RFC 7239(Forwarded HTTP Extension)標准之中。

6、獲取客戶端(用戶)的真實 IP

(1)服務器端獲取方式

以 Node.js(Express)為例:

這里我們用到第三方庫 request-ip,原理為:

① 查找 HTTP Header (針對源目標的 IP 地址,未必真實(HTTP Header 的頭可以隨便修改))

按順序查找:

X-Client-IP
X-Forwarded-For (Header may return multiple IP addresses in the format: "client IP, proxy 1 IP, proxy 2 IP", so we take the the first one.)
CF-Connecting-IP (Cloudflare)
Fastly-Client-Ip (Fastly CDN and Firebase hosting header when forwared to a cloud function)
True-Client-Ip (Akamai and Cloudflare)
X-Real-IP (Nginx proxy/FastCGI)
X-Cluster-Client-IP (Rackspace LB, Riverbed Stingray)
X-Forwarded, Forwarded-For and Forwarded (Variations of #2) 

② 查找 req 對象 (針對上一跳的 IP 地址(未必是源目標),但一定真實(因為基於 tcp 的可靠連接, ip 無法偽造))

按順序查找:

req.connection.remoteAddress
req.socket.remoteAddress
req.connection.socket.remoteAddress # 會報錯 Cannot read property,估計廢棄了
req.info.remoteAddress # 會報錯 Cannot read property,估計廢棄了

③ 如果還是找不到,返回 null

不建議使用 express 自帶的 req.ip,因為它只會取 X-Forwarded-For 的值返回數組。


④ 補充:

建議條件充足可以把都記錄下來,方便后續使用或者人工排查。

(2)nginx 配置

nginx 這里要做好上一跳 IP 地址的記錄(記錄在 Header 中),否則服務器端獲取的就是 nginx 的 IP 地址了。

① 方法1:使用 proxy_set_header 改寫 header

proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  • $remote_addr 為上一跳的 IP 地址。
  • $proxy_add_x_forwarded_for"X-Forwarded-For" + ”,“ + "$remote_addr",如果沒有 "X-Forwarded-For",直接 = "$remote_addr"。

② 方法2:使用 nginx 的 http_realip 模塊

編譯安裝的時候,add --with-http_realip_module

set_real_ip_from 192.168.1.0/24; #真實服務器上一級代理的IP地址或者IP段,可以寫多行。
set_real_ip_from 192.168.2.1; 
# real_ip_header X-REAL-IP; 
real_ip_header X-Forwarded-For;  #從哪個header頭檢索出要的IP地址。
real_ip_recursive on; #遞歸的去除所配置中的可信IP。排除set_real_ip_from里面出現的IP

實測跟 Node.js app 搭配不起作用,不知道為什么?這里待寫。

7、WebSockets

(1)HTTP Header 流程

步驟1:瀏覽器發送請求,申請升級為 WebSockets 協議。

Upgrade: websocket
Connection: Upgrade

步驟2:如果服務器支持 Websockets,它將以 HTTP 狀態碼 101 響應切換協議而不是正常的 200 響應,且返回一樣的 Header。

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
(2)nginx 如何配置

nginx 對 WebSockets 開箱即用。

location /chat {
 proxy_pass http://node_app;
 proxy_http_version 1.1;
 proxy_set_header Upgrade $http_upgrade;  # focus here
 proxy_set_header Connection "upgrade";  # focus here
 proxy_set_header Host $host;
} 

proxy_http_version 被顯式設置成 1.1,其實沒有,默認還是 1.1。

六、負載均衡


負載平衡實際上是反向代理的一種。

1、HTTP 負載均衡

詳細可看我之前的一篇:《nginx官方文檔 之 http負載均衡 學習筆記》

2、TCP 負載均衡

  • nginx 開源版 1.9.0 支持 TCP 負載平衡。
  • 之前只能使用商業版的 nginx,或者別的競品: LVS、Nginx、HAProxy。

    這些競品中,作者推薦使用 nginx。從技術上講,在負載方面,HAProxy 和 Varnish 可能比 nginx 快 5-10%。但是,nginx 簡單,且適應面更廣。AWS上 的 ELB 當然更簡單,但功能也有限。

(1)寫法
stream {
    upstream backend {
        server 127.0.0.1:8089; 
        server 127.0.0.2:1935;
        server 127.0.0.3:1935;  
    }
    server {
        listen 80;  
        proxy_pass backend;
    }
}

跟 HTTP 負載均衡寫法的異同:

  • stream block 取代 http block(stream 表示 TCP/UDP 流量
  • upstream 寫法不變
  • proxy_pass 屬於 server 而不是 location,server 也不存在 location。
(2)優缺點

優點:性能好。直接轉發原始TCP數據包,沒有數據解析;如果是 HTTPS ,也免去了 SSL 層的損耗。

缺點:功能損失。例如無法通過 proxy_set_header 改寫/注入 HTTP 標頭


[拓展] 既然 proxy_set_header 無法用,那 TCP 代理怎么傳遞客戶端的原始 IP 信息呢?

可以使用 proxy protocol

proxy protocol 是 HAProxy 的作者 Willy Tarreau 於 2010 年開發和設計的一個 Internet 協議,通過為 tcp 添加一個很小的頭信息,來方便的傳遞客戶端信息(協議棧、源IP、目的IP、源端口、目的端口等)。其本質是在三次握手結束后由代理在連接中插入了一個攜帶了原始連接四元組信息的數據包。

具體例子參考:https://docs.nginx.com/nginx/admin-guide/load-balancer/using-proxy-protocol/

proxy protocol 也可以用在 HTTP 負載均衡上。

3、郵件協議的負載均衡

# 郵件流量
mail{}

待寫。

七、重寫


1、介紹

rewrite 是 nginx 的重寫模塊,即ngx_http_rewrite_module

2、目的

  • 通知用戶他們正在請求的資源現在位於其他位置
  • 控制處理流程
  • ……

3、涉及命令

  • returnrewrite

    之前介紹的 try_files 也可以部分做到

  • 重寫模塊同時也涉及了 breakifset 等其它命令

4、return

(1)介紹

語法:

return URL; # 外部重定向
return code URL;
return code [text];

可以將 return 指令放置在 server 或 location 中。

(2)demo
return https://baidu.com; # 默認 302
return 301 https://baidu.com; # 必須是支持重定向的響應碼,比如 200 就不行
return 200 "okokok"; # 修改不了響應行文本,原因未知

5、rewrite

(1)介紹

語法:rewrite regex replacement [flag];

可以將 rewrite 指令放置在 server 或 location 中。

(2)參數

① regex 為正則表達式

② flag 取值如下:

flag break last redirect permanent
是否停下
重定向 不會 不會 內部重定向 外部重定向(臨時,302) 外部重定向(永久,301)

注意,rewrite 后 url 就會變了,下一個 rewrite 識別的就是改過的 url

③ replacement 為重寫的值

  • 如果 replacement 以協議頭開頭(如:http://,https:// 或 $scheme),則效果相當於 flag = redirect。
  • 如果想在 replacement 中拋棄之前路徑的請求參數,可以在最后加了個 ?,如 rewrite ^/users/(.*)$ /show?user=$1?
(3)demo

以 flag = 無 為例:

location / {  
    rewrite ^/user/(.*)$  /show/$1;
    rewrite ^/show/(.*)$  /hide/$1;
} 

location /show {
    return 200;
}
location /hide {
    return 201;
}    

訪問 https://example.com/user/a.jpg:

  • 1、/user/a.jpg 被 location / 捕獲
  • 2、第一個 rewrite 把 /user/a.jpg 改寫成 /show/a.jpg

    注意,這時的路徑已經被改寫成 /show/a.jpg 了。

  • 3、第二個 rewrite 把 /show/a.jpg 改寫成 /hide/a.jpg
  • 4、下面沒有命令了,執行內部重定向
  • 5、/hide/a.jpg 被 location /hide 捕獲,返回 201

注:諸如 $1$2 這種變量,表示正則表達式匹配的第 N 個參數.

(4)死循環

某些情況下,rewrite 重定向后,可能又會執行 rewrite,然后循環往復,進入死循環。

為了避免這種情況,nginx 會在循環 10 次時之后,直接返回 500 異常

6、其他命令

(1)set —— 設置變量
set $var1 "hello world";
return 200 "response ok $var1";
(2)if —— 判斷條件

if 比 rewrite 的正則匹配更靈活。

待寫。

7、實例

注意:rewrite 因為有正則,所以比 return 更加影響性能(if 語句也如是),且可讀性差,所以如果能用 return 就用 return。

(1)從舊網址重定向到新網址
server {
    listen 80;
    listen 443 ssl;
    server_name www.old-name.com old-name.com;

    return 301 $scheme://www.new-name.com$request_uri; 

    # 不推薦
    rewrite ^ $scheme://www.new-name.com$request_uri permanent;   
}
(2)強制所有請求使用 HTTPS
server {
    listen 80;
    server_name www.domain.com;

    return 301 https://www.domain.com$request_uri;

    # 不推薦
    if ($scheme != "https") {
        rewrite ^ https://www.mydomain.com$request_uri permanent;
    }
}
(3)添加/刪​​除 www 前綴
# add 'www'
server {
    listen 80;
    listen 443 ssl;
    server_name domain.com;
    return 301 $scheme://www.domain.com$request_uri;
}

# remove 'www'
server {
    listen 80;
    listen 443 ssl;
    server_name www.domain.com;
    return 301 $scheme://domain.com$request_uri;
}
(4)兜底,把其它流量重定向到正確的域名 or 返回錯誤
server {
    listen 80 default_server;
    listen 443 ssl default_server;
    server_name _;

    return 301 $scheme://www.domain.com;
    # or
    return 444
}

八、調試


1、調試 nginx

用到 --with-debug ,待寫。

2、如何調試 location

可以通過在不同 location 里添加 access_log 來調試。

3、如何調試 rewrite

  • 1、rewrite_log on; 開啟 nginx 日志
  • 2、設置 error_log 的 level 是 notice

九、監控


監控指標:

2021-04-07-00-14-35

1、使用 http_stub_status_module 模塊

待寫

2、使用 Nginx Plus

待寫

3、使用 openresty + lua

待寫

4、使用 阿里雲 日志服務

待寫

十、實例


1、nginx 禁止 / 允許某個(些) IP 訪問

使用到 ngx_http_access_module 模塊

deny,相反的為 allow

deny 1.1.1.1; 
deny 8.0.0.0/8;
deny all
 
allow 1.1.1.1; 
allow 8.0.0.0/8;
allow all; # default 

原理:通過 $remote_addr 來判斷。

在需要很多規則的情況下,最好使用 ngx_http_geo_module 模塊。

結果:返回 403。

2、錯誤處理

error_page

(1)常見 HTTP (響應碼)錯誤
  • 403 Forbidden
  • 404 Not Found
  • 500 Internal Server Error

    通常是 upstream server 返回的錯誤

  • 502 Bad Gateway

    通常是 nginx 無法到達 upstream server

  • 503 Service Unavailable

    通常是 nginx 因暫時超載或臨時維護,無法處理HTTP請求

  • 504 gateway timeout

    通常是 upstream server 返回超時

(2)用法

error_page 可以位於 server block / location block

原理:通過內部重定向。(跟上面介紹的 index 指令 一樣)

① 針對普通情況

基本:

# 指向錯誤頁面
error_page 404 /404.html;
error_page 500 502 503 504 /50x.html;

# 重定向到外部 url
error_page 500 http://www.google.com;

# 重定向到 named location block
location / {
    error_page 404 = @fallback;
}
location @fallback {
    proxy_pass http://different_backend;
}

注1:因為 error_page 是通過內部重定向,所以你可以顯示地寫一個與之對應的 location (如 location /404.html)來干更多事。

注2:因為 error_page 是通過內部重定向,所以 access 日志會有兩條記錄。


高級:

# 更改HTTP狀態碼(不推薦,這樣 nginx 日志記錄也是)
error_page 500 = 200 /index.html

② 針對反向代理(使用 proxy_intercept_errors,error_page 才會生效)

location @rails {
    proxy_pass http://rails_app;
    proxy_intercept_errors on; # focus here
    error_page 404 /404.html;
}

十一、優化


NGINX是第一個可同時建立10,000個並發連接(即“C10K問題”)的Web服務器軟件。

所以下面介紹的的優化,更多的還是為了實現高並發的目標。

這里介紹的只是簡單的優化,離 C10K 還有點距離,但是經過這種一般優化后,峰值能保持在 1~3w 左右。(內存和 CPU 核心數不同,會有進一步優化空間)

1、Linux (內核)配置

(1)sysctl 的使用

sysctl 命令被用於在內核運行時動態地修改內核的運行參數,它包含一些TCP/IP堆棧和虛擬內存系統的高級選項。

查看配置:sysctl -a

臨時添加配置(重啟后會失效):sysctl -w net.ipv4.ip_forward=1

永久添加配置:修改配置文件 /etc/sysctl.conf,並重啟使之生效 /sbin/sysctl -p

(2)net.core.somaxconn

net.core.somaxconn 參數表示 socket listen backlog(socket 的監聽隊列)上限。當一個請求尚未被處理或建立時,他會先進入 backlog 等着。所以當 server 處理請求較慢,以至於監聽隊列被填滿后,新來的請求就會被拒絕。

所以你也可以把 somaxconn 理解成 能接收新的 TCP 數 or TCP 最大連接數。

默認值:128

什么時候需要設置:並發量高的時候(如在 error log 中發現 [73920] possible SYN flooding on port 80. Sending cookies. 的報錯)

設置小了的危險:連接超時或重傳

推薦值:根據你的當前流量來,如果害怕突發流量,可以設置最大(65535)。

2、nginx 配置優化

(1)連接

① 最大連接數

Nginx 是多進程模型,Worker 進程用於處理請求;

  • 單個 worker 進程的最大連接數:worker_connections
  • worker 進程的數量:worker_processes

注意:worker_connections 受 Linux 的進程最大打開文件數限制,需執行ulimit -n 65535 或修改相應配置文件。

所以:max_clients(Nginx 的最大連接數) = worker_connections x worker_processes

注意:Nginx 作為反向代理服務器時,max_clients(Nginx 的最大連接數) = worker_connections x worker_processes / 2,因為 Nginx 會同時建立 Client 的連接和后端 Web Server 的連接,即占用 2 個連接。


② 最大打開文件數

worker_rlimit_nofile即單個 worker 進程最大打開文件數,最好與 ulimit -n 的值保持一致。

worker_rlimit_nofile 65535;

③ 同一時刻接收多個連接

默認情況下,Worker 進程只會在一個時刻接收一個新的連接,multi_accept 為 on 可以實現在一個時刻內可以接收多個新的連接,提高處理效率。

events {
  multi_accept on; # 默認 off
}
(2)各種 timeouts

待寫

(3)各種 buffer

buffer 即內存中處理,超過 buffer 指定的大小,則會被臨時寫入文件,這便影響了性能。

待寫。

(4)禁用 access_log

比如針對靜態資源,可以加上 access_log off;

(5)壓縮

Gzip 的壓縮頁面需要瀏覽器和服務器雙方都支持。

關於后來 google 推的 Brotli 壓縮算法,對比 Gzip 可再減少20%,這個待寫。

過程:

  • 瀏覽器發送請求頭包含:Accept-Encoding: gzip, deflate, br
  • 服務器響應請求頭包含:Content-Encoding: gzip

配置:

gzip on;                
gzip_min_length 1k;      # 設置允許壓縮的最小字節(從header頭的Content-Length中獲取) 。當值為0時,所有都進行壓縮。建議大於1k
gzip_buffers 4 16k;      # 設置gzip申請內存的大小,按塊大小的倍數
gzip_http_version 1.1;   # 設置允許壓縮的最低http版本
gzip_comp_level 5;       # 設置壓縮等級,等級1-9,越低,壓縮速度越快(越不占用 CPU)文件壓縮比越小,反之相反
gzip_types text/plain application/x-javascript text/css application/xml text/javascript application/x-httpd-php application/javascript application/json;    # 設置需要壓縮的MIME類型
gzip_vary on;            # 響應頭 add:"Vary: Accept-Encoding"
 
gzip_proxied off;        # 下面單獨介紹

解釋:

1、gzip_comp_level 按需求來把,不然適用即可。通常壓縮后的大小會減小到原來的 1/4 - 1/3。

2、gzip_types,默認為 text/html; 即只壓縮 html,但推薦壓縮 html 、js 、css 、xml 、shtml,不推薦壓縮圖片和視頻(因為本身就壓縮過了)
Nginx 判斷文件類型不是分析文件內容,而是分析文件后綴(根據文件擴展名確定其MIME類型)

3、gzip_proxied,當 nginx 自己處在反向代理中:

  • off (不啟用壓縮)【默認】
  • expired (啟用壓縮,如果header頭中包括"Expires"頭信息)
  • no-cache (啟用壓縮,如果header頭中包含"Cache-Control:no-cache")
  • no-store (啟用壓縮,如果header頭中包含"Cache-Control:no-store")
  • private (啟用壓縮,如果header頭中包含"Cache-Control:private")
  • no_last_modefied (啟用壓縮,如果header頭中不包含"Last-Modified")
  • no_etag (啟用壓縮,如果header頭中不包含"Etag"頭信息)
  • auth (啟用壓縮,如果header頭中包含"Authorization"頭信息)
  • any (無條件啟用壓縮)

十二、使用 Lua 擴展 Nginx 功能


最先將 Nginx,Lua 組合到一起的是 OpenResty,它有一個ngx_lua模塊,將Lua嵌入到了Nginx里面。

隨后Tengine也包含了ngx_lua模塊。至於二者的區別:OpenResty是Nginx的Bundle;而Tengine則是Nginx的Fork。

值得一提的是,OpenResty和Tengine均是國人自己創建的項目,前者主要由春哥和曉哲開發,后者主要由淘寶打理。

待寫。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM