你听说过网关、代理(正反向)、负载均衡和API网关吗?很有可能你听说过,即便如此,你可能还没有听说过像nginx、HAProxy、Envoy、Traefik、Gloo、Kong、Ambassador、Tyk等这样的产品。你很有可能正在使用网关,只是每次都不自知。
网关通常驻留在客户端和后端应用或服务之间。网关的工作就是代理或"协商"客户端和服务器之间的通信。想象一下从主入口走进商场的情景。当你走进去的时候,你可能会看到这样的场面:

一个购物中心的目录。无论去的是哪个商场,你都会遇到导航目录,它展示了所有门店的名字、位置,并且有可能也包括了一张地图。假定你想要去逛一下乐高的门店。你知道它就在商场里,但是你不知道确切的地址(你为何应该知道呢?!)。幸运的是,知道门店的名字就够了,然后使用目录就能找到这个店在商场里的具体位置。
现在如果你把这个商场看作是一台服务器(或者是一个服务器的集群),那么商场里的门店就是跑在服务器上的服务或者应用了。在这个背景下,客户端就是你或者你的电脑。如果商场里仅有一家门店,那问题就简单多了,那里就唯一一个地址,你自然知道怎么走。

可是,我们都知道那不现实。就像在一个商场里有好几百个门店一样,跑在服务器上的应用也可以有好几百个或是更多。我们可能会说,让客户记住所有地址至少是不实际的,更不用说在服务器上只跑一个应用程序了。

回到那个类比——你可以把商场的导航目录看作是软件世界的一个网关。这个网关知道所有的应用位于何处。它知道跑在服务器上每一个应用的实际地址(可以想成是IPs或者完全限定的域名)。
正如你不需要知道门店的确切地址一样,客户端也不需要通过网关发出请求。

如果你想去乐高门店或者调用应用程序,你可以请求stores.example.com/lego,网关自然会知道转发或代理请求给实际的地址stores.example.com/stores/level3/suite1610。
网关可以放在客户端和应用之间来接收进来的流量,因此得名入口网关。不用客户端向单独的应用发送请求,它只需要知道应用并且发送请求给网关。
路由这些进来的请求并通过公共的终结点暴露API只是网关的部分职责。网关可以做的其他典型的任务包括限速、SSL终端、负载均衡等等。
什么是限速呢?

把限速想成是漏斗,只让每单位时间内一定数量的请求通过应用。如果我延伸商场的类比并结合黑色星期五,门店满了,所以你需要限制有多少人能进到店里来。
限速器做一些很类似的事情,它限制了在特定时期内可以发出请求的数目。例如,在每秒限速10个请求的情况下,客户端只能每秒发送10个请求。如果客户端试图每秒发送多于10次的请求,我们就说它们被服务器限速了。这种情形时,服务器的HTTP响应是 429:请求太多了。
什么是SSL终结?
SSL代表了secure socket layer安全套接层协议。SSL终结或称SSL offloading是把加密流量解密的过程。SSL终结与网关模式合作简直完美。当加密的流量到达网关时,它在那里被解密,然后被传送到后台应用。在网关级别进行SSL终结也减少了服务器的负担,因为你只在网关级别做一次,而不是在每个应用里这么做。
你可以在每个应用或服务上实现这些,就像下图所展示的。

可是,如果在每个应用级别做的话,SSL终结和限速都会花费时间和资源。网关可以帮助分担这项功能,并且在网关级别执行一次。

以下是可以在网关级别分担和执行的一些功能的一个列表:
- 认证
- SSL终结
- 负载均衡
- 限速
- 断路器
- 基于客户端的响应转换
什么是断路器呢?
断路器是一种有助于服务提高弹性的模式。它可以用来防止在应用已经失败时对其发出不必要的请求。更多细节请查看相关文章。
出口网关
另一方面,出口网关在你的私有网络里运行,可以用来作为离开你网络的流量的一个出口点。例如,如果你的应用同一个外部的API交互(例如Github),任何发送给https://api.github.com的请求都会首先经过出口网关,然后出口网关能代理对外部服务的调用。

