Nginx proxy_pass 配置域名引发的故障

背景描述

业务场景:

1
用户 ----> waf ----> 后端服务

waf 是采用 Nginx 做的二次开发,做了一些安全验证后将请求转发到后端服务,通过 nginx proxy_pass 转发。 proxy_pass 后面直接配置的是域名(如:xxxxx-1760550967.cn-northwest-1.elb.amazonaws.com.cn )

故障现象

有部分用户开始反馈访问站点出错 504 Gateway Time-out, 通过监控查到有部分请求打了一个下线的 IP 上。这里简单简述下故障场景:使用nginx做反向代理,将请求发送到一个域名(例如: proxy_pass http://www.test.com 该域名对应的 IP 是 A) ,刚开始运行一切正常,但是当运行了一段时间以后,域名对应的 IP 变了(例如 http://www.test.com 对应的 IP 由 A 变为 B),nginx 的转发仍然还在向原先的 IP 发送请求,导致业务中断,此时reload nginx 后才会重新恢复正常,且日志显示数据转发到新的 IP B。

故障分析

此处只针对 nginx 向后端做代理,且后端代理为域名形式的这种情况做分析

  • 1、正常情况下启动 nginx 后(或者 -t / reload nginx 时),nginx 会通过操作系统配置的 DNS 服务器去解析域名对应的 IP
  • 2、当 nginx 配置文件中的所有涉及到的域名都可以被正常解析到以后,才能启动(或者检查/重新加载)通过
  • 3、这里需要提醒一点,在 nginx -t 或者 nginx -s reload 只是检查域名是否可以解析通过,并不会在此时缓存域名对应 IP,只有在通过 nginx 第一次向 proxy_pass 后端对应的域名做代理数据转发时,这里 nginx 会通过操作系统配置的 DNS 服务器解析域名,此时才会缓存域名对应的 IP,且会缓存很长时间,甚至一个月(整个过程均有生产实例证明,且抓包验证)

如何解决?

1、既然是因为 nginx 缓存域名对应 IP 的 DNS 记录造成的,那么怎么才能解决呢,方法有两种:

  • (1)、手动 reload nginx,让 nginx 重新解析域名,这个时候解析到域名对应的 IP 是最新的,不会包含已经被废弃的 IP
  • (2)、设置 nginx 的 DNS 缓存时间,比如 600s 失效,然后重新去解析

2、方法(2)当然是最好的,但是 nginx 的 DNS 缓存时间在哪里设置呢,我没有找到!

3、但是我找到另外一种方法 – nginx 的 resolver

nginx 的 resolver 解决方案

1、默认 nginx 会通过操作系统设置的 DNS 服务器(/etc/resolv.conf)去解析域名

2、其实 nginx 还可以通过自身设置 DNS 服务器,而不用去找操作系统的 DNS

3、下面来讲一个这个 resolver

示例配置如下:

1
2
3
4
5
6
7
8
9
10
server {
listen 8080;
server_name localhost;
resolver 114.114.114.114 223.5.5.5 valid=3600s;
resolver_timeout 3s;
set $qq "www.qq.com";
location / {
proxy_pass http://$qq;
}
}

参数说明:

  • resolver 可以在 http 全局设定,也可在 server 里面设定
  • resolver 后面指定 DNS 服务器,可以指定多个,空格隔开
  • valid 设置 DNS 缓存失效时间,自己根据情况判断,建议 600 以上
  • resolver_timeout 指定解析域名时,DNS 服务器的超时时间,建议 3 秒左右

注意:resolver 后面跟多个 DNS 服务器时,一定要保证这些 DNS 服务器都是有效的,因为这种是负载均衡模式的,当 DNS 记录失效了(超过 valid 时间),首先由第一个 DNS 服务器(114.114.114.114)去解析,下一次继续失效时由第二个 DNS 服务器(223.5.5.5)去解析,亲自测试的,如有任何一个 DNS 服务器是坏的,那么这一次的解析会一直持续到 resolver_timeout ,然后解析失败,且日志报错解析不了域名,通过页面抛出502错误。

重点:如上例,在代理到后端域名 http://www.qq.com 时,千万不要直接写在 proxy_pass 中,因为 server 中使用了 resolver,所以必须先把域名定义到一个变量里面,然后在 proxy_pass http://$变量名,否则 nginx 语法检测一直会报错,提示解析不了域名。

延展阅读

这里列举几个 proxy_passupstreamreslover 的应用场景

1. proxy_pass + upstream

1
2
3
4
5
6
7
8
9
10
11
12
upstream foo.example.com {
server 127.0.0.1:8001;
}

server {
listen 80;
server_name localhost;

location /foo {
proxy_pass http://foo.example.com;
}
}

访问 http://localhost/foo,proxy 模块会将请求转发到 127.0.0.1 的 8001 端口上。

2. 只有 proxy_pass,没有 upstream 与 resolver

1
2
3
4
5
6
7
8
server {
listen 80;
server_name localhost;

location /foo {
proxy_pass http://foo.example.com;
}
}

实际上是隐式创建了 upstreamupstream 名字就是 foo.example.com。upstream 模块利用本机设置的 DNS 服务器(或/etc/hosts),将 foo.example.com 解析成 IP,访问 http://localhost/foo,`proxy` 模块会将请求转发到解析后的 IP 上。

如果本机未设置 DNS 服务器,或者 DNS 服务器无法解析域名,则 nginx 启动时会报类似如下错误:

1
nginx: [emerg] host not found in upstream "foo.example.com" in /path/nginx/conf/nginx.conf:110

3. proxy_pass + resolver(变量设置域名)

1
2
3
4
5
6
7
8
9
10
server {
listen 80;
server_name localhost;

resolver 114.114.114.114;
location /foo {
set $foo foo.example.com;
proxy_pass http://$foo;
}
}

访问 http://localhost/foo,nginx 会动态利用 resolver 设置的 DNS 服务器(本机设置的 DNS 服务器或 /etc/hosts 无效),将域名解析成 IP,proxy 模块会将请求转发到解析后的 IP 上。

4. proxy_pass + upstream(显式) + resolver(变量设置域名)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
upstream foo.example.com {
server 127.0.0.1:8001;
}

server {
listen 80;
server_name localhost;

resolver 114.114.114.114;
location /foo {
set $foo foo.example.com;
proxy_pass http://$foo;
}
}

访问 http://localhost/foo 时,upstream 模块会优先查找是否有定义 upstream 后端服务器,如果有定义则直接利用,不再走 DNS 解析。所以 proxy 模块会将请求转发到127.0.0.1 的 8001 端口上。

5. proxy_pass + upstream(隐式) + resolver(变量设置域名)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
server {
listen 80;
server_name localhost;

resolver 114.114.114.114;
location /foo {
set $foo foo.example.com;
proxy_pass http://$foo;
}

location /foo2 {
proxy_pass http://foo.example.com;
}
}

location /foo2 实际上是隐式定义了 upstream foo.example.com,并由本地 DNS 服务器进行了域名解析,访问 http://localhost/foo 时,upstream 模块会优先查找 upstream,即隐式定义的 foo.example.com,proxy 模块会将请求转发到解析后的 IP 上。

6. proxy_pass + resolver(不用变量设置域名)

1
2
3
4
5
6
7
8
9
server {
listen 80;
server_name localhost;

resolver 114.114.114.114;
location /foo {
proxy_pass http://foo.example.com;
}
}

不使用变量设置域名,则 resolver 的设置不起作用,此时相当于场景 2,只有 proxy_pass 的场景。

7. proxy_pass + upstream + resolver(不用变量设置域名)

1
2
3
4
5
6
7
8
9
10
11
12
13
upstream foo.example.com {
server 127.0.0.1:8001;
}

server {
listen 80;
server_name localhost;

resolver 114.114.114.114;
location /foo {
proxy_pass http://foo.example.com;
}
}

不使用变量设置域名,则 resolver 的设置不起作用,此时相当于场景 1 proxy_pass + upstream

8. proxy_pass 直接指定 IP 加端口号

1
2
3
4
5
6
7
8
server {
listen 80;
server_name localhost;

location /foo {
proxy_pass http://127.0.0.1:8001/;
}
}

实际上是隐式创建了 upstreamproxy_pass 会将请求转发到 127.0.0.1 的 8001 端口上。

主要代码

解析 proxy_pass 指令的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
static char *
ngx_http_proxy_pass(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{

ngx_http_proxy_loc_conf_t *plcf = conf;

size_t add;
u_short port;
ngx_str_t *value, *url;
ngx_url_t u;
ngx_uint_t n;
ngx_http_core_loc_conf_t *clcf;
ngx_http_script_compile_t sc;

if (plcf->upstream.upstream || plcf->proxy_lengths) {
return "is duplicate";
}

clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module);

clcf->handler = ngx_http_proxy_handler;

if (clcf->name.data[clcf->name.len - 1] == '/') {
clcf->auto_redirect = 1;
}

value = cf->args->elts;

url = &value[1];

/* 查找指令中$符号的位置,判断是否使用了变量 */
n = ngx_http_script_variables_count(url);

if (n) {
/* 使用变量设置域名 */
ngx_memzero(&sc, sizeof(ngx_http_script_compile_t));

sc.cf = cf;
sc.source = url;
sc.lengths = &plcf->proxy_lengths;
sc.values = &plcf->proxy_values;
sc.variables = n;
sc.complete_lengths = 1;
sc.complete_values = 1;

if (ngx_http_script_compile(&sc) != NGX_OK) {
return NGX_CONF_ERROR;
}

#if (NGX_HTTP_SSL)
plcf->ssl = 1;
#endif

return NGX_CONF_OK;
}

if (ngx_strncasecmp(url->data, (u_char *) "http://", 7) == 0) {
add = 7;
port = 80;

} else if (ngx_strncasecmp(url->data, (u_char *) "https://", 8) == 0) {

#if (NGX_HTTP_SSL)
plcf->ssl = 1;

add = 8;
port = 443;
#else
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"https protocol requires SSL support");
return NGX_CONF_ERROR;
#endif

} else {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "invalid URL prefix");
return NGX_CONF_ERROR;
}

