写点什么

用容器来学习 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:513654

评论

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

一招,让停车管理不再难

天翼云开发者社区

数字化 云平台

火眼金睛,天翼云助力打造城市视觉中枢

天翼云开发者社区

大数据 云平台

大数据ZooKeeper(一):基本知识和集群搭建

Lansonli

大数据 zookeeper 7月月更

KunlunBase指导手册(二)对等部署最佳实践

KunlunBase昆仑数据库

国产数据库

关于微软 Edge 浏览器的 Tracking Prevention 特性在 Angular 应用中的影响

汪子熙

JavaScript typescript Web web开发 7月月更

leetcode 455. Assign Cookies 分发饼干(简)

okokabcd

LeetCode 数据结构与算法 贪心算法

LeetCode-119. 杨辉三角II(java)

bug菌

Leet Code 7月月更

Python 入门指南之标准库概览

海拥(haiyong.site)

7月月更

心寄开源,合规护航!2022 开放原子全球开源峰会开源合规分论坛即将开幕

kk-OSC

开源 开源峰会 开放原子全球开源峰会 开源合规

阿里云第四届全球数据库大赛火热开赛,40万奖金广纳英才

科技热闻

APISIX 如何与 Hydra 集成,搭建集中认证网关助力企业安全

API7.ai 技术团队

云原生 网关 身份验证 APISIX 网关

SVN 修订版本关键字

攻城狮杰森

svn 关键字 7月月更

面试官:Linux操作系统里一个进程最多可以创建多少个线程?

Java全栈架构师

程序员 多线程 操作系统 计算机 java面试

内行,阿里大牛离职带出内部“高并发系统设计”学习手册

程序知音

Java 阿里巴巴 程序员 后端 高并发

揭露数据不一致的利器 —— 实时核对系统

Shopee技术团队

数据分析 后端

小程序表单组件-1

小恺

7月月更

【容器篇】Docker实现资源隔离的秘籍

技术小生

Docker 7月月更

天翼云携手华为,强强联合,共创数据存储新生态

天翼云开发者社区

存储 数字化

拔掉网线几秒,再插回去,原本的 TCP 连接还存在吗?

程序员小毕

程序员 程序人生 计算机网络 java面试 TCP协议

A tour of gRPC:04 - gRPC unary call 一元调用

BUG侦探

gRPC RPC protocolBuffer

KunlunBase 指导手册(一)快速安装手册

KunlunBase昆仑数据库

国产数据库

数据也能进超市

天翼云开发者社区

云计算 大数据 云平台

项目进度管理和风险管理记录

老猎人

数据库审计和日志审计的三大区别分析

行云管家

数据库 日志 日志审计 数据库审计

大数据环境搭建:​​​​​​​​​​​​​​​​​​​​​Hadoop编译和分布式环境搭建

Lansonli

大数据 hadoop 环境搭建 7月月更

为安全而生!云安全漫谈开讲啦

云安全 云计算运维

java零基础入门-多态

喵手

Java 7月月更

告别缺电焦虑!充电桩装上“智慧大脑”

天翼云开发者社区

云主机 云平台

2022数十位Java架构师汇总产出,最新25个技术栈“Java面经”

程序知音

Java 程序员 面试 后端 八股文

KunlunBase指导手册(三)数据导入&同步

KunlunBase昆仑数据库

国产数据库

数据库审计部署方式有哪些?哪种比较好?

行云管家

数据库 数据库审计 数据库审计部署

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