众所周知,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;
}
该模块只有一条配置指令:
dynamic_resolve [fail_timeout=seconds] [fallback=next|stale|shutdown]
参数说明:
在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
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函数中需要特别说明一下的是:
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进行初始化。
下面来分析ngx_http_upstream_init_dynamic函数逻辑,这个函数会在nginx初始化的时候被回调,用于初始化upstream负载均衡上下文:
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;
}
这个初始化过程是在请求过来的时候进行的,在以上ngx_http_upstream_init_dynamic函数里面设置了拦截函数ngx_http_upstream_init_dynamic_peer,所以程序会运行到ngx_http_upstream_init_dynamic_peer函数里面来。
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;
}
peer.get 被拦截后,nginx在调用ngx_event_connect_peer发起向上游服务器进行连接的时候,会执行以下代码:
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中。
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;
}
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);
}
ngx_http_upstream_dynamic_module 主要采用了钩子函数的方式,拦截了负载均衡模块的对应处理函数,进行了动态域名解析的处理,实现上还是非常巧妙的。
虽然开启动态解析虽然会对系统性能或多或少有一些影响,但是由于它利用了nginx 的异步域名解析的能力,同时nginx本身具备域名解析的cahce能力,而且本模块在解释失败的时候还会有fail_timeout的保护机制,所以性能上的影响基本上是可以忽略的。