使用Linkerd在Kubernetes上进行拓扑感知的服务路由

2020-11-26 04:40:57

几个月前,我在Linkerd2存储库中浏览问题和功能请求,以寻找一个很酷的主题提交给LFX Mentorship程序。作为参与,要求学生提出一项针对该功能的RFC,如果该功能被接受,则可以实施。我的选择-服务拓扑-当时是新发布的Kubernetes功能。

实话实说,我起初只是想将服务拓扑学全神贯注。部分是由于感知到的复杂性,但主要是因为该主题上没有很多资源。作为一项新发布的功能,我在ServiceTopology上所拥有的只是提案及其实现。

快进到今天。由于维护者,贡献者和我本人的不懈努力(是的,我的RFC被接受了),Linkerd现在可以立即支持服务拓扑。

在本文中,我将讨论什么是服务拓扑,Linkerd如何支持它(无需配置)以及作为新手开源贡献者遇到的一些挑战。

与术语“拓扑”可能暗示的不同,服务拓扑不指群集内服务的安排。相反,它指的是基于预定节点拓扑为特定服务路由流量的能力。此功能在Kubernetes v1.17中作为Alpha功能引入。

对拓扑感知服务路由的需求源于多区域集群部署中跨域网络流量的较高成本。标准的Kubernetes代理(kube-proxy)会随机平衡负载,这意味着服务流量可以路由到任何Pod。但是,一些Kubernetes用户宁愿将目标定位在同一地点或至少靠近该服务的端点。 kube-proxy可能会随机选择一个吊舱,从而增加了等待时间和成本。这正是服务拓扑所要解决的问题。它允许服务所有者确定要路由到的拓扑域的优先级。

图1.1显示了拓扑感知的服务路由。假设我们有一个包含两个节点(节点1和节点2)的集群。假设服务A想与服务B通话。如果没有服务拓扑,则可以将服务B的Pod池(B-1,B-2或B-3)中的任何Pod作为目标*。*在图的下半部分,您将会看到相同的场景,但是这次启用了服务拓扑。服务A仅允许定位到同一位置的Pod。现在,服务A和服务B之间的所有流量都将专门通过B-1路由。

在编写服务拓扑RFC时,我经常问自己这个问题。最初,该功能似乎很复杂,但是有两个Kubernetes资源可以促进拓扑感知的服务路由:拓扑标签和EndpointSlice。

拓扑标签是典型的Kubernetes k / v对,用于表示拓扑集群域。拓扑标签通常与节点关联。设置群集时,每个节点都应获取关联的拓扑标签列表(尽管并非总是如此,请考虑kubeadm)。以下是与服务拓扑一起使用的标签:

在官方文档中,您将看到如何使用这三个标签的不同组合(如果计算通配符*,则为四个)可以根据您的拓扑首选项来路由流量。

EndpointSlices是一项令人兴奋却被低估的功能,该功能是在服务拓扑可用之前发布的版本。它优化了端点,端点是将服务的子集(吊舱的IP地址x端口组合)保持在一起的资源对象。事实证明,将服务的所有子集保留在同一对象中意味着该对象将不断增长。而且,它增长得越多,就越难处理(并且在性能方面更昂贵)。

EndpointSlices将端点分解为多个对象。这是服务拓扑的基础,因为作为更改的一部分,这些子集不只是分开的,它们还包含更多标识信息,例如它们所属的拓扑域。另外,EndpointSlices还包括对双栈地址的支持,为可以在Kubernetes生态系统中开发的更高级的网络逻辑铺平了道路。

那么Linkerd如何适应我们的服务拓扑讨论?您猜对了:它启用了拓扑感知的服务路由! Linkerd支持服务拓扑,而不是扩展它或对其进行特殊处理。它通过利用kube-proxy使用的相同资源来实现基于位置的路由。我们所做的大多数更改都是为了支持“目标”服务端的服务拓扑,我将在下面介绍。

图2.2概述了我所说的“服务发现的四个步骤”。目标服务接受来自sidecar代理的连接请求。每个请求都包含代理想要解析的服务的FQDN或IP地址,以及诸如上下文信息之类的元数据,即Pod位于哪个名称空间上。目标服务采用该FQDN或IP地址,在内部进行转换,然后将其与代理可以使用的Pod列表一起流回。我们之所以说流,是因为该服务将更新发送回代理,这不是一次性的事情。有两个主要组件可以帮助我们完成此任务:

EndpointWatcher本质上是一种缓存机制。它监视事件并在内存中保留服务及其端点。

EndpointTranslator是一个侦听器。它在EndpointsWatcher中订阅服务,并将对缓存资源的所有创建,删除或更新操作转换为代理的更新。

这与kube-proxy处理路由的方式大不相同。首先,我们不会让系统随机选择一个吊舱。相反,我们将更新发送到代理,代理又根据响应延迟来路由请求。这比iptable重写或随机选择的pod凉十倍。根据Destinationservice的工作方式,我们必须做两件事来支持Service Topology。首先,引入对EndpointSlices的支持,然后引入对实际服务拓扑的支持。

EndpointSlice支持非常简单。观察者只需要进行一些更改。第一个挑战是处理要素门时。Kubernetes的要素门可能会令人讨厌,并且碰巧,EndpointSlice和Service Topology都在要素门后面。我们不得不选择何时在Endpoints上使用EndpointSlices,或者同时使用reconcilethem和。由于kube-proxy在这两种资源之间不做任何调和,因此我们决定继续使用两者之一。经过一番尝试和错误后,我们想到了一个CLI标志来启用对Linkerd安装或升级的片的支持。加上其他一些检查,使我们相信,采用者只要满足本地Kubernetes服务拓扑先决条件:活动功能标志和最新的Kubernetes版本,就可以随时选择加入。解决了国旗后,其他一切都顺利进行了。我们仅添加了新的结构和功能来处理EndpointSlicesas资源。

服务拓扑支持说起来容易做起来难。首先,为了识别流量来源,我们必须向从代理发送到服务的请求中添加其他上下文数据。毕竟,要计算拓扑,我们必须同时查看源节点和目标节点。这本身就是一个专门的工作。它需要一种可靠的方法来获取源节点的名称,而又不会弄乱控制平面的内部结构。我们还需要确定向前添加上下文数据的最佳方法。我们决定将表示为字符串(表示pod的名称空间)的上下文信息转换为JSON。这样可以确保该值对于大多数API仍然是不透明的,而仅需要更改代码即可在目标服务端取消封送信息。最大的挑战是跟踪不同的拓扑首选项和可用的端点。在我即将学习的过程中,过滤意味着状态。我未在RFC中说明的许多边缘情况开始浮出水面:我们如何进行后备?在选择首选项后修改服务对象时会发生什么?等等。所有这些的答案都是状态**。**我们决定将EndpointTranslator组件更改为有状态的,以跟踪端点的总数以及以前过滤的集合的快照。这减轻了围绕充实边缘情况的大多数担忧,并使过滤逻辑的提出变得轻而易举。

我当然没想到此功能会发生变化。作为一个即将毕业的大四学生,想出一个毫无帮助的解决方案的前景似乎遥不可及,直到没有实现。知道我的工作会使别人的生活更轻松,这真令人难以置信。此外,我还告诉人们如何处理要素门和上下文数据!

但最重要的是,我很高兴能够实现(我认为是)Linkerd社区的其他成员可以构建和使用的相当酷的功能。服务拓扑允许您默认情况下根据端点的位置路由流量。在集群中启用这些功能门,并降低成本。