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 块配置了。

 

本文做者:金九

原文连接

本文为云栖社区原创内容,未经容许不得转载。

相关文章
相关标签/搜索