tcpdump抓包學習Nginx(反向代理),學完不怵nginx了

運維老男孩 發佈 2022-03-21T22:26:05+00:00

俄羅斯年輕程式設計師Igor Sysoev為了解決所謂C10K problem,也就是以前的Web Server不能支持超10k並發請求的問題,在2002年開啟了新的Web Server的開發。

0. 前話

俄羅斯年輕程式設計師Igor Sysoev為了解決所謂C10K problem,也就是以前的Web Server不能支持超10k並發請求的問題,在2002年開啟了新的Web Server的開發。

Nginx2004年在2-clause BSD證書下發布於眾,根據2021年3月Web Server的調查,Nginx持有35.3%的市場占有率,為4.196億網站提供服務。

感謝DigitalOcean公司的NGINXConfig項目,提供了很多寫好的Nginx模板供下載,這樣就可以在不理解Nginx配置的情況下複製粘貼配置Nginx。

這裡不是說複製粘貼是不對的,而是如果只是複製粘貼並不理解的話,遲早會出問題。所以,你必須理解Nginx的配置,通過學習本文,你能夠:

  • 理解工具生成或者別人配置的Nginx。
  • 從0到1配置Web伺服器、反向代理伺服器和負載均衡伺服器。
  • 優化Nginx獲取最大性能。

學習本文需要有一定的Linux基礎,會執行例如ls、cat等Linux命令,還需要你對前後端有一定的了解,不過這些對前端或者後端程式設計師都很容易。

1. Nginx基本介紹

Nginx是一個高性能的Web伺服器,著眼於高性能、高並發和低資源消耗。儘管Nginx作為一個Web伺服器被大家所熟知,它另外的一個核心功能是反向代理。

Nginx不是市場上唯一的Web伺服器,它最大的競爭對手Apache HTTP Server(httpd)在1995年就發布了。人們在選擇Nginx作為Web伺服器時候,基於下面兩點考慮:

  • 支持更高的並發。
  • 用更少的硬體資源提供靜態文件服務。

Nginx和Apache誰更好的爭論沒有意義,如果想了解更多Nginx和Apache的區別可以參考Justin Ellingwood的文章。

關於Nginx對請求處理的新特點,引用Justin的文章解釋如下:

Nginx在Apache之後出現,更多認識到網站業務擴大之後面臨的並發性問題,所以從一開始就設計為異步、非阻塞和事件驅動連接處理的算法。

Nginx工作時候會設定worker進程(worker process),每一個worker進程都能夠處理數千個連接。worker進程通過fast looping的機制來不斷輪詢處理事件。將具體處理請求的工作和連接解耦能夠讓每一個worker進程僅當新的事件觸發的時候將其與一個連接關聯。

Nginx基本工作原理圖:

Nginx之所以能夠在低資源消耗的情況下高性能提供靜態文件服務,是因為它沒有內置動態程式語言處理器。當一個靜態文件請求到達後,Nginx就是簡單的響應請求文件,並沒有做什麼額外的處理。

這不是說Nginx不能夠整合動態程式語言處理器,它可以將請求任務代理到獨立的進程上,例如PHP-FPM、Node.js或者Python。一旦第三方進程處理完請求,再將響應代理回客戶端,工作如圖:

2. 怎麼安裝nginx

Nginx的安裝網上示例很多,這裡以Ubuntu為例:

#更新源
sudo apt update && sudo apt upgrade -y

#安裝
sudo apt install nginx -y
複製代碼

這種方式安裝Nginx成功之後,Nginx會註冊為systemd系統服務,查看服務:

sudo systemctl status nginx

#如果沒有註冊為systemd服務,可以用service查看試下
sudo service nginx status
複製代碼

Nginx的配置文件經常放在/etc/nginx目錄中,默認的配置埠是80,如果啟動成功,可以訪問得到頁面:

恭喜!Nginx安裝成功了!

3. Nginx配置文件管理

Nginx為靜態或者動態文件提供服務,具體怎麼樣提供服務是由配置文件設置的。

Nginx的配置文件以.conf結尾,常常位於/etc/nginx目錄中。訪問/etc/nginx目錄:

cd /etc/nginx

ls -lh

# drwxr-xr-x 2 root root 4.0K Apr 21  2020 conf.d
# -rw-r--r-- 1 root root 1.1K Feb  4  2019 fastcgi.conf
# -rw-r--r-- 1 root root 1007 Feb  4  2019 fastcgi_params
# -rw-r--r-- 1 root root 2.8K Feb  4  2019 koi-utf
# -rw-r--r-- 1 root root 2.2K Feb  4  2019 koi-win
# -rw-r--r-- 1 root root 3.9K Feb  4  2019 mime.types
# drwxr-xr-x 2 root root 4.0K Apr 21  2020 modules-available
# drwxr-xr-x 2 root root 4.0K Apr 17 14:42 modules-enabled
# -rw-r--r-- 1 root root 1.5K Feb  4  2019 nginx.conf
# -rw-r--r-- 1 root root  180 Feb  4  2019 proxy_params
# -rw-r--r-- 1 root root  636 Feb  4  2019 scgi_params
# drwxr-xr-x 2 root root 4.0K Apr 17 14:42 sites-available
# drwxr-xr-x 2 root root 4.0K Apr 17 14:42 sites-enabled
# drwxr-xr-x 2 root root 4.0K Apr 17 14:42 snippets
# -rw-r--r-- 1 root root  664 Feb  4  2019 uwsgi_params
# -rw-r--r-- 1 root root 3.0K Feb  4  2019
複製代碼

該目錄中的/etc/nginx/nginx.conf就是Nginx的主配置文件。如果你打開這個配置文件,會發現很多內容,不要害怕,本文就是一點一點地要學會它。

在進行配置文件修改的時候,不建議直接修改/etc/nginx/nginx.conf,可以將之備份之後再修改:

#重命名文件
sudo mv nginx.conf nginx.conf.backup

#新建配置文件
sudo touch nginx.conf
複製代碼

4. Nginx配置為一個基本的Web Server

這一部分,將會從零一步步學習Nginx配置文件的書寫,目的是了解Nginx配置文件的基本語法和基本概念。

4.1 寫第一個配置文件

vim /etc/nginx/nginx.conf打開配置文件並更新內容:

events {

}

http {

    server {

        listen 80;
        server_name localhost;

        return 200 "Bonjour, mon ami!\n";
        #配置重定向
        #return 302 https://www.baidu.com$request_uri;
    }

}
複製代碼

重啟Nginx並訪問,你會得到如下信息:

curl -i http://127.0.0.1

HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Sat, 19 Feb 2022 08:31:59 GMT
Content-Type: text/plain
Content-Length: 21
Connection: keep-alive

Bonjour, mon ami!
複製代碼

4.2 校驗、重載Nginx配置文件

Nginx的配置文件是否正確可以通過-t參數校驗:

sudo nginx -t

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
複製代碼

如果有相關的語法錯誤,上述命令輸出結果會有相關提示。

如果你想改變Nginx的相關狀態,例如重啟、重載等,可以有三種辦法。一是通過-s(signal)參數向Nginx發送信號;二是使用系統服務管理工具systemd或者service等;三是使用kill命令對Linux進程操作。

向Nginx發送信號

nginx信號:nginx -s reload|quit|stop|reopen,分別表示重載配置文件、優雅停止Nginx、無條件停止Nginx和重新打開log文件。