为什么你想要使用出口网关呢?出口网关用来控制正在从你的网络出去的所有流量。例如,如果你知道你的外部依赖(例如Github API),你就可以阻止任何其他的出站连接。为了防止你的服务被破坏,阻止所有出站连接可以预防潜在的攻击者执行进一步的攻击。如果我们更进一步,你可以在专用的机器上运行出口网关,在那里你可以应用更严格的安全策略并单独监视机器。另一种常见的情况是你的服务器无法访问外部IP或公网。在这种情况下,你的出口网关(对你网络中的服务可访问)充当任何外部请求的出口点。
网关实践
让我们用一个简单的例子来结束这个话题,这个例子展示了网关的一些基本特性。我将使用HAProxy,但是同样的功能也可以用其他的代理来实现。你可以从GitHub仓库中获得源码。
我创建了一个名为Square的服务,它暴露了一个仅有的API。API从URL中获取一个参数(一个数字)并返回该数字的平方。这个服务打包成了Docker镜像。要在你的机器上运行它的话,你必须*载下**并安装Docker。可以按照说明*载下**和安装Docker Desktop。
在安装好Docker Desktop之后,打开终端窗口,运行learncloudnative/square:0.1.0 Docker镜像。
docker run -p 8080:8080 learncloudnative/square:0.1.0
第一次运行上述命令时,可能需要花点时间,因为Docker需要*载下**(如果使用Docker术语的话是拉取)镜像。由于Square服务暴露了一个API,所以我们需要提供一个端口号来访问API。因此,-p 8080:8080——第一个8080表示我们希望在本机上的8080端口暴露服务,第二个8080表示服务正在监听的端口号。
*载下**完镜像且容器在运行了,你会看到像这样的信息:
$ docker run -p 8080:8080 learncloudnative/square:0.1.0
{"level":"info","msg":"Running on 8080","time":"2020-04-25T21:20:01Z"}
让我们尝试向服务发送一个请求。打开第二个终端窗口,让服务继续运行,并运行一下命令
$ curl localhost:8080/square/25
625
服务返回结果(625)响应,你会注意到在前一个终端窗口,请求也被记录了:
$ docker run -p 8080:8080 learncloudnative/square:0.1.0
{"level":"info","msg":"Running on 8080","time":"2020-04-25T21:20:01Z"}
{"level":"info","msg":"GET | /square/25 | curl/7.64.1 | 6.781µs","time":"2020-04-25T21:22:50Z"}
你可以按下CTRL+C停止容器的运行。
添加代理
为了让事情更容易运行,我将使用Docker Compose来运行Square服务和HAProxy实例。如果你不熟悉Docker Compose,请不要担心,它只是同时运行多个Docker容器的一种方式。
docker-compose.yaml文件定义了两个服务,haproxy和square-service。docker-compose.yaml文件如下所示:
version: '3'
services:
haproxy:
image: haproxy:1.7
volumes:
- ./:/usr/local/etc/haproxy:ro
ports:
- '5000:80'
links:
- square-service
square-service:
image: learncloudnative/square:0.1.0
除了compose文件,我们还需要一个配置文件来配置HAProxy应该做什么。请记住,我们不会直接调用square-service,相反,我们会发请求给代理,然后代理会将请求传给square-service。
HAProxy使用文件haproxy.cfg来配置,文件有如下内容:
global
maxconn 4096
daemon
defaults
log global
mode http
timeout connect 10s
timeout client 30s
timeout server 30s
frontend api_gateway
bind 0.0.0.0:80
default_backend be_square
# Backend is called `be_square`
backend be_square
# There's only one instance of the server and it
# points to the `square-service:8080` (name is from the docker-compose)
server s1 square-service:8080
我们对两部分感兴趣,frontend和backend。我们会调用frontend部分的api_gateway,这是我们将代理绑定到地址和端口以及路由传入流量的位置。我们简单地把default_backend设置成be_square,这个后端定义就在前端定义之后。
在backend部分,我们生成了一台服务器叫做s1,它的终结点是square-service:8080,这就是我们在docker-compose.yaml文件里定义的square服务的名字。
让我们使用Docker compose把这两个服务跑起来。确保在docker-compose.yaml和haproxy.cfg文件所处文件夹的位置执行以下命令:
$ docker-compose up
Starting gateway_square-service_1 ... done
Starting gateway_haproxy_1 ... done
Attaching to gateway_square-service_1, gateway_haproxy_1
square-service_1 | {"level":"info","msg":"Running on 8080","time":"2020-04-25T21:41:12Z"}
haproxy_1 | <7>haproxy-systemd-wrapper: executing /usr/local/sbin/haproxy -p /run/haproxy.pid -db -f /usr/local/etc/haproxy/haproxy.cfg -Ds
Docker compose完成了它的工作,创建了一个新的网络和两个服务。从第二个终端,再一次运行curl命令:
$ curl localhost:5000/square/25
625
注意到这一次,我们使用了5000端口,因为这正是HAProxy暴露的端口(查看docker-compose.yaml文件的端口部分)。就像以前一样,你得到了从Square服务返回的响应。这次的不同之处在于请求首先经过了代理。
你可以再一次按下CTRL+C来停止Docker compose的运行。
启用HAProxy统计
由于每次请求经过了代理,它就可以收集关于请求、前端、后端服务器的统计信息。
让我们启用HAProxy的统计,可以通过往haproxy.cfg文件的末尾添加如下几行:
listen stats
bind *:8404
stats enable
stats uri /stats
stats refresh 5s
以上代码在端口8404和URI /stats上启用了统计。因为我们想要从代理访问统计信息,我们还需要在文件docker-compose.yaml里把它暴露出来。在文件docker-compose.yaml里在关键字ports下添加一行"8404:8404":
ports:
- "5000:80"
- "8404:8404"
若docker-compose在运行,按下CTRL+C停掉它,然后运行docker-compose down把服务移除,接着运行docker-compose up把它们再启动。
一旦容器启来后,在浏览器中打开http://localhost:8404/stats。运行curl localhost:5000/square/25做几次请求,进而产生一些数据。你就会注意到在HAProxy的统计报告里的会话数量。

启用健康检查
HAProxy也支持健康检查。HAProxy可以做配置,定期地发出TCP请求给后台服务,确保它们还在活动。为了启用健康检查,你可以在定义在haproxy.cfg文件的服务器后台的同一行添加check命令。像下面这样:
server s1 square-service:8080 check
一旦你更新了配置文件,就停下Docker compose(CTRL+C),然后再一次运行docker-compose up命令重启容器。
若你打开后刷新统计页http://localhost:8404/stats,你就会注意到在表be_square里的行现在是绿色的,这就意味着代理在执行健康检查并且服务是健康的。在报告的图例里,你会看到使用了active UP。另外,LastChk列会展示健康检查的结果。
拒绝请求
假如我们想要保护超级酷的Square服务,并且需要用户在发出请求时提供API key。如果他们没有API key,我们就不想让他们调用服务。
使用HAProxy时的一种办法是,以这么一种方式来配置它,这种方式拒绝没有API header集的所有请求。为达成目的,你可以在文件haproxy.cfg的frontend部分的bind命令之后添加如下行:
http-request deny unless { req.hdr(api-key) -m found }
这一行是告诉代理拒绝所有请求,除非有一个叫api-key集的header。让我们重启容器(CTRL+C然后docker-compose up)看看这是如何工作的。
如果你发出一个请求没带api-key header集,你就会得到403的响应,像这样:
$ curl localhost:5000/square/25
<html><body><h1>403 Forbidden</h1>
Request forbidden by administrative rules.
</body></html>
但是,如果你包含了一个api-key头,代理就让请求通过,然后就像以前一样得到来自服务的响应:
$ curl -H "api-key: hello" localhost:5000/square/25
625
限速请求
最后,让我们也实现一个限速器,以便单个用户不会发出太多请求进而给服务带来不必要的负担。
我们必须在haproxy.cfg文件里定义一些东西。我首先会单独解释它们然后把它们放在一起。
存储/计数请求
为了让计数器能正确地工作,我们需要一种方式来统计和存储发出的请求数。我们会使用内存中的存储,HAProxy称之为stick table。有了stick table你就能存储请求数然后在一定时间之后(我们这里是5分钟)自动终止它们(把它们移除)。
stick-table type string size 1m expire 5m store http_req_cnt
设置请求限制
我们还需要设定限制。这个限制是一个数字,在这个点上我们就会开始拒绝(或限速)请求。我们会使用一个访问控制列表或ACL来测试条件(例如,请求数比X大吗)并且据此执行动作(例如拒绝请求):
acl exceeds_limit req.hdr(api-key),table_http_req_cnt(api_gateway) gt 10
以上这行检查了带有特定api-key值的请求数是否超过了10。如果限定没被超的话,我们就会追踪这个请求,允许它继续:
http-request track-sc0 req.hdr(api-key) unless exceeds_limit
否则,如果超过了限定,我们就拒绝请求:
http-request deny deny_status 429 if exceeds_limit
所有这些变动需要在文件haproxy.cfg的frontend api_gateway部分做出。这里是该有的样子:
frontend api_gateway
bind 0.0.0.0:80
# Deny the request unless the api-key header is present
http-request deny unless { req.hdr(api-key) -m found }
# Create a stick table to track request counts
# The values in the table expire in 5m
stick-table type string size 1m expire 5m store http_req_cnt
# Create an ACL that checks if we exceeded the value of 10 requests
acl exceeds_limit req.hdr(api-key),table_http_req_cnt(api_gateway) gt 10
# Track the value of the `api-key` header unless the limit was exceeded
http-request track-sc0 req.hdr(api-key) unless exceeds_limit
# Deny the request with 429 if limit was exceeded
http-request deny deny_status 429 if exceeds_limit
default_backend be_square
....
是时候试一下了!重启容器并对服务发起10次请求。在第十一次请求的时候,你会收到响应429 请求次数太多,像这样:
$ curl -H "api-key: hello" localhost:5000/square/25
<html><body><h1>429 Too Many Requests</h1>
You have sent too many requests in a given amount of time.
</body></html>
你可以等5分钟让限速器的信息过期,或者,只是尝试使用一个不同的api-key,你会注意到请求就会通过:
$ curl -H "api-key: hello-1" localhost:5000/square/25
625
最后,你可以再次检查统计页,特别是在api_gateway表中的Denied列。Denied列会展示出被拒绝的请求数。
总结
在本文中,解释了网关和代理是什么,展示了几个实际的例子,用来说明如何使用网关来实现限速或拒绝请求。