寫給運維的 Nginx 祕籍

要說 Web 服務器、代理服務器和調度服務器層面,目前使用最大的要數 Nginx。對於一個運維工程師日常不可避免要和 Nginx 打交道。爲了更好地使用和管理 Nginx,本文就給大家介紹幾個蟲蟲日常常用的祕籍。

限制訪問

當 Nginx 開放到公網上以後,就會有大量的非正常訪問,這不光耗費服務器資源,而且有可能是某種信息探索,然後攻擊的前奏,有對針對性的限制這些訪問很有必要。在 Nginx 中可以通過一些內置的變量來進行限制訪問。

限制客戶端代理

在 nginx 可以使用 $http_user_agent 變量匹配客戶類型,然後對對匹配的訪問 return 4.3 來限制器訪問。

在 Nginx 配置的 server 部分,直接用 if 語句實現:

if ($http_user_agent ~ (Go-http-client/1.1|curl)) {return 403;}

但是如果要匹配的客戶端代理比較多時候,直接這樣拼寫就比較繁瑣也不好管理。這種情況下給大家一個技巧就是用 Map 函數。

Map 函數在 Nginx ngx_http_map_module 中實現的。利用 Map 函數可以創建一個變量,並將其與其他變量(比如內置的 $http_user_agent)關聯起來,可以同時關聯多個值到多個不同值並儲存到一個變量。其基本語法爲:

map $var1 $var2 { ... }

其作用於爲 http 模塊,這樣可以在開頭映射後,然後在具體的 server 部分進行封禁。

對應本例子中

map $http_user_agent:$arg_key $ban {
~*spider* 1;
~Go-http-client/1.1 1;
~curl;
default 0;
}

這樣在後續 if 封禁語句中就可以使用新建的 $ban 變量進行封禁了。

if ($ban = 1) {
return 403;
}

IP 限制

有時候對一些惡意來源的 IP 封禁則更爲直接簡單有效。Nginx 進行 IP 封禁的方法也很簡單,直接用 deny 語句,他是 Nginx 內置模塊 ngx_http_access_module,支持 allow 和 deny 兩個語句,基本語法爲:

deny address | CIDR | unix: | all;

可以在 http 或者 server 塊直接使用:

deny 135.125.180.235;

如果要封閉的 IP 很多,可以直接在 nginx 配置文件中 include 一個封禁文件專管理封禁的 IP。

include banip.conf;

在 banip.conf 文件中用:

deny 135.125.180.235;
deny 135.125.180.1/24;

這樣語句即可,當然也可以用 allow 和 deny all 搞成實時上的白名單限制模式:

allow 127.0.0.1;
allow 192.168.0.0/18;
allow 110.242.68.66;
…
deny all;

這樣除了本機、18 位的內網段和 110.242.68.66 外其他 IP 都會禁止訪問。

速率限制

除了直接限制訪問外很多時候,不能直接限制其訪問,但是需要針對特定請求限制訪問的速率(頻率)。在 Nginx 速率限制通過 limit_req_zone 和 limit_req 兩個指令實現。

limit_req_zone 用來定義請求限制區域。區域包含有關如何分類的配置請求速率限制和實際限制。

limit_req 將區域應用於特定 http 上下文對於全侷限制,server 每個虛擬服務器,以及 location 對於虛擬中的特定位置服務器。

爲了說明這一點,假設要實現速率限制配置:

全局速率限制 100 RPS

由 User-Agent 來限制特定來源(搜索蜘蛛)請求爲 1RPM。

通過 API 令牌將來自某些可以客戶端的請求限制爲 1RPS。

要對請求進行分類,需要提供索引到 limit_req_zone。鍵通常是一些變量,要麼由 nginx 預定義,要麼由通過 map 定義。

要通過 IP 設置全局速率限制,需要以 IP 作爲鍵。

limit_req_zone $binary_remote_addr zone=global:100m rate=100r/s;

