Nginx源码分析之 upstream指令

#Nginx 源码分析 upstream指令

想要的解决问题:

1:upstream存储结构
2:动态 upstream 流程(proxy_pass跟随变量或者域名)

最简单的配置文件

http {
    upstrem abc {
        server 1.1.1.1;
        server 2.2.2.2;
    }
    
    upstrem efg {
        server 3.3.3.3;
        server 4.4.4.4;
    }
    
    server {
        listen 80;

        location / {
            proxy_pass http://abc;
        }
    }
}

存储结构

首先,upstream是复杂指令,里面还需要解析 server 指令;所以其存储形式如下:
每个upstream指令用ngx_http_upstream_srv_conf_t表示,每个server指令,用ngx_http_upstream_server_t描述。

ngx_http_upstream_server_t
----ngx_http_upstream_srv_conf_t    
--------ngx_http_upstream_server_t
------------1.1.1.1
------------2.2.2.2

ngx_http_upstream_server_t
----ngx_http_upstream_srv_conf_t    
--------ngx_http_upstream_server_t
------------3.3.3.3
------------4.4.4.4

我们来看一下Nginx如何将upstream组织成如上存储结构的:

首先,每解析一个upstream块时,就往 ngx_http_upstream_main_conf_t 中插入upstreams:每个upstream指令都会往 ngx_http_upstream_main_conf_t->upstreams数组中插入一个对象。每个对象是 ngx_http_upstream_srv_conf_t类型的数据结构。ngx_http_upstream_srv_conf_t对象的名字就是 upstream指令后面跟的名字abc或者efg,方便proxy_pass指令根据名字查找到;

static char *
ngx_http_upstream(ngx_conf_t *cf, ngx_command_t *cmd, void *dummy)
{
    ........
    ngx_http_upstream_srv_conf_t  *uscf;
    /*每解析一个upstream,就生成一个ngx_http_upstream_srv_conf_t对象,当然也会查重*/
    uscf = ngx_http_upstream_add(cf, &u, NGX_HTTP_UPSTREAM_CREATE
                                         |NGX_HTTP_UPSTREAM_WEIGHT
                                         |NGX_HTTP_UPSTREAM_MAX_FAILS
                                         |NGX_HTTP_UPSTREAM_FAIL_TIMEOUT
                                         |NGX_HTTP_UPSTREAM_DOWN
                                         |NGX_HTTP_UPSTREAM_BACKUP);

}

换句话说,每个 ngx_http_upstream_srv_conf_t 挂在 ngx_http_upstream_main_conf_t中的upstreams成员下面。ngx_http_upstream_srv_conf_t用来描述一个upstream块,ngx_http_upstream_main_conf_t->upstreams 用来管理所有 upstream块。

每个 ngx_http_upstream_srv_conf_t中,有个 servers成员,其类型是ngx_http_upstream_server_t,用来描述一个 server,换句话说,每解析到一个server指令,则会新建一个ngx_http_upstream_server_t对象挂在其所属的upstream的servers字段中。

至此,解析完upstream以及其server指令后,其存储在内存的结构描述完成;http_proxy 模块是如何找到对应的upsream呢?

proxy_pass 引用 upstream

proxy_pass 为非变量

    location / {
        proxy_pass http://abc;
    }

在解析proxy_pass指令时通过ngx_http_upstream_add函数,找到对应的upstream(由ngx_http_upstream_srv_conf_t描述)

    plcf->upstream.upstream = ngx_http_upstream_add(cf, &u, 0);
    if (plcf->upstream.upstream == NULL) {
        return NGX_CONF_ERROR;
    }

由于Nginx可能会先解析到proxy_pass指令,也有可能先解析到upstream指令,所以统一调用ngx_http_upstream_add来创建/查找 upstream,通过flag来控制流程,核心就是谁先解析,就由谁来创建。
由此可见,Nginx在配置解析阶段,就就确定好了upstream,在滴啊用ngx_http_upstream_init_request时,通过uscf = u->conf->upstream;直接获得这个ngx_http_upstream_srv_conf_tu->conf是在函数ngx_http_proxy_handler时,指向了配置结构的内存)。

