新手學分散式-動態修改Nginx配置的一些想法
本人是分散式的新手,在實際工作中遇到了需要動態修改nginx的需求,因此寫下實現過程中的想法。Nginx功能強大且靈活,所以這些權當拋磚引玉,希望可以得到大家的討論和指點。(具體程式碼在 https://andy-zhangtao.github.io/nginx2svg/ )
如何動態配置Nginx引數
Nginx引數眾多,並且配置是非靈活,因此要達到完美的自動化配置是一件很有挑戰性的事情,這個工具並不能十分完美的自動化調整引數。目前支援自動化修改的引數有:
- server
- upstream
- proxy_pass
- root
下面將介紹Nginx2Svg
是如何實現自動化修改引數的。
預備知識
為了更好的理解Nginx2Svg
# 抄自nginx官網 http://nginx.org/en/docs/example.html 1 user www www; 2 3 worker_processes 2; 4 5 pid /var/run/nginx.pid; 6 7 # [ debug | info | notice | warn | error | crit ] 8 9 error_log /var/log/nginx.error_log info; 10 11 events { 12 worker_connections 2000; 13 14 # use [ kqueue | epoll | /dev/poll | select | poll ]; 15 use kqueue; 16 } 17 18 http { 19 20 include conf/mime.types; 21 default_type application/octet-stream; 22 23 24 log_format main '$remote_addr - $remote_user [$time_local] ' 25 '"$request" $status $bytes_sent ' 26 '"$http_referer" "$http_user_agent" ' 27 '"$gzip_ratio"'; 28 29 log_format download '$remote_addr - $remote_user [$time_local] ' 30 '"$request" $status $bytes_sent ' 31 '"$http_referer" "$http_user_agent" ' 32 '"$http_range" "$sent_http_content_range"'; 33 34 client_header_timeout 3m; 35 client_body_timeout 3m; 36 send_timeout 3m; 37 38 client_header_buffer_size 1k; 39 large_client_header_buffers 4 4k; 40 41 gzip on; 42 gzip_min_length 1100; 43 gzip_buffers 4 8k; 44 gzip_types text/plain; 45 46 output_buffers 1 32k; 47 postpone_output 1460; 48 49 sendfile on; 50 tcp_nopush on; 51 tcp_nodelay on; 52 send_lowat 12000; 53 54 keepalive_timeout 75 20; 55 56 #lingering_time 30; 57 #lingering_timeout 10; 58 #reset_timedout_connection on; 59 60 61 server { 62 listen one.example.com; 63 server_name one.example.com www.one.example.com; 64 65 access_log /var/log/nginx.access_log main; 66 67 location / { 68 proxy_pass http://127.0.0.1/; 69 proxy_redirect off; 70 71 proxy_set_header Host $host; 72 proxy_set_header X-Real-IP $remote_addr; 73 #proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 74 75 client_max_body_size 10m; 76 client_body_buffer_size 128k; 77 78 client_body_temp_path /var/nginx/client_body_temp; 79 80 proxy_connect_timeout 70; 81 proxy_send_timeout 90; 82 proxy_read_timeout 90; 83 proxy_send_lowat 12000; 84 85 proxy_buffer_size 4k; 86 proxy_buffers 4 32k; 87 proxy_busy_buffers_size 64k; 88 proxy_temp_file_write_size 64k; 89 90 proxy_temp_path /var/nginx/proxy_temp; 91 92 charset koi8-r; 93 } 94 95 error_page 404 /404.html; 96 97 location = /404.html { 98 root /spool/www; 99 } 100 101 location /old_stuff/ { 102 rewrite ^/old_stuff/(.*)$ /new_stuff/$1 permanent; 103 } 104 105 location /download/ { 106 107 valid_referers none blocked server_names *.example.com; 108 109 if ($invalid_referer) { 110 #rewrite ^/ http://www.example.com/; 111 return 403; 112 } 113 114 #rewrite_log on; 115 116 # rewrite /download/*/mp3/*.any_ext to /download/*/mp3/*.mp3 117 rewrite ^/(download/.*)/mp3/(.*)\..*$ 118 /$1/mp3/$2.mp3 break; 119 120 root /spool/www; 121 #autoindex on; 122 access_log /var/log/nginx-download.access_log download; 123 } 124 125 location ~* \.(jpg|jpeg|gif)$ { 126 root /spool/www; 127 access_log off; 128 expires 30d; 129 } 130 } 131 }
從18行到131行屬於http
配置內容,在這部分引數中,第61行到130行屬於server
配置內容,(一個server對應一個虛擬主機),server
的引數屬於http
引數的子集,當相同引數出現時,server
優先順序會高於http
。按照作用域來做類比,http
就是全域性變數,server
就是區域性變數。
所以18行到60行屬於全域性變數,而61行到130則屬於區域性變數。 為了簡化後面的操作,我們可以簡化http
和server
之間的包含關係,如下:
1 user nginx; 2 worker_processes 1; 3 4 error_log /var/log/nginx/error.log warn; 5 pid /var/run/nginx.pid; 6 7 8 events { 9 worker_connections 1024; 10 } 11 12 13 http { 15 include /etc/nginx/mime.types; 16 default_type application/octet-stream; 17 18 log_format main '$remote_addr - $remote_user [$time_local] ' 19 '"$request" $status $bytes_sent ' 20 '"$http_referer" "$http_user_agent" ' 21 '"$gzip_ratio"'; 22 23 log_format download '$remote_addr - $remote_user [$time_local] ' 24 '"$request" $status $bytes_sent ' 25 '"$http_referer" "$http_user_agent" ' 26 '"$http_range" "$sent_http_content_range"'; 27 28 access_log /var/log/nginx/access.log main; 29 30 sendfile on; 31 32 keepalive_timeout 65; 33 34 35 server { 36 listen 80 default_server; 37 server_name _; 38 39 location /status { 40 vhost_traffic_status_display; 41 vhost_traffic_status_display_format html; 42 } 43 } 44 45 include /etc/nginx/conf.d/*.conf; 46 }
通過include
引入其它server配置檔案,而上面的內容可以作為nginx.conf
全域性預設配置檔案,基本就不再修改了。而以後我們所要動態修改的配置檔案就是/etc/nginx/conf.d/*.conf
這部分。
配置規則
如果要達到自動化配置的目標,那麼就需要設定一些規則。 下面是為了滿足自動化而設定的規則:
- 配置檔案規則
- 必須存在server_name。
- 檔名以[server name].conf進行命名。 假設server_name為example.com, 則配置檔名就是example.com.conf。
- 一個檔案有並且只有一個server段
- 配置內容規則
- 同一個配置檔案中location不重複(正則表示式不在限制範圍內)
解析規則
在滿足上述兩個規則的前提下,我們來看如何實現Nginx引數的自動化配置。首先要明確實現nginx自動化配置的難點在哪裡? 基於我的使用經驗來看,難點在於以下三點:
nginx配置相當靈活,屬於
非結構化
語義
雖然nginx明確了配置檔案的內容和格式,但在配置上可以任意組合(在執行nginx -t或者reload時才會真正驗證)。因此配置檔案只規定了最低門檻的結構正規化
,而並沒有規定嚴謹的配置格式,造成了只要符合語義都可以驗證成功。這一點在使用者眼裡是非常靈活的優點,但從自動化角度來說則是很大的痛點,因為找不到一個統一的解析格式來理解語義。驗證和回滾
nginx是基於文字來進行配置的,每一次修改都是通過IO操作生成文字配置檔案而後在載入在每個worker中。 因此當驗證失敗時,如何將新增/刪除的內容恢復到上一個版本中,就變成了一個問題。個性化配置
在真實業務場景中,nginx配置必然無法做到一個配置吃遍天。當某些server需要添加個性化配置引數時,如何平衡個性化配置和自動化配置,也變成了一個需要考慮的問題。
當找到上述三個問題的答案時,大體就可以滿足自動化配置的要求了。
首先來看第一個問題。
如果因為nginx配置靈活而導致正面解析nginx配置檔案是一個很困難的事情,那麼可以嘗試換個角度來理解這個問題。 如果變化很多而不容易解析,那麼就不要讓它變化了
具體怎麼理解呢? nginx是通過語義來驗證的,也就是nginx自身其實對結構
不敏感的(可以反向證明,如果nginx是依賴結構來理解配置的,那麼它應該會規定嚴謹的配置結構)。所以我們可以事先定義好每個配置檔案的配置格式,如下:
1
2
3 upstream 5d148ba37f325500011770af {
4 server xxxxx ;
5 }
6
7
8 server{
9
10 server_name web1.example.com;
11
12
13
14
15 location /server1 {
16 proxy_pass http://5d148ba37f325500011770af;
17 proxy_set_header X-Real-IP $remote_addr;
18 proxy_set_header Host $host;
19 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
20 proxy_next_upstream error timeout http_500 http_502 http_503 http_504 non_idempotent;
21
22
23
24 }
25
26 }
27
每個配置檔案都規定好配置結構如下:
upstream
都統一放置在server
之前server_name
放置在location
之前proxy_pass
放置在每個location
首行
當每個配置檔案都滿足上述三個條件時,自動化解析程式就可以按照設定好的規則解析並嘗試理解每段語義。
只解析檔案還不夠,還需要能動態修改
才可以。 再回到上面的配置內容,裡面的變數有三部分,按照從上往下依次是:
- upstream的server IP列表
- server_name中的domain列表
- location列表
動態修改更準確的就是如何動態修改上面三部分值,這三部分的關聯關係如下:
+-------------+
| server_name |
| domain1 |
| domain2 | +-----------------+ +-----------------+
| domain3 |---------------> | location1 |--------------> | upstream1 |
| ....... | +-----------------+ +-----------------+
| domainN |
+-------------+
+-----------------+ +-----------------+
| location2 |--------------> | upstream2 |
+-----------------+ +-----------------+
+-----------------+ +-----------------+
| locationN |--------------> | upstreamN |
+-----------------+ +-----------------+
同一個組的server_name
共享所有的location
資料,而每一個location
則通過proxy_pass
指向特定的upstream
(可以是不同的,也可以是相同的upstream)。
從上圖可以看出server_name
和location
在一個作用域中(在同一個{}
中)而upstream
則遊離在外。
三個問題中,server_name可以通過server_name
準確定位,location
也可以準確定位,此時如何從location
通過proxy_pass
定位到upstream
則變成了當前的難點。
在實際使用過程中,我通過新增錨點
來解決這個問題,具體來說就是增加一組upstream
輔助定位資料,例如下圖中的資料:
1
2 ### [5d148ba37f325500011770af]-[/]-[upstream]-[start]
3 upstream 5d148ba37f325500011770af {
4 server xxxxx ;
5 }
6 ### [5d148ba37f325500011770af]-[/]-[upstream]-[end]
7
8 server{
9
10 server_name web1.example.com;
11
12
13
14
15 location /server1 {
16 proxy_pass http://5d148ba37f325500011770af;
17 proxy_set_header X-Real-IP $remote_addr;
18 proxy_set_header Host $host;
19 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
20 proxy_next_upstream error timeout http_500 http_502 http_503 http_504 non_idempotent;
21
22
23
24 }
25
26 }
27
第二行和第六行就是新增的錨點
。 錨點資料需要滿足的條件是:
- 同一個配置檔案中不重複
- 有良好的區分度
因此設計了上述的錨點
資料,其格式如下:
### [5d148ba37f325500011770af]-[/]-[upstream]-[start]
----------------------------------------------------
### [24位隨機數]-[/]-[upstream]-[開始/結束標示]
① ② ③ ④
① 三個#開頭
② 滿足錨點,upstream名稱和proxy_pass一致,也就是第二行,第三行和第十六行使用同一個24位隨機數
③ 固定格式,用來保證和其它註釋資訊不重複
④ start表示upstream開始, end表示upstream結束。
因此一個完整的自動化配置流程如下:
// 假設配置web1.example.com的/server1 反向配置
if web1.example.com.conf 存在
逐行讀取檔案內容
if 找到 server1的location行
解析 proxy_pass,找到 24位隨機數
從頭開始讀取檔案內容
if 找到 ### [xxxx]-[/]-[upstream]-[start]
找到錨點,此行往下兩行是ip列表,開始修改
else
沒找到錨點,配置檔案出錯,人工介入
else
// 當前沒有此location配置,新建location和upstream
新建location配置
新建相匹配的upstream配置
else
// 當前沒有此域名配置,新建一個
建立 web1.example.com.conf,內容按照既定格式建立
個性化支援
從上面的解析規則來看,如果要支援個性化支援,那麼在理解語義時要做到適可而止
,也就是隻需要解析到需要的資料就可以了,其它資料原樣複製。例如使用者在location
中添加了個性化引數(需要滿足配置規則第三條
),那麼只要解析出proxy_pass
就可以,後續的資料原樣複製不要做變更。