ngx_memzero(&u, sizeof(ngx_url_t));

u.url.len = url->len - add;
u.url.data = url->data + add;
u.default_port = port;
u.uri_part = 1;
u.no_resolve = 1;

plcf->upstream.upstream = ngx_http_upstream_add(cf, &u, 0);
if (plcf->upstream.upstream == NULL) {
return NGX_CONF_ERROR;
}

plcf->vars.schema.len = add;
plcf->vars.schema.data = url->data;
plcf->vars.key_start = plcf->vars.schema;

ngx_http_proxy_set_vars(&u, &plcf->vars);

plcf->location = clcf->name;

if (clcf->named
#if (NGX_PCRE)
|| clcf->regex
#endif
|| clcf->noname)
{
if (plcf->vars.uri.len) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"\"proxy_pass\" cannot have URI part in "
"location given by regular expression, "
"or inside named location, "
"or inside \"if\" statement, "
"or inside \"limit_except\" block");
return NGX_CONF_ERROR;
}

plcf->location.len = 0;
}

plcf->url = *url;

return NGX_CONF_OK;
}

upstream 模块初始化请求时的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
static void
ngx_http_upstream_init_request(ngx_http_request_t *r)
{

ngx_str_t *host;
ngx_uint_t i;
ngx_resolver_ctx_t *ctx, temp;
ngx_http_cleanup_t *cln;
ngx_http_upstream_t *u;
ngx_http_core_loc_conf_t *clcf;
ngx_http_upstream_srv_conf_t *uscf, **uscfp;
ngx_http_upstream_main_conf_t *umcf;

if (r->aio) {
return;
}

u = r->upstream;

/* NGX_HTTP_CACHE 等其他处理 */

cln->handler = ngx_http_upstream_cleanup;
cln->data = r;
u->cleanup = &cln->handler;

if (u->resolved == NULL) {
/* 如果没有使用resolver设置DNS,直接取upstream的设置 */
uscf = u->conf->upstream;

} else {

#if (NGX_HTTP_SSL)
u->ssl_name = u->resolved->host;
#endif

host = &u->resolved->host;

if (u->resolved->sockaddr) {

if (u->resolved->port == 0
&& u->resolved->sockaddr->sa_family != AF_UNIX)
{
ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
"no port in upstream \"%V\"", host);
ngx_http_upstream_finalize_request(r, u,
NGX_HTTP_INTERNAL_SERVER_ERROR);
return;
}

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;
}

umcf = ngx_http_get_module_main_conf(r, ngx_http_upstream_module);

uscfp = umcf->upstreams.elts;

/* 在显式/隐式定义的upstream中查找 */
for (i = 0; i < umcf->upstreams.nelts; i++) {

uscf = uscfp[i];

if (uscf->host.len == host->len
&& ((uscf->port == 0 && u->resolved->no_port)
|| uscf->port == u->resolved->port)
&& ngx_strncasecmp(uscf->host.data, host->data, host->len) == 0)
{
goto found;
}
}

if (u->resolved->port == 0) {
ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
"no port in upstream \"%V\"", host);
ngx_http_upstream_finalize_request(r, u,
NGX_HTTP_INTERNAL_SERVER_ERROR);
return;
}

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;
}

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->handler = ngx_http_upstream_resolve_handler;
ctx->data = r;
ctx->timeout = clcf->resolver_timeout;

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;
}