proxy_pass 为 变量

通常,我们需要根据不同条件来选择不同的upstream,使用变量就方便的多。

    set $ups abc;
    if ( $host = "www.efg.com" ) {
        set $ups efg;
    }
    location / {
        proxy_pass http://$ups;
    }

在解析proxy_pass指令时,发现后面跟随了变量

    url = &value[1];

    n = ngx_http_script_variables_count(url);

    if (n) {

        ngx_memzero(&sc, sizeof(ngx_http_script_compile_t));

        sc.cf = cf;
        sc.source = url;
        sc.lengths = &plcf->proxy_lengths;
        sc.values = &plcf->proxy_values;
        sc.variables = n;
        sc.complete_lengths = 1;
        sc.complete_values = 1;

        if (ngx_http_script_compile(&sc) != NGX_OK) {
            return NGX_CONF_ERROR;
        }

#if (NGX_HTTP_SSL)
        plcf->ssl = 1;
#endif

        return NGX_CONF_OK;
    }

plcf->proxy_lengthsplcf->proxy_values 被赋上了值。即plcf->upstream.upstream中的upstream字段未被赋值。

具体被赋值的时间点,是在业务处理流程函数ngx_http_proxy_handlerngx_http_upstream_init_request被处理的。

    if (plcf->proxy_lengths == NULL) {
        //不是变量
        ctx->vars = plcf->vars;
        u->schema = plcf->vars.schema;
#if (NGX_HTTP_SSL)
        u->ssl = (plcf->upstream.ssl != NULL);
#endif

    } else {
        //变量的情况下走这里
        if (ngx_http_proxy_eval(r, ctx, plcf) != NGX_OK) {
            return NGX_HTTP_INTERNAL_SERVER_ERROR;
        }
    }

所以核心函数是ngx_http_proxy_evalproxy_lengthsproxy_values保存着待处理的变量,该函数就是将其变量变成str。

static ngx_int_t
ngx_http_proxy_eval(ngx_http_request_t *r, ngx_http_proxy_ctx_t *ctx,
    ngx_http_proxy_loc_conf_t *plcf)
{
    ngx_str_t             proxy;

    if (ngx_http_script_run(r, &proxy, plcf->proxy_lengths->elts, 0,
                            plcf->proxy_values->elts)
        == NULL)
    {
        return NGX_ERROR;
    }

    //proxy.data 就是变量被替换后的字符串,例如此时proxy.data就是字符串"http://efg"

    ......
    u->resolved = ngx_pcalloc(r->pool, sizeof(ngx_http_upstream_resolved_t));
    if (u->resolved == NULL) {
        return NGX_ERROR;
    }
    
    if (url.addrs && url.addrs[0].sockaddr) {
        u->resolved->sockaddr = url.addrs[0].sockaddr;
        u->resolved->socklen = url.addrs[0].socklen;
        u->resolved->naddrs = 1;
        u->resolved->host = url.addrs[0].name;

    } else {
        //将"efg"赋值给 u->resolved->host
        u->resolved->host = url.host;
    }


}

此时,还没找真正的upstream,只是将变量翻译成了对应的值。

