在当今分布式的世界中,单体架构越来越多地被多个,更小,相互连接的服务(不管是好是坏)所取代,代理和负载平衡技术似乎正在复兴。除了老玩家以外,近年来还涌现出几种新的代理技术,它们以各种技术实现,并以不同的功能进行普及,例如易于集成到某些云提供商(“云原生”),高性能和低内存占用,或动态配置。
可以说,两种最流行的“经典”代理技术是NGINX(C)和HAProxy(C),而其中的一些新成员是Zuul(Java),Linkerd(Rust),Traefik(Go),Caddy(Go)和Envoy(C++)。
所有这些技术具有不同的功能集,并且针对某些特定场景或托管环境(例如,Linkerd经过微调,可在Kubernetes中使用)。
在本文中,我将不做这些比较,而只是关注一个特定的场景:如何将Envoy用作Kubernetes中运行的服务的负载平衡器。
Envoy是最初在Lyft实施的“高性能C++分布式代理”,但此后得到了广泛的采用。它性能高,资源占用少,支持由“控制平面” API管理的动态配置,并提供了一些高级功能,例如各种负载平衡算法,限流,熔断和影子镜像。
由于多种原因,我选择Envoy作为负载平衡器代理:
- 除了可以通过控制平面API动态控制之外,它还支持基于YAML的简单,硬编码配置,这对我而言很方便,并且易于入门。
- 它内置了对称为STRICT_DNS的服务发现技术的支持,该技术基于查询DNS记录,并期望看到上游群集每个节点都有IP地址的A记录。这使得Kubernetes中的无头服务变得易于使用。
- 它支持各种负载平衡算法,其中包括“最少请求”。
在开始使用Envoy之前,我是通过类型为LoadBalancer的服务对象访问Kubernetes中的服务的,这是从Kubernetes中从外部访问服务的一种非常典型的方法。负载均衡器服务的确切工作方式取决于托管环境。我使用的是Google Kubernetes引擎,其中每个负载平衡器服务都映射到TCP级别的Google Cloud负载平衡器,该负载平衡器仅支持循环负载平衡算法。
就我而言,这是一个问题,因为我的服务具有以下特征:
- 这些请求长期运行,响应时间从100ms到秒不等。
- 请求的处理占用大量CPU,实际上一个请求的处理使用了一个CPU内核的100%。
- 并行处理许多请求会降低响应时间。 (这是由于该服务的工作原理的内部原因,它不能有效地并行运行少数几个请求。)
由于上述特性,轮循负载均衡算法不太适合,因为经常(偶然)多个请求最终在同一节点上结束,这使得平均响应时间比群集的平均响应时间差得多。所以需要分配更均匀的负载。
在本文的其余部分中,我将描述将Envoy部署为在Kubernetes中运行的服务之前用作负载平衡器的必要步骤。
1. 为我们的应用创建headless服务
在Kubernetes中,有一种称为headless服务的特定服务,恰好与Envoy的STRICT_DNS服务发现模式一起使用时非常方便。
Headless服务不会为底层Pod提供单个IP和负载平衡,而只是具有DNS配置,该配置为我们提供了一个A记录,其中包含与标签选择器匹配的所有Pod的Pod IP地址。我们希望在实现负载平衡并自己维护与上游Pod的连接的情况下使用此服务类型,这正是我们使用Envoy可以做到的。
我们可以通过将.spec.clusterIP字段设置为“None”来创建headless服务。因此,假设我们的应用程序pod的标签app的值为myapp,我们可以使用以下yaml创建headless服务。
服务的名称不必等于我们的应用程序名称或应用程序标签,但这是一个很好的约定。
现在,如果我们在Kubernetes集群中检查服务的DNS记录,我们将看到带有IP地址的单独的A记录。如果我们有3个Pod,则会看到与此类似的DNS摘要。
$ nslookup myapp
Server: 10.40.0.10
Address: 10.40.0.10#53
Non-authoritative answer:
Name: myapp.namespace.svc.cluster.local Address: 10.36.224.5
Name: myapp.namespace.svc.cluster.local Address: 10.38.187.17
Name: myapp.namespace.svc.cluster.local Address: 10.38.1.8
Envoy的STRICT_DNS服务发现的工作方式是,它维护DNS返回的所有A记录的IP地址,并且每隔几秒钟刷新一次IP组。
2. 创建Envoy镜像
在不以动态API形式提供控制平面的情况下使用Envoy的最简单方法是将硬编码配置添加到静态yaml文件中。
以下是一个基本配置,该配置将负载均衡到域名myapp给定的IP地址。
注意以下几个部分:
-
type: STRICT_DNS
:在这里,我们指定服务发现类型。将其设置为STRICT_DNS很重要,因为它可以与我们设置的无头服务一起使用。 -
lb_policy:LEAST_REQUEST
:我们可以从各种负载平衡算法中进行选择,可能ROUND_ROBIN和LEAST_REQUEST是最常见的。(请记住,LEAST_REQUEST不会检查所有上游节点,而只会从2个随机选择的选项中进行选择。) -
hosts: [{ socket_address: { address: myapp, port_value: 80 }}]
:这是我们在其中用address字段指定Envoy必须从中获取要发送到A记录的域名的部分。
您可以在文档中找到有关各种配置参数的更多信息。
现在,我们必须将以下Dockerfile放在envoy.yaml配置文件同一目录层级。
FROM envoyproxy/envoy:latest
COPY envoy.yaml /etc/envoy.yaml
CMD /usr/local/bin/envoy -c /etc/envoy.yaml
最后一步是构建镜像,并将其推送到某个地方(例如Dockerhub或云提供商的容器注册表),以便能够从Kubernetes使用它。
假设我想将此推送到我的个人Docker Hub帐户,可以使用以下命令来完成。
$ docker build -t markvincze/myapp-envoy:1 .
$ docker push markvincze/myapp-envoy:1
3. 可选项: 使Envoy镜像可参数化
如果我们希望能够使用环境变量自定义Envoy配置的某些部分而无需重建Docker镜像,则可以在yaml配置中进行一些env var替换。假设我们希望能够自定义要代理的headless服务的名称以及负载均衡器算法,然后我们必须按以下方式修改yaml配置。
然后实施一个小shell脚本(docker-entrypoint.sh),在其中执行环境变量替换。
#!/bin/sh
set -e
echo "Generating envoy.yaml config file..."
cat /tmpl/envoy.yaml.tmpl | envsubst \$ENVOY_LB_ALG,\$SERVICE_NAME > /etc/envoy.yaml
echo "Starting Envoy..."
/usr/local/bin/envoy -c /etc/envoy.yaml
并更改我们的Dockerfile以运行此脚本,而不是直接启动Envoy。
FROM envoyproxy/envoy:latest
COPY envoy.yaml /tmpl/envoy.yaml.tmpl
COPY docker-entrypoint.sh /
RUN chmod 500 /docker-entrypoint.sh
RUN apt-get update && \
apt-get install gettext -y
ENTRYPOINT ["/docker-entrypoint.sh"]
请记住,如果使用这种方法,则必须在Kubernetes部署中指定这些环境变量,否则它们将为空。
4. 创建Envoy deployment
仅当我们使Envoy Docker镜像可参数化时,才需要env变量。
Apply此Yaml后,Envoy代理应该可以运行,并且您可以通过将请求发送到Envoy服务的主端口来访问基础服务。
在此示例中,我仅添加了类型为ClusterIP的服务,但是如果要从群集外部访问代理,还可以使用LoadBalancer服务或Ingress对象。
下图说明了整个设置的体系结构。
该图仅显示一个Envoy Pod,但是如果需要,您可以将其扩展以具有更多实例。当然,您可以根据需要使用Horizontal Pod Autoscaler自动创建更多副本。 (所有实例将是自治的且彼此独立。)
实际上,与基础服务相比,代理所需的实例可能要少得多。在当前使用Envoy的生产应用程序中,我们在 〜400个上游Pod上提供了〜1000个请求/秒,但是我们只有3个Envoy实例在运行,CPU负载约为10%。
故障排除和监视
在Envoy配置文件中,您可以看到admin:部分,用于配置Envoy的管理端点。可用于检查有关代理的各种诊断信息。
如果您没有发布admin端口的服务,默认情况下为9901,您仍然可以通过端口转发到带有kubectl的容器来访问它。假设其中一个Envoy容器称为myapp-envoy-656c8d5fff-mwff8,那么您可以使用命令kubectl port-forward myapp-envoy-656c8d5fff-mwff8 9901开始端口转发。然后您可以访问http://localhost:9901上的页面。
一些有用的端点:
-
/config_dump
:打印代理的完整配置,这对于验证正确的配置是否最终在Pod上很有用。 -
/clusters
:显示Envoy发现的所有上游节点,以及为每个上游节点处理的请求数。例如,这对于检查负载平衡算法是否正常工作很有用。
进行监视的一种方法是使用Prometheus从代理pods获取统计信息。 Envoy对此提供了内置支持,Prometheus统计信息在管理端口上的/ stats/prometheus路由上发布。
您可以从该存储库下载可视化这些指标的Grafana仪表板,这将为您提供以下图表。
关于负载均衡算法
负载平衡算法会对集群的整体性能产生重大影响。对于需要均匀分配负载的服务(例如,当服务占用大量CPU并很容易超载时),使用最少请求算法可能是有益的。另一方面,最少请求的问题在于,如果某个节点由于某种原因开始发生故障,并且故障响应时间很快,那么负载均衡器会将不成比例的大部分请求发送给故障节点,循环负载均衡算法不会有问题。
我使用dummy API进行了一些基准测试,并比较了轮询和最少请求LB算法。事实证明,最少的请求可以带来整体性能的显着提高。
我使用不断增加的输入流量对API进行了约40分钟的基准测试。在整个基准测试中,我收集了以下指标:
- 服务器执行的请求数("requests in flight")
- 每台服务器平均正在执行的请求数
- 请求速率(每5分钟增加一次)
- 错误率(通常没有,但是当事情开始放慢时,这开始显示出超时)
- 服务器上记录的响应时间百分位数(0.50、0.90和0.99)
您可以从结果中看到LEAST_REQUEST可以导致流量在节点之间的分配更加顺畅,从而在高负载下降低了平均响应时间。
确切的改进取决于实际的API,因此,我绝对建议您也使用自己的服务进行基准测试,以便做出决定。
总结
我希望此介绍对在Kubernetes中使用Envoy有所帮助。顺便说一下,这不是在Kubernetes上实现最少请求负载平衡的唯一方法。可以执行相同操作的各种ingress控制器(其中一个是在Envoy之上构建的Ambassador)。