所謂的「優雅停止」Nginx,是指處理完目前的請求再停止;而「無條件停止」Nginx,相當於kill -9,進程直接被殺死。

系統服務管理Nginx

#使用systemctl
sudo systemctl start|restart|stop nginx

#或者使用service
sudo service nginx start|restart|stop
複製代碼

kill命令殺死進程並手動啟動

#殺死主進程及各子進程
sudo kill -TERM $MASTER_PID

#指定配置文件啟動Nginx
sudo /usr/sbin/nginx -c /etc/nginx/nginx.conf
複製代碼

4.3 理解Nginx配置文件中的Directives和Contexts

Nginx的配置文件雖然看起來只是簡單的配置文本,但它是包含語法的。實際上配置文件中的內容都是Directives。Directives分為兩種:

  • Simple Directives
  • Block Directives

Simple Directives:包含名稱和空格,以分號(;)結尾。例如listen、return等。

Block Directives:包裹在{}中,{}由Simple Directives組成,稱之為Contexts。

Nginx配置中核心的Contexts:

  • events{}:總體配置nginx如何處理請求,只能在配置文件中出現一次。
  • http{}:配置nginx如何處理http或者https請求,只能在配置文件中出現一次。
  • server{}:內嵌在http{}中,用來配置一個獨立主機上指定的虛擬主機。http{}可以配置多個server{},表示多個虛擬主機。
  • main:上述3個Contexts之外的配置都在該Contex上。

在主機上設置不同的虛擬主機(多個server{}、相同server_name),監聽不同的埠(listen不同):

http {
    server {
        listen 80;
        server_name localhost;

        return 200 "hello from port 80!\n";
    }


    server {
        listen 8080;
        server_name localhost;

        return 200 "hello from port 8080!\n";
    }
}
複製代碼

不同的虛擬主機,監聽同一個埠(多個server{}、不同server_name),監聽同一個埠(listen相同):

這種情況必須用域名,Nginx會將請求頭中Host信息取出來和服務端配置server_name做匹配,匹配到哪個就就進入到那個處理塊中。

http {
    server {
        listen 8088;
        server_name library.test;

        return 200 "your local library!\n";
    }


    server {
        listen 8088;
        server_name librarian.library.test;

        return 200 "welcome dear librarian!\n";
    }
}
複製代碼

當訪問不同的域名時,會返回不同的結果:

curl -i http://library.test:8088

HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Sun, 20 Feb 2022 08:02:20 GMT
Content-Type: application/octet-stream
Content-Length: 21
Connection: keep-alive

your local library !

curl -i http://librarian.library.test:8088

HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Sun, 20 Feb 2022 08:04:26 GMT
Content-Type: application/octet-stream
Content-Length: 24
Connection: keep-alive

welcome dear librarian!
複製代碼

這樣能成功的提前是指定的域名解析到同一個IP,或者在本地的hosts文件中配置好域名進行本地測試:

172.19.146.188 library.test librarian.library.test
複製代碼

注意,這裡return這個Directive後面跟兩個參數,一個是狀態碼,一個是返回的文本信息,文本信息要用引號引起來。

4.4 使用Nginx作為靜態文件伺服器

更新Nginx配置文件如下:

events {

}

http {

    server {

        listen 8088;
        server_name localhost;

        root /usr/share/nginx/html;
    }

}
複製代碼

這裡對Nginx默認的展示頁面做了修改,在文件/usr/share/nginx/html/assets/mystyle.css寫入p {background: red;}並在html文件中引入該css,這樣正常情況段落的背景會變成紅色。

訪問頁面,展示的是index.html,但是段落的背景色沒有生效。debug一下css文件:

curl -i http://fengmengzhao.hypc:8088/assets/mystyle.css

HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Sun, 20 Feb 2022 08:43:58 GMT
Content-Type: text/plain
Content-Length: 27
Last-Modified: Sun, 20 Feb 2022 08:38:54 GMT
Connection: keep-alive
ETag: "6211fe1e-1b"
Accept-Ranges: bytes

p {
    background: red;
}
複製代碼

注意,這裡響應頭信息Content-Type是text/plain,而不是text/css。也就是說Nginx將css文件做為一個普通的文本提供服務,而沒有當做stylesheet,瀏覽器自然就不會渲染樣式。

本文會在本地hosts文件增加域名解析,所以會在示例中看到對域名請求。在操作本文示例時,要根據自己環境對ip(域名)或者埠做相應修改。

4.5 Nginx中處理靜態文件類型解析

實際上這裡涉及到Nginx對靜態文件類型解析的處理,默認不進行任何設置情況下,Nginx認為文本文件的類型是text/plain。

修改配置文件如下:

events {

}

http {

    types {
        text/html html;
        text/css css;
    }

    server {

        listen 8088;
        server_name localhost;

        root /usr/share/nginx/html;
    }
}
複製代碼

重新訪問頁面,樣式正常,mystyle.css文件的response頭Content-Type為text/css:

這裡在http{}中引入了types{},通過文件的後綴映射文件的類型。需要注意,如果沒有types{},nginx會認為.html文件的類型是text/html,但是一旦引入types{},nginx只會解析定義的類型映射。所以這裡引入types{}後,不能只定義css的類型映射,同樣要顯式定義html的類型映射,否則nginx會將html解析為普通文本文件。

4.6 Nginx子配置引入

手動在http{}中增加types{}來映射文件類型對於小項目還可以,對大型項目來說手動配置就太繁瑣了,Nginx提供了默認的解析映射(常常在/etc/nginx/mime.types文件中),可以通過include語法將子配置引入配置文件中。

修改配置如下:

events {

}

http {

    include /etc/nginx/mime.types;

    server {

        listen 80;
        server_name localhost;

        root /usr/share/nginx/html;
    }

}
複製代碼

重啟Nginx,自定義的css文件能夠正常展示。

5. Nginx的動態路由

上面的示例非常簡單,訪問root定義目錄下的文件,存在就返回,不存在就返回默認404頁面。

接下來學習Nginx的location動態路由用法,包括重定向、重寫和try_files Directive。

所謂的動態路徑就是用戶訪問的路逕到達Nginx後,Nginx如何匹配訪問內容。

Location Matches

修改配置文件,如下:

events {

}

http {

    server {

        #設置默認的Content-Type text/html,否則將以流的方式下載
        default_type text/html;
        #設置字符編碼為utf-8,否則頁面會亂碼
        charset utf-8;

        listen 80;
        server_name localhost;
        #前綴匹配,示例:http://fengmengzhao.hypc:8088/agatha----
        location /agatha {
            return 200 "前綴匹配-Miss Marple.\nHercule Poirot.\n";
        }
        #完全匹配,示例:http://fengmengzhao.hypc:8088/agatha
        location = /agatha {
            return 200 "完全匹配-Miss Marple.\nHercule Poirot.\n";
        }
        #正則匹配,默認大小寫敏感,示例:http://fengmengzhao.hypc:8088/agatha01234
        #正則匹配的優先級要高於前綴匹配,低於優先前綴匹配
        location ~ /agatha[0-9]{
            return 200 "正則匹配,大小寫敏感-Miss Marple.\nHercule Poirot.\n";
        }
        #正則匹配,大小寫不敏感,示例:http://fengmengzhao.hypc:8088/AGatHa01234
        location ~* /agatha[0-9]{
            return 200 "正則匹配,大小寫不敏感-Miss Marple.\nHercule Poirot.\n";
        }
        #優先前綴匹配,示例:http://fengmengzhao.hypc:8088/Agatha01234
        #在前綴匹配前加^~即可轉化為優先前綴匹配
        location ^~ /Agatha {
            return 200 "優先前綴匹配-Miss Marple.\nHercule Poirot.\n";
        } 
    }
}
複製代碼

