本文主要内容
域名查询的函数接口介绍
域名解析流程分析
查询场景分析及实现介绍
域名查询的函数
查询过程
为了不阻塞当前线程,nginx采用了异步的方式进行域名查询。
整个查询过程主要分为三个步骤:
1、准备函数调用需要的信息,并设置回调方法
2、调用函数
3、处理结束后回调方法被调用
使用同步io的情况时,调用gethostbyname()或gethostbyname_r(),根据域名查询IP。
由此可能会进行公网递归查询耗时较长。
为了尽量减少查询花费的时间,nginx还对查询结果做了本地缓存。
为了初始化DNS Server地址和本地缓存等信息,需要在真正查询前需要先进行一些全局的初始化操作。
详细分析
初始化域名查询所需要的全局信息
需要初始化的全局信息包括:
DNS服务器的地址如指定了多个,nginx会采用Round Robin轮流查询每个服务器。
查询结果缓存采用rb?tree数据结构,使用要查询名字的Hash作为Key, 节点信息存放在 struct?ngx_resolver_node_t中。
resolver为全局与任何一个connection无关,需要放在一个随时都可以取到的地方。
如 ngx_mail_core_srv_conf_t结构体上,在使用时从当前session找到ngx_mail_core_srv_conf_t然后找到resolver。
DNS 服务器的信息需要在配置
resolver?8.8.8.8
#nginx 默认会根据DNS请求结果里的TTL值来进行缓存,
#当然也可以通过一个可选的参数valid来设置过期时间,如:
#resolver 127.0.0.1 [::1]:5353 valid=30s;
根据配置中的resolver参数,初始化全局的ngx_resolver_t其保存了前面提及的DNS服务器地址和查询结果等信息:
static char *
ngx_mail_core_resolver(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
????ngx_mail_core_srv_conf_t? *cscf = conf;
????ngx_str_t? *value;
????value = cf->args->elts;
????cscf->resolver = ngx_resolver_create(cf, &value[1],
?????????????????????????????????????????cf->args->nelts - 1);
????return NGX_CONF_OK;
}
准备本次查询的信息
和本次查询相关的信息放在ngx_resolver_ctx_t结构体中,包括要查询的名称,查询完的回调方法,以及超时时间等。如果本次要查询的地址已经是IPv4用点分隔的地址了,比如74.125.128.100, nginx会在ngx_resolve_start中进行判断,并设置好标志位,在调用ngx_resolve_name时不会发送真正的DNS查询请求。
static void
ngx_mail_smtp_resolve_name(ngx_event_t *rev)
{
????ngx_connection_t????????? *c;
????ngx_mail_session_t??????? *s;
????ngx_resolver_ctx_t??????? *ctx;
????ngx_mail_core_srv_conf_t? *cscf;
????c = rev->data;
????s = c->data;
????cscf = ngx_mail_get_module_srv_conf(s, ngx_mail_core_module);
????ctx = ngx_resolve_start(cscf->resolver, NULL);
????if (ctx == NULL) {
????????ngx_mail_close_connection(c);
????????return;
????}
????ctx->name = s->host;
????ctx->type = NGX_RESOLVE_A;
????ctx->handler = ngx_mail_smtp_resolve_name_handler;
????ctx->data = s;
????ctx->timeout = cscf->resolver_timeout;
????//根据名字进行IP地址查询
????if (ngx_resolve_name(ctx) != NGX_OK) {
????????ngx_mail_close_connection(c);
????}
}
根据名字进行IP地址查询
前面方法的最后通过ngx_resolve_name方法进行IP地址查询。
查询时,Nginx会先检查本地缓存,如果在缓存中,就更新缓存过期时间,并回调设置的handler, 如前面设置的:ngx_mail_smtp_resolve_name_handler,然后整个查询过程结束。
如果没有在缓存中就发送查询请求给dns server,同时方法返回。
查询完成后回调在ngx_resolver_ctx_t中指定的方法
真正的DNS查询完成后,不管成功,失败或是超时,nginx会回调相应查询的handler, 如前面设置的:ngx_mail_smtp_resolve_name_handler。在handler中都需要调用ngx_resolve_addr_done来标识查询结束。
static void
ngx_mail_smtp_resolve_name_handler(ngx_resolver_ctx_t *ctx)
{
????in_addr_t??????????? addr;
????ngx_uint_t?????????? i;
????ngx_connection_t??? *c;
????struct sockaddr_in? *sin;
????ngx_mail_session_t? *s;
????s = ctx->data;
????c = s->connection;
????if (ctx->state) {
????????ngx_log_error(NGX_LOG_ERR, c->log, 0,
??????????????????????""%V" could not be resolved (%i: %s)",
??????????????????????&ctx->name, ctx->state,
??????????????????????ngx_resolver_strerror(ctx->state));
????} else {
????????/* AF_INET only */
????????sin = (struct sockaddr_in *) c->sockaddr;
????????for (i = 0; i < ctx->naddrs; i++) {
????????????addr = ctx->addrs[i];
????????????ngx_log_debug4(NGX_LOG_DEBUG_MAIL, c->log, 0,
???????????????????????????"name was resolved to %ud.%ud.%ud.%ud",
???????????????????????????(ntohl(addr) >> 24) & 0xff,
???????????????????????????(ntohl(addr) >> 16) & 0xff,
???????????????????????????(ntohl(addr) >> 8) & 0xff,
???????????????????????????ntohl(addr) & 0xff);
????????????if (addr == sin->sin_addr.s_addr) {
????????????????goto found;
????????????}
????????}
????????s->host = smtp_unavailable;
????}
found:
????//不管成功失败都要执行
????ngx_resolve_name_done(ctx);
}
域名解析流程分析
nginx进行域名查询的流程图如下,调用过程分为三种:
首先判断是不是IPv4地址,如果是就直接调用handler
再次检查是不是在缓存中,如果有,就调用handler
最后发送远程DNS请求,收到回复后调用handler
查询场景分析及实现介绍
查询的地址是IP v4地址如166.177.188.100,nginx会在ngx_resolve_start中通过ngx_inet_addr方法进行判断,如果是IPv4的地址,就设置好标志位?ngx_resolver_ctx_t->quick,在接下来的ngx_resolve_name中会对这个标志位进行判断,如果为1,就直接调用ngx_resolver_ctx_t->handler
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);
????????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;
????????}
????}
????...
}
超时没有得到查询结果
调用ngx_resolve_name时设置的回调方法被调用,同时ngx_resolver_ctx_t->state被设置为NGX_RESOLVE_TIMEDOUT。相应的代码为:
static void
ngx_resolver_timeout_handler(ngx_event_t *ev)
{
????ngx_resolver_ctx_t? *ctx;
????ctx = ev->data;
????ctx->state = NGX_RESOLVE_TIMEDOUT;
????ctx->handler(ctx);
}
正常查询一个不在缓存中的域名
如果要查询的域名不在缓存中,首先把域名按hash值放在缓存中,然后准备查询需要的数据,发送DNS查询的UDP请求给DNS服务器,
static ngx_int_t
ngx_resolve_name_locked(ngx_resolver_t *r, ngx_resolver_ctx_t *ctx)
{
????ngx_resolver_node_t? *rn;
????rn = ngx_resolver_alloc(r, sizeof(ngx_resolver_node_t));
????ngx_rbtree_insert(&r->name_rbtree, &rn->node);
????ngx_resolver_create_name_query(rn, ctx);
????ngx_resolver_send_query(r, rn);
????rn->cnlen = 0;
????rn->naddrs = 0;
????rn->valid = 0;
????rn->waiting = ctx;
????ctx->state = NGX_AGAIN;
}
收到DNS查询结果后的回调方法
static void
ngx_resolver_read_response(ngx_event_t *rev)
{
????ssize_t??????????? n;
????ngx_connection_t? *c;
????u_char???????????? buf[NGX_RESOLVER_UDP_SIZE];
????c = rev->data;
????do {
????????n = ngx_udp_recv(c, buf, NGX_RESOLVER_UDP_SIZE);
????????if (n < 0) {
????????????return;
????????}
????????ngx_resolver_process_response(c->data, buf, n);
????} while (rev->ready);
}
static void
ngx_resolver_process_a(ngx_resolver_t *r, u_char *buf, size_t last,
????ngx_uint_t ident, ngx_uint_t code, ngx_uint_t nan, ngx_uint_t ans)
{
????hash = ngx_crc32_short(name.data, name.len);
????rn = ngx_resolver_lookup_name(r, &name, hash);
????//copy addresses to cached node
????rn->u.addrs = addrs;
????//回调所有等待本域名解析的请求
????next = rn->waiting;
????rn->waiting = NULL;
????while (next) {
?????????ctx = next;
?????????ctx->state = NGX_OK;
?????????ctx->naddrs = naddrs;
?????????ctx->addrs = (naddrs == 1) ? &ctx->addr : addrs;
?????????ctx->addr = addr;
?????????next = ctx->next;
?????????ctx->handler(ctx);
????}
}
对同一域名查询多次查询
如果多次查询时,之前的查询结果还在缓存中并且没有失效,就直接从缓存中取到查询结果,并调用设置的回调方法。
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_uint_t??????????? naddrs;
????ngx_resolver_ctx_t?? *next;
????ngx_resolver_node_t? *rn;
????hash = ngx_crc32_short(ctx->name.data, ctx->name.len);
????rn = ngx_resolver_lookup_name(r, &ctx->name, hash);
????if (rn) {
????????if (rn->valid >= ngx_time()) {
????????????naddrs = rn->naddrs;
????????????if (naddrs) {
????????????????ctx->next = rn->waiting;
????????????????rn->waiting = NULL;
????????????????do {
????????????????????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);
????????????????return NGX_OK;
????????????}
????????}
????}
}
得到查询结果时同时超时了
如果在得到查询结果的同时,设置的超时时间也到期了,那该怎么办呢? Nginx会先处理各种网络读写事件,再处理超时事件,在处理网络事件时,会相应地把设置的定时器删除,所以在执行超时事件时就不会再执行了。
void
ngx_process_events_and_timers(ngx_cycle_t *cycle)
{
????ngx_uint_t? flags;
????ngx_msec_t? timer, delta;
????//处理各种网络事件
????(void) ngx_process_events(cycle, timer, flags);
????//处理各种timer事件,其中包含了查询超时
????ngx_event_expire_timers();
}
得到查询结果时客户端已经关闭连接
如果不做任何处理,那么在收到dns查询结果后,会回调查询时设置的回调方法,但因为连接已经被关闭,相应的内存已经被释放,所以会有非法内存访问的问题。怎么避免呢?在处理连接关闭事件时,同时需要调用ngx_resolve_name_done(ctx)方法,调用时需要把state设为NGX_AGAIN或者NGX_RESOLVE_TIMEDOUT,这样就会删除查询所设置的回调信息:
void ngx_close_xxx_session(ngx_xxx_session_t *s)
{
????if(s->resolver_ctx != NULL) {
????????s->resolver_ctx->state = NGX_RESOLVE_TIMEDOUT;
????????ngx_resolve_name_done(s->resolver_ctx);
????????s->resolver_ctx = NULL;
????}
}
void ngx_resolve_name_done(ngx_resolver_ctx_t *ctx)
{
????uint32_t????????????? hash;
????ngx_resolver_t?????? *r;
????ngx_resolver_ctx_t?? *w, **p;
????ngx_resolver_node_t? *rn;
????r = ctx->resolver;
????if (ctx->state == NGX_AGAIN || ctx->state == NGX_RESOLVE_TIMEDOUT) {
????????hash = ngx_crc32_short(ctx->name.data, ctx->name.len);
????????rn = ngx_resolver_lookup_name(r, &ctx->name, hash);
????????if (rn) {
????????????p = &rn->waiting;
????????????w = rn->waiting;
????????????while (w) {
????????????????if (w == ctx) {
????????????????????*p = w->next;
????????????????????goto done;
????????????????}
????????????????p = &w->next;
????????????????w = w->next;
????????????}
????????}
????}
done:
????ngx_resolver_free_locked(r, ctx);
}
本地缓存的地址没有再次被查询
每次在查询结束的时候(调用ngx_resolve_addr_done),都会检查有没有缓存过期,如果有,就会进行释放。
static void
ngx_resolver_expire(ngx_resolver_t *r, ngx_rbtree_t *tree,
????????????????????ngx_queue_t *queue)
{
????time_t??????????????? now;
????ngx_uint_t??????????? i;
????ngx_queue_t????????? *q;
????ngx_resolver_node_t? *rn;
????now = ngx_time();
????for (i = 0; i < 2; i++) {
????????if (ngx_queue_empty(queue)) {
????????????return;
????????}
????????q = ngx_queue_last(queue);
????????rn = ngx_queue_data(q, ngx_resolver_node_t, queue);
????????if (now <= rn->expire) {
????????????return;
????????}
????????ngx_log_debug2(NGX_LOG_DEBUG_CORE, r->log, 0,
????????????"resolver expire "%*s"", (size_t) rn->nlen, rn->name);
????????ngx_queue_remove(q);
????????ngx_rbtree_delete(tree, &rn->node);
????????ngx_resolver_free_node(r, rn);
????}
}
域名对应这多个IP地址
如果对应的有多个ip,那么在每次查询时,会随机的重新排列顺序,然后返回。
对于调用者来说,只要去第一个地址,就可以达到取随机地址的目的了。
static ngx_int_t
ngx_resolve_name_locked(ngx_resolver_t *r, ngx_resolver_ctx_t *ctx)
{
????if (naddrs) {
????????if (naddrs != 1) {
????????????addr = 0;
????????????addrs = ngx_resolver_rotate(r, rn->u.addrs, naddrs);
????????????if (addrs == NULL) {
????????????????return NGX_ERROR;
????????????}
????????} else {
????????????addr = rn->u.addr;
????????????addrs = NULL;
????????}
????}
}
static in_addr_t *
ngx_resolver_rotate(ngx_resolver_t *r, in_addr_t *src, ngx_uint_t n)
{
????void??????? *dst, *p;
????ngx_uint_t?? j;
????dst = ngx_resolver_alloc(r, n * sizeof(in_addr_t));
????j = ngx_random() % n;
????if (j == 0) {
????????ngx_memcpy(dst, src, n * sizeof(in_addr_t));
????????return dst;
????}
????p = ngx_cpymem(dst, &src[j], (n - j) * sizeof(in_addr_t));
????ngx_memcpy(p, src, j * sizeof(in_addr_t));
????return dst;
}
指定了多个dns server地址会怎么查询
如果在配置文件里指定了多个dns server如:
resolver?8.8.8.8 8.8.4.4
会采用Round Robin 的方式轮流查询各个dns server。
在方法ngx_resolver_send_query中通过在每次调用时改变last_connection来轮流使用不同的dns server进行查询
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_connections.elts;
????uc = &uc[r->last_connection++];
????if (r->last_connection == r->udp_connections.nelts) {
????????r->last_connection = 0;
????}
????...
}
结束
2021 09 10 19:28?晴?19°~28°C? 南风<3级
Good night~
|