查看原文
其他

nginx中利用resolver实现动态upstream

运维菜鸟 运维研习社 2022-11-05

nginx中如何利用resolver实现动态upstream呢?

首先了解下resolver,在nginx中,nginx有一套自己的域名解析过程,在nginx配置中,通过resolver指令来设置DNS服务器地址,来启动nginx的域名解析

首先通过源码看一下nginx是如何做的,本文基于nginx1.14.1版本分析

首先,resolver的初始化,在源码http中ngx_http_core_module.h中ngx_http_loc_conf_s的申明中可以看到对resolver的申明,在文件364行

resolver中保存了与域名解析相关的一些数据,它保存了DNS的本地缓存,通过红黑书的方式来组织数据,快速查找

在nginx初始化的时候,通过ngx_resolver_create来初始化,如果在配置文件中设置了resolver,则在ngx_http_core_resolver中有调用

在ngx_resolver_create的第二个参数,就是我们设置的域名解析服务器的IP地址

继续看一下ngx_resolver_create做了什么工作

ngx_resolver_create(ngx_conf_t *cf, ngx_str_t *names, ngx_uint_t n)

{

    ngx_str_t                   s;

    ngx_url_t                   u;

    ngx_uint_t                  i, j;

    ngx_resolver_t             *r;

    ngx_pool_cleanup_t         *cln;

    ngx_resolver_connection_t  *rec;


    cln = ngx_pool_cleanup_add(cf->pool, 0);

    if (cln == NULL) {

        return NULL;

    }


    cln->handler = ngx_resolver_cleanup;


    r = ngx_calloc(sizeof(ngx_resolver_t), cf->log);

    if (r == NULL) {

        return NULL;

    }


    cln->data = r;


    r->event = ngx_calloc(sizeof(ngx_event_t), cf->log);

    if (r->event == NULL) {

        return NULL;

    }


    ngx_rbtree_init(&r->name_rbtree, &r->name_sentinel,

                    ngx_resolver_rbtree_insert_value);


    ngx_rbtree_init(&r->srv_rbtree, &r->srv_sentinel,

                    ngx_resolver_rbtree_insert_value);


    ngx_rbtree_init(&r->addr_rbtree, &r->addr_sentinel,

                    ngx_rbtree_insert_value);


    ngx_queue_init(&r->name_resend_queue);

    ngx_queue_init(&r->srv_resend_queue);

    ngx_queue_init(&r->addr_resend_queue);


    ngx_queue_init(&r->name_expire_queue);

    ngx_queue_init(&r->srv_expire_queue);

    ngx_queue_init(&r->addr_expire_queue);


#if (NGX_HAVE_INET6)

    r->ipv6 = 1;


    ngx_rbtree_init(&r->addr6_rbtree, &r->addr6_sentinel,

                    ngx_resolver_rbtree_insert_addr6_value);


    ngx_queue_init(&r->addr6_resend_queue);


    ngx_queue_init(&r->addr6_expire_queue);

#endif


    r->event->handler = ngx_resolver_resend_handler;

    r->event->data = r;

    r->event->log = &cf->cycle->new_log;

    r->event->cancelable = 1;

    r->ident = -1;


    r->resend_timeout = 5;

    r->tcp_timeout = 5;

    r->expire = 30;

    r->valid = 0;


    r->log = &cf->cycle->new_log;

    r->log_level = NGX_LOG_ERR;


    if (n) {

        if (ngx_array_init(&r->connections, cf->pool, n,

                           sizeof(ngx_resolver_connection_t))

            != NGX_OK)

        {

            return NULL;

        }

    }


    for (i = 0; i < n; i++) {

        if (ngx_strncmp(names[i].data, "valid=", 6) == 0) {

            s.len = names[i].len - 6;

            s.data = names[i].data + 6;


            r->valid = ngx_parse_time(&s, 1);


            if (r->valid == (time_t) NGX_ERROR) {

                ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,

                                   "invalid parameter: %V", &names[i]);

                return NULL;

            }


            continue;

        }


#if (NGX_HAVE_INET6)

        if (ngx_strncmp(names[i].data, "ipv6=", 5) == 0) {


            if (ngx_strcmp(&names[i].data[5], "on") == 0) {

                r->ipv6 = 1;


            } else if (ngx_strcmp(&names[i].data[5], "off") == 0) {

                r->ipv6 = 0;


            } else {

                ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,

                                   "invalid parameter: %V", &names[i]);

                return NULL;

            }


            continue;

        }