匹配規則總結:

匹配

關鍵字

完全

=

優先前綴

^~

正則

~或者~*

前綴

None

如果一個請求滿足多個配置的匹配,正則匹配的優先級大於前綴匹配,而優先前綴匹配的優先級大於正則匹配,完全匹配優先級最高。

nginx中的變量(Variables)

設置變量:

set $<variable_name> <variable_value>;

# set name "Farhan"
# set age 25
# set is_working true*
複製代碼

變量類型:

  • String
  • Integer
  • Boolean

除了自定義變量外,nginx有內置的變量,參考nginx.org/en/docs/var…。

例如,如下配置中使用內置變量:

events {

}

http {

    server {

        listen 80;
        server_name localhost;

        return 200 "Host - $host\ - $uri\nArgs - $args\n";
    }

}

# curl http://localhost/user?name=Farhan

# Host - localhost
# URI - /user
# Args - name=Farhan
複製代碼

上面使用了$host、$uri和$args內置變量,分別表示主機名、請求相對路徑和請求參數。變量可以作為值賦值給自定義變量,例如:

events {

}

http {

    server {

        listen 80;
        server_name localhost;
        
        set $name $arg_name; # $arg_<query string name>

        return 200 "Name - $name\n";
    }

}
複製代碼

上面出現了$arg_*內置變量,使用$arg_<query string name>可以獲取$args變量中指定的query string。

重定向(Redirects)和重寫(Rewrites)

nginx中的重定向和其他平台上見到的重定向一樣,response返回3xx的狀態碼和location頭信息。如果是在瀏覽器中訪問,瀏覽器會自動重新發起location指定的請求,地址欄url也會發生改變。

重定向示例:

events {

}

http {

    include /etc/nginx/mime.types;

    server {

        listen 80;
        server_name localhost;

        root /usr/share/nginx/html;

        location = /index_page {
                return 307 https://fengmengzhao.github.io;
        }

        location = /about_page {
                return 307 https://fengmengzhao.github.io/about;
        }
    }
}

#curl -I http://localhost/about_page

HTTP/1.1 307 Temporary Redirect
Server: nginx/1.18.0 (Ubuntu)
Date: Mon, 21 Feb 2022 11:47:42 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 180
Connection: keep-alive
Location: https://fengmengzhao.github.io/about
複製代碼

重寫(Rewrites)和重定向不一樣,重寫內部轉發了請求,地址欄不會發生改變。示例如下:

events {

}

http {

    include /etc/nginx/mime.types;

    server {

        listen 80;
        server_name localhost;

        root /usr/share/nginx/html;

        rewrite /image /assets/generate.png;
    }
}

#curl -i http://localhost/image

HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Mon, 21 Feb 2022 11:56:42 GMT
Content-Type: image/png
Content-Length: 144082
Last-Modified: Sun, 20 Feb 2022 08:35:21 GMT
Connection: keep-alive
ETag: "6211fd49-232d2"
Accept-Ranges: bytes

Warning: Binary output can mess up your terminal. Use "--output -" to tell
Warning: curl to output it to your terminal anyway, or consider "--output
Warning: <FILE>" to save to a file.
複製代碼

如果在瀏覽器上訪問http://fengmengzhao.hypc:8088/image,即可展示圖片。

try_files嘗試多個文件

try_files示例:

events {

}

http {

    include /etc/nginx/mime.types;

    server {

        listen 80;
        server_name localhost;

        root /usr/share/nginx/html;

        try_files /assets/xxx.jpg /not_found;

        location /not_found {
                return 404 "sadly, you've hit a brick wall buddy!\n";
        }
    }
}
複製代碼

示例查找/assets/xxx.jpg文件,如果不存在就查找/not_found路徑。

try_files常常和$uri內置變量一起使用:

events {

}

http {

    include /etc/nginx/mime.types;

    server {

        listen 80;
        server_name localhost;

        root /usr/share/nginx/html;

        try_files $uri /not_found;
        #當訪問http://localhost返回404
        #這裡表示,當訪問$uri文件不存在時,嘗試$uri/作為一個目錄訪問
        #try_files $uri $uri/ /not_found;

        location /not_found {
                return 404 "sadly, you've hit a brick wall buddy!\n";
        }
    }
}
複製代碼

6. Nginx的日誌

日誌位置(常常在/var/log/nginx):

ls -lh /var/log/nginx/

# -rw-r----- 1 www-data adm     0 Apr 25 07:34 access.log
# -rw-r----- 1 www-data adm     0 Apr 25 07:34 error.log
複製代碼

刪除日誌文件並reopen Nginx:

# delete the old files
sudo rm /var/log/nginx/access.log /var/log/nginx/error.log

# create new files
sudo touch /var/log/nginx/access.log /var/log/nginx/error.log

# reopen the log files
sudo nginx -s reopen
複製代碼

這裡如果採用上面刪除文件後再創建文件的方法清空日誌,就需要nginx -s reopen重載Nginx,否則新的日誌文件不會被寫入日誌,因為Nginx的輸出流指向還是之前刪除的日誌文件。實際上這裡想清空日誌文件可以採用echo "" > /var/log/nginx/access.log的方法,這樣就不用reopen Nginx了。

訪問Nginx並查看日誌:

curl -I http://localhost

# HTTP/1.1 200 OK
# Server: nginx/1.18.0 (Ubuntu)
# Date: Sun, 25 Apr 2021 08:35:59 GMT
# Content-Type: text/html
# Content-Length: 960
# Last-Modified: Sun, 25 Apr 2021 08:35:33 GMT
# Connection: keep-alive
# ETag: "608529d5-3c0"
# Accept-Ranges: bytes

sudo cat /var/log/nginx/access.log 

# 192.168.20.20 - - [25/Apr/2021:08:35:59 +0000] "HEAD / HTTP/1.1" 200 0 "-" "curl/7.68.0"
複製代碼

默認情況下,任何訪問的日誌都會記錄在access.log文件中,也可以通過access_log Directive來自定義路徑:

events {

}

http {

    include /etc/nginx/mime.types;

    server {

        listen 80;
        server_name localhost;
        
        location / {
            #日誌會在默認配置日誌文件輸出
            return 200 "this will be logged to the default file.\n";
        }
        
        location = /admin {
            #日誌會輸出在/var/logs/nginx/admin.log文件中
            access_log /var/logs/nginx/admin.log;
            
            return 200 "this will be logged in a separate file.\n";
        }
        
        location = /no_logging {
            #禁止日誌輸出
            access_log off;
            
            return 200 "this will not be logged.\n";
        }
    }
}
複製代碼

在location{}中可以自定義access.log的路徑,也可以用access_log off來關閉log輸出。

同樣,error_log也可以自定義Nginx的error.log路徑:

events {

}

http {

    include /etc/nginx/mime.types;

    server {

        listen 80;
        server_name localhost;
        
        error_log /var/log/error.log;
        #return後面只能跟兩個參數,這裡是為了讓Nginx報錯,輸出錯誤日誌
        return 200 "..." "...";
    }

}
複製代碼

