nginx dns域名解析源码分析

本文主要内容

域名查询的函数接口介绍

域名解析流程分析

查询场景分析及实现介绍

域名查询的函数

查询过程

为了不阻塞当前线程,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~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值