#endif


        ngx_memzero(&u, sizeof(ngx_url_t));


        u.url = names[i];

        u.default_port = 53;


        if (ngx_parse_url(cf->pool, &u) != NGX_OK) {

            if (u.err) {

                ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,

                                   "%s in resolver \"%V\"",

                                   u.err, &u.url);

            }


            return NULL;

        }


        rec = ngx_array_push_n(&r->connections, u.naddrs);

        if (rec == NULL) {

            return NULL;

        }


        ngx_memzero(rec, u.naddrs * sizeof(ngx_resolver_connection_t));


        for (j = 0; j < u.naddrs; j++) {

            rec[j].sockaddr = u.addrs[j].sockaddr;

            rec[j].socklen = u.addrs[j].socklen;

            rec[j].server = u.addrs[j].name;

            rec[j].resolver = r;

        }

    }


    return r;

}

在resolver初始化完成之后,就可以调用了。在nginx中,upstream和proxy_pass中使用到了此方法的域名解析,所以下面结合这两个模块来看一下。

在proxy中,一般在配置文件中会配置proxy_pass变量,通过nginx的DNS对变量进行解析,上面的代码,是从ngx_http_proxy_handler中的对于proxy_pass变量部分的判断,可以看到,当没有变量的时候,是不进行域名解析的,只有当proxy_pass有变量的时候, 才会在ngx_http_proxy_eval中添加变量,进行域名解析,下面看下ngx_http_proxy_eval

ngx_http_proxy_eval(ngx_http_request_t *r, ngx_http_proxy_ctx_t *ctx,

    ngx_http_proxy_loc_conf_t *plcf)

{

    u_char               *p;

    size_t                add;

    u_short               port;

    ngx_str_t             proxy;

    ngx_url_t             url;

    ngx_http_upstream_t  *u;


    if (ngx_http_script_run(r, &proxy, plcf->proxy_lengths->elts, 0,

                            plcf->proxy_values->elts)

        == NULL)

    {

        return NGX_ERROR;

    }

    //判断http or https,添加端口

    if (proxy.len > 7

        && ngx_strncasecmp(proxy.data, (u_char *) "http://", 7) == 0)

    {

        add = 7;

        port = 80;


#if (NGX_HTTP_SSL)


    } else if (proxy.len > 8

               && ngx_strncasecmp(proxy.data, (u_char *) "https://", 8) == 0)

    {

        add = 8;

        port = 443;

        r->upstream->ssl = 1;


#endif


    } else {

        ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,

                      "invalid URL prefix in \"%V\"", &proxy);

        return NGX_ERROR;

    }


    u = r->upstream;


    u->schema.len = add;

    u->schema.data = proxy.data;


    ngx_memzero(&url, sizeof(ngx_url_t));

    //proxy要转向的url

    url.url.len = proxy.len - add;

    url.url.data = proxy.data + add;

    url.default_port = port;

    url.uri_part = 1;

    //不用域名解析

    url.no_resolve = 1;

    //上面配置不用域名解析,所以在ngx_parse_url中不会对域名进行解析

    if (ngx_parse_url(r->pool, &url) != NGX_OK) {

        if (url.err) {

            ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,

                          "%s in upstream \"%V\"", url.err, &url.url);

        }


        return NGX_ERROR;

    }


    if (url.uri.len) {

        if (url.uri.data[0] == '?') {

            p = ngx_pnalloc(r->pool, url.uri.len + 1);

            if (p == NULL) {

                return NGX_ERROR;

            }


            *p++ = '/';

            ngx_memcpy(p, url.uri.data, url.uri.len);


            url.uri.len++;

            url.uri.data = p - 1;

        }

    }


    ctx->vars.key_start = u->schema;


    ngx_http_proxy_set_vars(&url, &ctx->vars);

    //保存需要解析域名相关信息

    u->resolved = ngx_pcalloc(r->pool, sizeof(ngx_http_upstream_resolved_t));

    if (u->resolved == NULL) {

        return NGX_ERROR;

    }


    if (url.addrs) {

        //如果域名已经是ip地址的格式,直接保存,这样在upstream里面就不会再进行解析

        //在upstream模块中会对u->resolved->sockaddr进行判断

        u->resolved->sockaddr = url.addrs[0].sockaddr;

        u->resolved->socklen = url.addrs[0].socklen;

        u->resolved->name = url.addrs[0].name;

        u->resolved->naddrs = 1;

    }


    u->resolved->host = url.host;

    u->resolved->port = (in_port_t) (url.no_port ? port : url.port);

    u->resolved->no_port = url.no_port;


    return NGX_OK;

}

接下来在upstream中的ngx_http_upstream_init_request初始化请求时,当u->resolved不为空时,进行域名解析

然后开始查找域名,ngx_resolve_start初始化域名解析器,如果返回NGX_NO)RESOLVER无法进行域名解析

设置需要解析的域名,以及解析超时时间,handler解析完成后回调函数,然后ngx_resolve_name开始解析,超时没有解析完成,直接return

然后看一下ngx_resolve_star过程

ngx_resolver_ctx_t *

ngx_resolve_start(ngx_resolver_t *r, ngx_resolver_ctx_t *temp)

{

    in_addr_t            addr;

    ngx_resolver_ctx_t  *ctx;


    if (temp) {

        addr = ngx_inet_addr(temp->name.data, temp->name.len);

        //如果要解析的地址已经是ip地址,则设置temp->quick为1,在ngx_resolve_name调用时就不会再进行解析

        if (addr != INADDR_NONE) {

            temp->resolver = r;

            temp->state = NGX_OK;

            temp->naddrs = 1;

            temp->addrs = &temp->addr;

            temp->addr.sockaddr = (struct sockaddr *) &temp->sin;

            temp->addr.socklen = sizeof(struct sockaddr_in);

            ngx_memzero(&temp->sin, sizeof(struct sockaddr_in));

            temp->sin.sin_family = AF_INET;

            temp->sin.sin_addr.s_addr = addr;

            //不需要进行域名解析

            temp->quick = 1;


            return temp;

        }

    }

    //如果r->connections.nelts为0,则表示配置文件中没有配置dns服务器地址

    if (r->connections.nelts == 0) {

        return NGX_NO_RESOLVER;

    }


    ctx = ngx_resolver_calloc(r, sizeof(ngx_resolver_ctx_t));


    if (ctx) {

        ctx->resolver = r;

    }


    return ctx;

}

接着看下ngx_resolve_name解析的过程

ngx_int_t

ngx_resolve_name(ngx_resolver_ctx_t *ctx)

{

    size_t           slen;

    ngx_int_t        rc;

    ngx_str_t        name;

    ngx_resolver_t  *r;


    r = ctx->resolver;


    if (ctx->name.len > 0 && ctx->name.data[ctx->name.len - 1] == '.') {

        ctx->name.len--;

    }


    ngx_log_debug1(NGX_LOG_DEBUG_CORE, r->log, 0,

                   "resolve: \"%V\"", &ctx->name);

    //如果已经是IPi地址了,quick被设置为1,这里可以看到直接返回

    if (ctx->quick) {

        ctx->handler(ctx);

        return NGX_OK;

    }

    //开始域名查找

    if (ctx->service.len) {

        slen = ctx->service.len;


        if (ngx_strlchr(ctx->service.data,

                        ctx->service.data + ctx->service.len, '.')

            == NULL)

        {

            slen += sizeof("_._tcp") - 1;

        }


        name.len = slen + 1 + ctx->name.len;


        name.data = ngx_resolver_alloc(r, name.len);

        if (name.data == NULL) {

            goto failed;

        }


        if (slen == ctx->service.len) {

            ngx_sprintf(name.data, "%V.%V", &ctx->service, &ctx->name);


        } else {

            ngx_sprintf(name.data, "_%V._tcp.%V", &ctx->service, &ctx->name);

        }


        /* lock name mutex */

       

        rc = ngx_resolve_name_locked(r, ctx, &name);


        ngx_resolver_free(r, name.data);


    } else {

        /* lock name mutex */


        rc = ngx_resolve_name_locked(r, ctx, &ctx->name);

    }


    if (rc == NGX_OK) {

        return NGX_OK;

    }


    /* unlock name mutex */


    if (rc == NGX_AGAIN) {

        return NGX_OK;

    }


    /* NGX_ERROR */


    if (ctx->event) {

        ngx_resolver_free(r, ctx->event);

    }


failed:


    ngx_resolver_free(r, ctx);


    return NGX_ERROR;

}


在上面我们可以看到,调用ngx_resolve_name_locked来查找域名,接着看(这里代码很长,所以只截取部分重要的,感兴趣的可以去看一下源码)

可以看到,先在本地的DNS缓存中查找域名,如果本地缓存中能找到,则判断缓存时效,若没有超时,则更新DNS有效期,并移动到队列最前面

并设置解析状态和IP地址,执行回调函数

如果解析类型是CNAME,则回调查询IP地址,如果没有找到,则通过ngx_resolver_create_name_query创建新的DNS查询请求,并通过ngx_resolver_send_query发送请求,先看下创建请求

将DNS请求内容放入rn->query中,然后接着看ngx_resolver_send_query

static ngx_int_t

ngx_resolver_send_query(ngx_resolver_t *r, ngx_resolver_node_t *rn)

{

    ngx_int_t                   rc;

    ngx_resolver_connection_t  *rec;

    

    rec = r->connections.elts;

    rec = &rec[rn->last_connection];


    if (rec->log.handler == NULL) {

        rec->log = *r->log;

        rec->log.handler = ngx_resolver_log_error;

        rec->log.data = rec;

        rec->log.action = "resolving";

    }

    //判断采用TCP或UDP发送

    if (rn->naddrs == (u_short) -1) {

        rc = rn->tcp ? ngx_resolver_send_tcp_query(r, rec, rn->query, rn->qlen)

                     : ngx_resolver_send_udp_query(r, rec, rn->query, rn->qlen);


        if (rc != NGX_OK) {

            return rc;

        }

    }


#if (NGX_HAVE_INET6)


    if (rn->query6 && rn->naddrs6 == (u_short) -1) {

        rc = rn->tcp6

                    ? ngx_resolver_send_tcp_query(r, rec, rn->query6, rn->qlen)

                    : ngx_resolver_send_udp_query(r, rec, rn->query6, rn->qlen);


        if (rc != NGX_OK) {

            return rc;

        }

    }


#endif


    return NGX_OK;

}


采用UDP发送数据,并设置ngx_resolver_udp_read回调

采用TCP的方式和UDP的方式略有不同,当DNS服务器响应时,会调用ngx_resolver_udp_read或ngx_resolver_tcp_read函数,接收数据,并调用ngx_resolver_process_responese来处理响应

在ngx_resolver_process_response中根据响应的类别分别调用

ngx_resolver_process_a或ngx_resolver_process_srv或ngx_resolver_process_ptr,并将DNS缓存,且更新有效期,最后调用回调函数

在回调函数ngx_http_upstream_resolve_handler中

static void

ngx_http_upstream_resolve_handler(ngx_resolver_ctx_t *ctx)

{

    ngx_uint_t                     run_posted;

    ngx_connection_t              *c;

    ngx_http_request_t            *r;

    ngx_http_upstream_t           *u;

    ngx_http_upstream_resolved_t  *ur;


    run_posted = ctx->async;


    r = ctx->data;

    c = r->connection;


    u = r->upstream;

    ur = u->resolved;


    ngx_http_set_log_request(c->log, r);


    ngx_log_debug2(NGX_LOG_DEBUG_HTTP, c->log, 0,

                   "http upstream resolve: \"%V?%V\"", &r->uri, &r->args);


    if (ctx->state) {

        ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,

                      "%V could not be resolved (%i: %s)",

                      &ctx->name, ctx->state,

                      ngx_resolver_strerror(ctx->state));


        ngx_http_upstream_finalize_request(r, u, NGX_HTTP_BAD_GATEWAY);

        goto failed;

    }


    ur->naddrs = ctx->naddrs;

    ur->addrs = ctx->addrs;


#if (NGX_DEBUG)

    {

    u_char      text[NGX_SOCKADDR_STRLEN];

    ngx_str_t   addr;

    ngx_uint_t  i;


    addr.data = text;


    for (i = 0; i < ctx->naddrs; i++) {

        addr.len = ngx_sock_ntop(ur->addrs[i].sockaddr, ur->addrs[i].socklen,

                                 text, NGX_SOCKADDR_STRLEN, 0);


        ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,

                       "name was resolved to %V", &addr);

    }

    }

#endif


    if (ngx_http_upstream_create_round_robin_peer(r, ur) != NGX_OK) {

        ngx_http_upstream_finalize_request(r, u,

                                           NGX_HTTP_INTERNAL_SERVER_ERROR);

        goto failed;

    }

    //结束DNS解析

    ngx_resolve_name_done(ctx);

    ur->ctx = NULL;


    u->peer.start_time = ngx_current_msec;


    if (u->conf->next_upstream_tries

        && u->peer.tries > u->conf->next_upstream_tries)

    {

        u->peer.tries = u->conf->next_upstream_tries;

    }

    //连接upstream

    ngx_http_upstream_connect(r, u);


failed:


    if (run_posted) {

        ngx_http_run_posted_requests(c);

    }

}

所以在nginx配置resolver后,会通过DNS内部的域名解析方法来进行域名解析。resolver的语法如下:

Syntax: resolver address ... [valid=time] [ipv6=on|off];Default:    —Context:    http, server, location

在resolver后面可以配置多个DNS地址,nginx会采用轮询的方式去访问,并对解析结果缓存,这里的valid就是指定缓存的时间。

另外有一个参数是配合resolver使用的,就是resolver_timeout,语法如下:

Syntax: resolver_timeout time;

Default:    resolver_timeout 30s;

Context:    http, server, location

该参数是用于指定DNS解析的超时时间。

理解了resolver的原理之后,利用resolver实现upstream就显而易见了,通过resolver指定nginx的DNS解析,在upstream中设置域名,反向代理后端,那么我们只需要修改域名的解析,就可以实现动态upstream,而无需重启nginx修改upstream配置。


公众号ID:运维实谈最实用的运维知识


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存