使用nginx -s reload重載Nginx:

sudo nginx -s reload

# nginx: [emerg] invalid number of arguments in "return" directive in /etc/nginx/nginx.conf:14
複製代碼

訪問錯誤日誌文件,有同樣的錯誤信息:

sudo cat /var/log/nginx/error.log 

# 2021/04/25 08:35:45 [notice] 4169#4169: signal process started
# 2021/04/25 10:03:18 [emerg] 8434#8434: invalid number of arguments in "return" directive in /etc/nginx/nginx.conf:14
複製代碼

Nginx error日誌信息是有級別的:

  • debug:能幫忙排查哪裡出錯了。
  • info:可以了解但是不必要的信息。
  • notice:比info更值得了解的信息,但不知道也沒什麼。
  • warn:意料之外的事情發生了,哪裡出問題了,但還能工作。
  • error:什麼失敗了的信息。
  • crit:嚴重問題,急需解決。
  • alert:迫在眉睫。
  • emerg:系統不穩定,十萬火急。

默認情況下,Nginx記錄所有級別的Error信息,可以通過error_log第二個參數覆寫。如果要設置最低級別的日誌輸出為warn,更新配置文件如下:

events {

}

http {

    include /etc/nginx/mime.types;

    server {

        listen 80;
        server_name localhost;
        
        error_log /var/log/error.log warn;

        return 200 "..." "...";
    }

}
複製代碼

重載Nginx並查看日誌:

cat /var/log/nginx/error.log

# 2021/04/25 11:27:02 [emerg] 12769#12769: invalid number of arguments in "return" directive in /etc/nginx/nginx.conf:16
複製代碼

這裡可以看到,沒有輸出之前的[notice]日誌了。

7. Nginx作為反向代理伺服器

7.1 什麼是反向代理?

所謂的反向代理,首先是一種代理,是客戶端和服務端之外的第三方。把正向代理(Forward proxy)和反向代理(Reverse proxy)比較起來看就很容易理解。

正向代理一般代理的是客戶端,用戶(客戶端)是知道代理存在(一般是客戶端配置的)。客戶端對目標服務的請求會經由代理轉發並將目標服務響應返回給客戶端。常見的VPN代理、瀏覽器(設置)代理、Git(設置)代理和Fiddler抓包軟體等都是正向代理。

本文中所述的「目標服務」、「被代理的上游服務」、「被代理的服務」、「服務端」均指代proxy_pass配置的被代理的服務。「代理服務」、「代理服務的服務端」指代的是Nginx提供的代理服務。

正向代理示意圖:

反向代理一般代理的是服務端,客戶端直接和代理服務打交道(如果有反向代理的話),而對被代理的服務一無所知。客戶端請求到達代理服務之後,代理服務再將請求轉發到被代理的服務並將響應返回給客戶端。

反向代理示意圖:

上面二圖,可以理解藍色背景的服務是相互知曉的。

Nginx作為反向代理時,處在客戶端和服務端之間。客戶端發送請求到Nginx(反向代理),Nginx將請求發送給服務端。一旦服務端處理完請求,會將結果返回給Nginx,Nginx再將結果返回給客戶端。在這整個過程中,客戶端並不知道實際上誰處理了請求(真正的處理請求並產生響應,而不是代理)。

7.2 反向代理基本原理

筆者剛接觸反向代理的時候,感覺這是一個很神奇的事情。進行簡單的配置就能將第三方的網站代理到自己的主機上嗎?

實際上,不盡然。有些網站能夠將主頁代理過來,但功能不能完全使用;有些代理過來樣式、圖片等加載會出問題。只有理解了個中原理,才能夠解釋各種各樣的情況。

所謂的反向代理就是將客戶端發送來的請求轉發給實際處理請求服務端(proxy_pass指定的服務端),服務端響應之後,再將響應代理回客戶端。

既然是代理,就不僅僅簡單的只做轉發,在代理收到客戶端請求後,準備轉發到指定代理服務端之前,會對請求的header信息進行重寫,例如重寫規則如下(反向代理header重寫章節會對規則做詳細介紹):

  1. 值為空的header不會進行轉發;header的key中包含有_下劃線的不會進行轉發。
  2. 默認改寫Host和Connection兩個header,分別為:Host: $proxy_host、Connection: close。

如果代理伺服器只是轉發,還要什麼代理?就像生活中的代理一樣,會提供增值服務,什麼事情都幫你搞定。

反向代理就是將客戶端的請求,重寫header信息之後,在代理服務的服務端轉發請求到被代理服務,被代理服務處理請求將響應返回給代理服務,代理服務進而轉發響應回客戶端。

代理服務轉發的請求是代理服務端重新發起,因此在客戶端的瀏覽器或者Fiddler工具進行網絡抓包是抓不到的。要看具體的代理髮起網絡請求需要用Wireshark工具抓包代理伺服器對應的網卡。

