nginx 的 ngx_http_upstream_dynamic_module 动态域名解析功能的使用和源码详解

tengine ngx_http_upstream_dynamic_module 动态域名解析功能的代码详细解析

  • [1. 为什么需要域名动态解析](#1. 为什么需要域名动态解析)
  • [2. 配置指令](#2. 配置指令)
  • [3. 加载模块](#3. 加载模块)
  • [3. 源码分析](#3. 源码分析)
    • [3.1 指令解析](#3.1 指令解析)
    • [3.2 upstream负载均衡算法的初始化](#3.2 upstream负载均衡算法的初始化)
    • [3.3 upstream负载均衡上下文的初始化](#3.3 upstream负载均衡上下文的初始化)
    • [3.4 获取upstream的服务器地址](#3.4 获取upstream的服务器地址)
    • [3.5 域名解析回调处理](#3.5 域名解析回调处理)
  • [4. 总结](#4. 总结)

1. 为什么需要域名动态解析

众所周知,nginx可以配置成代理后端web服务器的模式运行,如下配置:

	 upstream{
		  server server1.com;
		  server server2.com;
	 }

但是有一个问题,就是这里用到的server1.com 和server2.com的域名是在nginx启动的时候通过域名解析的方式解析成IP并将其存储起来的,如果在nginx运行的过程中server1.com或者server2.com域名的解析记录变化了,nginx是感知不到的,这就导致了无法通过域名解析记录的切换来实现upstream中的real server的切换。不过tengine提供了ngx_http_upstream_dynamic_module来满足这个需求。
在tengine编译的时候添加了ngx_http_upstream_dynamic_module模块之后,可以通过dynamic_resolve指令来开启动态域名解析,如下:

	upstream{
	    dynamic_resolve fall_back_stable faile_timeout=30s;
	    server server1.com;
	    server server2.com;
	 }

2. 配置指令

该模块只有一条配置指令:

dynamic_resolve [fail_timeout=seconds] [fallback=next|stale|shutdown]

参数说明:

  • fail_timeout : 指定了当某次DNS请求失败后,后续多长的时间内DNS服务依然不可用,以减少对无效DNS的查询。
  • fallback : 如果域名解析失败的情况下采用哪种策略进行处理,包括:
    • next : 认为当前的server故障,继续选择下一个server。
    • stale : 返回旧的解析记录。
    • shutdown : 结束当前的请求,返回502。

3. 加载模块

在configure的时候需要添加ngx_http_upstream_dynamic_module来将其编译进来,

命令如下:

./configure --add-module=modules/ngx_http_upstream_dynamic_module

或者可以将ngx_http_upstream_dynamic_module编译成so库进行加载,

命令如下:

./configure --add-dynamic-module=modules/ngx_http_upstream_dynamic_module

nginx的相关配置如下:

# 如果编译成动态库模式则在nginx的配置文件头部增加这条指令
load_module "objs/ngx_http_upstream_dynamic_module.so";

upstream backup {
	  ip_hash;
	  dynamic_resolve fall_back_stable faile_timeout=30s;
	
	  server www.baidu.com:80;
	  server 2.2.2.2:80;
}   

需要注意的是,iphash 和 dynamic_resolve 这两行代码顺序不能交换,因为在初始化调用ngx_http_upstream_init_dynamic的时候,ngx_http_upstream_dynamic_module需要ngx_http_upstream_module已经设置好相应的负载均衡模块,否则nginx启动的时候会出现以下警告信息:

nginx: [warn] load balancing method redefined in /opt/nginx/conf/nginx.conf:44

3. 源码分析

3.1 指令解析

c 复制代码
static ngx_command_t  ngx_http_upstream_dynamic_commands[] = {

    { ngx_string("dynamic_resolve"),
      NGX_HTTP_UPS_CONF|NGX_CONF_TAKE12|NGX_CONF_NOARGS,
      ngx_http_upstream_dynamic,
      0,
      0,
      NULL },

      ngx_null_command
};
从以上代码知道,dynamic_resolve指令只能在upstream块里面进行配置,一旦nginx发现dynamic_resolve指令,就调用ngx_http_upstream_dynamic函数进行配置解析。ngx_http_upstream_dynamic本身还是非常好理解的,具体可以看代码,ngx_http_upstream_dynamic函数中需要特别说明一下的是:
c 复制代码
  dcf->original_init_upstream = uscf->peer.init_upstream
                                  ? uscf->peer.init_upstream
                                  : ngx_http_upstream_init_round_robin;

  uscf->peer.init_upstream = ngx_http_upstream_init_dynamic;
这段代码的意思是保存ngx_http_upstream_module设置的init_upstream函数指针,并用ngx_http_upstream_dynamic_module模块的ngx_http_upstream_init_dynamic函数来代替。
这个就是我们常用的系统钩子函数的方法。这样子,当nginx需要初始化upstream负载均衡算法的时候,就会转而调用ngx_http_upstream_init_dynamic进行初始化。

3.2 upstream负载均衡算法的初始化

下面来分析ngx_http_upstream_init_dynamic函数逻辑,这个函数会在nginx初始化的时候被回调,用于初始化upstream负载均衡上下文:
c 复制代码
static ngx_int_t
ngx_http_upstream_init_dynamic(ngx_conf_t *cf,
    ngx_http_upstream_srv_conf_t *us)
{
    ngx_uint_t                             i;
    ngx_http_upstream_dynamic_srv_conf_t  *dcf;
    ngx_http_upstream_server_t            *server;
    ngx_str_t                              host;

    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, cf->log, 0,
                   "init dynamic resolve");

    dcf = ngx_http_conf_upstream_srv_conf(us,
                                      ngx_http_upstream_dynamic_module);

    /*
     * Keep one static address for each server to resolve name only one
     * time. And server[].addrs should not be used in this case.
     */
	/* 对于每个upstream中用域名配置的server,强制将其IP地址数量设置为1 */
    if (us->servers) {
        server = us->servers->elts;

        for (i = 0; i < us->servers->nelts; i++) {
            host = server[i].host;
            if (ngx_inet_addr(host.data, host.len) == INADDR_NONE) {
                if (server[i].naddrs > 1) {
                    server[i].naddrs = 1;
                }
            }
        }
    }
	/* 调用原始的init_upstream函数进行初始化 */
    if (dcf->original_init_upstream(cf, us) != NGX_OK) {
        return NGX_ERROR;
    }
	/* 如果upstream中配置的server都不是域名形式给出的,那么禁用本模块
	   即设置dcf->enabled = 0
	*/
    if (us->servers) {
        server = us->servers->elts;

        for (i = 0; i < us->servers->nelts; i++) {
            host = server[i].host;
            if (ngx_inet_addr(host.data, host.len) == INADDR_NONE) {
                break;
            }
        }

        if (i == us->servers->nelts) {
            dcf->enabled = 0;

            return NGX_OK;
        }
    }
    
    /* 再次拦截peer.init回调函数, 用于在请求进入的时候,在进行负载均衡的前进行回调 */
    dcf->original_init_peer = us->peer.init;

    us->peer.init = ngx_http_upstream_init_dynamic_peer;

    dcf->enabled = 1;

    return NGX_OK;
}

3.3 upstream负载均衡上下文的初始化

这个初始化过程是在请求过来的时候进行的,在以上ngx_http_upstream_init_dynamic函数里面设置了拦截函数ngx_http_upstream_init_dynamic_peer,所以程序会运行到ngx_http_upstream_init_dynamic_peer函数里面来。
c 复制代码
static ngx_int_t
ngx_http_upstream_init_dynamic_peer(ngx_http_request_t *r,
    ngx_http_upstream_srv_conf_t *us)
{
    ngx_http_upstream_dynamic_peer_data_t  *dp;
    ngx_http_upstream_dynamic_srv_conf_t   *dcf;

    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
                   "init dynamic peer");

    dcf = ngx_http_conf_upstream_srv_conf(us,
                                          ngx_http_upstream_dynamic_module);

    dp = ngx_palloc(r->pool, sizeof(ngx_http_upstream_dynamic_peer_data_t));
    if (dp == NULL) {
        return NGX_ERROR;
    }
	/* 调用原始的init_peer函数进行负载均衡上下文的初始化 */
    if (dcf->original_init_peer(r, us) != NGX_OK) {
        return NGX_ERROR;
    }
    
	/* 拦截peer.get和peer.free函数
	如果开启了ssl,则同时需要拦截peer.set_session和peer.save_session
	*/
    dp->conf = dcf;
    dp->upstream = r->upstream;
    dp->data = r->upstream->peer.data;
    dp->original_get_peer = r->upstream->peer.get;
    dp->original_free_peer = r->upstream->peer.free;
    dp->request = r;

    r->upstream->peer.data = dp;
    r->upstream->peer.get = ngx_http_upstream_get_dynamic_peer;
    r->upstream->peer.free = ngx_http_upstream_free_dynamic_peer;

#if (NGX_HTTP_SSL)
    dp->original_set_session = r->upstream->peer.set_session;
    dp->original_save_session = r->upstream->peer.save_session;
    r->upstream->peer.set_session = ngx_http_upstream_dynamic_set_session;
    r->upstream->peer.save_session = ngx_http_upstream_dynamic_save_session;
#endif

    return NGX_OK;
}

3.4 获取upstream的服务器地址

peer.get 被拦截后,nginx在调用ngx_event_connect_peer发起向上游服务器进行连接的时候,会执行以下代码:
c 复制代码
    rc = pc->get(pc, pc->data);
    if (rc != NGX_OK) {
        return rc;
    }
这里pc->get指向的正好就是ngx_http_upstream_get_dynamic_peer。
pc->get这个调用的目的就是要求负载均衡模块把上游服务器的IP和端口设置到pc->sockaddr中。
c 复制代码
static ngx_int_t
ngx_http_upstream_get_dynamic_peer(ngx_peer_connection_t *pc, void *data)
{
    ngx_http_upstream_dynamic_peer_data_t  *bp = data;
    ngx_http_request_t                     *r;
    ngx_http_core_loc_conf_t               *clcf;
    ngx_resolver_ctx_t                     *ctx, temp;
    ngx_http_upstream_t                    *u;
    ngx_int_t                               rc;
    ngx_http_upstream_dynamic_srv_conf_t   *dscf;

    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, pc->log, 0,
                   "get dynamic peer");

    /* The "get" function will be called twice if
     * one host is resolved into an IP address.
     * (via 'ngx_http_upstream_connect' if resolved successfully)
     *
     * So here we need to determine if it is the first
     * time call or the second time call. 
     */
    /* 在域名resolve完成后已经设置好了目标upstream的地址 */
    if (pc->resolved == NGX_HTTP_UPSTREAM_DR_OK) {
        return NGX_OK;
    }

    dscf = bp->conf;
    r = bp->request;
    u = r->upstream;

    if (pc->resolved == NGX_HTTP_UPSTREAM_DR_FAILED) {

        ngx_log_debug1(NGX_LOG_DEBUG_HTTP, pc->log, 0,
                       "resolve failed! fallback: %ui", dscf->fallback);

        switch (dscf->fallback) {
		/* 解析失败,返回老的解析记录 */
        case NGX_HTTP_UPSTREAM_DYN_RESOLVE_STALE:
            return NGX_OK;
		/* 解析失败,shutdown模式直接结束请求,返回502 */
        case NGX_HTTP_UPSTREAM_DYN_RESOLVE_SHUTDOWN:
            ngx_http_upstream_finalize_request(r, u, NGX_HTTP_BAD_GATEWAY);
            return NGX_YIELD;
		/* 让负载均衡逻辑找下一个上游server */
        default:
            /* default fallback action: check next upstream */
            return NGX_DECLINED;
        }

        return NGX_DECLINED;
    }
	/* 这里判断如果在最近一次域名解析失败的时间内,则不再请求域名解析,
	   因为当前请求第一次进入到这个函数的时候,pc->resolved == NGX_HTTP_UPSTREAM_DR_INIT
	   但是dscf->fail_check可能因为最近有一次域名解析失败而设置了失败的时间,
	   所以会进入到这段代码的逻辑中
	*/
    if (dscf->fail_check
        && (ngx_time() - dscf->fail_check < dscf->fail_timeout))
    {
        ngx_log_debug1(NGX_LOG_DEBUG_HTTP, pc->log, 0,
                       "in fail timeout period, fallback: %ui", dscf->fallback);

        switch (dscf->fallback) {
		/* 直接调用设定的负载均衡模块返回对应的upstream server的ip地址 */
        case NGX_HTTP_UPSTREAM_DYN_RESOLVE_STALE:
            return bp->original_get_peer(pc, bp->data);
		/* shutdown模式直接结束请求,返回502 */
        case NGX_HTTP_UPSTREAM_DYN_RESOLVE_SHUTDOWN:
            ngx_http_upstream_finalize_request(r, u, NGX_HTTP_BAD_GATEWAY);
            return NGX_YIELD;
		/* next模式在本函数第一次被调用的时候也是
		   直接调用设定的负载均衡模块返回对应的upstream server的ip地址 */
        default:
            /* default fallback action: check next upstream, still need
             * to get peer in fail timeout period
             */
            return bp->original_get_peer(pc, bp->data);
        }

        return NGX_DECLINED;
    }

    /* NGX_HTTP_UPSTREAM_DYN_RESOLVE_INIT,  ask balancer */
    /* 通过负载均衡获取到使用哪个server,然后对该server进行域名解析 */
    rc = bp->original_get_peer(pc, bp->data);

    if (rc != NGX_OK) {
        return rc;
    }

    /* resolve name */

    if (pc->host == NULL) {
        ngx_log_debug0(NGX_LOG_DEBUG_HTTP, pc->log, 0,
                       "load balancer doesn't support dyn resolve!");
        return NGX_OK;
    }
    /* host是ip地址,直接连接不需要解析 */
    if (ngx_inet_addr(pc->host->data, pc->host->len) != INADDR_NONE) {
        ngx_log_debug0(NGX_LOG_DEBUG_HTTP, pc->log, 0,
                       "host is an IP address, connect directly!");
        return NGX_OK;
    }

    clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);
    if (clcf->resolver == NULL) {
        ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
                      "resolver has not been configured!");
        return NGX_OK;
    }
	/* 分配并设置异步域名调用的上下文 */
    temp.name = *pc->host;
	
    ctx = ngx_resolve_start(clcf->resolver, &temp);
    if (ctx == NULL) {
        ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
                      "resolver start failed!");
        return NGX_OK;
    }

    if (ctx == NGX_NO_RESOLVER) {
        ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
                      "resolver started but no resolver!");
        return NGX_OK;
    }
	
    ctx->name = *pc->host;
    /* TODO remove */
    // ctx->type = NGX_RESOLVE_A;
    /* END */
    ctx->handler = ngx_http_upstream_dynamic_handler;
    ctx->data = bp;
    ctx->timeout = clcf->resolver_timeout;
    /* 发起异步域名解析, 解析完成后会回调函数ngx_http_upstream_dynamic_handler*/
    u->dyn_resolve_ctx = ctx;

    if (ngx_resolve_name(ctx) != NGX_OK) {
        ngx_log_error(NGX_LOG_ERR, pc->log, 0,
                      "resolver name failed!\n");

        u->dyn_resolve_ctx = NULL;

        return NGX_OK;
    }
    /* tengine 定制的返回标记,即直接返回,等待epoll事件发生,待域名解析完成后,
    将重新调用ngx_http_upstream_connect,
    ngx_event_connect_peer的时候还会进入到本ngx_http_upstream_get_dynamic_peer,
    以便返回目标服务器地址 */
    return NGX_YIELD;
}

3.5 域名解析回调处理

c 复制代码
static void
ngx_http_upstream_dynamic_handler(ngx_resolver_ctx_t *ctx)
{
    ngx_http_request_t                    *r;
    ngx_http_upstream_t                   *u;
    ngx_peer_connection_t                 *pc;
#if defined(nginx_version) && nginx_version >= 1005008
    socklen_t                              socklen;
    struct sockaddr                       *sockaddr, *csockaddr;
#else
    struct sockaddr_in                    *sin, *csin;
#endif
    in_port_t                              port;
    ngx_str_t                             *addr;
    u_char                                *p;

    size_t                                 len;
    ngx_http_upstream_dynamic_srv_conf_t  *dscf;
    ngx_http_upstream_dynamic_peer_data_t *bp;

    bp = ctx->data;
    r = bp->request;
    u = r->upstream;
    pc = &u->peer;
    dscf = bp->conf;

    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));
		/* 设置解析失败的时间 */
        dscf->fail_check = ngx_time();

        pc->resolved = NGX_HTTP_UPSTREAM_DR_FAILED;

    } else {
        /* dns query ok */
#if (NGX_DEBUG) /* 这里只是debug模式下打印解析到的IP地址列表 */
        {
        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(ctx->addrs[i].sockaddr, 
                                     ctx->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
        dscf->fail_check = 0;
#if defined(nginx_version) && nginx_version >= 1005008
        csockaddr = ctx->addrs[0].sockaddr;         /* 取解析到的第一个地址 */
        socklen = ctx->addrs[0].socklen;
        /* 如果peer_connection中的地址和解析出来的地址一致,
           就直接返回OK,否则要重新分配一个sockaddr,最后赋值给peer_connection*/
        if (ngx_cmp_sockaddr(pc->sockaddr, pc->socklen, csockaddr, socklen, 0)
            == NGX_OK)
        {
            pc->resolved = NGX_HTTP_UPSTREAM_DR_OK;
            goto out;
        }

        sockaddr = ngx_pcalloc(r->pool, socklen);
        if (sockaddr == NULL) {
            ngx_http_upstream_finalize_request(r, u,
                                               NGX_HTTP_INTERNAL_SERVER_ERROR);
            return;
        }

        ngx_memcpy(sockaddr, csockaddr, socklen);
        port = ngx_inet_get_port(pc->sockaddr);
        
        switch (sockaddr->sa_family) {
#if (NGX_HAVE_INET6)
        case AF_INET6:
            ((struct sockaddr_in6 *) sockaddr)->sin6_port = htons(port);
            break;
#endif
        default: /* AF_INET */
            ((struct sockaddr_in *) sockaddr)->sin_port = htons(port);
        }

        p = ngx_pnalloc(r->pool, NGX_SOCKADDR_STRLEN);
        if (p == NULL) {
            ngx_http_upstream_finalize_request(r, u,
                                               NGX_HTTP_INTERNAL_SERVER_ERROR);
            return;
        }

        len = ngx_sock_ntop(sockaddr, socklen, p, NGX_SOCKADDR_STRLEN, 1);

        addr = ngx_palloc(r->pool, sizeof(ngx_str_t));
        if (addr == NULL) {
            ngx_http_upstream_finalize_request(r, u,
                                               NGX_HTTP_INTERNAL_SERVER_ERROR);
            return;
        }

        addr->data = p;
        addr->len = len;
        pc->sockaddr = sockaddr;        /* 设置upstream服务器目标地址 */
        pc->socklen = socklen;
        pc->name = addr;

#else
        /* for nginx older than 1.5.8 */
        /* 以下仅仅针对 1.5.8 版本以前的代码 */
        sin = ngx_pcalloc(r->pool, sizeof(struct sockaddr_in));
        if (sin == NULL) {
            ngx_http_upstream_finalize_request(r, u,
                                               NGX_HTTP_INTERNAL_SERVER_ERROR);
            return;
        }

        ngx_memcpy(sin, pc->sockaddr, pc->socklen);

        /* only the first IP addr is used in version 1 */

        csin = (struct sockaddr_in *) ctx->addrs[0].sockaddr;
        if (sin->sin_addr.s_addr == csin->sin_addr.s_addr) {

            pc->resolved = NGX_HTTP_UPSTREAM_DR_OK;

            goto out;
        }

        sin->sin_addr.s_addr = csin->sin_addr.s_addr;

        len = NGX_INET_ADDRSTRLEN + sizeof(":65535") - 1;

        p = ngx_pnalloc(r->pool, len);
        if (p == NULL) {
            ngx_http_upstream_finalize_request(r, u,
                                               NGX_HTTP_INTERNAL_SERVER_ERROR);
            return;
        }

        port = ntohs(sin->sin_port);
        len = ngx_inet_ntop(AF_INET, &sin->sin_addr.s_addr,
                            p, NGX_INET_ADDRSTRLEN);
        len = ngx_sprintf(&p[len], ":%d", port) - p;

        addr = ngx_palloc(r->pool, sizeof(ngx_str_t));
        if (addr == NULL) {
            ngx_http_upstream_finalize_request(r, u,
                                               NGX_HTTP_INTERNAL_SERVER_ERROR);
            return;
        }

        addr->data = p;
        addr->len = len;

        pc->sockaddr = (struct sockaddr *) sin;
        pc->socklen = sizeof(struct sockaddr_in);
        pc->name = addr;
#endif

        ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
                "name was resolved to %V", pc->name);

        pc->resolved = NGX_HTTP_UPSTREAM_DR_OK;
    }

out:
    ngx_resolve_name_done(ctx);   /* 释放域名解析上下文 */
    u->dyn_resolve_ctx = NULL;
    /* 这里重新发起上游服务器的连接, 会重新进入ngx_event_connect_peer函数,
       并在ngx_event_connect_peer函数里面重新调用ngx_http_upstream_get_dynamic_peer
    */
    ngx_http_upstream_connect(r, u);
}

4. 总结

ngx_http_upstream_dynamic_module 主要采用了钩子函数的方式,拦截了负载均衡模块的对应处理函数,进行了动态域名解析的处理,实现上还是非常巧妙的。
虽然开启动态解析虽然会对系统性能或多或少有一些影响,但是由于它利用了nginx 的异步域名解析的能力,同时nginx本身具备域名解析的cahce能力,而且本模块在解释失败的时候还会有fail_timeout的保护机制,所以性能上的影响基本上是可以忽略的。
相关推荐
ajsbxi6 小时前
苍穹外卖学习记录
java·笔记·后端·学习·nginx·spring·servlet
JustCouvrir1 天前
macOS|前端工程部署到Nginx服务器
服务器·前端·nginx
AlbertS1 天前
使用 Let’s Encrypt 获取免费SSL证书
nginx·免费·centos7·ssl证书·let’s encrypt
航月1 天前
FTP、ISCSI、CHRONY、DNS、NFS、DOCKER、MARIADB、NGINX、PHP、CA各服务开启方法
nginx·docker·mariadb
IT-民工211101 天前
nginx监控指标有哪些
运维·nginx
陌路物是人非2 天前
docker对nginx.conf进行修改后页面无变化或页面报错
nginx·docker
草明2 天前
Nginx 做反向代理,一个服务优先被使用,当无法提供服务时才使用其他的备用服务
运维·nginx·github
吉吉612 天前
Nginx:我自己的网站
运维·nginx
凉忆-2 天前
nginx安装ssl模块教程
运维·nginx·ssl
俎树振2 天前
树莓派上安装与配置 Nginx Web 服务器教程
服务器·前端·nginx