現在,通過以下方式限制搜索蜘蛛的 User-Agent,此處我們使用 map 函數:

map $http_user_agent $crawler {
~*.*( Baiduspider|bot|spider|slurp).* $http_user_agent;
default "";
}
limit_req_zone $crawler zone=crawlers:1M rate=1r/m;

上面配置中通過 map 設置 $crawler 變量作爲 limit_req_zone 的鍵。limit_req_zone 對於不同的客戶端必須有不同的值才能正確計算請求計數。如果請求不是來自 crawler,使用一個空字符串來禁用速率限制。

對 API 令牌限制請求,使用 map 創建一個多個鍵,對應其速率限制區域:

map $http_authorization $eclients {
~.*6d96270004515a0486bb7f76196a72b40c55a47f.* 6d96270004515a0486bb7f76196a72b40c55a47f;
~.*956f7fd1ae68fecb2b32186415a49c316f769d75.* 956f7fd1ae68fecb2b32186415a49c316f769d75;
default "";
}
limit_req_zone $eclients zone=eclients:1M rate=1r/s;

下面我們來看看 AuthorizationAPI 令牌的標頭,如 Authorization: Bearer 1234567890. 如果我們匹配一些已知的標記,我們使用該值 $eclients 爲了變量,然後其作爲鍵引入到 limit_req_zone。

server {
listen 80;
server_name test.show;
limit_req zone=crawlers;
limit_req zone=global;
# ...
}
server {
listen 80;
server_name api.test.show;
# ...
location /heavy/method {
# ...
limit_req zone=eclients;
limit_req zone=global;
# ...
}
# ...
}

請注意,配置中必須添加 globa 區域作爲後備,非匹配的情況。

最後總結一下速率限制的流程:

創建保存速率限制的變量的鍵。不同鍵值對應於不同的速率限制區域。

空鍵表示禁用速率限制。

使用帶限速鍵的變量來配置限速區域配置。

在需要的地方應用速率限制區域 limit_req。

速率限制將有助於保持系統穩定。

除了速率限制,Nginx 也有一個請求頻率限制方法 limit_conn_zone 和對應的 limit_conn 用來限制請求的頻次。其使用方法,具體和 limit_req_zone 以及 limit_req 的方法也類似,下面是一個例子:

http {
limit_conn_zone $binary_remote_addr zone=perip:10m;
limit_conn_zone $server_name zone=perserver:10m;
server {
location / {
limit_conn perip 10;
limit_conn perserver 1000;
}
}
}

緩存

Nginx 最大的用途是作爲代理緩存服務器。假設請求代理到某個後端應用服務器,後端服務器返回請求數據的成本很高。則可以通過緩存它來減少後端的負載。

http {
# ...
proxy_cache_path /var/cache/nginx/test keys_zone=test:500m max_size=1000m inactive=1d;
# ...
server {
# ...
location /test {
proxy_pass test.show_backend;
proxy_cache test;
proxy_cache_key "$scheme$proxy_host$request_uri $http_customer_token";
proxy_cache_valid 200 302 1d;
proxy_cache_valid 404 400 10m;
}
}
}

在此示例中,通過添加 $http_customer_token 保存值的變 Customer-Token HTTP 標題。然後,與速率限制一樣,定義緩存區域應用於服務器、位置或全局使用 proxy_cache 指示。另外還要配置緩存失效。默認情況下,僅對 200、301 和 302 HTTP 狀態碼響應緩存,超過 10 分鐘更新一次緩存內容。另外對於後端服務器 Nginx 會遵守其指示性的 Http 頭,例如 Cache-Control 標頭。如果標頭包含類似 no-store,must-revalidate,nginx 則不會對其緩存響應。可以在 Nginx 配置

proxy_ignore_headers "Cache-Control";

來覆蓋該行爲。

因此,要配置 nginx 緩存失效,請執行以下操作:

