写点什么

用容器来学习 Nginx 反向代理

  • 2020 年 4 月 22 日
  • 本文字数:6690 字

    阅读完需:约 22 分钟

用容器来学习Nginx反向代理

本文翻译自“Using Containers to Learn Nginx Reverse Proxy”,翻译已获得原作者Rosemary Wang授权。


作为 Nginx 及其反向代理功能的初学者,我本来并不知道该从哪里开始下手,也不知道该如何理解它。为了迈出第一步,我决定自己试着使用反向代理容器来探索它的这部分功能。做测试时其实我并没有网络连接,因为我在飞机上,所以只能在本地用 Docker 做测试。幸运的是,它运转起来了,实例都是在云里运行起来的!


有趣的是,我是在飞往西雅图的航班上开始写这篇文章的,那天还是个阴天。所以我猜我的实例真的是在云里运行起来的。


反向代理是什么?

维基百科上是这么定义的:


在计算机网络中,反向代理是代理服务器的一种。服务器根据客户端的请求,从其关联的一组或多组后端服务器上获取资源,然后再将这些资源返回给客户端,客户端只会得知反向代理的 IP 地址,而不知道在代理服务器后面的真实服务器集群的存在。


我把反向代理想像成快递员。快递员们骑着车,穿梭于大街小巷,收取各种各样的包裹,再尽快尽量高效地派送出去,就好像发件人自己把它们投送出去一样。


为什么要在云环境中运行反向代理呢?

我所说的云环境,指的是在公有云或私有云上面运行一组应用程序。我进行了思考,并做了些研究来寻找答案。在这过程中,我发现了一篇2012年发表的好文章,概括了反向代理的主要功能:


  • 负载均衡器

  • 应用层的安全保证(请求并没有直接发送给应用程序)

  • 单点认证、日志和审计

  • 静态内容服务器

  • 缓存

  • 压缩器

  • URL 改写器


有了上面提到的这些功能,用反向代理就可以很好地满足我的需求了。在云上,应用程序的部署都是比较动态的,很难预计应用程序会从哪里连接过来,以及它们使用的认证方法,等等。使用反向代理可以减轻这些工作量。


反向代理与服务发现有什么不同?

我有时候也会怀疑自己理解得不对。我认为服务发现解决的问题与反向代理不同。我之前做过有关服务发现的测试,我记得服务发现指的是在云环境里,新服务会主动进行注册,让各种服务之间可以动态地相互发现。Nginx 更多的是一个服务注册表,而不是发现和注册机制,还需要有另一个组件来负责改变反向代理的配置。


怎样可以把 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; } }}
复制代码


详细讲解一下:


  • worker_processes 告诉你将会运行多少个 Nginx 实例。为了能处理更大的负载,建议设置成 auto(每核一个)。

  • worker_connections 定义 worker 可以同时处理多少个连接。这里有篇文章,详细讨论了worker_connections

  • log_format 为日志增加了指定的字段。带上更多指令的话,它可以填充指定的字段,方便调试。我则希望能打印出我的 upstream 服务器地址。

  • upstream 是一组服务器,可以用包括 proxy_pass 在内的特定指令访问。在 upstream 下面还有一个 server 指令,可以启动一个 Nginx 网站服务器,并告诉它监听哪个端口。在我的例子里是 8080 端口。

  • 还有 location 指令,包含着该怎样代理请求的信息。也就是说,proxy_pass 定义了协议和地址,指示着代理该往哪里转发。


接下来看个例子

为了方便使用,我用上面的 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:80Hello World!# curl test:80/another?user=joatmon08joatmon08 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=joatmon08joatmon08 says Hello!
复制代码


Nginx 会把我的请求转发给我的程序。两种配置会返回相同的输出。我也可以再增加一个程序,映射到另一个 Nginx 位置,比如/goodbye。


再次查看我的 Nginx 反向代理日志,也就是 Docker 日志,可以看到我通过 curl 对 API 的访问都被记录下来了。在一台普通的 Nginx 服务器上,你也可以在 access.log 中找到这些信息。


$ docker logs nginxtest_reverseproxy_1172.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 地址实际上是我的应用程序容器,要了解更多细节,请参考我以前关于容器网络的文章。


如果把多个实例链接到了 upstream 服务器的 URL 上,会怎样?

我又部署了一个 test 应用的实例,现在有两个实例了。这意味着当我试图访问http://test时,我的请求可能会被转发到这两个不同 IP 地址上的任意一个容器中。


$ docker-compose up -d --scale test=2nginxtest_reverseproxy_1 is up-to-dateStarting nginxtest_test_1 ... doneCreating 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.3test.   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_1172.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>
复制代码


