Tengine 如何查找 server 塊

概述

本文的目標讀者是Tengine/Nginx 研發或者運維同窗,若是本身對這塊邏輯很是清楚,那能夠略過,若是在配置或者開發 Tengine/Nginx 過程當中,有以下疑問的同窗,本文或許能解答你多年的疑惑:nginx

  1. 請求到達匹配的是哪一個 server 塊?
  2. 爲啥明明配置了 server 塊,仍是沒有生效?
  3. 沒有這個域名的 server 塊,請求到底使用了哪一個 server 塊?
  4. 要本身去匹配 server 塊的話,該從哪裏入手?
    ……

等等此類 server 塊有關的問題,在使用 Tengine 時可能常常有遇到,在配置的 server 塊較少時,比較容易識別出,但在 CDN 或者雲平臺接入層這種場景下,配置的 server 塊通常都很是多,少的有幾十上百個,多的成千上萬個都有可能,因此瞭解 Tengine 如何查找 server 塊很是有利於平常問題排查。數組

配置

先來看看幾個配置:數據結構

server {
    listen       10.101.192.91:80 default_server;
    listen       80 default_server;
    listen       8080 default_server;

    server_name  www.aa.com;

    default_type  text/plain;

    location / {
        return 200 "default-server: $server_name, host: $host";
    }
}


server {
    listen       10.101.192.91:80;

    server_name  www.bb.com;

    default_type  text/plain;

    location / {
        return 200 "80server: $server_name, host: $host";
    }
}

server {
    listen       10.101.192.91:8080;

    server_name  *.bb.com;

    default_type  text/plain;

    location / {
        return 200 "8080server: $server_name, host: $host";
    }
}

server {
    listen       10.101.192.91:8080;

    server_name  www.bb.com;

    default_type  text/plain;

    location / {
        return 200 "8080server: $server_name, host: $host";
    }
}

上面配置了四個 server 塊,配置也很是簡單,第一個 server 塊配置了 default_server 參數,這個代表了這個是默認 server 塊的意思(準確地說是這個 listen 的 IP:Port 進來的請求默認 server 塊),監聽了兩個端口80和8080,匹配域名爲 www.aa.com,第二個是監聽了 10.101.192.91:80 和匹配域名爲www.bb.com 的 server 塊,第三個是監聽了 10.101.192.91:8080 和匹配泛域名 *.bb.com 的 server 塊,第四個是監聽了 10.101.192.91:8080 和匹配精確域名 www.bb.com 的 server 塊。下面來驗證一下: 運維

能夠看出:socket

  1. 127.0.0.1:80 和 127.0.0.1:8080 都訪問到了第一個 server 塊函數

    • 這是由於第一個 server 監聽了 :80 和 :8080 端口,其餘 server 塊沒有監聽 127.0.0.1 相應的端口,127.0.0.1 的訪問只能匹配第一個 server 塊。
  2. 10.101.192.91:80 的訪問,域名和 server 塊匹配時使用了相應的 server 塊,不匹配時使用了第一個默認 server 塊優化

    • IP:Port 匹配的狀況下,再匹配到域名所在的 server 塊,域名跟 server_name 不匹配則匹配默認 server 塊。
  3. 10.101.192.91:8080 的訪問,域名先精確匹配到了 www.bb.com 的 server 塊,而後再匹配到了泛域名 *.bb.com 的 server 塊,不匹配時使用了第三個隱式默認 server 塊ui

    • 這裏涉及到泛域名和隱式默認 server 塊,泛域名的匹配是在精確域名以後,這個也比較好理解,隱式默認 server 塊是沒有在 listen 後面指定 default_server 參數的 server 塊, Tengine/Nginx 在解析配置時,每一個 IP:Port 都有一個默認 server 塊,若是 listen 後面顯式指定了 default_server 參數則該 listen 所在的 server 就是這個 IP:Port 的默認 server 塊,若是沒有顯式指定 default_server 參數則該 IP:Port 的第一個 server 塊就是隱式默認 server 塊。

上面這些配置能夠衍生出一些 debug 技巧:this

if ($http_x_alicdn_debug_get_server = "on") {
    return 200 "$server_addr:$server_port, server_name: $server_name";
}

