新手學分布式-動態修改Nginx配置的一些想法


本人是分布式的新手,在實際工作中遇到了需要動態修改nginx的需求,因此寫下實現過程中的想法。Nginx功能強大且靈活,所以這些權當拋磚引玉,希望可以得到大家的討論和指點。(具體代碼在 https://andy-zhangtao.github.io/nginx2svg/ )

如何動態配置Nginx參數

Nginx參數眾多,並且配置是非靈活,因此要達到完美的自動化配置是一件很有挑戰性的事情,這個工具並不能十分完美的自動化調整參數。目前支持自動化修改的參數有:

  • server
  • upstream
  • proxy_pass
  • root

下面將介紹Nginx2Svg是如何實現自動化修改參數的。

預備知識

為了更好的理解Nginx2Svg,需要一些很簡單的預備知識。 首先需要了解Nginx的配置文件格式,一個典型的Nginx配置文件(假設此處Nginx作為7層反向負載使用)看起來應該是下面的樣子:

# 抄自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則屬於局部變量。 為了簡化后面的操作,我們可以簡化httpserver之間的包含關系,如下:

     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首行

當每個配置文件都滿足上述三個條件時,自動化解析程序就可以按照設定好的規則解析並嘗試理解每段語義。

只解析文件還不夠,還需要能動態修改才可以。 再回到上面的配置內容,里面的變量有三部分,按照從上往下依次是:

  1. upstream的server IP列表
  2. server_name中的domain列表
  3. location列表

動態修改更准確的就是如何動態修改上面三部分值,這三部分的關聯關系如下:


    +-------------+
    | server_name |
    |   domain1   |
    |   domain2   |                 +-----------------+                 +-----------------+
    |   domain3   |---------------> |    location1    |-------------->  |   upstream1     |
    |   .......   |                 +-----------------+                 +-----------------+
    |   domainN   |
    +-------------+
                                    +-----------------+                 +-----------------+
                                    |    location2    |-------------->  |   upstream2     |
                                    +-----------------+                 +-----------------+


                                    +-----------------+                 +-----------------+
                                    |    locationN    |-------------->  |  upstreamN      |
                                    +-----------------+                 +-----------------+

同一個組的server_name共享所有的location數據,而每一個location則通過proxy_pass指向特定的upstream(可以是不同的,也可以是相同的upstream)。

從上圖可以看出server_namelocation在一個作用域中(在同一個{}中)而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就可以,后續的數據原樣復制不要做變更。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM