写点什么

Nginx 利用 resolver 实现动态 upstream

发布于: 2021 年 05 月 30 日

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

发布于: 2021 年 05 月 30 日阅读数: 56
用户头像

运维技术社区,坚持运维技术研究与分享 2020.07.14 加入

「运维研习社」技术社区发起人,同名公众号「运维研习社」、知识星球,专注运维技术研究分享,坚持原创,希望能和大家在运维路上结伴而行!

评论

发布
暂无评论
Nginx利用resolver实现动态upstream