起因

公司有一个项目,部署有多个AWS环境,但由于一系列复杂的原因,需要先在账号A下备案的域名来使用在账号B中部署的服务,后续再将域名备案转到账号B中。
临时的解决办法就是在账号A下起一个Instance,通过Nginx反向代理到账号B的一个Classic ELB中。

起初服务跑的很顺畅,后来一天突然服务不可用,页面显示504 Gateway Timeout,查看了Nginx的日志,发现有多条upstream timed out (110: Connection timed out) while connecting to upstream的错误日志。

调查

AWS 的ELB是一个托管的负载均衡器,通过对外提供的域名来进行访问,北京Region的域名类似service-3332222.cn-north-1.elb.amazonaws.com.cn,但实际上底层还是会有对应的ENI来承载实际的流量,这些ENI有自己临时分配的IP地址。这些ENI可能会随着流量的扩展或AWS底层服务器的变动而随时改变。所以这也是AWS要求使用域名来访问ELB的原因。

Nginx侧使用upstream时,里面如果填写的是域名,那么Nginx会在请求DNS后把对应的IP信息缓存起来,后续的请求就一直用缓存的IP。直到下次reload的时候才会再次查询domain

此时问题就出现了,ELB的域名解析是时刻会更新的,当底层有变动时,承载ELB流量的Public IP就会变化。而Nginx的upstream中,如果使用了域名,第一次DNS请求后就会一直使用缓存的Public IP。当ELB的ENI有变化时,原先的Nginx反向代理缓存的IP地址就失效了,导致连接不上服务。出现upstream timed out (110: Connection timed out) while connecting to upstream的错误。

解决方案

有这么几个方法可以用来解决这个问题:

  1. 写一个监控的cron脚本,时刻检测ELB对应的IP是否有变化,一旦有变化,就reload Nginx。这个方法需要额外的开发。
  2. Nginx Plus支持在upstream中添加resolve,可以周期性的重新解析DNS域名。可以完美解决所遇到的问题,可一个Nginx Plus Instance的授权费用就要$2500+/year,穷人表示用不起。
  3. Nginx社区版的解决方案,不使用upstream,使用set将域名设为一个变量,再传递给proxy_pass。

Nginx官方这篇Using DNS for Service Discovery with NGINX and NGINX Plus的文章讲述了第二点和第三点的解决方案。

Nginx的免费解决方案的配置如下:

1
2
3
4
5
6
7
8
9
10
11
server {
listen 80;
server_name example.com;
resolver 172.31.0.2;
set $upstream_endpoint http://service-3332222.cn-north-1.elb.amazonaws.com.cn;
location / {
proxy_pass $upstream_endpoint$request_uri;
}
}

在 proxy_pass中使用变量来代替URI或者Upstream Server Group,Nginx就会在解析缓存过期后再次请求DNS服务器来解析域名。

注意: 此处的resolver所要填写的IP地址,取决于EC2所在的子网划分。如果网络是VPC,那需要设置为EC2所在子网的DNS地址,AWS会为每个子网保留5个IP地址,第三个IP地址用作DNS,例如如果子网CIDR为10.0.0.0/24,那10.0.0.2就是AWS保留的用作DNS的地址, 保留IP地址的官网说明VPCs and Subnets。如果是Classic的网络,那么AWS的DNS服务器地址是固定的172.16.0.23,AWS官网说明EC2-Classic Platform

关于免费方案的缺陷

当location的参数不是‘/’,并且proxy_pass后面的参数是一个变量时,proxy_pass的转发规则和正常的规则有所不同。如下是相关的说明以及解决方案。
以下文字转自Nginx with dynamic upstreams

proxy_pass不使用变量时的正常转发规则:
nginx配置为如下时:

1
2
3
location /foo/ {
proxy_pass http://127.0.0.1:8080;
}

如果请求的网页是/foo/bar/baz,那么Nginx会转发请求至http://127.0.0.1:8080/foo/bar/baz。但如果nginx的配置如下,在proxy_pass参数后面加上了‘/’时。

1
2
3
4
location /foo/ {
# Note the trailing slash ↓
proxy_pass http://127.0.0.1:8080/;
}

Nginx在将请求转发到upstream时,会将location中匹配的部分截掉,/foo/bar/baz会转发到http://127.0.0.1:8080/bar/baz

但当我们在proxy_pass中使用变量后,转发行为会发生变化。
当我们在转发的地址后面有‘/’时:

1
2
3
4
5
resolver 172.31.0.2;
set $upstream_endpoint http://service-1234567890.us-east-1.elb.amazonaws.com/;
location /foo/ {
proxy_pass $upstream_endpoint;
}

当你请求/foo/bar/baz时,请求会转发到‘/’而不是期望的/bar/baz

解决办法:

1
2
3
4
5
6
resolver 172.31.0.2;
set $upstream_endpoint http://service-1234567890.us-east-1.elb.amazonaws.com;
location /foo/ {
rewrite ^/foo/(.*) /$1 break;
proxy_pass $upstream_endpoint;
}

删掉set $upstream_endpoint后面的‘/’,手动rewrite地址,这样就可以将请求/foo/bar/baz转发到upstream的/bar/baz了。

