Tengine 如何查詢 server 塊

大濤學長發表於2019-09-10

概述

本文的目標讀者是Tengine/Nginx 研發或者運維同學,如果自己對這塊邏輯非常清楚,那可以略過,如果在配置或者開發 Tengine/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 塊。下面來驗證一下: 
image.png
可以看出:

  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 塊

    • 這裡涉及到泛域名和隱式預設 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 技巧:

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 沒有類似的配置命令)。

資料結構

我們再來看看 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 在核心是這樣監聽的:
image.png
雖然 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 的大體關聯關係如下: 
image.png
(可以通過這個圖來理解一下 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 塊配置了。

️本文作者:金九

原文連結

本文為雲棲社群原創內容,未經允許不得轉載。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69947441/viewspace-2656619/,如需轉載,請註明出處,否則將追究法律責任。

相關文章