本文翻译自“Using Containers to Learn Nginx Reverse Proxy”,翻译已获得原作者Rosemary Wang授权。
作为Nginx及其反向代理功能的初学者,我本来并不知道该从哪里开始下手,也不知道该如何理解它。为了迈出第一步,我决定自己试着使用反向代理容器来探索它的这部分功能。做测试时其实我并没有网络连接,因为我在飞机上,所以只能在本地用Docker做测试。幸运的是,它运转起来了,实例都是在云里运行起来的! 有趣的是,我是在飞往西雅图的航班上开始写这篇文章的,那天还是个阴天。所以我猜我的实例真的是在云里运行起来的。
维基百科上是这么定义的:
在计算机网络中,反向代理是代理服务器的一种。服务器根据客户端的请求,从其关联的一组或多组后端服务器上获取资源,然后再将这些资源返回给客户端,客户端只会得知反向代理的IP地址,而不知道在代理服务器后面的真实服务器集群的存在。
我把反向代理想像成快递员。快递员们骑着车,穿梭于大街小巷,收取各种各样的包裹,再尽快尽量高效地派送出去,就好像发件人自己把它们投送出去一样。
我所说的云环境,指的是在公有云或私有云上面运行一组应用程序。我进行了思考,并做了些研究来寻找答案。在这过程中,我发现了一篇2012年发表的好文章,概括了反向代理的主要功能:
有了上面提到的这些功能,用反向代理就可以很好地满足我的需求了。在云上,应用程序的部署都是比较动态的,很难预计应用程序会从哪里连接过来,以及它们使用的认证方法,等等。使用反向代理可以减轻这些工作量。
我有时候也会怀疑自己理解得不对。我认为服务发现解决的问题与反向代理不同。我之前做过有关服务发现的测试,我记得服务发现指的是在云环境里,新服务会主动进行注册,让各种服务之间可以动态地相互发现。Nginx更多的是一个服务注册表,而不是发现和注册机制,还需要有另一个组件来负责改变反向代理的配置。
Nginx有许多功能,包括HTTP服务器。除了响应请求,你还可以为Nginx创建一个配置文件,指定把请求发往哪里去处理,这样就成了一个反向代理。一个简单的例子就是,一个默认运行在8080端口的测试程序。不管请求具体是被哪里处理的,我希望用户把所有请求都发往同一个地方。而且,如果某台服务器宕机了,我还希望它可以把请求发往另一台可用的服务器,这就是负载均衡机制。用upstream就可以实现这个功能。
worker_processes 1;
events { worker_connections 1024; }
http {
log_format compression '$remote_addr - $remote_user [$time_local] '
'"$request" $status $upstream_addr '
'"$http_referer" "$http_user_agent" "$gzip_ratio"';
upstream testapp {
server test:80;
}
server {
listen 8080;
access_log /var/log/nginx/access.log compression;
location /hello/ {
proxy_pass http://testapp/;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $server_name;
}
}
}
详细讲解一下:
为了方便使用,我用上面的Nginx反向代理配置创建了一个Docker镜像,并把镜像命名为reverseproxy。
FROM nginx:latest
COPY nginx.conf /etc/nginx/nginx.conf
我还创建了Docker compose,用于启动reverseproxy和我的应用程序test。
version: '2'
services:
reverseproxy:
image: reverseproxy:latest
ports:
- 8080:8080
restart: always
test:
image: joatmon08/testapp:latest
restart: always
我的测试程序打印的输出示例如下:
# curl test:80
Hello World!
# curl test:80/another?user=joatmon08
joatmon08 says Hello!
在这一组Docker里,我的reverseproxy程序只会通过8080端口为外部提供服务。如果从localhost:8080用路径/hello/来访问反向代理,我的测试程序会返回“Hello World!”。如果通过路径/hello/another来访问我的API,就会将用户名作为参数,返回一条消息。
$ curl localhost:8080/hello/
Hello World!
$ curl localhost:8080/hello/another?user=joatmon08
joatmon08 says Hello!
Nginx会把我的请求转发给我的程序。两种配置会返回相同的输出。我也可以再增加一个程序,映射到另一个Nginx位置,比如/goodbye。
再次查看我的Nginx反向代理日志,也就是Docker日志,可以看到我通过curl对API的访问都被记录下来了。在一台普通的Nginx服务器上,你也可以在access.log中找到这些信息。
$ docker logs nginxtest_reverseproxy_1
172.19.0.1 - - [22/Jul/2017:00:14:22 +0000] "GET /hello/ HTTP/1.1" 200 172.19.0.3:80 "-" "curl/7.43.0" "-"
172.19.0.1 - - [22/Jul/2017:00:14:54 +0000] "GET /hello/another?user=joatmon08 HTTP/1.1" 200 172.19.0.3:80 "-" "curl/7.43.0" "-"
它还记录下了我的upstream服务器172.19.0.3:80。test会被解析成这个IP地址和端口。这个IP地址实际上是我的应用程序容器,要了解更多细节,请参考我以前关于容器网络的文章。
我又部署了一个test应用的实例,现在有两个实例了。这意味着当我试图访问http://test时,我的请求可能会被转发到这两个不同IP地址上的任意一个容器中。
$ docker-compose up -d --scale test=2
nginxtest_reverseproxy_1 is up-to-date
Starting nginxtest_test_1 ... done
Creating nginxtest_test_2 ...
Creating nginxtest_test_2 ... done
为了确认URL http://test会被转发到两个不同实例上,我在同一个网络内的另一个容器里运行dig命令。
$ dig test
; <<>> DiG 9.9.5-3ubuntu0.2-Ubuntu <<>> test
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 64355
;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;test. IN A
;; ANSWER SECTION:
test. 600 IN A 172.19.0.3
test. 600 IN A 172.19.0.4
;; Query time: 0 msec
;; SERVER: 127.0.0.11#53(127.0.0.11)
;; WHEN: Fri Jul 21 02:43:21 UTC 2017
;; MSG SIZE rcvd: 62
在answer块里有两条记录。当我再次通过反向代理去访问test应用时,我会查看upstream服务器会不会被解析成这两个IP地址之一。
$ docker logs nginxtest_reverseproxy_1
172.19.0.1 - - [22/Jul/2017:00:16:49 +0000] "GET /hello/ HTTP/1.1" 200 172.19.0.3:80 "-" "curl/7.43.0" "-
答案是肯定的,它被解析成了与之前相同的一个,即172.19.0.3。
我想知道如果我把172.19.0.3删了会怎样。Nginx应该会转发到172.19.0.4去,因为test应该把请求转发到另一个仍然活着的服务器上。于是我删了172.19.0.3,即nginxtest_test_1。为了确认,我再次运行dig命令,看我的test应用的DNS记录是不是会指向172.19.0.4。
$ dig test
; <<>> DiG 9.9.5-3ubuntu0.2-Ubuntu <<>> test
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 35920
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;test. IN A
;; ANSWER SECTION:
test. 600 IN A 172.19.0.4
;; Query time: 0 msec
;; SERVER: 127.0.0.11#53(127.0.0.11)
;; WHEN: Sat Jul 22 00:47:28 UTC 2017
;; MSG SIZE rcvd: 42
接下来再次测试,通过localhost:8080访问我的反向代理。
$ curl localhost:8080/hello/
<html>
<head><title>502 Bad Gateway</title></head>
<body bgcolor="white">
<center><h1>502 Bad Gateway</h1></center>
<hr><center>nginx/1.13.1</center>
</body>
</html>
172.19.0.4工作正常,为什么Nginx访问不到172.19.0.4呢?也有另一种可能是Nginx压根就不会访问172.19.0.4。也许我该试试重启反向代理容器,Nginx就能获取到剩下的最后一个IP地址了。
$ docker restart d2
d2
$ curl localhost:8080/hello/
Hello World!
现在它被指向172.19.0.4了,即最后一个容器的IP地址。
$ docker logs nginxtest_reverseproxy_1
172.19.0.1 - - [22/Jul/2017:00:18:22 +0000] "GET /hello/ HTTP/1.1" 200 172.19.0.4:80 "-" "curl/7.43.0" "-"
结论是,Nginx会缓存它第一次通过upstream解析到的IP地址,而且不会刷新缓存,至少对于开源版本是这样。
老实说,从设计初衷上来说,我并不知道upsteam到底可不可以用于动态DNS解析。在官方的Nginx示例中,他们用upstream指令对一个IP地址集合做负载均衡。upstream通常用于:
我用下面的Nginx配置来更清晰地声明我的容器IP地址。
worker_processes 1;
events { worker_connections 1024; }
http {
log_format compression '$remote_addr - $remote_user [$time_local] '
'"$request" $status $upstream_addr '
'"$http_referer" "$http_user_agent" "$gzip_ratio"';
upstream testapp {
server 172.19.0.3:80;
server 172.19.0.4:80;
}
server {
listen 8080;
access_log /var/log/nginx/access.log compression;
location /hello/ {
proxy_pass http://testapp/;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $server_name;
}
}
}
用上面的配置做测试,Nginx可以帮我做负载均衡。当我再次删除172.19.0.3的容器时,Nginx会在172.19.0.4上重试。
$ docker logs nginxtest_reverseproxy_1
172.19.0.1 - - [22/Jul/2017:00:21:49 +0000] "GET /hello/ HTTP/1.1" 200 172.19.0.3:80 "-" "curl/7.43.0" "-"
2017/07/22 00:21:53 [error] 7#7: *8 connect() failed (113: No route to host) while connecting to upstream, client: 172.19.0.1, server: , request: "GET /hello/ HTTP/1.1", upstream: "http://172.19.0.3:80/", host: "localhost:8080"
172.19.0.1 - - [22/Jul/2017:00:21:53 +0000] "GET /hello/ HTTP/1.1" 200 172.19.0.4:80, 172.19.0.4:80 "-" "curl/7.43.0" "-"
如果用URL做为upstream服务器,那么你应该已经在它的前面部署了负载均衡。如果选择用URL或经过负载均衡的DNS记录来配置upstream,那么当负载均衡的IP地址发生变化时,你就会有Nginx反向代理无法重新解析IP地址的风险。通常,在下面这些场景可能会碰到上面提到的问题:
幸运的是,有许多博客已经就开源版Nginx的这个问题进行了详细讨论。下面这个简单配置就是根据他们的建议总结的:
worker_processes 1;
events { worker_connections 1024; }
http {
log_format compression '$remote_addr - $remote_user [$time_local] '
'"$request" $status $upstream_addr '
'"$http_referer" "$http_user_agent" "$gzip_ratio"';
server {
listen 8080;
access_log /var/log/nginx/access.log compression;
location /hello {
resolver 127.0.0.11 valid=5s;
set $upstream_endpoint http://test:80;
rewrite ^/hello(/.*) $1 break;
proxy_pass $upstream_endpoint;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $server_name;
}
}
}
简单来说就是别用upstream,换成resolver,请注意它解析出的DNS服务器是Docker内嵌的DNS!其实就是应该把upstream端点设置成一个动态变量,当每隔5秒钟执行解析器时,都会重新生成它的值。
非常重要的一点就是:应该增加rewrite指令来传入正确的URI。没有它,我的URI没能被正确传入,所以返回了一个“404 Not Found”错误。
$ curl localhost:8080/hello/
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>404 Not Found</title>
<h1>Not Found</h1>
<p>The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.</p>
Nginx的proxy_pass指令需要结尾的“/”来对URI进行修整(trim)。然而当你把proxy_pass设置成一个动态变量时,Nginx就会忽略它。如果你想让URI被转发到正确的地方,在这种情况下千万别忘了引入rewrite指令。
我想测试在相同情况下它是否仍然能正确工作:
测试最后的结果让人很满意。在删除了172.19.0.3上面的第一个应用容器之后,我再向应用程序的端点发起一次调用:
$ curl localhost:8080/hello/
Hello World!
$ curl localhost:8080/hello/another?user=joatmon08
joatmon08 says Hello!
和上次不同,我没收到“502 Bad Gateway”的错误。为了再次确认Nginx反向代理解析结果的正确性,我查看了Nginx的日志:
$ docker logs nginxtest_reverseproxy_1
172.19.0.1 - - [22/Jul/2017:00:34:37 +0000] "GET /hello/ HTTP/1.1" 200 172.19.0.3:80 "-" "curl/7.43.0" "-"
172.19.0.1 - - [22/Jul/2017:00:35:02 +0000] "GET /hello/ HTTP/1.1" 200 172.19.0.4:80 "-" "curl/7.43.0" "-"
请注意,并不需要重启Nginx容器,Nginx就把应用程序的IP地址重新指向了172.19.0.4。
为了深入了解Nginx解析器的特定行为,我对它的行为和各种指令的含义进行了研究。而且,用容器来模拟这些行为并获得最终收获的过程让人尤其印象深刻,我发现容器实在是一个优秀的学习工具,可以帮我们探索和进行测试。它可以帮我把一个问题拆解成可理解可测试的部分,把功能与技术和基础设施解耦开来。
参考资料:
原文链接:
https://medium.com/@joatmon08/using-containers-to-learn-nginx-reverse-proxy-6be8ac75a757
领取专属 10元无门槛券
私享最新 技术干货