天天看點

Tengine 如何查找 server 塊

概述

本文的目标讀者是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 塊。下面來驗證一下:

Tengine 如何查找 server 塊

可以看出:

  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 在核心是這樣監聽的:

Tengine 如何查找 server 塊

雖然 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 塊

(可以通過這個圖來了解一下 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 塊配置了。

(全文完)

繼續閱讀