您听说过网关、代理(正向和反向)、负载均衡器、API 网关吗?您可能做到了,即使您还没有,您也可能听说过诸如nginx、Haproxy、Envoy、[TraefikTyk等等。您可能每次都在使用网关,甚至都不知道。

获取本文PDF 和 epub 格式

网关通常位于客户端和支持应用程序或服务之间。网关的工作是代理或“协商”客户端和服务器之间的通信。想象一下通过正门走进商场。当您走进去时,您可能会遇到以下情况之一:

商城目录 - Steven Damron 拍摄 (https://www.flickr.com/photos/sadsnaps/)

商场目录。无论您去哪个商场,您都会遇到一个目录,该目录显示所有商店的名称、它们的位置,并且可能还包括一张地图。假设您想参观乐高商店。你知道商店在商场里,但你不知道确切的地址(你为什么要知道?!)。幸运的是,知道商店的名称并使用目录查找商店在商场内的位置就足够了。

现在,如果我们将商城视为服务器(或服务器集群),那么商城中的商店就是在该服务器上运行的服务或应用程序。在这种情况下,客户端是您或您的计算机。如果商场里只有一家商店,问题就很简单了——只有一个地址,你就知道去哪里了。

客户端与服务器通信

然而,我们都知道这不是现实。就像商场里有数百家商店一样,服务器上也可以运行数百个或更多应用程序。我们可以说客户端记住所有地址至少是不切实际的,更不用说只有一个应用程序在服务器上运行。

客户端应该调用哪个服务?

回到类比 - 您可以将 mall 目录视为软件世界中的网关。此网关知道所有应用程序所在的位置。它知道服务器上运行的每个应用程序的实际地址(想想 IP 或完全限定的域名)。

就像您不需要知道商店的确切地址一样,客户端也不需要通过网关发出请求。

API 网关

如果您想访问乐高商店或调用应用程序,您请求stores.example.com/lego,网关将知道将请求转发或代理到实际地址stores.example.com/stores/level3/suite1610

可以在客户端和传入流量的应用程序之间放置一个网关,因此名称为 ingress gateway。客户端不需要向单独的应用程序发出请求,它只需要知道网关并向网关发出请求。

通过公共端点路由传入请求和公开 API 只是网关可以承担的一些职责。网关执行的其他典型任务是速率限制、SSL 终止、负载平衡等。

什么是限速?

速率限制器

将速率限制视为一个漏斗,每单位时间只允许一定数量的请求通过应用程序。如果我把商场类比延伸到黑色星期五——商店已经满了,所以你需要限制可以进入商店的人数。

速率限制器做了非常相似的事情——它限制了在一定时期内可以发出的请求数量。例如,对于每秒 10 个请求的速率限制,客户端每秒只能发出 10 个请求。如果客户端试图每秒发出超过 10 个请求,我们说它们的速率受到服务器的限制。在这种情况下,服务器的 HTTP 响应是 429: Too Many Requests

什么是 SSL 终止?

SSL 代表安全套接字层协议。 SSL 终止或也称为 SSL 卸载是解密加密流量的过程。 SSL 终止与网关模式完美配合。当加密流量到达网关时,它会在那里被解密,然后传递给后端应用程序。在网关级别执行 SSL 终止还可以减轻服务器的负担,因为您只在网关级别执行一次,而不是在每个应用程序中执行。

您可以在每个应用程序或服务中实现这些,如下图所示。

每个应用程序的 SSL 和速率限制

但是,如果在每个应用程序级别完成 SSL 终止和速率限制“成本”时间和资源。网关可以帮助卸载此功能并在网关级别执行一次。

SSL 和网关的速率限制

以下是可以在网关级别卸载和执行的一些功能的列表:

  • 认证

  • SSL 终止

  • 负载均衡

  • 速率限制

  • 断路

  • 基于客户端的响应转换

什么是断路?

断路是一种有助于提高服务弹性的模式。它用于防止在应用程序已经失败后向应用程序发出不必要的请求。您可以查看“什么是断路?”发布了解更多详情。

网关方法也有其缺点。它是您必须开发、维护或至少配置的附加软件(如果使用现有网关解决方案)。您还需要确保您的网关不会成为瓶颈 - 如果不需要,不要尝试将太多东西卸载到网关。

出口网关

另一方面(或另一边),出口网关在您的私有网络内运行,可用作退出网络的流量的出口点。例如,如果您的应用程序与外部 API(例如 Github)进行交互,则对https://api.github.com的任何请求都将首先通过出口网关,然后出口网关可以代理该调用到外部服务。

出口网关

为什么要使用出口网关?出口网关用于控制退出网络的所有流量。例如,如果您知道您的外部依赖项(例如 Github API),您可以阻止任何其他出站连接。如果您的服务受到威胁,阻止所有出站连接将阻止潜在的攻击者执行进一步的攻击。如果我们更进一步,您可以在专用机器上运行出口网关,您可以在其中应用更严格的安全策略并单独监控机器。另一种常见情况是您的服务器无法访问外部 IP 或公共互联网。在这种情况下,您的出口网关(可访问网络中的服务)充当任何外部请求的出口点。

实践中的网关

让我们用一个显示网关的一些基本功能的快速示例来结束这一点。我将使用HAProxy,但其他代理也可以实现相同的功能。您可以从 GitHub 存储库](https://github.com/peterj/square-service-gateway)获取[源代码。

GitHub 徽标peterj/square-service-gateway

我创建了一个名为 Square 的服务,它公开了一个 API。 API 从 URL 中获取一个参数(一个数字)并返回该数字的平方。该服务打包在 Docker 映像中。要在您的机器上运行它,您必须下载并安装 Docker。您可以按照说明下载和安装 Docker Desktop。

安装 Docker Desktop 后,打开终端窗口,让我们运行learnloudnative/square:0.1.0Docker 映像。

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文件定义了两个服务 -haproxysquare-servicedocker-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

进入全屏模式 退出全屏模式

我们对两个部分感兴趣 -frontendbackend。我们调用前端部分api_gateway,这是我们将代理绑定到地址和端口以及路由传入流量的位置。我们只是将default_backend设置为在前端部分之后定义的be_square后端。

在后端部分,我们正在创建一个名为s1的服务器,其端点为square-service:8080- 这是我们在docker-compose.yaml文件中为 square 服务定义的名称。

让我们使用 Docker compose 来运行这两个服务。确保从docker-compose.yamlhaproxy.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.cfg文件的末尾来启用 HAProxy 中的统计信息:

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 还支持健康检查。 HAProxy 可以配置为定期向后端服务发出 TCP 请求,以确保它们处于“活动状态”。要启用健康检查,您可以在haproxy.cfg文件中定义服务器后端的同一行添加单词check。像这样:

server s1 square-service:8080 check

进入全屏模式 退出全屏模式

更新配置文件后,停止 Docker 撰写 (CTRL+C),然后再次运行docker-compose up以重新启动容器。

如果您打开或刷新统计信息页面http://localhost:8404/stats,您会注意到be_square表中的行现在是绿色的,这意味着代理正在执行健康检查并且服务是健康的。在报告图例中,您将看到使用的active UP。此外,LastChk列将显示健康检查的结果。

拒绝请求

假设我们要保护我们超酷的 Square 服务,并要求用户在发出请求时提供 API 密钥。如果他们没有 API 密钥,我们不想让他们调用服务。

使用 HAProxy 执行此操作的一种方法是将其配置为拒绝所有未设置 API 标头的请求。为此,您可以在haproxy.cfg文件的前端部分中的bind命令之后添加以下行:

http-request deny unless { req.hdr(api-key) -m found }

进入全屏模式 退出全屏模式

此行告诉代理拒绝所有请求,除非设置了一个名为api-key的标头。让我们重新启动容器(CTRL+C 和docker-compose up),看看它是如何工作的。

如果您在没有设置api-key标头的情况下发出请求,您将收到 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 的内存存储。使用棒表,您可以存储请求的数量,然后在一定时间(在我们的例子中为 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 Too Many Requests 响应,如下所示:

$ 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 列将显示被拒绝的请求数。

结论

在本文中,我解释了网关或代理是什么,并展示了几个关于如何使用网关来实现速率限制或拒绝请求的实际示例。

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