使用 nginx 的 proxy_pass 功能做域名转发导致一次事故

proxy 模块是 nginx 中非常实用的一个模块。可以方便地实现一个域名的流量转发到另一域名,也可以将一个端口的流量转发到另一个端口。而结合在 AWS 等云服务中的 elb(负载均衡器) 使用本功能时则要注意一下域名被缓存问题,否则可能导致整个域名无法提供服务。

架构描述

服务是使用的 AWS 的云主机,并且使用 AWS 的 elb 做 web 层的负载均衡器。历史域名是 www.old.com,新域名是 www.new.com。www.new.com 域名解析到 AWS 的 elb 地址上。

问题描述

由于域名的的变更,而历史域名在已发出去的版本的 App 还在使用中。为了管理方便,考虑使用 nginx 提供的 proxy_pass 功能来做流量转发,将历史域名流量转发到新的域名上来。

通过以下配置便实现了需求:
历史域名的 nginx 配置 www.old.com.conf:

location / {
    proxy_pass https://www.new.com;
    proxy_redirect off;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

配置改完后,经测试没有问题,但大概过了一段时间后,出现该域名下服务整体不可用情况。

问题排查

通过分析 www.old.com 的 nginx 日志,查到:

2017/01/16 13:41:57 [error] 11185#0: *20490083 connect() failed (110: Connection timed out) while connecting to upstream, client: x.x.x.x, server: www.old.com, request: "POST /api/get HTTP/1.1", upstream: "https://y.y.y.y:443/api/get", host: "www.old.com"

这样的错误。从日志中可以看到流量转发到了 y.y.y.y ip 上,但 y.y.y.y 下的服务无法提供服务,从而导致了错误。并且通过 telnet y.y.y.y 443 命令排查,发现其无法通信。大概定位了问题后,在网上查到:nginx proxy_pass 在转发时会对 ip 地址进行缓存。www.new.com 指向的是 aws 的 elb,而 AWS 的 elb 的 ip 会动态变更,导致 nginx proxy_pass 到老的 ip 上了,从而出现了问题。

解决方案

对 www.old.com.conf 的 proxy_pass 配置进行优化:

location / {
     resolver 10.90.0.2 valid=10s; // ip 为 aws 的 dns ip
     set $backend "https://www.new.com";
     proxy_pass $backend;
     proxy_redirect off;
     proxy_set_header X-Real-IP $remote_addr;
     proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

也就是设置 dns 服务的 ip 缓存失效时间。这样在 elb 变更了 ip 后相对能较及时地重新解析 www.new.com 域名为新的 ip。

扩展

在做域名转发时不能简单地使用 nginx 的 rewrite 功能,因为 rewrite 会导致 POST 参数数据丢失。