NVIDIA 初创加速计划,免费加速您的创业启动 了解详情
写点什么

用容器来学习 Nginx 反向代理

  • 2020-04-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-04-22 08:513371

评论

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

面试官:什么时候 MySQL 查询会变慢?

做梦都在改BUG

Java MySQL 数据库

测试左移右移,到底是什么?

老张

测试右移 测试左移

【Netty】「NIO」(二)阻塞模式与非阻塞模式

sidiot

Java 后端 Netty 6 月 优质更文活动

前端AST详解,手写babel插件

不叫猫先生

6 月 优质更文活动

中企出海,数智人力重构企智人效的人才供应体系

用友BIP

人力资源 中企出海 数智人力

亚信科技AntDB数据库荣获赛宝优选信创优秀解决方案奖

亚信AntDB数据库

AntDB AntDB数据库 企业号 6 月 PK 榜

数字经济快速发展下企业需要什么样的数智平台?

用友BIP

企业数智化 数智平台

Flink实例:处理IoT事件流

TiAmo

IoT Flink实例 事件流 6 月 优质更文活动

漫画告诉你什么是生成式 AI(二)

FN0

AIGC

深度学习应用篇-计算机视觉-图像分类[2]:LeNet、AlexNet、VGG、GoogleNet、DarkNet模型结构、实现、模型特点详细介绍

汀丶人工智能

人工智能 深度学习 计算机视觉 图像分类 6 月 优质更文活动

人工智能创新挑战赛:海洋气象预测Baseline[4]完整版(TensorFlow、torch版本)含数据转化、模型构建、MLP、TCNN+RNN、LSTM模型训练以及预测

汀丶人工智能

人工智能 数据挖掘 深度学习 LSTM 6 月 优质更文活动

选择优质的led透明屏的7个步骤

Dylan

产品、 LED显示屏 售后服务

聚焦数智人力,让员工“跳槽”不再尴尬!

用友BIP

人力资源 数智人力

BH1750 传感器实战教学 —— 驱动移植篇

矜辰所致

传感器 I2C 6 月 优质更文活动

跨平台开发工具 kbone 实操经验分享

Onegun

小程序 跨平台 前端框架 kbone

vue2.x,vue3.x使用provide/inject注入区别

不叫猫先生

Vue 6 月 优质更文活动

线程池性能提升之道:核心线程数选择与定制实战

做梦都在改BUG

Java 多线程 线程池

【Netty】「NIO」(三)剖析 Selector

sidiot

Java 后端 Netty 6 月 优质更文活动

顶象发布《车企App安全研究白皮书》,剖析车企App两大风险

说山水

Vue3项目中Pinia状态管理工具的使用

不叫猫先生

Vue Pinia 6 月 优质更文活动

GaussDB(DWS)查询过滤器原理与应用

华为云开发者联盟

数据库 华为云 华为云开发者联盟 企业号 6 月 PK 榜

前端内存泄漏详解

不叫猫先生

内存泄露 6 月 优质更文活动

【体验有奖】玩转 AIGC,函数计算 x 通义千问预体验,一键部署 AI 应用赢 Airpods

阿里巴巴云原生

阿里云 云原生 AIGC

立即报名 | Microservices June 微服务之月再度开启

NGINX开源社区

nginx 微服务

【JavaScript】手写Promise

不叫猫先生

JavaScript Promise 6 月 优质更文活动

【云原生】Docker的基本使用方法与优势

不叫猫先生

Docker 6 月 优质更文活动

进阶篇丨链路追踪(Tracing)很简单:链路成本指南

阿里巴巴云原生

阿里云 云原生 链路追踪

旺链科技赋能泳池卫士守护人身安全

旺链科技

区块链 泳池安全 泳池卫士

vue2.x与vue3.x中自定义指令详解

不叫猫先生

Vue 自定义指令

漫画告诉你什么是生成式AI(一)

FN0

AIGC

Vue项目中实现ElementUI按需引入

不叫猫先生

Vue Element UI 按需加载 6 月 优质更文活动

用容器来学习Nginx反向代理_软件工程_Rosemary Wang_InfoQ精选文章