初探 Nginx

14 min

language: ja bn en es hi pt zh-cn zh-tw

你好,標題是模仿那個的。只是突然想起的一首歌。

最近文章更新異常頻繁,是因為一旦開始在意就停不下來,剛好到了那個時期而已。

Nginx

到目前為止,我暫時將其作為反向代理兼快取伺服器來架設,但隨著 IPv6 的支援,我開始在意起來,因此重新檢視了包含重構在內的設定。

檔案結構

雖然隱藏了一部分,但大概是這種感覺

├── http.d
│   ├── bot_rate_limit.conf
│   ├── gzip.conf
│   ├── proxy_cache_zones.conf
│   └── proxy_common.conf
├── mime.types
├── mime.types-dist
├── nginx.conf
├── scgi_params
├── selfsigned.crt
├── selfsigned.key
├── sites-enabled
│   ├── 1btc.love.conf
│   ├── btclol.xyz.conf
│   ├── damepo.jp.conf
│   ├── git.soulminingrig.com.conf
│   ├── soulminingrig.com.conf
│   ├── starlink.soulminingrig.com.conf
│   ├── stg.api.1btc.love.conf
├── snippets
│   ├── common_error_pages.conf
│   ├── proxy_headers.conf
│   └── ssl_common.conf
├── uwsgi_params
└── win-utf

對於以 location 指令為單位讀取的 conf 檔案該用什麼名稱的資料夾來管理,我稍微煩惱了一下,在與 ChatGPT 討論後決定成這樣。 http.d 是從 nginx.confhttp 指令中 include 的檔案,所以我覺得這樣很合理。

不過變成 snipets 感覺有點微妙,但算了,就這樣吧。

接著來看看各個設定項目。

http.d/bot_rate_limit.conf

因為 Meta 的爬蟲太誇張了,所以我決定不只使用 fail2ban,還要針對 UA 單位進行限制。

另外,關於 feed/RSS,雖然有人特地聯繫我,但基本上套用限制也沒意義,而且幾乎都是快取回應,不太會對 Origin 造成負載,所以我就排除了(開了個洞)。

# 僅針對 Bot 或連結展開爬蟲進行速率限制
map $http_user_agent $is_bot {
    default 0;
    ~*bot 1;
    ~*crawler 1;
    ~*spider 1;
    ~*facebookexternalhit 1;
    ~*slackbot 1;
    ~*discordbot 1;
    ~*twitterbot 1;
    ~*linkedinbot 1;
    ~*embedly 1;
    ~*quora 1;
    ~*skypeuripreview 1;
    ~*whatsapp 1;
    ~*telegrambot 1;
    ~*applebot 1;
    ~*pingdom 1;
    ~*uptimerobot 1;
}
# stg.api.1btc.love 用於驗證,即使是 Bot 也不受速率限制
# 將 key 設為空字串時,limit_req_zone 不會進行計數
map $server_name $bot_limit_host_key {
    stg.api.1btc.love "";
    default $binary_remote_addr;
}
# feed.xml / feed.json 即使是 Bot 也不受速率限制
map $uri $is_feed_path {
    default 0;
    ~*feed\.(xml|json)$ 1;
}
# 僅在為 Bot 且非 feed.xml / feed.json 時使用以 IP 為單位的 key
map "$is_bot:$is_feed_path" $bot_limit_key {
    default "";
    "1:0" $bot_limit_host_key;
}
limit_req_zone $bot_limit_key zone=bot:10m rate=1r/s;
limit_req_status 429;
limit_req zone=bot burst=5 nodelay;

當然,因為是套用到 http 指令,所以基本上是全域套用,但我盡量讓它能進行最低限度的排除。我判斷這原本就應該是預設啟用的功能。

如果是偽造 UA 或偽裝瀏覽器進行明顯的 DoS 攻擊,則會被 fail2ban 封鎖並視為 drop 處理,一段時間內無法發送請求,這是一種雙重防禦。

http.d/gzip.conf

以前曾支援過 brotli 等,但因為需要另外編譯,升級版本時很麻煩,所以就放棄了。能透過 pkg/apt 更新的優點很大。

gzip on;
gzip_vary off;
gzip_proxied any;
gzip_min_length 1024;
gzip_comp_level 7;
gzip_http_version 1.1;
gzip_types text/plain
text/xml
text/css
text/javascript
image/gif
image/png
image/svg+xml
application/javascript
application/json
application/xml
application/x-javascript
application/font-woff
application/font-woff2
application/font-ttf
application/octet-stream;

這部分沒什麼特別要說的,以前沒加,但幾年前開始加入了 gzip_min_length。那是因為我覺得進行低效率的壓縮也沒意義,所以決定好好設定的時候。

http.d/proxy_cache_zones.conf

