Docker 解决部署多个服务时 nginx 反向代理无法解析后端服务名的问题
当你通过 docker 部署多个服务时,无论是单机服务,还是采用 swarm 模式的集群服务情况下,nginx 作为对外入口服务,接收到客户端流量后,需要通过反向代理转发到后端服务。
通常采用服务名的形式进行反向代理,如:
location / {
proxy_pass http://servicename:80;
}
如上配置中,nginx 将在反向代理时,通过把 servicename 解析为对应的后端服务名然后转发流量。
同样的 servicename 需要是已经定义的服务名才可以。
此时会有几种情况:
如果 servicename 的容器先启动,而后启动 nginx 容器,则整个服务没有任何问题。
如果因为某种原因,比如后端服务出现错误,或者延迟启动等其他问题,nginx 则先创建容器,在启动时就会遇到这个 servicename 无法解析,因为此时并没有 servicename 这个服务,也无法得到有效的解析,出现报错: host not found in upstream "php",进而反复创建新的容器,反复启动,反复失败,直至最终成功,这部分体验会非常糟糕,还会影响其他服务。
我们解决这个问题,有两种方案:
第一种方案,必须要使得后端服务也就是 servicename 先运行,再运行 nginx 容器,就不会出现错误了。
第二种方案,让 nginx 启动时不进行解析域名或者出现错误跳过继续启动就可以解决问题。
depends_on 这个选项可以配置服务之间的依赖关系,通过判断当前服务所依赖的服务是否已经启动,如果已经启动则再启动当前服务,以此来解决服务之间的依赖关系。
配置起来也非常简单,如:
version: "3.9"
services:
nginx:
image: nginx
depends_on:
- phpservice
phpservice:
image: php
但这种方式也仅限于本地开发或者比较简单的项目中使用。不能在集群环境中使用,不能在 swarm 中进行配置。
nginx 中的 resolver 是一个行业的常见解决方案,它的作用是用于 nginx 进行反向代理相关操作时,当目标是一个域名时通过 resolver 配置的 DNS 服务器进行解析,得到具体的 IP 再进行反向代理操作。
上文说了可以通过屏蔽 nginx 启动错误使其继续运行的方式,通过探索,nginx 并未提供这种方案。
需要注意的是,在反向代理配置中:
如果目标地址出现域名,nginx 则在启动时进行解析这个域名是否能够得到 IP,如果不能则会报错,这个错误并不能够跳过。
反之,nginx 成功解析到了域名之后,会将对应的 IP 缓存起来,后续将一直向这个 IP 输送数据。
线索来自:https://www.nginx.com/blog/dns-service-discovery-nginx-plus/
If the domain name can’t be resolved, NGINX fails to start or reload its configuration.
NGINX caches the DNS records until the next restart or configuration reload, ignoring the records’ TTL values.
We can’t specify another load‑balancing algorithm, nor can we configure passive health checks or other features defined by parameters to the server
directive, which we’ll describe in the next section.
我们先来看一段配置,这是比较推荐且完整的配置。
server {
index index.php index.html;
server_name default;
error_log /var/log/nginx/error.log;
access_log /var/log/nginx/access.log;
root /code;
location ~ \.php$ {
resolver 127.0.0.11 valid=30s;
set $backend "php";
proxy_pass http://$backend:80;
}
}
resolver 指向的是尝试去 dns 解析的 DNS 服务器,而 127.0.0.11 是 docker 默认提供的 dns 服务器,它将首先在 docker 内部/集群内部进行解析域名,如果不存在则向外部的 dns 服务器发出请求进行解析。
valid 是表示解析到的结果缓存多久
$backend 先进行设置变量,再到反向代理时进行引用域名并解析,是为了能够让 nginx 启动时不直接解析域名,以避免因为名称无法解析造成错误。
那么,上文的 resolver 是必须的吗?感觉只需要 $backend 就能够解决问题,我们来进行验证下。
server {
index index.php index.html;
server_name default;
error_log /var/log/nginx/error.log;
access_log /var/log/nginx/access.log;
root /code;
location ~ \.php$ {
set $backend "php";
proxy_pass http://$backend:80;
}
}
当为反向代理仅配置变量时:
nginx: 正常顺利启动,因为不再需要启动时解析域名。
外部访问:无法访问,502 Bad Gateway, 同时 nginx 报错 [error] 24#24: *14 no resolver defined to resolve php
容器内部:有效,curl 能够通过 curl 得到有效结果,并且每一次得到的 IP 都是不同的,因为每次 curl 都会进行一次域名解析,而 docker 会对每次 DNS 查询请求,采用轮询机制,分配到不同的 IP 地址给到客户端。
方案:无效
server {
index index.php index.html;
server_name default;
error_log /var/log/nginx/error.log;
access_log /var/log/nginx/access.log;
root /code;
location ~ \.php$ {
resolver 127.0.0.11 valid=30s;
proxy_pass http://phpcopy:80;
}
}
这里仅配置 dns 服务器,并指定一个不存在的容器名phpcopy,是为了避免后端服务优先创建了容器,导致测试无效。此处的测试目的是为了验证,只提供 resolver 是否能够不进行解析域名而先启动 nginx,待访问时再进行解析域名。
测试结果:nginx 无法正常启动
方案:无效
如果你需要测试或者复现,这是本文中环境配置的补全。
stack 集群配置参考:
version: "3.7"
services:
nginx:
image: nginx:latest
configs:
- source: nginx_test_conf_copy7
target: /etc/nginx/conf.d/default.conf
ports:
- 9995:80
deploy:
replicas: 1
placement:
constraints:
- node.role == manager
php:
image: php:latest
command: php -S 0.0.0.0:80
configs:
- source: index_php_v1
target: /app/index.php
working_dir: /app
deploy:
replicas: 3
placement:
constraints:
- node.role == manager
configs:
index_php_v1:
external: true
nginx_test_conf_copy7:
external: true
nginx 配置: 采用上文提供的配置。
php 配置:如 index_php_v1,可打印执行代码所在容器的 IP 地址,内容为:
<?php
var_dump(gethostbyname(gethostname()));
前面也提到了,通过 curl 后端服务名,或者网页访问 nginx 出现的 IP 地址都不同,而且是以轮询的方式逐个访问。
我们在 nginx 配置文件中,指定 valid=30s,理论在这 30s 内不会重复进行 dns 查询。即首次访问时得到 10.0.9.5 这个 IP 地址,那么在这之后的 30 秒有效期内,则都应该将流量传递到这个 10.0.9.5 这个地址里面。
实际测试中发现,每一次访问 nginx 服务时,这个 IP 都会变化,也就是说每一次用户请求都查询了 dns 请求,其 valid 是无效的?
为了寻找原因,进行了几项测试:
tcpdump 抓包
其中通过 tcpdump 抓包工具,观察 docker0 这个网卡的 dns 查询记录,此项只能观察外部的公网域名,而不能观察 docker 的 dns 查询记录。
tcpdump -i docker0 udp port 53 -vvv
我通过公网域名进行测试,更改 nginx 配置文件为:
server {
index index.php index.html;
listen 80;
listen 81;
server_name default.com;
error_log /var/log/nginx/error.log;
access_log /var/log/nginx/access.log;
root /code;
resolver 1.1.1.1 valid=30s;
#resolver 127.0.0.11 valid=30s;
location ~ \.php$ {
set $backend "test3.mydomain.com";
proxy_pass http://$backend:80;
}
}
重载配置文件后进行测试,并在 cloudflare 解析我的域名 test3.xxxx 到 10.0.9.7,开始观察抓包记录。
开始依然是,每一次用户请求都会进行一次 dns 查询记录。怎么测试都是每一次请求都会查询 DNS,无论 valid 是否过期。
当我尝试去 cloudflare 修改 ttl 记录为 15 分钟时,再次查询发现 nginx 竟然开始缓存了。即 30s 内不再进行 dns 查询。
当我再次把记录修改为 auto(5 分钟)时,再次请求 nginx,nginx 依然是履行 30s 的有效缓存,只有缓存过期才会发起新的 DNS 查询。奇了怪了。
通过抓包记录发现,修改前后的 dns 包除了 TTL,看不出来别的差别,就当它是抽风了吧。
修改 TTLS 前的包:
2400 604800 1800 (93)
01:35:34.944257 IP (tos 0x0, ttl 63, id 26824, offset 0, flags [DF], proto UDP (17), length 57)
server1.com.48561 > one.one.one.one.domain: [bad udp cksum 0x7494 -> 0x17a9!] 10665+ A? test3.mydomain.com. (29)
01:35:34.944299 IP (tos 0x0, ttl 63, id 26825, offset 0, flags [DF], proto UDP (17), length 57)
server1.com.48561 > one.one.one.one.domain: [bad udp cksum 0x7494 -> 0x9a18!] 35897+ AAAA? test3.mydomain.com. (29)
01:35:34.993654 IP (tos 0x0, ttl 57, id 15537, offset 0, flags [DF], proto UDP (17), length 73)
one.one.one.one.domain > server1.com.48561: [udp sum ok] 10665 q: A? test3.mydomain.com. 1/0/0 test3.mydomain.com. [5m] A 10.0.9.7 (45)
01:35:34.994034 IP (tos 0x0, ttl 57, id 15538, offset 0, flags [DF], proto UDP (17), length 121)
one.one.one.one.domain > server1.com.48561: [udp sum ok] 35897 q: AAAA? test3.mydomain.com. 0/1/0 ns: mydomain.com. [30m] SOA jeremy.ns.cloudflare.com. dns.cloudflare.com. 2320000000 10000 2400 604800 1800 (93)
修改 TTL 为 15 分钟后的包:
01:36:09.607769 IP (tos 0x0, ttl 63, id 51275, offset 0, flags [DF], proto UDP (17), length 57)
server1.com.58011 > one.one.one.one.domain: [bad udp cksum 0x7494 -> 0x6b08!] 45407+ A? test3.mydomain.com. (29)
01:36:09.607824 IP (tos 0x0, ttl 63, id 51276, offset 0, flags [DF], proto UDP (17), length 57)
server1.com.58011 > one.one.one.one.domain: [bad udp cksum 0x7494 -> 0xf919!] 2126+ AAAA? test3.mydomain.com. (29)
01:36:09.611396 IP (tos 0x0, ttl 57, id 28375, offset 0, flags [DF], proto UDP (17), length 73)
one.one.one.one.domain > server1.com.58011: [udp sum ok] 45407 q: A? test3.mydomain.com. 1/0/0 test3.mydomain.com. [15m] A 10.0.9.7 (45)
01:36:09.611786 IP (tos 0x0, ttl 57, id 28376, offset 0, flags [DF], proto UDP (17), length 121)
one.one.one.one.domain > server1.com.58011: [udp sum ok] 2126 q: AAAA? test3.mydomain.com. 0/1/0 ns: mydomain.com. [30m] SOA jeremy.ns.cloudflare.com. dns.cloudflare.com. 2320000000 10000 2400 604800 1800 (93)
就暂时认为 valid 是正常工作的吧。当改回 nginx 配置为:
server {
index index.php index.html;
listen 80;
listen 81;
server_name default.com;
error_log /var/log/nginx/error.log;
access_log /var/log/nginx/access.log;
root /code;
resolver 127.0.0.11 valid=30s;
location ~ \.php$ {
set $backend "php";
proxy_pass http://$backend:80;
}
}
重载配置,尝试请求,依然是每次请求的 IP 地址是不同的。
此时猜测,是不是 nginx 忽略了 docker dns 的查询结果或者是两者时间哪个更短,则取哪个 TTL 时间。又想也不对,这里是每一次请求都会变,即使多个请求中间间隔不到 1 秒。
在容器中安装 nslookup 进行测试
apt install dnsutils
分别查询了本地的 DNS 服务器以及刚刚解析的 cloudflare 服务器给到的结果:
# 本地 Docker DNS 查询
nslookup php -debug
# 公网 DNS 服务器查询
nslookup test3.mydomain.com -debug
通过结果来看,两者几乎没有什么区别,而且本地 DNS 返回的 TTL 时间更长有 600s,(authority 可以忽略)
正不知道问题所在时,然后通过 nslookup 反复查询看是否有更新时间或者是多条解析结果时,会不会是 nginx 拿到了多条解析结果,然后缓存起来挑选使用。
发现 nginx 并没有多条解析记录!而且每次都一样。
欸,这个 IP 好像不是几个后端容器的 IP 地址。这才想起来这个是 Docker 为集群环境中配置 VIP(虚拟 IP),而这个虚拟 IP 对于单个服务来说只有一个,虚拟 IP 拿到流量后会自动负载均衡到后端有效的服务中。
之后请求验证了一下,果真每次访问的 IP 都是不同的。所以 nginx 配置的 valid 也是有效的,它会缓存 30s,之后即使再次请求也是拿到的这个 VIP,并将流量转给 VIP。(至于公网开始测试时的每次都进行 DNS 查询就不得而知了,可能是抽风或者 BUG 吧)
VIP 的好处就是不需要前端知道后端有多少服务容器是有效的,他会自动将流量负载均衡到后端。当然也可以配置 dnsrr 模式,更多参考 docker 文档。
https://docs.docker.com/engine/swarm/networking/#configure-service-discovery
通过上面的方案测试,我们总结以下主要内容:
nginx 启动不能忽略域名解析失败的错误。
集群中,反向代理域名地址时,只能采用变量的配置方式使其延后解析。
当 nginx 解析域名时得到了一个有效的 IP,在有效期内会一直缓存着,当配置了 valid 时,则会采用 vailid 的有效期,默认为 30s.
depends_on 仅用于小型本地项目或者 docker-compose 中,不能用于集群环境。
集群环境中,反向代理后端服务,必须采用变量名+resolver 的方式。
nginx 反向代理时,默认不需要配置负载均衡,而将有 docker 提供的 VIP 模式会自动将流量负载均衡到后端容器,resolver valid 配置的值大小是不影响结果的。