概述
本文的目标读者是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 的访问,域名先精确匹配到了
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 在内核是这样监听的:
虽然 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 块配置了。
(全文完)