static void
ngx_http_upstream_init_request(ngx_http_request_t *r)
{
    ......
    if (u->resolved == NULL) {

        uscf = u->conf->upstream;

    } else {

#if (NGX_HTTP_SSL)
        u->ssl_name = u->resolved->host;
#endif

        host = &u->resolved->host;

        //如果proxy_pass 配置为变量,且变量翻译后为一个ip时,走这里
        if (u->resolved->sockaddr) {

            if (u->resolved->port == 0
                && u->resolved->sockaddr->sa_family != AF_UNIX)
            {
                ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
                              "no port in upstream \"%V\"", host);
                ngx_http_upstream_finalize_request(r, u,
                                               NGX_HTTP_INTERNAL_SERVER_ERROR);
                return;
            }

            if (ngx_http_upstream_create_round_robin_peer(r, u->resolved)
                != NGX_OK)
            {
                ngx_http_upstream_finalize_request(r, u,
                                               NGX_HTTP_INTERNAL_SERVER_ERROR);
                return;
            }

            ngx_http_upstream_connect(r, u);

            return;
        }

        umcf = ngx_http_get_module_main_conf(r, ngx_http_upstream_module);

        uscfp = umcf->upstreams.elts;

        //如果proxy_pass 配置为变量,且变量翻译后为非IP时走这里
        //根据 u->resolved->host 从 存储结构中找到对应的upstream,找到后,后续的流程就和未配置变量一样。
        for (i = 0; i < umcf->upstreams.nelts; i++) {

            uscf = uscfp[i];

            if (uscf->host.len == host->len
                && ((uscf->port == 0 && u->resolved->no_port)
                     || uscf->port == u->resolved->port)
                && ngx_strncasecmp(uscf->host.data, host->data, host->len) == 0)
            {
                goto found;
            }
        }

}

proxy_pass 为 域名

例如

    location / {
        proxy_pass http://www.example.com;
    }

此时,在 proxy_pass 时创建了 upstream对象 ngx_http_upstream_srv_conf_t,但是里面的 servers 字段为空,则会在ngx_http_upstream_init_main_conf时,查询域名对应的dns。这也就是为什么如果proxy_pass 写死域名时,不会动态更新DNS的原因,比较获取DNS的流程是在进程启动阶段,后续就不更新了。

调用栈如下:

ngx_inet_resolve_host
ngx_http_upstream_init_round_robin
ngx_http_upstream_init_main_conf

proxy_pass 为 ip

例如

    location / {
        proxy_pass http://127.0.0.1;
    }

在解析 proxy_pass 命令时,调用 ngx_http_upstream_add 创建了 ngx_http_upstream_srv_conf_t 对象,然后在执行ngx_parse_url函数时,解析到了发现参数是ip地址,此时他会创建一个 ngx_http_upstream_server_t 对象,而不是等待 server 指令时创建 ngx_http_upstream_server_t 对象。

server指令参数为域名

    upstrem abc {
        server www.example.com;
    }
    
    server {
        listen 80;

        location / {
            proxy_pass http://abc;
        }
    }

在处理 upsream 的 server 指令时,会调用 ngx_parse_url 处理 server参数,其中会获取域名对应的ip,解析到ip后,其余的处理流程,就和server指令参数是ip的一样了。

proxy_pass 如何区分参数 是 域名 还是 upstream块的名字?

例如

    upstrem www.exapmle.com {
        server 127.0.0.1;
    }
    
    server {
        listen 80;

        location / {
            proxy_pass http://www.exapmle.com;
        }
    }

Nginx是去 解析www.example.com 的ip然后去访问,还是去127.0.0.1去访问?

Nginx 逻辑是这样的:
1:proxy_pass 参数 除了是ip地址以为,其余的全部默认为 upstream 块名字。
2:proxy_pass 参数 如果是变量,变量解析完成之后,按照1处理。
3:当 配置解析完成之后,发现 upstream 里面的 server 不存在时,则在ngx_http_upstream_init_round_robin中,进行解析。

由此可见,域名解析都在配置阶段进行了处理,若是想要通过变量形式动态获取到某个域名,默认情况下,解析域名是行不通的。因为变量处理时在请求处理阶段早就过了配置解析阶段。

例如下面这个配置,想要访问 "www.efg.com"是不成功的,因为Nginx会去找名为"www.efg.com"的upstream块,但是未找到。

    server {
        listen 80;

        set $ups abc;
        if ( $host = "www.efg.com" ) {
            set $ups "www.efg.com";
        }
        location / {
            proxy_pass http://$ups;
        }
    }

想要动态解析域名的方式是 配置 resolver指令。

    resolver 114.114.114.114;
    server {
        listen 80;

        set $ups abc;
        if ( $host = "www.efg.com" ) {
            set $ups "www.efg.com";
        }
        location / {
            proxy_pass http://$ups;
        }
    }

由此可见,Nginx代码真烂

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值