概述
本文的目标讀者是Tengine/Nginx 研發或者運維同學,如果自己對這塊邏輯非常清楚,那可以略過,如果在配置或者開發 Tengine/Nginx 過程中,有如下疑問的同學,本文或許能解答你多年的疑惑:
- 請求到達比對的是哪個 server 塊?
- 為啥明明配置了 server 塊,還是沒有生效?
- 沒有這個域名的 server 塊,請求到底使用了哪個 server 塊?
-
要自己去比對 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 塊。下面來驗證一下:
可以看出:
- 127.0.0.1:80 和 127.0.0.1:8080 都通路到了第一個 server 塊
- 這是因為第一個 server 監聽了 :80 和 :8080 端口,其他 server 塊沒有監聽 127.0.0.1 相應的端口,127.0.0.1 的通路隻能比對第一個 server 塊。
- 10.101.192.91:80 的通路,域名和 server 塊比對時使用了相應的 server 塊,不比對時使用了第一個預設 server 塊
- IP:Port 比對的情況下,再比對到域名所在的 server 塊,域名跟 server_name 不比對則比對預設 server 塊。
- 10.101.192.91:8080 的通路,域名先精确比對到了
的 server 塊,然後再比對到了泛域名 *.bb.com 的 server 塊,不比對時使用了第三個隐式預設 server 塊www.bb.com
- 這裡涉及到泛域名和隐式預設 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 在核心是這樣監聽的:
雖然 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 塊配置了。
(全文完)