只要帶上請求頭 X-Alicdn-Debug-Get-Server: on 便可知道請求命中的是哪一個 server 塊,這個配置對 server 塊很是多的系統 debug 很是有用,須要注意的是這個配置須要放到一個配置文件和用 server_auto_include 加載,而後 tengine 會自動在全部 server 塊生效(nginx 沒有相似的配置命令)。spa

數據結構

咱們再來看看 http 核心模塊 server 塊的配置在數據結構上怎麼關聯的,其數據結構是:

typedef struct {
    /* array of the ngx_http_server_name_t, "server_name" directive */
    ngx_array_t                 server_names;

    /* server ctx */
    ngx_http_conf_ctx_t        *ctx;

    u_char                     *file_name;
    ngx_uint_t                  line;

    ngx_str_t                   server_name;
#if (T_NGX_SERVER_INFO)
    ngx_str_t                   server_admin;
#endif

    size_t                      connection_pool_size;
    size_t                      request_pool_size;
    size_t                      client_header_buffer_size;

    ngx_bufs_t                  large_client_header_buffers;

    ngx_msec_t                  client_header_timeout;

    ngx_flag_t                  ignore_invalid_headers;
    ngx_flag_t                  merge_slashes;
    ngx_flag_t                  underscores_in_headers;

    unsigned                    listen:1;
#if (NGX_PCRE)
    unsigned                    captures:1;
#endif

    ngx_http_core_loc_conf_t  **named_locations;
} ngx_http_core_srv_conf_t;

這裏不細說這些字段是幹嗎用的,主要看 ngx_http_core_srv_conf_t 怎麼與其餘數據結構關聯,從上面的配置能夠知道 server 是與 IP:Port 有關聯的,在 tengine/nginx 裏的關係以下:

typedef struct {
    ngx_http_listen_opt_t      opt;

    ngx_hash_t                 hash;
    ngx_hash_wildcard_t       *wc_head;
    ngx_hash_wildcard_t       *wc_tail;

#if (NGX_PCRE)
    ngx_uint_t                 nregex;
    ngx_http_server_name_t    *regex;
#endif

    /* the default server configuration for this address:port */
    ngx_http_core_srv_conf_t  *default_server;
    ngx_array_t                servers;  /* array of ngx_http_core_srv_conf_t */
} ngx_http_conf_addr_t;

能夠看出,IP:Port 的核心數據結構 ngx_http_conf_addr_t 裏面有默認 server 塊 default_server,以及該 IP:Port 關聯的全部 server 塊數組 servers,其餘幾個字段不細展開了。tengine 把全部的 IP:Port 按 Port 拆分後將 ngx_http_conf_addr_t 放到了 ngx_http_conf_port_t 裏面了:

typedef struct {
    ngx_int_t                  family;
    in_port_t                  port;
    ngx_array_t                addrs;     /* array of ngx_http_conf_addr_t */
} ngx_http_conf_port_t;

爲何將 IP:Port 拆分呢,這是由於 listen 的 Port 若是沒有指定 IP,好比 listen 80; ,那 tengine/nginx 在建立監聽 socket 時的地址是 0.0.0.0 ,若是還有其餘配置 listen 了精確 ip 和端口,好比 listen 10.101.192.91:80; ,那在內核是無法建立這個 socket 的,第2節配置裏面的幾個 listen 在內核是這樣監聽的:

雖然 listen 了 80 和 10.101.192.91:80,但在內核都是 0.0.0.0:80,因此 tengine 須要用 ngx_http_conf_port_t 來記錄該端口的全部精確地址。但這個結構只是使用在配置階段,在監聽 socket 時轉換成告終構 ngx_http_port_t 和 ngx_http_in_addr_t(這是由於 ip:port 和 server 塊是多對多的關係,須要從新組織和優化):

typedef struct {
    /* ngx_http_in_addr_t or ngx_http_in6_addr_t */
    void                      *addrs;
    ngx_uint_t                 naddrs;
} ngx_http_port_t;

typedef struct {
    in_addr_t                  addr;
    ngx_http_addr_conf_t       conf;
} ngx_http_in_addr_t;

typdef  ngx_http_addr_conf_s ngx_http_addr_conf_t;

struct ngx_http_addr_conf_s {
    /* the default server configuration for this address:port */
    ngx_http_core_srv_conf_t  *default_server;

    ngx_http_virtual_names_t  *virtual_names;

    unsigned                   ssl:1;
    unsigned                   http2:1;
    unsigned                   proxy_protocol:1;
};