雖然看起來縮排有點微妙地亂掉,但這是格式化工具的限制。

刪掉不需要的東西後,zone 從 4 開始或是變得零散,不過嘛...

關於 inactive,如果 7 天沒有 hit 就會刪除;關於 use_temp_path,則是設定為直接在快取路徑進行快取。即使有 proxy_temp_path /tmp/nginx; 之類的設定,似乎也會跳過它直接快取,從而變快。

proxy_cache_path /tmp/nginx/zone4 levels=1:2 keys_zone=zone4:10m
inactive=7d
max_size=3g
use_temp_path=off;proxy_cache_path /tmp/nginx/posts levels=1:2 keys_zone=posts:10m
inactive=7d
max_size=2g
use_temp_path=off;
proxy_cache_path /tmp/nginx/git levels=1:2 keys_zone=git:10m
inactive=7d
max_size=2g
use_temp_path=off;
proxy_cache_path /tmp/nginx/static levels=1:2 keys_zone=static_cache:10m
inactive=7d
max_size=1g
use_temp_path=off;
proxy_cache_path /tmp/nginx/1btc_cache levels=1:2 keys_zone=1btc_cache:10m
inactive=7d
max_size=512m
use_temp_path=off;

http.d/proxy_common.conf

proxy_cache_valid 作為通用規則進行設定,其餘部分則在 location 指令中進行覆寫。

這樣一來,即使沒有特別加入快取設定,也能進行快取運作。

透過設定 proxy_cache_bypass $http_cookie,可以防止帶有 Cookie 的請求(例如登入後)仍然顯示登入前的畫面。

此外,針對重新導向、錯誤以及 any 進行快取回應是為了防禦攻擊。這樣做可以確保至少有快取回應,從而防止異常流量到達 Origin(源站)。

proxy_temp_path 本來設定在可持久化的路徑會比較理想,但基本上在我的用途中沒有這種需求,所以設定在 /tmp。如果在有一定流量的網站上這樣做,雖然剛才提到有不使用的選項,但伺服器重啟後快取可能會全部消失,導致 Origin 負載升高,增加故障風險。

proxy_buffering on;
proxy_cache_bypass $http_cookie;
proxy_cache_background_update on;
proxy_cache_key "$scheme$request_method$host$request_uri";
proxy_cache_revalidate on;
proxy_cache_use_stale updating;
proxy_connect_timeout 60;
proxy_no_cache $http_cookie;
proxy_read_timeout 90;
proxy_send_timeout 60;
proxy_temp_path /tmp/nginx;
proxy_cache_valid 200 201 60s;
proxy_cache_valid 301 1d;
proxy_cache_valid 302 3h;
proxy_cache_valid 304 1d;
proxy_cache_valid 404 1m;
proxy_cache_valid any 5s;
proxy_cache_lock on;

nginx.conf

只是設定了當直接存取 A 紀錄的 IP 時回傳專用錯誤,沒有太多特別要說的。

multi_accept 設為 on 是因為我的反向代理兼快取伺服器運作在規格不高的弱小實例(Instance)上,所以啟用了它。

原本直接寫在 http 指令中的設定,現在根據目的進行 include,可讀性變得好多了。

worker_processes auto;
worker_cpu_affinity auto;
worker_rlimit_nofile 65535;
events {
    multi_accept on;
    worker_connections 65535;
}
http {
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    server_tokens off;
    types_hash_max_size 4096;
    client_max_body_size 16M;
    # MINE
    include mime.types;
    default_type application/octet-stream;
    include ./http.d/bot_rate_limit.conf;
    include ./http.d/proxy_common.conf;
    include ./http.d/proxy_cache_zones.conf;
    include ./http.d/gzip.conf;
    server {
        listen 80;
        listen [::]:80;
        server_name 163.44.113.145 91.98.169.80 2400:8500:2002:3317:163:44:113:145;
        include snippets/common_error_pages.conf;
        return 444;
    }
    server {
        listen 443 ssl;
        listen [::]:443 ssl;
        server_name 163.44.113.145 91.98.169.80 2400:8500:2002:3317:163:44:113:145;
        ssl_certificate ./selfsigned.crt;
        ssl_certificate_key ./selfsigned.key;
        include snippets/common_error_pages.conf;
        # 拒絕存取 IP 位址
        return 444;
    }
    ### Damepo.jp
    include ./sites-enabled/damepo.jp.conf;
    ### Soulminingrig My Blog
    include ./sites-enabled/soulminingrig.com.conf;
# ~~~略~~~~
}

sites-enabled/soulminingrig.com.conf

作為範例,我只介紹 site-enabled 下的一個檔案。這是本站的設定。

另外,我沒有建立符號連結(Symbolic Link),只是為了能快速刪除不需要的東西。

實際上有些地方仍然是寫死的(Hard-coded),目前還在修改中,請見諒。

順帶一提,最近雖然改成了 www.soulminingrig.com,但之前是用不帶 www 的根網域(Root Domain)發布的,所以目前根網域仍然設定為不跳轉直接接收請求。

在驗證期間,我設定了可以回傳標頭(Header),以便清楚辨識是伺服器快取回應還是用戶端快取回應。

還有,我在想圖片、字體、CSS 等的快取規則能不能再優化一下,目前還在苦惱中……這到底該怎麼辦才好呢……。

設定 upstream 是為了在後端增加時能快速應對。雖然目前只有一個,但放在最上方可以讓人一眼看出設定指向哪個後端。

upstream backend_sm {
    server 10.1.0.228:8888 max_fails=3 fail_timeout=3s;
    keepalive 16;
    keepalive_timeout 30s;
}
map $uri $static_cache {
    ~\.(jpg|jpeg|png|webp|gif|mp4|css|js|ico|woff2)(\?.*)?$ "public, max-age=604800";
    ~\.html$ "public, max-age=600";
    default "public, max-age=600";
}
map $upstream_cache_status $server_cache_status {
    default $upstream_cache_status;
    "" "NONE";
}
map "$http_if_none_match:$http_if_modified_since" $client_cache_request {
    default "MISS";
    "~.+:.+" "REVALIDATE";
    "~.+:" "REVALIDATE";
    "~:.+" "REVALIDATE";
}
server {
    listen 80;
    listen [::]:80;
    server_name soulminingrig.com www.soulminingrig.com;
    return 301 https://$host$request_uri;
}
server {
    listen 443 ssl reuseport backlog=65535 rcvbuf=256k sndbuf=256k fastopen=256 so_keepalive=on;
    listen [::]:443 ssl reuseport backlog=65535 rcvbuf=256k sndbuf=256k fastopen=256 so_keepalive=on ipv6only=on;
    listen 443 quic reuseport;
    listen [::]:443 quic reuseport;
    http2 on;
    http3 on;
    server_name soulminingrig.com www.soulminingrig.com;
    client_max_body_size 50M;
    location ~* \.(jpg|jpeg|png|webp|gif|ico|mp4|js|css|woff2)(\?.*)?$ {
        proxy_pass http://backend_sm;
        include snippets/proxy_headers.conf;
        proxy_http_version 1.1;
        proxy_redirect off;
        proxy_cache static_cache;
        proxy_cache_valid 200 301 302 7d;
        proxy_cache_valid 404 1m;
        proxy_cache_revalidate on;
        proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504;
        proxy_cache_background_update on;
        proxy_cache_lock on;
        proxy_connect_timeout 3s;
        proxy_read_timeout 15s;
        expires 7d;
        add_header X-Cache-Status $upstream_cache_status always;
        add_header X-Server-Cache-Status $server_cache_status always;
        add_header X-Client-Cache-Request $client_cache_request always;
        add_header X-Client-Cache-Policy "public, max-age=604800" always;
        add_header Cache-Control "public, max-age=604800" always;
    }
    location / {
        proxy_pass http://backend_sm/;
        include snippets/proxy_headers.conf;
        proxy_http_version 1.1;
        proxy_redirect off;
        proxy_cache posts;
        proxy_cache_key $scheme$host$request_uri;
        proxy_cache_valid 200 10m;
        proxy_cache_valid 301 1h;
        proxy_cache_valid 404 1m;
        proxy_cache_revalidate on;
        proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504 http_403 http_404;
        proxy_cache_background_update on;
        proxy_cache_lock on;
        proxy_connect_timeout 3s;
        proxy_read_timeout 15s;
        expires $static_cache;
        add_header X-Cache-Status $upstream_cache_status always;
        add_header X-Server-Cache-Status $server_cache_status always;
        add_header X-Client-Cache-Request $client_cache_request always;
        add_header X-Client-Cache-Policy $static_cache always;
        add_header Cache-Control $static_cache always;
    }
    include snippets/common_error_pages.conf;
    include snippets/ssl_common.conf;
    ssl_certificate /hoge/fullchain.pem; # managed by Certbot
    ssl_certificate_key /hoge/oulminingrig.com/privkey.pem;
    # managed by Certbot
}

補充:nginxfmt.py

這個非常棒。

雖然是格式化工具,但我覺得它的品質是最高的。

可以在 AUR 中找到。

yay -S nginx-config-formatter

用法是 nginxfmt.py example.conf,它會幫你進行格式化,所以

find . -name "*conf" | xargs -I{} nginxfmt.py {}

可以像這樣進行批次格式化。

不久前我還在使用 nginxbeautifier,但因為它會弄壞重定向相關的語法,所以我就改用這個了。

Related Posts