如何在Kubernetes中将Envoy用作负载均衡器

在当今分布式的世界中,单体架构越来越多地被多个,更小,相互连接的服务(不管是好是坏)所取代,代理和负载平衡技术似乎正在复兴。除了老玩家以外,近年来还涌现出几种新的代理技术,它们以各种技术实现,并以不同的功能进行普及,例如易于集成到某些云提供商(“云原生”),高性能和低内存占用,或动态配置。

可以说,两种最流行的“经典”代理技术是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
lb_policy:LEAST_REQUEST
hosts: [{ socket_address: { address: myapp, port_value: 80 }}]

您可以在文档中找到有关各种配置参数的更多信息。

现在,我们必须将以下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本身创建一个部署。

仅当我们使Envoy Docker镜像可参数化时,才需要env变量。

Apply此Yaml后,Envoy代理应该可以运行,并且您可以通过将请求发送到Envoy服务的主端口来访问基础服务。

在此示例中,我仅添加了类型为ClusterIP的服务,但是如果要从群集外部访问代理,还可以使用LoadBalancer服务或Ingress对象。

下图说明了整个设置的体系结构。

该图仅显示一个Envoy窗格,但是如果需要,您可以将其扩展以具有更多实例。当然,您可以根据需要使用Horizo​​ntal 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
/clusters

进行监视的一种方法是使用Prometheus从代理pods获取统计信息。 Envoy对此提供了内置支持,Prometheus统计信息在管理端口上的/ stats/prometheus路由上发布。

您可以从该存储库下载可视化这些指标的Grafana仪表板,这将为您提供以下图表。

关于负载均衡算法

负载平衡算法会对集群的整体性能产生重大影响。对于需要均匀分配负载的服务(例如,当服务占用大量CPU并很容易超载时),使用最少请求算法可能是有益的。另一方面,最少请求的问题在于,如果某个节点由于某种原因开始发生故障,并且故障响应时间很快,那么负载均衡器会将不成比例的大部分请求发送给故障节点,循环负载均衡算法不会有问题。

我使用 dummy API 进行了一些基准测试,并比较了轮询和最少请求LB算法。事实证明,最少的请求可以带来整体性能的显着提高。

我使用不断增加的输入流量对API进行了约40分钟的基准测试。在整个基准测试中,我收集了以下指标:

  • 服务器执行的请求数(”requests in flight”)
  • 每台服务器平均正在执行的请求数
  • 请求速率(每5分钟增加一次)
  • 错误率(通常没有,但是当事情开始放慢时,这开始显示出超时)
  • 服务器上记录的响应时间百分位数(0.50、0.90和0.99)

ROUND_ROBIN的统计数据看起来像这样:

这些是LEAST_REQUEST的结果:

您可以从结果中看到LEAST_REQUEST可以导致流量在节点之间的分配更加顺畅,从而在高负载下降低了平均响应时间。

确切的改进取决于实际的API,因此,我绝对建议您也使用自己的服务进行基准测试,以便做出决定。

总结

我希望此介绍对在Kubernetes中使用Envoy有所帮助。顺便说一下,这不是在Kubernetes上实现最少请求负载平衡的唯一方法。可以执行相同操作的各种ingress控制器(其中一个是在Envoy之上构建的Ambassador)。