寫給運維的Nginx秘籍
要說Web服務(wù)器、代理服務(wù)器和調(diào)度服務(wù)器層面,目前使用最大的要數(shù)Nginx。對于一個運維工程師日常不可避免要和Nginx打交道。為了更好地使用和管理Nginx,本文就給大家介紹幾個蟲蟲日常常用的秘籍。
限制訪問
當(dāng)Nginx開放到公網(wǎng)上以后,就會有大量的非正常訪問,這不光耗費服務(wù)器資源,而且有可能是某種信息探索,然后攻擊的前奏,有對針對性的限制這些訪問很有必要。在Nginx中可以通過一些內(nèi)置的變量來進(jìn)行限制訪問。
限制客戶端代理
在nginx可以使用$http_user_agent變量匹配客戶類型,然后對對匹配的訪問return 4.3來限制器訪問。
在Nginx配置的server部分,直接用if語句實現(xiàn):
- if ($http_user_agent ~ (Go-http-client/1.1|curl)) {
- return 403;
- }
但是如果要匹配的客戶端代理比較多時候,直接這樣拼寫就比較繁瑣也不好管理。這種情況下給大家一個技巧就是用Map函數(shù)。
Map函數(shù)在Nginx ngx_http_map_module中實現(xiàn)的。利用Map函數(shù)可以創(chuàng)建一個變量,并將其與其他變量(比如內(nèi)置的$http_user_agent)關(guān)聯(lián)起來,可以同時關(guān)聯(lián)多個值到多個不同值并儲存到一個變量。其基本語法為:
- map $var1 $var2 { ... }
其作用于為http模塊,這樣可以在開頭映射后,然后在具體的server部分進(jìn)行封禁。
對應(yīng)本例子中:
- map $http_user_agent:$arg_key $ban {
- ~*spider* 1;
- ~Go-http-client/1.1 1;
- ~curl;
- default 0;
- }
這樣在后續(xù)if封禁語句中就可以使用新建的$ban變量進(jìn)行封禁了。
- if ($ban = 1) {
- return 403;
- }
IP限制
有時候?qū)σ恍阂鈦碓吹腎P封禁則更為直接簡單有效。Nginx進(jìn)行IP封禁的方法也很簡單,直接用deny語句,他是Nginx內(nèi)置模塊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;
- …
這樣語句即可,當(dāng)然也可以用allow和deny all搞成實時上的白名單限制模式:
- allow 127.0.0.1;
- allow 192.168.0.0/18;
- allow 110.242.68.66;
- …
- deny all;
這樣除了本機(jī)、18位的內(nèi)網(wǎng)段和110.242.68.66外其他IP都會禁止訪問。
速率限制
除了直接限制訪問外很多時候,不能直接限制其訪問,但是需要針對特定請求限制訪問的速率(頻率)。在Nginx速率限制通過limit_req_zone和limit_req兩個指令實現(xiàn)。
limit_req_zone用來定義請求限制區(qū)域。區(qū)域包含有關(guān)如何分類的配置請求速率限制和實際限制。
limit_req將區(qū)域應(yīng)用于特定http上下文對于全局限制,server每個虛擬服務(wù)器,以及l(fā)ocation對于虛擬中的特定位置服務(wù)器。
為了說明這一點,假設(shè)要實現(xiàn)速率限制配置:
- 全局速率限制100 RPS
- 由User-Agent來限制特定來源(搜索蜘蛛)請求為1RPM。
- 通過API令牌將來自某些可以客戶端的請求限制為1RPS。
要對請求進(jìn)行分類,需要提供索引到 limit_req_zone。鍵通常是一些變量,要么由nginx預(yù)定義,要么由通過map定義。
要通過IP設(shè)置全局速率限制,需要以IP作為鍵。
- limit_req_zone $binary_remote_addr zone=global:100m rate=100r/s;
現(xiàn)在,通過以下方式限制搜索蜘蛛的User-Agent,此處我們使用map函數(shù):
- map $http_user_agent $crawler {
- ~*.*( Baiduspider|bot|spider|slurp).* $http_user_agent;
- default "";
- }
- limit_req_zone $crawler zone=crawlers:1M rate=1r/m;
上面配置中通過map設(shè)置$crawler變量作為limit_req_zone的鍵。limit_req_zone對于不同的客戶端必須有不同的值才能正確計算請求計數(shù)。如果請求不是來自crawler,使用一個空字符串來禁用速率限制。
對API令牌限制請求,使用map創(chuàng)建一個多個鍵,對應(yīng)其速率限制區(qū)域:
- map $http_authorization $eclients {
- ~.*6d96270004515a0486bb7f76196a72b40c55a47f.* 6d96270004515a0486bb7f76196a72b40c55a47f;
- ~.*956f7fd1ae68fecb2b32186415a49c316f769d75.* 956f7fd1ae68fecb2b32186415a49c316f769d75;
- default "";
- }
- limit_req_zone $eclients zone=eclients:1M rate=1r/s;
下面我們來看看 AuthorizationAPI 令牌的標(biāo)頭,如 Authorization: Bearer 1234567890. 如果我們匹配一些已知的標(biāo)記,我們使用該值$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區(qū)域作為后備,非匹配的情況。
最后總結(jié)一下速率限制的流程:
- 創(chuàng)建保存速率限制的變量的鍵。不同鍵值對應(yīng)于不同的速率限制區(qū)域。
- 空鍵表示禁用速率限制。
- 使用帶限速鍵的變量來配置限速區(qū)域配置。
- 在需要的地方應(yīng)用速率限制區(qū)域limit_req。
- 速率限制將有助于保持系統(tǒng)穩(wěn)定。
除了速率限制,Nginx也有一個請求頻率限制方法limit_conn_zone和對應(yīng)的 limit_conn用來限制請求的頻次。其使用方法,具體和limit_req_zone以及l(fā)imit_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 最大的用途是作為代理緩存服務(wù)器。假設(shè)請求代理到某個后端應(yīng)用服務(wù)器,后端服務(wù)器返回請求數(shù)據(jù)的成本很高。則可以通過緩存它來減少后端的負(fù)載。
- 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 標(biāo)題。然后,與速率限制一樣,定義緩存區(qū)域應(yīng)用于服務(wù)器、位置或全局使用 proxy_cache指示。另外還要配置緩存失效。 默認(rèn)情況下,僅對200、301 和 302 HTTP狀態(tài)碼響應(yīng)緩存,超過10分鐘更新一次緩存內(nèi)容。另外對于后端服務(wù)器Nginx會遵守其指示性的Http頭,例如Cache-Control標(biāo)頭。如果標(biāo)頭包含類似no-store,must-revalidate,nginx則不會對其緩存響應(yīng)??梢栽贜ginx配置
- proxy_ignore_headers "Cache-Control";
來覆蓋該行為。
因此,要配置 nginx 緩存失效,請執(zhí)行以下操作:
- 設(shè)置max_size在 proxy_cache_path限制磁盤的占用。如果nginx需要緩存超過max_size,將從緩存中移除最近最少使用的值
- 設(shè)置inactive參數(shù)輸入proxy_cache_path配置TTL整個緩存區(qū)??梢杂?proxy_cache_valid指示。
- 最后,添加proxy_cache_valid將指示TTL的指令在給定位置或服務(wù)器中緩存項目,這將為緩存設(shè)置TTL條目。
結(jié)構(gòu)化日志
從Nginx訪問日志是個大寶藏,我們可以通過其挖掘當(dāng)前Web服務(wù)的在線狀態(tài),使用狀態(tài)和用戶信息。但是其默認(rèn)訪問日志有點太簡陋,需要對其進(jìn)行配置增加必須的字段,調(diào)整其位置,使其更加格式化。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雙向認(rèn)證時候客戶的端用CA簽發(fā)dn信息,用戶證書序列號用來記錄合法認(rèn)證的用戶信息。
另外為了和ELK或者其他日志系統(tǒng)的集成使用json格式的結(jié)構(gòu)化日志很有必要,可以使用graylog將文本日志轉(zhuǎn)化,也可以直接在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選項將替換不可打印的字符,如換行符和轉(zhuǎn)義值,例如\n. 引號和反斜杠也將被轉(zhuǎn)義。
如果是K8S容器云節(jié)點的服務(wù)可以,直接用filter用來指定:
- filter {
- json {
- source => "log"
- remove_field => ["log"]
- }
- }
灰度發(fā)布(A/B測試)
運維部門為了保證服務(wù)升級,往往會采用灰度發(fā)布的方式,逐步將用戶切換到新的版本中。
在Nginx 可以用split_client模塊實現(xiàn)提供逐步升級的功能。他有點類似像map函數(shù),但不是通過某種模式設(shè)置變量,而是創(chuàng)建來自源變量分布的變量。下面一個例子:
- 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 值結(jié)合生成調(diào)度鍵。如果 cookie設(shè)置為設(shè)置$destination調(diào)度到上游的new為1。 否則,從 split_clients調(diào)度。這是在生產(chǎn)一種用于測試新系統(tǒng)的功能標(biāo)志:擁有cookie集用戶都將始終請求到new。
鍵的分布是一致的。如果已將API鍵用于split_clients那么具有相同API鍵的用戶將始終被放入同一組。
使用此配置,可以將流量分流到新系統(tǒng),從小百分比開始并逐漸增加。當(dāng)然修改百分比參數(shù)后,不需要reload才能生效。
結(jié)論
本文我們介紹一些日常運維中Nginx的管理秘籍,當(dāng)然密不密不是絕對只是個人看法,希望以此拋磚引玉,如果你有任何建議和建議補(bǔ)充,可以回復(fù)說明。