实验验证

来做几个实验来验证下上面的配置是否可以生效。

环境准备

来做几个实验来验证下效果,新建几台Instance,配置好对应的SG。如下是几台实验机器的基本信息:

Instance Public IP Private IP SG Rule
EC2-A 18.237.0.231 172.31.39.103 SSH&HTTP From 0.0.0.0/0
EC2-B 52.26.25.237 172.31.35.141 SSH&HTTP from 0.0.0.0/0
EC2-Reverse 52.36.100.113 172.31.18.5 SSH&HTTP from 0.0.0.0/0

一个域名: dynamic-upstream.jibing57.com

环境设置

分别在EC2-A和EC2-B上安装简单的httpd服务

登陆EC2-A中执行命令安装httpd服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 切换为root用户
sudo bash
# 安装httpd
yum install httpd -y
# 建立一个最简单的page
# EC2-A中建立Hello, I'm EC2-A的首页
echo "<h1> Hello, I'm EC2-A </h1>" > /var/www/html/index.html
# 建立多级目录foo/bar/baz/并建立欢迎页
mkdir -p /var/www/html/foo/bar/baz/
echo "<h1> Hello, I'm EC2-A in dir foo/bar/baz/ </h1>" > /var/www/html/foo/bar/baz/index.html
# 建立多级目录bar/baz/并建立欢迎页
mkdir -p /var/www/html/bar/baz/
"<h1> Hello, I'm EC2-A in dir bar/baz/ </h1>" > /var/www/html/bar/baz/index.html
# 启动http服务
systemctl start httpd.service

登陆EC2-B中执行命令安装httpd服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 切换为root用户
sudo bash
# 安装httpd
yum install httpd -y
# EC2-B中建立Hello, I'm EC2-B的首页
echo "<h1> Hello, I'm EC2-B </h1>" > /var/www/html/index.html
# 建立多级目录foo/bar/baz/并建立欢迎页
mkdir -p /var/www/html/foo/bar/baz/
echo "<h1> Hello, I'm EC2-B in dir foo/bar/baz/ </h1>" > /var/www/html/foo/bar/baz/index.html
# 建立多级目录bar/baz/并建立欢迎页
mkdir -p /var/www/html/bar/baz/
echo "<h1> Hello, I'm EC2-B in dir bar/baz/ </h1>" > /var/www/html/bar/baz/index.html
# 启动http服务
systemctl start httpd.service

配置好后,访问如下页面,确认网页能够正常展示

配置EC2-Reverse的nginx,将请求重定向到dynamic-upstream.jibing57.com去。

1
2
3
4
5
6
7
8
9
10
11
12
server {
listen 80;
server_name 52.36.100.113;
access_log /var/log/nginx/access.log main;
error_log /var/log/nginx/error.log notice;
resolver 172.31.0.2;
set $upstream_endpoint http://dynamic-upstream.jibing57.com;
location / {
proxy_pass $upstream_endpoint;
}
}

进行试验

基本设置设置完后,让我们来进行试验

  1. 首先将dynamic-upstream.jibing57.com解析到EC2-A的IP地址18.237.0.231
    aliyun_parser_to_EC2-A
    此时访问http://52.36.100.113, 可以看到的是EC2-A Instance上的主页内容 Hello, I’m EC2-A
    upstream_to_EC2-A

  2. 再将dynamic-upstream.jibing57.com解析修改为EC2-B的IP地址52.26.25.237
    aliyun_parser_to_EC2-B
    当修改后的域名解析扩散到AWS的DNS服务器后,此时访问http://52.36.100.113,可以看到的是EC2-B Instance上的主页内容Hello, I’m EC2-B
    upstream_to_EC2-B

说明设置的upstream起作用了,我们并没有重新启动load Nginx,就可以将请求转发到EC2-B中。

再来修改EC2-Reverse的nginx,测试location /foo/的情形

1
2
3
4
5
6
7
8
9
10
11
12
server {
listen 80;
server_name 52.36.100.113;
access_log /var/log/nginx/access.log main;
error_log /var/log/nginx/error.log notice;
resolver 172.31.0.2;
set $upstream_endpoint http://dynamic-upstream.jibing57.com/;
location /foo/ {
proxy_pass $upstream_endpoint;
}
}

注意此时dynamic-upstream.jibing57.com域名后面带有‘/’。
此时访问http://52.36.100.113/foo/bar/baz/,页面内容是EC2-B中/index.html的内容Hello, I'm EC2-B
需改Nginx配置为rewrite。

1
2
3
4
5
6
7
8
9
10
11
12
13
server {
listen 80;
server_name 52.36.100.113;
access_log /var/log/nginx/access.log main;
error_log /var/log/nginx/error.log notice;
resolver 172.31.0.2;
set $upstream_endpoint http://dynamic-upstream.jibing57.com;
location /foo/ {
rewrite ^/foo/(.*) /$1 break;
proxy_pass $upstream_endpoint;
}
}

此时访问http://52.36.100.113/foo/bar/baz/, 页面内容为EC2-B中/bar/baz/index.html的内容Hello, I'm EC2-B in dir bar/baz/

Reference

留言