什么?怎么会这样?我竟然收到了 502 Bad Gateway 的响应!

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_1172.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 地址,而且不会刷新缓存,至少对于开源版本是这样。


如果反向代理不能再次解析到一个新的 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_1172.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 地址的风险。通常,在下面这些场景可能会碰到上面提到的问题:


  • 公有云负载均衡

  • 嵌入 Docker 的 DNS 服务器

  • 任意其它类型的动态负载均衡


使用动态负载均衡时,该怎样做,才能让 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 指令。


新的 Nginx 反向代理配置能正确工作吗?

我想测试在相同情况下它是否仍然能正确工作:


  1. 创建一个反向代理容器(reverseproxy)。

  2. 创建我的应用容器(test)。

  3. 向 reverseproxy 发起一次调用,转发到我的应用去处理。

  4. 把我的应用容器扩展到两个。

  5. 删掉我的第一个应用容器(nginxtest_test_1)。


测试最后的结果让人很满意。在删除了 172.19.0.3 上面的第一个应用容器之后,我再向应用程序的端点发起一次调用:


$ curl localhost:8080/hello/    Hello World!    $ curl localhost:8080/hello/another?user=joatmon08joatmon08 says Hello!
复制代码


和上次不同,我没收到“502 Bad Gateway”的错误。为了再次确认 Nginx 反向代理解析结果的正确性,我查看了 Nginx 的日志:


$ docker logs nginxtest_reverseproxy_1172.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


2020 年 4 月 22 日 08:512772

评论

发布
暂无评论
发现更多内容

Vue进阶(四):使用 Vuex + axios 发送请求

No Silver Bullet

Vue axios vuex 7月日更 vue-resources

在线条码生成器

入门小站

工具

从鉴黄师到阿里程序员,我成功逆袭上岸

Java 编程 程序员 计算机

大三就拿到字节提前批,你不想成长,生活总会逼着你成长

Java架构师迁哥

阿里大牛把电商购物、电商秒杀、12306抢票、淘宝天猫各种活动的系统架构层面全部记载到这份《高并发系统架构》手册里了

Java 编程 架构 计算机

模块三作业

A先生

双非本科,面试两个月成功收获阿里、美团等6个offer,经验分享

互联网架构师小马

Java 面试

根据四个商业指标找到MOT

石云升

读书笔记 用户体验 商业洞察 关键时刻 7月日更

加速基因测序进程,北鲲云高性能计算平台再发力

北鲲云

深入浅出 Java 泛型,一文搞定

猴哥一一 cium

Java 翻译 泛型

一文读懂区块链技术如何改变非洲贸易(下)

CECBC

大厂的产品研发流程,你知道么?

Simon郎

产品 研发体系 大厂 互联网公司

Spring之 EL表达式

邱学喆

语法规则 Expression ExpressionParser ParserContext EvaluationContext

Vue进阶(七十八):Vue 定时器与 JS 定时器

No Silver Bullet

Vue 定时器 7月日更

什么是 Druid

HoneyMoose

Go语言:运行时反射,深度解析!

微客鸟窝

Go 语言

【Kafka技术专题】「实践操作篇」单机部署实践手册(2.8.0)

浩宇天尚

kafka MQ kafka配置 消息队列 kafka架构

近几天fil价格暴跌:fil还有希望吗?

区块链 分布式存储 IPFS fil fil行情

kubernetes/k8s CSI分析-容器存储接口分析

良凯尔

Kubernetes 源码分析 CSI Kubernetes Plugin #Kubernetes#

如何在Go 服务中做链路追踪

Rayjun

微服务 Go 语言

过去几个月里面的几家大厂(美团、字节、腾讯、阿里)均拿到 offer,最终去了字节跳动

Java 编程 程序员 架构 面试

AI解锁无人时代 仍需数据安全保驾护航

CECBC

继续

IT蜗壳-Tango

7月日更

从鉴黄师到阿里程序员,我成功拿下阿里offer

白亦杨

Java 编程 程序员 计算机

去阿里应聘P7Java岗,都会被问到哪些问题?

Java架构师迁哥

我应该在什么时候使用 Apache Druid

HoneyMoose

完整视频+源码!十六天带你精通基于Spring Cloud微服务电商项目

Java架构追梦

Java 架构 面试 微服务 SpringCloud

不要让这2个坏习惯限制了你的成长

俞凡

认知

Docker的学习体验

DisonTangor

,docker

用容器来学习Nginx反向代理_运维_Rosemary Wang_InfoQ精选文章