nginx关于域名解析的源码分析

在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!!:)

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值