其中,ngx_http_port_t 記錄了該端口的全部精確地址和對應的 server 塊。而 ngx_http_port_t 放到了監聽的 socket 核心結構 ngx_listening_t 中:

typedef struct ngx_listening_s  ngx_listening_t;

struct ngx_listening_s {
    ngx_socket_t        fd;

    struct sockaddr    *sockaddr;
    socklen_t           socklen;    /* size of sockaddr */
    size_t              addr_text_max_len;
    ngx_str_t           addr_text;

    // 省略……

    /* handler of accepted connection */
    ngx_connection_handler_pt   handler;

    void               *servers;  /* array of ngx_http_in_addr_t, for example */

    // 省略……
};

struct ngx_connection_s {
    // 省略……

    ngx_listening_t    *listening;

    // 省略……
};

因此一個鏈接能夠從 c->listening->servers 來查找匹配的 server 塊。

tengine 中 ip:port 和 server 的大致關聯關係以下: 

(能夠經過這個圖來理解一下 tengine 如何查找 server 塊)

從請求到 server 塊

上面講了 ip:port 和 server 的一些關係和核心數據結構,這一節來說講 tengine 從處理請求到匹配 server 的邏輯。ngx_http_init_connection 是初始化鏈接的函數,在這個函數裏面咱們看到有這樣的邏輯:

void
ngx_http_init_connection(ngx_connection_t *c)
{
    // 省略……
    ngx_http_port_t        *port;
    ngx_http_in_addr_t     *addr;
    ngx_http_connection_t  *hc;
    // 省略……

    /* find the server configuration for the address:port */

    port = c->listening->servers;

    if (port->naddrs > 1) {
            // 省略……
            sin = (struct sockaddr_in *) c->local_sockaddr;

            addr = port->addrs;

            /* the last address is "*" */

            for (i = 0; i < port->naddrs - 1; i++) {
                if (addr[i].addr == sin->sin_addr.s_addr) {
                    break;
                }
            }

            hc->addr_conf = &addr[i].conf;
            // 省略……
    } else {
            // 省略……
            addr = port->addrs;
            hc->addr_conf = &addr[0].conf;
            // 省略……
    }

    /* the default server configuration for the address:port */
    hc->conf_ctx = hc->addr_conf->default_server->ctx;
    // 省略……
}

能夠看出,初始化時,拿到了 socket 的 ip:port 後去匹配了最合適的配置,存到了 hc->addr_conf 指針中,這個就是上面講到的數據結構 ngx_http_addr_conf_t 指針,這裏面存了該 ip:port 關聯的全部 server 塊核心配置,在以後收到 HTTP 請求頭處理請求行或者處理 Host 頭時,再根據域名去 hc->addr_conf 裏面匹配出真實的 server 塊:

static ngx_int_t
ngx_http_set_virtual_server(ngx_http_request_t *r, ngx_str_t *host)
{
    // 省略……
    ngx_http_connection_t     *hc;
    ngx_http_core_srv_conf_t  *cscf;

    // 省略……

    hc = r->http_connection;

    // 省略……

    rc = ngx_http_find_virtual_server(r->connection,
                                      hc->addr_conf->virtual_names,
                                      host, r, &cscf);

    //建立 r 時,r->srv_conf 和 r->loc_conf 是 hc->conf_ctx 的默認配置
    //查不到匹配的 server 塊則不須要設置 r->srv_conf 和 r->loc_conf
    if (rc == NGX_DECLINED) {
        return NGX_OK;
    }

    // 查到匹配的 server,使用真實 server 塊的配置
    r->srv_conf = cscf->ctx->srv_conf;
    r->loc_conf = cscf->ctx->loc_conf;
    // 省略……
}

函數 ngx_http_find_virtual_server 是查找域名對應的 server 塊接口(這個函數還有另外一個地方調用是在處理 SSL 握手遇到 SNI 時,這是由於在握手時也須要找到匹配的 server 塊裏面配置的證書)。
至此,server 塊配置的查找邏輯結束,後續其餘模塊處理時能夠從 r->srv_conf 和 r->loc_conf 查到本身模塊的 server/location 塊配置了。

 

本文做者:金九

原文連接

本文爲雲棲社區原創內容,未經容許不得轉載。

相關文章
相關標籤/搜索