Nginx proxy_pass 配置域名引发的故障

微信扫一扫,分享到朋友圈

Nginx proxy_pass 配置域名引发的故障

业务场景:

用户 ----> 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

示例配置如下:

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
resolver
valid
resolver_timeout

注意:当 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

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

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 启动时会报类似如下错误:

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

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

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(变量设置域名)

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(变量设置域名)

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(不用变量设置域名)

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(不用变量设置域名)

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 加端口号

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 指令的代码:

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(≻, 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(≻) != 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 模块初始化请求时的逻辑:

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 有些类似。

参考:

微信扫一扫,分享到朋友圈

Nginx proxy_pass 配置域名引发的故障

拼多多的留存焦虑,多多买菜真的能缓解吗?

上一篇

腾讯微视“选角”上线 海量角色全面开启征选

下一篇

你也可能喜欢

Nginx proxy_pass 配置域名引发的故障

长按储存图像,分享给朋友