found:

if (uscf == NULL) {
ngx_log_error(NGX_LOG_ALERT, r->connection->log, 0,
"no upstream configuration");
ngx_http_upstream_finalize_request(r, u,
NGX_HTTP_INTERNAL_SERVER_ERROR);
return;
}

#if (NGX_HTTP_SSL)
u->ssl_name = uscf->host;
#endif

if (uscf->peer.init(r, uscf) != NGX_OK) {
ngx_http_upstream_finalize_request(r, u,
NGX_HTTP_INTERNAL_SERVER_ERROR);
return;
}

u->peer.start_time = ngx_current_msec;

if (u->conf->next_upstream_tries
&& u->peer.tries > u->conf->next_upstream_tries)
{
u->peer.tries = u->conf->next_upstream_tries;
}

ngx_http_upstream_connect(r, u);
}

详细分析

场景1

解析 proxy_pass 的函数 ngx_http_proxy_pass 中,没有找到 $ 符号(即,变量设置域名),走 ngx_http_proxy_pass 后半部分的处理逻辑。ngx_http_upstream_init_round_robin 初始化 upstream 时,走显式定义 upstream 的逻辑。proxy_pass 转发请求初始化时,ngx_http_upstream_init_request 中直接使用 upstream 中的后端 server 建立连接。

场景2

ngx_http_upstream_init_round_robin 初始化 upstream 时,走隐式定义 upstream 的逻辑,会调用 ngx_inet_resolve_hostproxy_pass 中的域名进行解析,设置 upstreamproxy_pass 转发请求初始化时,ngx_http_upstream_init_request 中直接使用 upstream 中的设置,也就是利用本地设置的 DNS 服务器解析出的 IP,建立连接。

场景3

解析 proxy_pass 指令时,找到了 $ 符号,设置 ngx_http_script_compile_t,并利用 ngx_http_script_compile 进行编译,不走后半部分逻辑。配置文件没有显式/隐式定义 upstream,所以不会调用 ngx_http_upstream_init_round_robin 方法。proxy_pass 转发请求初始化时,ngx_http_upstream_init_request 中发现没有显式也没有隐式定义的 upstream,随后调用 ngx_resolve_start,对域名进行解析,之后将请求转发过去。

场景4

解析 proxy_pass 指令时,找到了 $ 符号,设置 ngx_http_script_compile_t,并利用 ngx_http_script_compile 进行编译,不走后半部分逻辑。显式调用了 upstream,所以调用 ngx_http_upstream_init_round_robin 方法中的显式 upstream 的处理逻辑。proxy_pass 转发请求初始化时,ngx_http_upstream_init_request 中优先查找 upstream,如果找到了,直接将请求转发到 upstream 中的后端 server 上。如果 upstream 中没有找到,则对域名进行解析,然后将请求转发到解析后的 IP 上。

场景5

基本与场景 4 相同,不同之处在于调用 ngx_http_upstream_init_round_robin 方法时,走隐式 upstream 部分的处理逻辑。

场景6

与场景 2 相同。

场景7

与场景 1 相同。

场景8

实际上是隐式创建了 upstream,但是因为 proxy_pass 中指定了 IP 和端口号,所以ngx_http_upstream_init_round_robin 初始化 upstream 时,us->servers 不为空,所以走该函数的上半部分逻辑。与场景 1 有些类似。

参考:

微信订阅号

-------------本文结束感谢您的阅读-------------