在nginx中,nginx需要频繁进行域名解析的过程做了自己的优化,使用了自己的一套域名解析过程,并做了缓存处理。我们可以设置DNS解析服务器的地址,即通过resolver指令来设置DNS服务器的地址,由此来启动nginx的域名解析。
本文,我们来看看nginx是如何做的,这里我们只选出重要的代码进行分析,完整代码请参考nginx源代码,本文基于nginx-1.0.6版本进行的分析。
首先,来看看resolver的初始化。
在ngx_http_core_loc_conf_s的声明中,可以看到对reolver:
struct ngx_http_core_loc_conf_s { ngx_resolver_t *resolver; /* resolver */ }
resolver中保存了与域名解析相关的一些数据,它保存了DNS的本地缓存,通过红黑树的方式来组织数据,以达到快速查找。
typedef struct { ngx_event_t *event; // 用于连接dns服务器 ngx_udp_connection_t *udp_connection; // 保存了本地缓存的DNS数据 ngx_rbtree_t name_rbtree; ngx_rbtree_node_t name_sentinel; } ngx_resolver_t;
在nginx初始化的时候,通过ngx_resolver_create来初始化这一结构体,如果有设置resolver,则在ngx_http_core_resolver中有调用:
static char * ngx_http_core_resolver(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { ngx_http_core_loc_conf_t *clcf = conf; // 初始化,第二个参数是我们设置的域名解析服务器的IP地址 clcf->resolver = ngx_resolver_create(cf, &u.addrs[0]); if (clcf->resolver == NULL) { return NGX_OK; } return NGX_CONF_OK; }
来看看ngx_resolver_create做了些什么:
ngx_resolver_t * ngx_resolver_create(ngx_conf_t *cf, ngx_addr_t *addr) { ngx_resolver_t *r; ngx_udp_connection_t *uc; r = ngx_calloc(sizeof(ngx_resolver_t), cf->log); if (r == NULL) { return NULL; } // 省略了其它数据的初始化过程 r->event = ngx_calloc(sizeof(ngx_event_t), cf->log); if (r->event == NULL) { return NULL; } // 设置事件的句柄 r->event->handler = ngx_resolver_resend_handler; r->event->data = r; // 设置dns服务器的地址 if (addr) { uc = ngx_calloc(sizeof(ngx_udp_connection_t), cf->log); if (uc == NULL) { return NULL; } r->udp_connection = uc; uc->sockaddr = addr->sockaddr; uc->socklen = addr->socklen; uc->server = addr->name; } return r; }
初始化好了之后,就可以调用了。在nginx中,upstream中使用到了此方法的域名解析。我们结合proxy模块与upstream模块来实例讲解吧,注意在proxy中,只有当proxy_pass中包含有变量时,才会用到nginx自己的DNS解析。而且这里有一个需要特别注意的,如果proxy_pass中包含变量,那么nginx中就需要配置resolver来指定DNS服务器地址了,否则,将直接返回502错误。从下面的代码中我们可以看到。
首先,在ngx_http_proxy_handler函数中,有如下代码:
static ngx_int_t ngx_http_proxy_handler(ngx_http_request_t *r) { // 这里的意思是,如果没有变量,就不进行变量解析 if (plcf->proxy_lengths == NULL) { ctx->vars = plcf->vars; u->schema = plcf->vars.schema; } else { // 只有当proxy_pass里面包含变量时,才解析变量,在ngx_http_proxy_eval中会添加域名解析的需求,请看ngx_http_proxy_eval的实现 if (ngx_http_proxy_eval(r, ctx, plcf) != NGX_OK) { return NGX_HTTP_INTERNAL_SERVER_ERROR; } } }
而在proxy模块的ngx_http_proxy_eval函数中,可以看到如下代码:
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; ngx_url_t url; // proxy为要转向的url 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) { return NGX_ERROR; } // 保存与需要解析域名相关的信息 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) { // 如果域名已经是ip地址的格式,就保存起来,这样在upstream里面就不会再进行解析 // 在upsteam模块里面会判断u->resolved->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 { u->resolved->host = url.host; u->resolved->port = (in_port_t) (url.no_port ? port : url.port); u->resolved->no_port = url.no_port; } }
所以,可以看出,只在当proxy_pass到包含变量的url时,才有可能进行域名的解析。因为如果是固定的url,则完全可以在初始化的时候解析域名,而不用在请求的时候进行了。关于这部分代码的实现,可以参考ngx_http_upstream_init_round_robin函数,而且注意,在proxy_pass时,是直接添加upstream来实现的,等有机会介绍upstream代码时再做解释。
接下来在upstream中ngx_http_upstream_init_request在初始化请求时,当u->resolved为不空时,需要解析域名。看代码:
static void ngx_http_upstream_init_request(ngx_http_request_t *r) { ngx_str_t *host; ngx_http_upstream_t *u; u = r->upstream; // 如果已经是ip地址格式了,就不需要再进行解析 if (u->resolved->sockaddr) { 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; } // 接下来就要开始查找域名 host = &u->resolved->host; temp.name = *host; // 初始化域名解析器 ctx = ngx_resolve_start(clcf->resolver, &temp); if (ctx == NULL) { ngx_http_upstream_finalize_request(r, u, NGX_HTTP_INTERNAL_SERVER_ERROR); return; } // 返回NGX_NO_RESOLVER表示无法进行域名解析 if (ctx == NGX_NO_RESOLVER) { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "no resolver defined to resolve %V", host); ngx_http_upstream_finalize_request(r, u, NGX_HTTP_BAD_GATEWAY); return; } // 设置需要解析的域名的类型与信息 ctx->name = *host; ctx->type = NGX_RESOLVE_A; // 解析完成后的回调函数 ctx->handler = ngx_http_upstream_resolve_handler; ctx->data = r; u->resolved->ctx = ctx; // 开始解析域名 if (ngx_resolve_name(ctx) != NGX_OK) { u->resolved->ctx = NULL; ngx_http_upstream_finalize_request(r, u, NGX_HTTP_INTERNAL_SERVER_ERROR); return; } // 域名还没有解析完成,则直接返回 return; // 其它动作 }
在上面的代码中,我们可以看到,需要解析域名,我们调用ngx_resolve_start,设置好回调函数等上下文信息后,然后再调用ngx_resolve_name,等域名解析完成后会调用ngx_http_upstream_resolve_handler。
那ngx_resolve_start函数的主要工作是初始化当前解析请求的上下文:
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 = addr; // 不需要再进行域名解析了 temp->quick = 1; return temp; } } // r->udp_connection如果不为空,则表示在配置文件中有配置dns服务器地址 // 即当ngx_resolver_create在调用时的第二个参数不为空 // 看到这里,我们应该就能想到,如果在proxy_pass里面包含变量,而且没有配置resolver,那将会返回错误了。 if (r->udp_connection == NULL) { 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) { ngx_int_t rc; ngx_resolver_t *r; r = ctx->resolver; // 结合前面的我们可以看到,如果已经是ip地址了quick会被设置成1,所以就直接返回了 if (ctx->quick) { ctx->handler(ctx); return NGX_OK; } // 开始查找域名 rc = ngx_resolve_name_locked(r, ctx); if (rc == NGX_OK) { return NGX_OK; } if (rc == NGX_AGAIN) { return NGX_OK; } /* NGX_ERROR */ if (ctx->event) { ngx_resolver_free(r, ctx->event); } ngx_resolver_free(r, ctx); return NGX_ERROR; }
在ngx_resolve_name中,我们看到会调用ngx_resolve_name_locked来查找域名,这里包含有本地查找与DNS服务器查找了,请看代码(代码很长,有省略):
static ngx_int_t ngx_resolve_name_locked(ngx_resolver_t *r, ngx_resolver_ctx_t *ctx) { uint32_t hash; in_addr_t addr, *addrs; ngx_int_t rc; ngx_uint_t naddrs; ngx_resolver_ctx_t *next; ngx_resolver_node_t *rn; // 先在本地保存的DNS缓存中查找域名 rn = ngx_resolver_lookup_name(r, &ctx->name, hash); if (rn) { // 如果本地缓存的DNS中能找到域名,则判断该值的时效性 // 当前dns还没有超时 if (rn->valid >= ngx_time()) { // 更新dns的有效期,并移到队列最前面 ngx_queue_remove(&rn->queue); rn->expire = ngx_time() + r->expire; ngx_queue_insert_head(&r->name_expire_queue, &rn->queue); naddrs = rn->naddrs; if (naddrs) { /* NGX_RESOLVE_A answer */ if (naddrs != 1) { addr = 0; addrs = ngx_resolver_dup(r, rn->u.addrs, naddrs * sizeof(in_addr_t)); if (addrs == NULL) { return NGX_ERROR; } } else { addr = rn->u.addr; addrs = NULL; } ctx->next = rn->waiting; rn->waiting = NULL; do { // 设置状态与ip地址 ctx->state = NGX_OK; ctx->naddrs = naddrs; ctx->addrs = (naddrs == 1) ? &ctx->addr : addrs; ctx->addr = addr; next = ctx->next; // 执行回调函数 ctx->handler(ctx); ctx = next; } while (ctx); if (addrs) { ngx_resolver_free(r, addrs); } return NGX_OK; } // 如果是CNAME,则回调查询ip地址 if (ctx->recursion++ < NGX_RESOLVER_MAX_RECURSION) { ctx->name.len = rn->cnlen; ctx->name.data = rn->u.cname; return ngx_resolve_name_locked(r, ctx); } // 执行到这里,说明DNS解析失败了 ctx->next = rn->waiting; rn->waiting = NULL; do { ctx->state = NGX_RESOLVE_NXDOMAIN; next = ctx->next; ctx->handler(ctx); ctx = next; } while (ctx); return NGX_OK; } // 其它事情 } else { // 如果没有找到,则为新的DNS缓存结点做准备,代码略去 } // 创建一个新的DNS查询请求 rc = ngx_resolver_create_name_query(rn, ctx); // 发送请求 if (ngx_resolver_send_query(r, rn) != NGX_OK) { goto failed; } if (ctx->event == NULL) { // 添加查询超时事件 ctx->event = ngx_resolver_calloc(r, sizeof(ngx_event_t)); if (ctx->event == NULL) { goto failed; } ctx->event->handler = ngx_resolver_timeout_handler; ctx->event->data = ctx; ctx->event->log = r->log; ctx->ident = -1; ngx_add_timer(ctx->event, ctx->timeout); } return NGX_AGAIN; }
从上面的代码中,我们可以看到,nginx先查询本地缓存,然后再从DNS服务器中找到。
首先,nginx调用ngx_resolver_create_name_query来创建请求,然后再通过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) { ssize_t n; ngx_udp_connection_t *uc; uc = r->udp_connection; if (uc->connection == NULL) { // 执行udp连接 if (ngx_udp_connect(uc) != NGX_OK) { return NGX_ERROR; } uc->connection->data = r; // 设置读回调函数 uc->connection->read->handler = ngx_resolver_read_response; uc->connection->read->resolver = 1; } // 发送udp数据 n = ngx_send(uc->connection, rn->query, rn->qlen); if (n == -1) { return NGX_ERROR; } if ((size_t) n != (size_t) rn->qlen) { ngx_log_error(NGX_LOG_CRIT, &uc->log, 0, "send() incomplete"); return NGX_ERROR; } return NGX_OK; }
当DNS服务器响应时,会调用ngx_resolver_read_response函数,ngx_resolver_read_response会接收数据然后调用ngx_resolver_process_response来处理响应。
ngx_resolver_process_response则会根据响应类别调用ngx_resolver_process_a或ngx_resolver_process_ptr,在ngx_resolver_process_a与ngx_resolver_process_ptr中则会将DNS缓存并更新有效期,调用最后的回调函数。这部分代码没什么难度,就不做分析了。
在回调函数ngx_http_upstream_resolve_handler中:
static void ngx_http_upstream_resolve_handler(ngx_resolver_ctx_t *ctx) { // 结束DNS的解析 ngx_resolve_name_done(ctx); ngx_http_upstream_connect(r, u); }
总结一下, 要使用Nginx的DNS缓存,首先需要配置resolver来配置DNS服务器地址,则会调用ngx_resolver_create来初始化DNS解析器。然后ngx_resolve_name解析,并设置回调函数,在回调函数中调用ngx_resolve_name_done来结束DNS的查询。
OK,DNS解析的过程就介绍到这里了。Have fun!!:)