別理解複雜了,就是客戶端<--->代理服務<--->被代理服務。Nginx的反向代理默認不會改變響應的內容,被代理服務響應頁面的絕對引用(/assets/image/abc.jpg)、相對引用(assets/image/abc.jpg)或者圖床引用(https://image.com/image/abc.jpg)代理回客戶端的時候不會發生改變。這些引用在客戶端解析html時候會重新發起請求,如果請求指向了代理服務,會同樣進行請求<--->代理服務<--->被代理服務這個流程。

--->表示請求,<---表示響應。

有些時候代理之後之所以情況變得複雜,是因為被代理服務存在重定向或者權鑒的約束產生的,而代理的過程就是請求<--->代理服務<--->被代理服務這麼簡單,並且不會改變被代理服務的響應內容。

7.3 反向代理基本配置

看一個簡單的反向代理配置:

events {

}

http {

    include /etc/nginx/mime.types;

    server {
        listen 80;
        server_name localhost;

        location / {
                proxy_pass "https://bbs.tianya.cn/";
        }
    }
}
複製代碼

代理後頁面如下:

因為是http反向代理了https,運營商竟然還在右下角插入了廣告(https://bbs.tianya.cn/不會被插入廣告)。

proxy_pass能夠簡單的將客戶端請求轉發給第三方服務端並反向代理響應結果返回給客戶端。

這只是簡單的代理,如果你要反向代理一個接口並且使用WebSocket,那麼就要覆寫header信息:

#WebSocket需要http/1.1,默認是http/1.0
proxy_http_version 1.1;
#覆寫header Upgrade為$http_upgrade的值,該值為Nginx獲取客戶端請求過來的Upgrade頭信息值
proxy_set_header Upgrade $http_upgrade;
#覆寫header Connection為'upgrade'
proxy_set_header Connection 'upgrade';
複製代碼

7.4 Nginx反向代理地址匹配規則

客戶端發送給Nginx的請求,究竟Nginx會怎樣拼接到proxy_pass指定的上游服務呢?Nginx有一定的規則:

  1. 如果proxy_pass代理的上游服務是域名加埠(沒有埠時默認埠為80或者443),那麼客戶端請求的代理路徑會直接拼到上游服務地址上。示例,proxy_pass http://redis.cn就只是對域名(和埠)的代理。
  2. 如果proxy_pass代理的上游服務有請求路徑,那麼客戶端請求的代理路徑將會是把客戶端請求路徑裁剪掉匹配路徑後再拼到上游服務地址上。示例,proxy_pass http://redis.cn/或者proxy_pass http://redis.cn/commands是有請求路徑的代理。

上面1、2分別定義為「情況1」和「情況2」,下面中有引用。

events {

}

http {

    include /etc/nginx/mime.types;

    server {
        listen 8088;
        server_name localhost;

        location / {
            #情況1,客戶端路徑和代理路徑映射:
            #http://fengmengzhao.hypc:8088/commands --> http://redis.cn/commands
            proxy_pass http://redis.cn;
        }

        #location /redis {
            #情況1,客戶端路徑和代理路徑映射:
            #http://fengmengzhao.hypc:8088/redis/commands --> http://redis.cn/redis/commands
        #   proxy_pass http://redis.cn;
        #}

        location /redis {
            #情況2,客戶端路徑和代理路徑映射:
            #http://fengmengzhao.hypc:8088/redis/commands --> http://redis.cn//commands
            proxy_pass http://redis.cn/;
        }

        location /redis/ {
            #情況2,客戶端路徑和代理路徑映射:
            #http://fengmengzhao.hypc:8088/redis/commands --> http://redis.cn/commands
            proxy_pass http://redis.cn/;
        }

        #location /redis-commands {
            #情況2,客戶端路徑和代理路徑映射:
            #http://fengmengzhao.hypc:8088/redis-commands --> http://redis.cn/commands
            #http://fengmengzhao.hypc:8088/redis-commands/keys.html --> http://redis.cn/commands/keys.html
        #   proxy_pass http://redis.cn/commands;
        #}

        #location /redis-commands/ {
             #情況2,客戶端路徑和代理路徑映射:
        #    #http://fengmengzhao.hypc:8088/redis-commands/keys.html --> http://fengmengzhao.hypc:8088/commandskeys.html
        #    proxy_pass http://redis.cn/commands;
        #}

        #location /redis-commands/ {
             #情況2,客戶端路徑和代理路徑映射:
        #    #http://fengmengzhao.hypc:8088/redis-commands/keys.html --> http://fengmengzhao.hypc:8088/commands/keys.html
        #    proxy_pass http://redis.cn/commands/;
        #}

        location /redis-commands {
            #情況2,客戶端路徑和代理路徑映射:
            #http://fengmengzhao.hypc:8088/redis-commands/keys.html --> http://fengmengzhao.hypc:8088/commands//keys.html
            proxy_pass http://redis.cn/commands/;
        }

    }
}
複製代碼

總結客戶端請求和代理端轉發請求的對應關係,如下:

匹配路徑

proxy_pass

客戶端請求

代理後請求

/

redis.cn



/redis

redis.cn

/redis

/redis

/

redis.cn/


/

/

redis.cn/

/

/

/redis

redis.cn/

/redis

/

/redis

redis.cn/

/redis/commands

//commands

/redis/

redis.cn/

/redis

/

/redis/

redis.cn/

/redis/commands

/commands

/redis-commands

redis.cn/commands

/redis-commands

/commands

/redis-commands

redis.cn/commands

/redis-commands/keys.html

/commands/keys.html

/redis-commands/

redis.cn/commands

/redis-commands

/commands

/redis-commands/

redis.cn/commands

/redis-commands/keys.html

/commandskeys.html

/redis-commands

redis.cn/commands/

/redis-commands

/commands/

/redis-commands

redis.cn/commands/

/redis-commands/keys.html

/commands//keys.html

/redis-commands/

redis.cn/commands/

/redis-commands

/commands/

/redis-commands/

redis.cn/commands/

/redis-commands/keys.html

/commands/keys.html

表格中為空表示只有域名+埠的訪問,沒有請求路徑。

代理後的請求在客戶端看不到網絡請求,可以用tcpdump抓包代理服務所在主機的網卡生成.cap文件,並在Wireshark中查看具體請求。

tcpdump監聽命令:

#172.19.146.188是Nginx代理IP;121.42.46.75是被代理上游服務IP,也就是redis.cn域名的解析IP
#ech0是172.19.146.188使用的網卡IP
sudo tcpdump -i eth0 tcp port 8088 and host 172.19.146.188 or host 121.42.46.75 -c 100 -n -vvv -w /opt/nginx-2.cap
複製代碼

啟動後,訪問代理服務,數據包經過網卡eth0就會被捕捉到。將nginx-2.cap文件在Wireshark中打開即可查看具體網絡包。

以下表請求為demo,抓包獲取代理請求。

請求如下:

匹配路徑

proxy_pass

客戶端請求

代理後請求

/redis-commands/

redis.cn/commands

/redis-commands/keys.html

/commandskeys.html

抓取請求包如圖:

7.5 反向代理header重寫

Nginx在服務端代理的請求和客戶端發的請求不是完全相同的,主要的不同在於請求的header信息,Nginx會對客戶端發過來的請求的header進行修改,規則如下:

  1. Nginx刪除空值的header。Nginx這樣做是因為空值的Header發送服務端也沒有意義,當然利用這一點,如果想讓代理不發送某個header信息,可以在配置中用proxy_set_header覆寫header值為空。
  2. Nginx默認header的名稱中如果包含_下劃線是無效header。這個行為也可以通過配置文件中設置underscores_in_headers on來開啟,否則任何含有_的header信息都不會被代理到目標上游服務。
  3. 代理的Host頭信息會被覆寫為變量$proxy_host,該變量是被代理上游服務的IP(或域名)加埠,其值在proxy_pass中定義。
  4. 代理的Connection頭信息會被覆寫為"close",該請求頭告訴被代理上游服務,一旦服務端響應代理請求,該連接就會被關閉,不會被持久化(persistent)。

第3點的Host頭信息覆寫在Nginx的反向代理中是比較重要的,Nginx定義不同的變量代表不同的值:

  • $proxy_host:上面提過了,是默認反向代理覆寫的header,其值是proxy_pass定義的上游服務IP和埠。
  • $http_host:是Nginx獲取客戶端請求的Host頭。Nginx使用$http_作為前綴加上客戶端header名稱的小寫,並將-符號用_替換拼接後就代表客戶端實際請求的頭信息。
  • $Host:常常和$http_host一樣,但是會將http_host轉化為小寫(域名情況)並去除埠。如果http_host不存在或者是空的情況,$host的值等於Nginx配置中server_name的值。

Nginx可以通過proxy_set_header來覆寫客戶端發送過來請求的header再轉發。除了上面說的Host頭比較重要,經常用到的header還有:

  • X-Forwarded-Proto:配置值$schema。告訴上游被代理服務,原始的客戶端請求是http還是https。
  • X-Real-IP:配置值$remote_addr。告訴代理服務客戶端的IP位址,輔助代理服務做出某種決定或者日誌輸出。
  • X-Forwarded-For:配置值$proxy_add_x_forwarded_for。包含請求經過每一次代理的IP。

7.6 反向代理試試,tcpdump抓包解析,探個中究竟

筆者也一直在理解這個Host在http請求中的作用,正常當一個http請求發送之後,tcp連接已經指定了IP和埠,那還需要Host頭信息做什麼呢?

首先,MDN Web Docs對Host頭的說明:

所有HTTP/1.1 請求報文中必須包含一個Host頭欄位。對於缺少Host頭或者含有超過一個Host頭的HTTP/1.1 請求,可能會收到400(Bad Request)狀態碼。

那Nginx反向代理默認對Host頭覆寫為$proxy_host的作用是什麼,如果改寫為其他會怎麼樣?用tcpdump工具抓包一探究竟。

看示例,反向代理http://redis.cn,配置如下(情況一):

events {

}

http {

    include /etc/nginx/mime.types;

    server {
        listen 8088;
        server_name localhost;

        location / {
            proxy_pass http://redis.cn;
        }

}
複製代碼

最普通的反向代理設置,沒有進行任何header覆寫。用tcpdump工具監控網卡:

#先用ping或者nslookup找到redis.cn的IP,這裡找到是121.42.46.75
#這裡 host 121.42.46.75,代表過濾指定IP的包。不過濾的話包會很多,不太好看
#-c 100 捕捉到100個包,會自動退出並生產文件
#需要將cap文件Wireshark中打開
sudo tcpdump -i eth0 host 121.42.46.75 -c 100 -n -vvv -w /opt/nginx-redis-1.cap
複製代碼

這時候訪問http://fengmengzhao.hypc:8088/,代理頁面很正常:

Nginx服務端的tcpdump包也抓到了:

用Wireshark查看包請求:

修改Nginx配置proxy_set_header Host $http_host(情況二):

events {

}

http {

    include /etc/nginx/mime.types;

    server {
        listen 8088;
        server_name localhost;

        location / {
            proxy_pass http://redis.cn;
            proxy_set_header Host $http_host;
        }

}
複製代碼

訪問http://fengmengzhao.hypc:8088/,代理頁面:

這是什麼頁面?如果直接用redis.cn的IP位址http://121.42.46.75訪問,得到同樣的頁面。為什麼?

看看抓到的包情況:

從tcpdump抓包來看,該響應是正常從服務端響應的。那為何不同的Host頭返回的頁面會不同呢?

情況二設置proxy_set_header $http_host之後Nginx代理請求的Host為客戶端請求的Host(fengmengzhao.hypc:8088),而情況一的Host為上游被代理服務的Host(redis.cn)。可能在redis.cn該域名對應的主機121.42.46.75不止提供一個80埠的服務。

這種在一個主機上提供多個域名服務(埠相同)稱之為虛擬主機。理解Nginx配置文件中的Directives和Contexts章節中提到的Nginx可以設置不同域名同一埠的虛擬主機就可以實現這種情況。另外,Apache也支持配置不同域名的虛擬主機。這兩種情況,歸根結底都是在請求到達服務端後,服務端會獲取請求中的Host頭信息並匹配到不同的虛擬服務。

所以,Nginx反向代理中對Host頭信息的覆寫要看上游被代理服務是否有特殊需要到該信息。如果沒有特殊實現上需要,默認的proxy_host就可以;如果是特殊的實現機制,就要小心對待。

這裡的特殊需要是例如上面虛擬主機那種情況,Host頭信息在HTTP/1.1中是必須帶的。

7.7 反向代理處理相對路徑問題

基於上面講解的對反向代理的理解,我們處理一下實際工作中遇到的問題,增加對Nginx反向代理的認識。

假設被代理的上游服務是一個簡單的靜態頁面(http://127.0.0.1:80),頁面中引用了兩個相同的圖片,分別是絕對引用/assets/generate.png和相對引用assets/generate.png。我們進行如下的反向代理配置:

events {

}

http {

    include /etc/nginx/mime.types;

    server {
        listen 8088;
        server_name localhost;

        location /static/ {
            proxy_pass http://127.0.0.1/;
        }

}
複製代碼

這時候,訪問http://fengmengzhao.hypc:8088/static會發現其中絕對引用(/assets/generate.png)的圖片加載失敗,通過瀏覽器網絡查看,其客戶端加載的請求是http://fengmengzhao.hypc:8088/assets/generate.png。該請求在我們的配置中會默認尋找root匹配(一般默認是/usr/share/nginx/html路徑),找不到對應的資源。

實際上不管是絕對應用還是相對應用我們想讓客戶端的請求都是http://fengmengzhao.hypc:8088/static/assets/generate.png,這裡可以看到,如果採用上面的代理方式,並且上游服務有絕對路徑的引用,就會出現加載異常的情況。示例:

這裡我們也可以看出來,Nginx反向代理默認對響應的內容是不會修改的,目標服務中相對路徑或者絕對路徑的引用反向代理之後返回給客戶端的跟直接訪問目標服務端響應是一樣的。

怎麼樣解決呢,有如下方案:

1). 如果目標上游服務可以修改,可以將所有的絕對路徑的引用改為相對路徑引用。一級目錄靜態文件引用/assets/generate.png要改為./assets/generate.png或者assets/generate.png;二級目錄靜態文件引用要改為../xxx/assets/generate.png。總之,頁面上絕對路徑的引用要改為相對路徑的引用。

2). 可以將不能正常代理的圖片添加代理,如下配置:

events {

}

http {

    include /etc/nginx/mime.types;

    server {
        listen 8088;
        server_name localhost;

        location /static/ {
            proxy_pass http://127.0.0.1/;
        }
        
        location /assets/ {
            proxy_pass http://127.0.0.1/assets/;
        }

}
複製代碼

這樣絕對引用http://fengmengzhao.hypc:8088/assets/generate.png就能夠代理到http://127.0.0.1/assets/generate.png,就能夠正常加載圖片了。

3). 放棄子目錄的方案,用獨立域名就沒問題了,配置如下:

events {

}

http {

    include /etc/nginx/mime.types;

    server {
        listen 8088;
        server_name static.fengmengzhao.hypc;

        location / {
            proxy_pass http://127.0.0.1/;
        }

}
複製代碼

這樣訪問http://static.fengmengzhao.hypc:8088就能夠成功代理http://127.0.0.1了。

4). Nginx重寫目標服務端響應內容

文中強調過多次,Nginx反向代理默認是不會修改目標服務端響應內容的。但Nginx也支持對響應內容進行修改,需要開啟Nginx的ngx_http_sub_module。

可以通過nginx -V查看是否包含http_sub_module就知道當前Nginx是否有ngx_http_sub_module模塊。

開啟ngx_http_sub_module模塊後,修改配置如下:

events {

}

http {

    include /etc/nginx/mime.types;

    server {
        listen 8088;
        server_name localhost;

        location /static/ {
            sub_filter 'src="/assets/' 'src="./assets/';
            sub_filter_once off;
            proxy_pass http://127.0.0.1/;
        }
        
}
複製代碼

通過上面的任意方法,可以獲取正確的代理響應:

這裡要注意一個點,當你的訪問路徑是http://fengmengzhao.hypc:8088/static(情況一),其響應html中有引用assets/generate.png,對該generate.png的請求路徑是:http://fengmengzhao.hypc:8088/assets/gnerate.png。而當你的訪問路徑是http://fengmengzhao.hypc:8088/static/(情況二),其響應html同樣引用assets/generate.png,對圖片的請求會變為:http://fengmengzhao.hypc:8088/static/assets/generate.png。情況二訪問路徑和情況一的區別是URI的最後有沒有跟/,如果有/結尾的話,認為當前訪問是一個目錄,所以其相對引用就從當前地址欄中的路徑開始;如果沒有/結尾的話,認為當前訪問是一個文件,其相對路徑就是文件所在的路徑,也就是URI往前數有出現/那個層級,在這裡就是根目錄,所以情況一雖然是相對引用,但是請求路徑還是從根目錄開始。

8. Nginx作為一個負載均衡伺服器

學習完反向代理,就很容易理解基於反向代理做進一步的負載均衡了。

配置示例:

events {

}

http {

    upstream backend_servers {
        server localhost:3001;
        server localhost:3002;
        server localhost:3003;
    }

    server {

        listen 80;
        server_name localhost;

        location / {
            proxy_pass http://backend_servers;
        }
    }
}
複製代碼

upstream{}可以包含多個服務並且作為一個上游服務被引用。

測試負載均衡:

while sleep 0.5; do curl http://localhost; done

# response from server - 2.
# response from server - 3.
# response from server - 1.
# response from server - 2.
# response from server - 3.
# response from server - 1.
# response from server - 2.
# response from server - 3.
# response from server - 1.
# response from server - 2.
複製代碼

9. 優化Nginx性能

本文介紹三個方面優化Nginx的性能:根據主機參數調優Worker Processes及Worker Connections配置、緩存靜態文件和響應數據壓縮。

9.1 怎麼設置工作進程數(Worker Processes)和工作連接數(Worker Connections

文章開始的時候已經提到過,Nginx會設置Worker進程並在進程間進行切換,能夠同時並發處理「成千上萬」個請求。可以通過status命令查看Worker進程數:

sudo systemctl status nginx

# ● nginx.service - A high performance web server and a reverse proxy server
#      Loaded: loaded (/lib/systemd/system/nginx.service; enabled; vendor preset: enabled)
#      Active: active (running) since Sun 2021-04-25 08:33:11 UTC; 5h 54min ago
#        Docs: man:nginx(8)
#     Process: 22610 ExecReload=/usr/sbin/nginx -g daemon on; master_process on; -s reload (code=exited, status=0/SUCCESS)
#    Main PID: 3904 (nginx)
#       Tasks: 3 (limit: 1136)
#      Memory: 3.7M
#      CGroup: /system.slice/nginx.service
#              ├─ 3904 nginx: master process /usr/sbin/nginx -g daemon on; master_process on;
#              ├─22611 nginx: worker process
#              └─22612 nginx: worker process

#也可以通過ps查看進程
#能夠看到master進程是各個Worker進程的父進程
ps -ef | grep nginx
複製代碼

這裡可以看到有1個master進程和2個Worker進程。Worker進程數在Nginx中很容易配置:

#一般情況,主機有多少核,就設置Worker進程的個數為多少
worker_processes 2;
#根據主機cpu核心數的不同自動設置Worker進程的個數
#worker_processes auto;

events {

}

http {

    server {

        listen 80;
        server_name localhost;

        return 200 "worker processes and worker connections configuration!\n";
    }
}
複製代碼

假設說主機有4個核心,worker_processes如果配置為4,表示每一個Worker理論上能夠利用100%的cpu。worker_processes如果配置為8,表示一個Worker理論上能夠利用50%的cpu,意味著當主機cpu滿負荷運轉時Worker每運行1分鐘就需要等待一分鐘。所以,worker_processes不是配置的越大越好,數量如果超出主機cpu核心數,就會有時間浪費在作業系統級別對進程的調度。

可以很方便的通過nproc命令查看主機的cpu核心數:

nproc

# 4
複製代碼

worker_processes auto配置會根據主機cpu核心數的不同自動設置Worker進程的個數。如果你的主機只用來運行Nginx,可以這樣配置;如果主機上還有其他服務部署,要斟酌合理分配資源。

worker_connections表示一個Worker進程能夠處理的最大連接數,該參數跟主機cpu core個數和一個core能打開的文件個數有關(該值可以通過命令ulimit -n查詢)。

ulimit -n

# 1024
複製代碼

worker_connections設置:

worker_processes auto;

events {
    worker_connections 1024;
}

http {

    server {

        listen 80;
        server_name localhost;

        return 200 "worker processes and worker connections configuration!\n";
    }
}
複製代碼

注意,這裡本文中第一次使用到events這個Context。

9.2 怎樣緩存靜態文件

不管使用Nginx提供什麼樣的服務,總是有一些靜態文件(js或者css等)是不經常發生改變的,可以將它們緩存起來提高Nginx的性能。Nginx對靜態文件的緩存配置非常方便:

worker_processes auto;

events {
    worker_connections 1024;
}

http {

    include /env/nginx/mime.types;

    server {

        listen 80;
        server_name localhost;

        root /usr/share/nginx/html;
        #正則匹配,大小寫不敏感
        #以.css或者.js或者.jpg結尾的匹配
        location ~* \.(css|js|jpg|png)$ {
            access_log off;
            
            add_header Cache-Control public;
            add_header Pragma public;
            add_header Vary Accept-Encoding;
            #1M代表一個月
            expires 1M;
        }
    }
}
複製代碼

像之前反向代理設置中的proxy_set_header給可以給代理到後端的請求增加header一樣,使用add_header可以給response增加header。

Cache-Control頭信息設置為public,是在告訴client該請求內容可以被緩存。Pragma是Cache-Control的old version。

Vary頭信息設置為Accept-Encoding,後續詳解。

expires directive表示Nginx緩存響應的時間,可以幫助很方便設置響應Expires頭信息,其值可以是1M(1 month)、10m/10 minutes或者24h/24 hours等。

Cache-Control告訴客戶端,該response在服務端緩存,客戶端可以以任意的形式緩存。另外根據Nginx的expires設置的緩存時間,增加Cache-Control: max-age=2592000,這裡Cache-Control: max-age代表該response在max-age時間內不會刷新。2592000單位是秒,等於expire設置的1M(一個月,30x24x3600=2592000)。

重啟Nginx之後,測試請求的響應信息:

curl -I http://fengmengzhao.hypc:8088/assets/generate.png

HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Tue, 01 Mar 2022 05:04:17 GMT
Content-Type: image/png
Content-Length: 144082
Last-Modified: Sun, 20 Feb 2022 08:35:21 GMT
Connection: keep-alive
ETag: "6211fd49-232d2"
Expires: Thu, 31 Mar 2022 05:04:17 GMT #注意這個時間和下面的比較
Cache-Control: max-age=2592000
Cache-Control: public
Pragma: public
Vary: Accept-Encoding
Accept-Ranges: bytes
複製代碼

這裡可以看到,response中已經增加了Cache-Control頭信息,說明配置已經生效。至於Nginx服務端有沒有緩存響應,可以用tcpdump抓包看一看,這裡不再演示。

需要注意的是,如果在瀏覽器上訪問http://fengmengzhao.hypc:8088/assets/generate.png,第一次返回的是200狀態碼,表示是服務端成功返回。第二次返回的是304狀態碼,表示瀏覽器根據第一次response頭信息Cache-Control: public的指示,第二次訪問的時候,直接使用客戶端緩存。也可以通過F12打開控制台,勾選Network --> Disable Cache選項,這樣瀏覽器端就不使用緩存。

9.3 怎樣壓縮響應(response)

壓縮配置:

worker_processes auto;

events {
    worker_connections 1024;
}

http {
    include /env/nginx/mime.types;
    #開啟gzip,默認只對html進行壓縮
    gzip on;
    #不是設置的越大越好,一般設置為1-4
    gzip_comp_level 3;
    #對css和js文件進行壓縮
    gzip_types text/css text/javascript;

    server {

        listen 80;
        server_name localhost;

        root /usr/share/nginx/html;
        
        location ~* \.(css|js|jpg)$ {
            access_log off;
            
            add_header Cache-Control public;
            add_header Pragma public;
            add_header Vary Accept-Encoding;
            expires 1M;
        }
    }
}
複製代碼

默認nginx會對html文件進行gzip壓縮,如果要對其他類型文件壓縮,需要設置gzip_types text/css text/javascript;。

gzip_comp_level不是設置的越大越好,一般設置為1-4。

服務端設置gzip之後,要想真正的壓縮傳輸到客戶端,客戶端需要增加header信息"Accept-Encoding: gzip"才能完成服務端到客戶端的壓縮傳輸。

客戶端請求沒有"Accept-Encoding: gzip"的示例:

curl -I http://localhost/mini.min.css

# HTTP/1.1 200 OK
# Server: nginx/1.18.0 (Ubuntu)
# Date: Sun, 25 Apr 2021 16:30:32 GMT
# Content-Type: text/css
# Content-Length: 46887
# Last-Modified: Sun, 25 Apr 2021 08:35:33 GMT
# Connection: keep-alive
# ETag: "608529d5-b727"
# Expires: Tue, 25 May 2021 16:30:32 GMT
# Cache-Control: max-age=2592000
# Cache-Control: public
# Pragma: public
# Vary: Accept-Encoding
# Accept-Ranges: bytes
複製代碼

客戶端請求設置"Accept-Encoding: gzip"的示例:

curl -I -H "Accept-Encoding: gzip" http://localhost/mini.min.css

# HTTP/1.1 200 OK
# Server: nginx/1.18.0 (Ubuntu)
# Date: Sun, 25 Apr 2021 16:31:38 GMT
# Content-Type: text/css
# Last-Modified: Sun, 25 Apr 2021 08:35:33 GMT
# Connection: keep-alive
# ETag: W/"608529d5-b727"
# Expires: Tue, 25 May 2021 16:31:38 GMT
# Cache-Control: max-age=2592000
# Cache-Control: public
# Pragma: public
# Vary: Accept-Encoding
# Content-Encoding: gzip
複製代碼

注意,這裡response的header中有Vary: Accept-Encoding信息,該頭信息告訴客戶端,根據客戶端設置的Accept-Encoding頭信息的不同,服務端響應會發生變化。

對比壓縮前後傳輸內容的大小:

cd ~
mkdir compression-test && cd compression-test

curl http://localhost/mini.min.css > uncompressed.css

curl -H "Accept-Encoding: gzip" http://localhost/mini.min.css > compressed.css

ls -lh

# -rw-rw-r-- 1 vagrant vagrant 9.1K Apr 25 16:35 compressed.css
# -rw-rw-r-- 1 vagrant vagrant  46K Apr 25 16:35 uncompressed.css
複製代碼

沒壓縮的版本大小是46k,而壓縮後的版本大小是9.1k。

10. 理解Nginx整個配置文件

完整nginx配置文件:

user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;

events {
        worker_connections 768;
        # multi_accept on;
}

http {

        ##
        # Basic Settings
        ##

        sendfile on;
        tcp_nopush on;
        tcp_nodelay on;
        keepalive_timeout 65;
        types_hash_max_size 2048;
        # server_tokens off;

        # server_names_hash_bucket_size 64;
        # server_name_in_redirect off;

        include /etc/nginx/mime.types;
        default_type application/octet-stream;

        ##
        # SSL Settings
        ##

        ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE
        ssl_prefer_server_ciphers on;

        ##
        # Logging Settings
        ##

        access_log /var/log/nginx/access.log;
        error_log /var/log/nginx/error.log;

        ##
        # Gzip Settings
        ##

        gzip on;

        # gzip_vary on;
        # gzip_proxied any;
        # gzip_comp_level 6;
        # gzip_buffers 16 8k;
        # gzip_http_version 1.1;
        # gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

        ##
                # Virtual Host Configs
        ##

        include /etc/nginx/conf.d/*.conf;
        include /etc/nginx/sites-enabled/*;
}


#mail {
#       # See sample authentication script at:
#       # http://wiki.nginx.org/ImapAuthenticateWithApachePhpScript
# 
#       # auth_http localhost/auth.php;
#       # pop3_capabilities "TOP" "USER";
#       # imap_capabilities "IMAP4rev1" "UIDPLUS";
# 
#       server {
#               listen     localhost:110;
#               protocol   pop3;
#               proxy      on;
#       }
# 
#       server {
#               listen     localhost:143;
#               protocol   imap;
#               proxy      on;
#       }
#}
複製代碼

上文中已經講解過的配置,不再做重複說明。

user www-data;設置Nginx進程的用戶,這裡會涉及到權限問題,如果用戶為www-data讀取沒有權限的目錄,就不能正常的提供服務,這時候查看Nginx的error日誌,就會報權限相關的錯。

pid /run/nginx.pid;設置nginx進程的process id。

include /etc/nginx/modules-enabled/*.conf;設置include指定目錄中任何.conf結尾的配置文件。該目錄用來加載nginx的動態模塊(本文中並沒有涉及)。

在http{}下,有基本的優化設置,如下:

  • sendfile on;:禁止靜態文件buffering。
  • tcp_nopush on;:允許在一個響應包中發送頭信息。
  • tcp_nodelay on;:靜態文件快傳中禁用Nagle's Algorithm。

keepalive_timeout設置http connection的連接時間。types_hash_maxsize設置Hash map的大小。

SSL的配置在本文中不做講解。

mail Context可以將Nginx配置為一個郵件服務端,本文僅討論Nginx作為web服務端,所以不做說明。

重點看一下如下配置:

##
# Virtual Host Configs
##

include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
複製代碼

該配置表示Nginx會加載/etc/nginx/conf.d/ 和/etc/nginx/sites-enabled/目錄內匹配的配置。這樣,一般認為這兩個目錄就是放置Nginx配置的最好的選擇,實際上並不是。

有另外一個目錄/etc/nginx/sites-available/,該目錄用來存放Nginx的虛擬主機(也就是server{}塊)配置。/etc/nginx/sites-enabled/目錄用來存放符號連結指向目錄/etc/nginx/sites-available/中配置。例如:

ln -lh /etc/nginx/sites-enabled/

# lrwxrwxrwx 1 root root 34 Apr 25 08:33 default -> /etc/nginx/sites-available/default
複製代碼

這樣通過符號連結的方式可以激活或者禁用/etc/nginx/sites-available/目錄中的配置。符號連結unlink`和創建的命令如下:

#刪除符號連結,用rm也可以
sudo unlink /etc/nginx/sites-enabled/default

#創建符號連結,第一個參數是被連結的文件,第二個參數是創建符號連結的路徑
#也就是,連結某個文件到某個符號連結上
sudo ln -s /etc/nginx/sites-available/nginx-handbook.conf /etc/nginx/sites-enabled/nginx-handbook 
複製代碼

引用

  • www.freecodecamp.org/news/the-ng…
  • serverfault.com/questions/9…
  • www.cnblogs.com/sky-cheng/p…
  • stackoverflow.com/questions/1…
  • www.jscape.com/blog/bid/87…
  • tarunlalwani.com/post/nginx-…
  • www.digitalocean.com/community/t…
  • stackoverflow.com/questions/4…
  • blog.csdn.net/gui951753/a…


連結:https://juejin.cn/post/7070269868553011230

關鍵字: