点击查看目录
本文为翻译文章,点击查看原文。
基于 Kubernetes 云原生的Ambassador API网关所提供的速率限制功能是完全可定制的,其允许任何实现 gRPC 服务端点的服务自行决定是否需要对请求进行限制。本文在先前第 1 部分和第 2 部分的基础上,阐述如何为 Ambassador API 网关创建和部署简单的基于 Java 的速率限制服务。
部署 Docker Java Shop
在我之前的教程“使用 Kubernetes 和 Ambassador API 网关部署 Java 应用”中,我将开源的 Ambassador API 网关添加到现有的一个部署于 Kubernetes 的 Java(Spring Boot 和 Dropwizard)服务中。如果你之前不了解这个,建议你先阅读下此教程及其他相关内容来熟悉基础知识。 本文假定你熟悉如何构建基于 Java 的微服务并将其部署到 Kubernetes,同时已经完成安装所有的必备组件(我在本文中使用Docker for Mac Edge,并启用其内置的 Kubernetes 支持。若使用 minikube 或远程群集应该也类似)。
先决条件
需要在本地安装:
- Docker for Desktop:我使用 edge community edition (18.04.0-ce),内置了对本地 Kubernetes 集群的支持。由于 Java 应用对内存有一定要求,我还将 Docker 可用内存增加到 8G。
- 编辑器选择:Atom 或者 VS code;当写 Java 代码时也可以使用 IntelliJ。
你可以在这里获取最新版本的“Docker Java Shop”源代码:
https://github.com/danielbryantuk/oreilly-docker-java-shopping
你可以通过如下命令使用 SSH 克隆仓库:
$ git clone git@github.com:danielbryantuk/oreilly-docker-java-shopping.git
第一阶段的服务和部署架构如下图所示:
从图中可以看到,Docker Java Shopping 应用程序主要由三个服务组成。在先前的教程中,你已经添加 Ambassador API 网关作为系统的“front door”(大门)。需要注意的是,Ambassador API 网关直接使用 Web 80 号端口,因此需要确保本地运行的其他应用没有占用该端口。
Ambassador API 网关速率限制入门
我在本教程的仓库中增加了一个新文件夹“kubernetes-ambassador-ratelimit”,用于包含 Kubernetes 相关配置。请通过命令行导航到此目录。此目录应包含如下文件:
(master *) oreilly-docker-java-shopping $ cd kubernetes-ambassador-ratelimit/
(master *) kubernetes-ambassador-ratelimit $ ll
total 48
0 drwxr-xr-x 8 danielbryant staff 256 23 Apr 09:27 .
0 drwxr-xr-x 19 danielbryant staff 608 23 Apr 09:27 ..
8 -rw-r — r — 1 danielbryant staff 2033 23 Apr 09:27 ambassador-no-rbac.yaml
8 -rw-r — r — 1 danielbryant staff 698 23 Apr 10:30 ambassador-rate-limiter.yaml
8 -rw-r — r — 1 danielbryant staff 476 23 Apr 10:30 ambassador-service.yaml
8 -rw-r — r — 1 danielbryant staff 711 23 Apr 09:27 productcatalogue-service.yaml
8 -rw-r — r — 1 danielbryant staff 659 23 Apr 10:02 shopfront-service.yaml
8 -rw-r — r — 1 danielbryant staff 678 23 Apr 09:27 stockmanager-service.yaml
你可以使用以下命令来提交 Kubernetes 配置:
$ kubectl apply -f .
通过以上命令部署,这与之前架构的区别在于添加了ratelimiter
服务。这个服务是用 Java 编写的,且没有使用微服务框架。它发布了一个 gRPC 端点,可供 Ambassador 来使用以实现速率限制。这种方案允许灵活定制速率限制算法(关于这点的好处请查看我以前的文章)。
探索部署于 Kubernetes 的限速器服务
与任何其他服务一样,部署到 Kubernetes 的限速服务也可以根据需要进行水平扩展。以下是 Kubernetes 配置文件ambassador-rate-limiter.yaml
的内容:
---
apiVersion: v1
kind: Service
metadata:
name: ratelimiter
annotations:
getambassador.io/config: |
---
apiVersion: ambassador/v0
kind: RateLimitService
name: ratelimiter_svc
service: "ratelimiter:50051"
labels:
app: ratelimiter
spec:
type: ClusterIP
selector:
app: ratelimiter
ports:
- protocol: TCP
port: 50051
name: http
---
apiVersion: v1
kind: ReplicationController
metadata:
name: ratelimiter
spec:
replicas: 1
template:
metadata:
labels:
app: ratelimiter
spec:
containers:
- name: ratelimiter
image: danielbryantuk/ratelimiter:0.3
ports:
- containerPort: 50051
这里不需要关注最后 Docker Image 处的danielbryantuk/ratelimiter:0.3
,而需要注意的是:此服务在集群使用 50051 TCP 端口。
在ambassador-service.yaml
配置文件中,还更新了 Ambassador Kubernetes annotations 配置,以确保能通过包含rate_limits
属性来限制对 shopfront 服务的请求。我还添加了一些额外的元数据- descriptor: Example descriptor
,这将在下一篇文章中更详细地解释。这里我们需要注意的是,如果要将元数据传递到速率限制服务,这种方法不错。
---
apiVersion: v1
kind: Service
metadata:
labels:
service: ambassador
name: ambassador
annotations:
getambassador.io/config: |
---
apiVersion: ambassador/v0
kind: Mapping
name: shopfront_stable
prefix: /shopfront/
service: shopfront:8010
rate_limits:
- descriptor: Example descriptor
你可以使用 kubectl 命令来检查部署是否成功:
(master *) kubernetes-ambassador-ratelimit $ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
ambassador LoadBalancer 10.105.253.3 localhost 80:30051/TCP 1d
ambassador-admin NodePort 10.107.15.225 <none> 8877:30637/TCP 1d
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 16d
productcatalogue ClusterIP 10.109.48.26 <none> 8020/TCP 1d
ratelimiter ClusterIP 10.97.122.140 <none> 50051/TCP 1d
shopfront ClusterIP 10.98.207.100 <none> 8010/TCP 1d
stockmanager ClusterIP 10.107.208.180 <none> 8030/TCP 1d
6 个业务服务看起来都不错(去除 Kubernetes 服务):包含 3 个 Java 服务,2 个 Ambassador 服务和 1 个 ratelimiter 服务。
你可以通过 curl 命令对 shopfront 的服务端点进行测试,其应绑定在外部 IP localhost 的 80 端口上(如上文所示):
(master *) kubernetes-ambassador-ratelimit $ curl localhost/shopfront/
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8" />
...
</div>
</div>
<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
<!-- Include all compiled plugins (below), or include individual files as needed -->
<script src="js/bootstrap.min.js"></script>
</body>
</html>(master *) kubernetes-ambassador-ratelimit $
你会注意到这里显示了一些 HTML,这只是 Docker Java Shop 的首页。虽然可以通过浏览器在http://localhost/shopfront/访问,对于我们的速率限制实验,最好还是使用 curl 命令。
速率限制测试
对于这种演示性质的速率限制服务,这里仅对服务本身进行限制。比如当速率限制服务需要计算是否需要限制请求时,唯一需要考虑的指标是在一段时间内针对特定后端的请求数量。在代码实现中使用令牌桶算法。假设桶中令牌容量为 20,并且每秒钟的补充 10 个令牌。由于速率限制与请求相关联,这意味着你可以每秒发出 10 次 API 请求,这没有任何问题,同时由于存储桶最初包含 20 个令牌,你可以暂时超过此并发数量。但是,一旦最初额外的令牌使用完,并且你仍在尝试每秒发出 10 个以上请求,那么你将收到 HTTP 429“Too Many Requests”状态码。这时,Ambassador API 网关不会再将请求转发到后端服务。
让我看下如何通过 curl 发出大量请求来模拟这个操作。避免显示的 HTML 页面(通过-output /dev/null
参数)及 curl 请求(通过--silent
参数),但需要显示符合预期的 HTTP 响应状态(通过-- show-error --fail
参数)。下文通过一个 bash 循环脚本,并记录时间输出(以显示发出请求的时间),以此来创建一个非常粗颗粒度的负载发生器(可以通过CTRL-C
来终止循环):
$ while true; do curl --silent --output /dev/null --show-error --fail http://localhost/shopfront/; echo -e $(date);done
(master *) kubernetes-ambassador-ratelimit $ while true; do curl --silent --output /dev/null --show-error --fail http://localhost/shopfront/; echo -e $(date);done
Tue 24 Apr 2018 14:16:31 BST
Tue 24 Apr 2018 14:16:31 BST
Tue 24 Apr 2018 14:16:31 BST
Tue 24 Apr 2018 14:16:31 BST
...
Tue 24 Apr 2018 14:16:35 BST
curl: (22) The requested URL returned error: 429 Too Many Requests
Tue 24 Apr 2018 14:16:35 BST
curl: (22) The requested URL returned error: 429 Too Many Requests
Tue 24 Apr 2018 14:16:35 BST
Tue 24 Apr 2018 14:16:35 BST
curl: (22) The requested URL returned error: 429 Too Many Requests
Tue 24 Apr 2018 14:16:35 BST
curl: (22) The requested URL returned error: 429 Too Many Requests
Tue 24 Apr 2018 14:16:35 BST
^C
如你所见,从输出日志来看,前几个请求显示日期且没有错误,一切正常。过不了多久,当在我测试的 Mac 上的请求循环超过每秒 10 次,HTTP 429 错误便开始出现。
顺便说一下,我通常使用 Apache Benchmarking “ab” 负载生成工具来进行这种简单实验,但这工具在调用本地 localhost 会有问题(同时 Docker 配置也给我带来了额外问题)。
检验速率限制器服务
Ambassador Java 限速服务的源代码在我 GitHub 帐户的ambassador-java-rate-limiter仓库中。其中也包含用于构建我推送到 DockerHub 中容器镜像的Dockerfile。你可以以此 Dockerfile 作为模板进行修改,然后构建和推送自己的镜像至 DockerHub。你也可以修改在 Docker Java Shopping 仓库中的ambassador-rate-limiter.yaml文件来扩展使用你自己的速率限制服务。
研究 Java 代码
如果你深入研究 Java 代码,最需要关注的类应该是RateLimiterServer,它实现了在 Ambassador API 中使用的Envoy 代理所定义的速率限制 gRPC 接口。我创建了一个ratelimit.proto接口的副本,其通过 Maven pom.xml中定义的 gRPC Java 构建工具来构建使用。代码主要涉及三点:实现 gRPC 接口,运行 gRPC 服务器,并实现速率限制。下面让我们来进一步分析。
实现速率限制 gRPC 接口
查看RateLimitServer
中的内部类RateLimiterImpl
,其对RateLimitServiceGrpc.RateLimitServiceImplBase
进行扩展,你可以看到此抽象类中的下列方法被重写:
public void shouldRateLimit(Ratelimit.RateLimitRequest rateLimitRequest, StreamObserver<Ratelimit.RateLimitResponse> responseStreamObserver)
这里使用的很多命名规约来自于 Java gRPC 库,进一步信息请参阅gRPC Java 文档。尽管这样,如果查看ratelimit.proto文件,你可以清楚看到很多命名根,这些命名根定义了在 Ambassador 中使用的 Envoy 代理所需要的速率限制接口。例如,你可以看到此文件中定义的核心服务名为RateLimitService
(第 9 行),并且在服务rpc ShouldRateLimit (RateLimitRequest) returns (RateLimitResponse) {}
(第 11 行)中定义了一个 RPC 方法,它在 Java 中实现通过上面所定义的shouldRateLimit
方法。
如果有兴趣,可以看看那些由protobuf-maven-plugin
(pom.xml的第 99 行)生成的 Java gRPC 代码。
运行 gRPC 服务器
一旦你实现了用ratelimit.proto
定义的 gRPC 接口,下一件事情就是创建一个 gRPC 服务器用来监听和回复请求。可以根据main
方法调用链来查看RateLimitServer的内容。简而言之,main
方法创建一个RateLimitServer
类的实例,调用start()
方法,再调用blockUntilShutdown()
方法。这将启动一个应用实例,并在指定的服务端点上发布 gRPC 接口,同时侦听请求。
实现 Java 速率限制
负责速率限制过程的实际 Java 代码包含在RateLimiterImpl
内部类的shouldRateLimit()
方法(第 75 行)中。我没有自己实现算法,而是使用基于令牌桶算法的 Java 速度限制开源库bucket4j。由于我限制了对每个服务的请求,因此每个存储桶与服务名称所绑定。对每个服务的请求都会从其所关联的存储桶中删除一个令牌。在本案例中,桶没有存储在外部数据库,而是存储在内存中的ConcurrentHashMap
中。如果在生产环境中,通常会使用类似 Redis 的外部持久化存储方案来实现横向扩展。这里必须注意,如果在不更改每个服务桶限制的前提下水平扩展速率限制服务,那么将直接导致(非速率限制)请求数量的增加,但实际服务可支持的请求数量没有增加。
创建 bucket4j 存储桶的RateLimiterImpl
大致代码如下:
private Bucket createNewBucket() {
long overdraft = 20;
Refill refill = Refill.smooth(10, Duration.ofSeconds(1));
Bandwidth limit = Bandwidth.classic(overdraft, refill);
return Bucket4j.builder().addLimit(limit).build();
}
在下面可以看到shouldRateLimit
方法的代码,它只是简单地尝试执行tryConsume(1)
使用桶中一个令牌,并返回适当的 HTTP 响应。
@Override
public void shouldRateLimit(Ratelimit.RateLimitRequest rateLimitRequest, StreamObserver<Ratelimit.RateLimitResponse> responseStreamObserver) {
logDebug(rateLimitRequest);
String destServiceName = extractDestServiceNameFrom(rateLimitRequest);
Bucket bucket = getServiceBucketFor(destServiceName);
Ratelimit.RateLimitResponse.Code code;
if (bucket.tryConsume(1)) {
code = Ratelimit.RateLimitResponse.Code.OK;
} else {
code = Ratelimit.RateLimitResponse.Code.OVER_LIMIT;
}
Ratelimit.RateLimitResponse rateLimitResponse = generateRateLimitResponse(code);
responseStreamObserver.onNext(rateLimitResponse);
responseStreamObserver.onCompleted();
}
代码比较容易解释。如果当前请求不需要进行速率限制,则此方法返回Ratelimit.RateLimitResponse.Code.OK;
如果当前请求由于速度限制而被拒绝,则此方法返回Ratelimit.RateLimitResponse.Code.OVER_LIMIT
。根据此 gRPC 服务的响应,Ambassador API 网关将请求传递给后端服务,或者中断请求并返回 HTTP 状态码 429“Too Many Requests”而不再调用后端服务。
这个简单案例只可以防止一个服务的访问过载,但也希望这能够阐明速率限制的核心概念,进而可以相对容易实现基于请求元数据(例如用户 ID 等)的速率限制。
下一阶段
本文演示了如何在 Java 中创建速率限制服务,并轻易与 Ambassador 网关所集成。如果需要,你也可以基于任何自定义的速率限制算法实现。在本系列的最后一篇文章中,您将更深入地了解 Envoy 速率限制 API,以便进一步学习如何设计速率限制服务。
如果有任何疑问,欢迎在 Ambassador Gitter 或通过@danielbryantuk及@datawireio联系。