設置 max_size 在 proxy_cache_path 限制磁盤的佔用。如果 nginx 需要緩存超過 max_size,將從緩存中移除最近最少使用的值

設置 inactive 參數輸入 proxy_cache_path 配置 TTL 整個緩存區。可以用 proxy_cache_valid 指示。

最後,添加 proxy_cache_valid 將指示 TTL 的指令在給定位置或服務器中緩存項目,這將爲緩存設置 TTL 條目。

結構化日誌

從 Nginx 訪問日誌是個大寶藏,我們可以通過其挖掘當前 Web 服務的在線狀態,使用狀態和用戶信息。但是其默認訪問日誌有點太簡陋,需要對其進行配置增加必須的字段,調整其位置,使其更加格式化。Nginx 日誌的配置需要用 log_format 語句。一個典型的配置如下:

log_format main '$remote_addr - $remote_user [$time_iso8601] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent - $ssl_client_s_dn $ssl_client_serial $ssl_client_verify" "$http_x_forwarded_for"';

上述配置中,除了常見的各種字段外,另外增加了 $ssl_client_s_dn $ssl_client_serial 和 $ssl_client_verify,用於在 https 雙向認證時候客戶的端用 CA 簽發 dn 信息,用戶證書序列號用來記錄合法認證的用戶信息。

另外爲了和 ELK 或者其他日誌系統的集成使用 json 格式的結構化日誌很有必要,可以使用 graylog 將文本日誌轉化,也可以直接在 Nginx 配置生成:

http {
# ...
log_format json escape=json '{'
'"server_name": "test.show",'
'"ts":"$time_iso8601",'
'"remote_addr":"$remote_addr","host":"$host","origin":"$http_origin","url":"$request_uri",'
'"request_id":"$request_id","upstream":"$upstream_addr",'
'"response_size":"$body_bytes_sent","upstream_response_time":"$upstream_response_time","request_time":"$request_time",'
'"status":"$status"'
'"$https_info": "$ssl_client_s_dn $ssl_client_serial $ssl_client_verify"'
'}';
# ...
}

escape=json 選項將替換不可打印的字符,如換行符和轉義值,例如 \ n. 引號和反斜槓也將被轉義。

如果是 K8S 容器雲節點的服務可以,直接用 filter 用來指定:

f

ilter {
json {
source => "log"
remove_field => ["log"]
}
}

灰度發佈(A/B 測試)

運維部門爲了保證服務升級,往往會採用灰度發佈的方式,逐步將用戶切換到新的版本中。

在 Nginx 可以用 split_client 模塊實現提供逐步升級的功能。他有點類似像 map 函數,但不是通過某種模式設置變量,而是創建來自源變量分佈的變量。下面一個例子:

http {
upstream current {
server backend1;
serverbackend2;
}
upstream new {
server dev.show max_fails=0;
}
split_clients $arg_key $new_api {
5% 1;
* 0;
}
map $new_api:$cookie_app_switch $destination {
~.*:1 new;
~0:.* current;
~1:.* new;
}
server {
# ...
location /api {
proxy_pass $destination/;
}
}
}

在此示例中,app_switch 和 split_clients cookie 值結合生成調度鍵。如果 cookie 設置爲設置 $destination 調度到上游的 new 爲 1。否則,從 split_clients 調度。這是在生產一種用於測試新系統的功能標誌:擁有 cookie 集的用戶都將始終請求到 new。

鍵的分佈是一致的。如果已將 API 鍵用於 split_clients 那麼具有相同 API 鍵的用戶將始終被放入同一組。

使用此配置,可以將流量分流到新系統,從小百分比開始並逐漸增加。當然修改百分比參數後,不需要 reload 才能生效。

結論

本文我們介紹一些日常運維中 Nginx 的管理祕籍,當然密不密不是絕對只是個人看法,希望以此拋磚引玉,如果你有任何建議和建議補充,可以回覆說明。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/X5KOkGCSrisvak1BrGrVPg