Kubernetes Services 负载平衡和反向代理

在这篇文章中,我将讨论一个主题,任何K8新手一旦了解了基本概念,便会开始考虑。如何将K8s集群中部署的Services暴露给外部流量?我在帖子中使用的内容和一些图表来自我在WSO2进行的一次内部技术演讲。

让我们定义一些术语!

在继续进行实际讨论之前,让我们定义一些术语并达成一致,因为如果不首先定义为特定术语,它们可能会与每次使用混淆。让我们定义一个示例部署,并在稍后的讨论中引用该示例部署的各个部分。

有基本的了解PodsServices,以及的使用Services类型在K8S预计本文中讨论的主题。

  • K8s集群—在其中创建和维护Pod,Services和其他K8s构造的边界。这包括K8s主节点和调度实际Pod的节点。
  • Node(大写N)—这些是所有Pod,Volumes和其他计算构造都将在其中生成的K8s节点。它们不受K8的管理,并且位于由IaaS提供程序分配给它们的专用网络内生成虚拟机时。
  • Node专用网络— K8s节点位于Cloud Service Provider中的专用网络中(例如:AWS VPC)。对于示例部署,让我们将此专用网络CIDR定义为10.128.0.0/24。将设置路由,以便将流量正确路由到该专用网络和从该专用网络路由(例如:使用Internet和NAT网关)。
  • 容器联网-在其中创建K8s构造的联网边界。这是一个软件定义的网络,几乎总是实现为Flannel。该网络内路由的工作方式可能因不同的云Services提供商而异,但是通常情况下,此空间内分配的每个专用IP都可以彼此通信,而不必在两者之间进行NAT。每当创建新Services和Pod时,此网络就由kube-proxy操纵。在此空间内通常定义两个CIDR,即Pod Network CIDR和Service Network CIDR。
  • Pod Network CIDR -K8s群集中产生的Pod将从其获得IP地址的IP地址范围。对于示例部署,让我们继续10.244.0.0./16
  • Services网络CIDR -K8s群集中创建的Services将从其获取IP地址的IP地址范围。对于示例部署,让我们继续10.250.10.0/20
  • (K8s)Services(大写S)—本文重点介绍的K8s Service构造。
  • Services-特定Pod集将提供的应用程序Services

以下是单个Service定义的摘录myservice.yml。该.spec.type字段可以具有值ClusterIPNodePortLoadBalancer(或ExternalName它是不相关的本文的范围)。此Services公开8080映射到Pod端口的端口8080。Services的名称为myservice

apiVersion: v1
kind: Service
metadata:
name: myservice
...
spec:
type: <type>
...
ports:
- name: myserviceport1
port: 8080
targetPort: 8080
protocol: TCP
...

问题

Pods是短暂的。

在K8中,Pod是计算的基本单位。但是,不能保证Pod在用户期望的整个生命周期内都存在。实际上,关于Pod的活泼性,K8s唯一遵守的合同是仅保留所需数量的Pod(又名Replicas)。它们是短暂的,在诸如缩放,内存或CPU使用率过高之类的情况下容易受到来自K8的信号的杀伤,为了更有效地利用资源而重新安排时间,甚至由于外部因素(例如:整个K8节点关闭)而导致停机。在创建时分配给Pod的IP地址将无法幸免。

因此,尽管每个Pod都有一个群集范围内的可路由IP地址,但这些Pod IP地址不能用作直接Services端点,这仅仅是因为在任何给定时间都有一个或多个将停止响应的事实。应该有一个结构,可以作为给定Pod集合的反向代理作为单个固定Services端点。

这就是K8sServices发挥作用的地方。

K8sServices在集群内部(以及集群外部)的可达性方面是固定的,这就是本文要解决的问题。一旦获得内部群集范围的IP地址,就可以使用它来引用它,直到有意删除该Services为止。也可以使用Service在K8s集群中引用它name

可能会注意到,如上所述的问题说明和K8sServices的主要功能不是K8s部署所具有的。裸机或虚拟化部署还需要对提供特定Services的给定计算实例集进行某种反向代理。对于AWS中的一个示例,对于由自动伸缩组管理的一组EC2计算实例,应该有一个ELB / ALB,它既充当固定的可参考地址,又充当负载平衡机制。这与K8s Services提供的功能类型相同,除了在K8s中,Services不是由提供者管理的计算实例,而并非由最终用户管理。它们更像是软件定义网络和基于Linux内核的数据包过滤的产品,而不是在某些CPU上运行的单个计算过程。

尽管K8sServices执行基本的负载平衡,但将在以下各节中了解到,有时,当需要高级负载平衡和反向代理功能(例如:基于路径的路由等L7功能)时,这些高级功能会抵消实际的负载平衡进程作为计算进程运行,或者作为Pods在K8s集群内部,或者作为外部托管Services。K8sServices将仅执行L3路由。

但是,仍然无法从K8s内部网络外部的客户端访问由Services处理的Pod集。上面讨论的每个IP地址都在创建K8s覆盖网络时定义的CIDR中。因此,这些是专用空间中的地址。如果客户端尝试从网络外部调用它们,它们将不会被路由到K8s集群,因为它们的路由规则是在覆盖网络中设置的。

解决方案:Services类型

为了解决此问题,type可以利用几个Service来允许外部流量进入具有私有IP地址的Pod。让我们研究每种类型,看看是否可以通过任何一种实现以域名映射为目标的公共IP地址。

类型:ClusterIP

优点:没有特定于此类型的优点

如果未为Services定义显式指定字段,则这是将创建的默认Services类型type。群集范围内的内部IP地址将作为Services的固定IP地址提供,该IP地址是在设置容器网络时划分的Services网络CIDR之一。该IP地址(和Servicesname)可在K8s覆盖网络内的任何地方进行路由。

curl http://myservice:8080/
curl http://$(kubectl get svc myservice --template='{{.spec.clusterIP}}'):8080/

图片发布

图片发布

但是,如上所述,在本文范围内,此IP地址本身是(多种)无效的。除非能以某种方式实施以下一系列步骤,否则无法从群集网络外部路由该路由。

  1. 在K8s Master上观看和检测新的Services创建-可行,简单
  2. 获取为每个Services分配的ClusterIP地址-可行,简单
  3. 创建并更新从所有可能的客户端到K8s节点的路由表,以将请求路由到群集网络-不需要那么多

还可以通过使用命令将Services暴露给外部流量。但是,这暴露了完整的K8s API Server,因此不建议将其用于生产系统。 kubectl proxy

优点:

  • 没有特定于此类型的

缺点:

  • 公开Services并非易事
  • 使用其他变通办法这样做可能会暴露出最可能的攻击面

继续!

类型:NodePort

就本文探索Services类型的顺序而言,这是第一个允许我们在生产部署中以有意义的方式公开Services的顺序。稍后将看到,这将是将Services端口映射到物理端口的其他机制的基础。

具有NodePort类型的Services将在K8s节点上为.spec.ports[*]Services定义中定义的每个端口条目分配一个物理端口。此外,它将对集群中的所有K8s节点执行此操作(与仅在生成Services处理的Pod的节点中执行此操作相反)。这样,Services端口将映射到在eth0每个节点的接口上打开的端口。

如果myservice.yml创建,.spec.type=NodePort则会在K8s群集的每个节点中打开一个随机(但一致)的物理端口。在创建--service-node-port-range默认范围为32000—的K8s群集时,将从可修改范围内选择此端口32767.spec.ports[*]可以通过查看列kubectl get svc <svc_name>下方位置的输出来检查分配给每个条目的端口,每个端口的PORT(S)a后面都有一个值:

图片发布

图片发布

如果将(例如)myserviceport1中的myservice8080)分配给NodePort 31644,则有权访问Node专用网络的外部客户端可以使用任何Node IP地址和NodePort直接访问Services。假设这些节点IP地址之一是10.128.0.1310.128.0.0/24在上述定义期间定义的范围内),我们可以在节点专用网络内使用以下命令调用myservice端口。8080``curl

curl http://10.128.0.13:31644/

另外,如果需要担心端口的随机分配,则可以使用.spec.ports[*].nodePort每个条目的值来指定固定的端口值(在上述范围内)。预定义的NodePort的缺点是必须在将要部署的潜在Services数量之间进行协调,以使一个Services不会与另一个Services冲突。

只要有设置从Node专用网络进出的机制,任何外部客户端都将能够myservice使用Node公用IP地址进行访问。

下一步,可以在与K8s节点相同的网络组(例如VPC)中的IaaS公共地址空间中设置反向代理,以便将外部流量路由到NodePorts(并在它们之间进行负载平衡)->Services端口-> Pod端口。

图片发布

图片发布

优点:

  • 将内部Services暴露给外部流量的最简单方法
  • 设置外部负载平衡和反向代理时可提供更大的自由度
  • 提供Services提供的L3负载平衡功能(例如.spec.sessionAffinity:)
  • 简单易懂的机制,在故障排除时尤其有用(特别是使用预定义的NodePort)

缺点:

  • 必须管理K8s管理外部的负载平衡和反向代理
  • 必须在Services之间协调NodePort

这是从现在开始讨论的所有其他类型的反向代理的基础。必须进入K8s群集网络内部的任何流量都必须通过某种NodePort。所有变体都试图解决其他因素,例如使用K8s指令动态管理外部负载均衡器,但是NodePort是连接Node Private Network和Service Network的桥梁。

类型:LoadBalancer

也称为云负载均衡器方法,将其指定为.spec.type在支持的Cloud Service Provider中部署K8的时间,将导致在云中配置负载均衡器以代理特定Services。

例如,在AWS中部署的K8s集群中,这将导致提供一个ELB实例,该实例可以代理K8s集群中Services的流量。到达ELB的流量将通过暴露的随机NodePort到达覆盖网络,以将流量从Cloud Service Provider专用网络运送到K8s覆盖网络。

有关如何实现此流程的确切细节可能会因Cloud Service Provider而异。但是,总体思路是快速配置负载均衡器,而不必分别通过Cloud Service Provider API。K8s将指定Services定义并代表用户执行API调用以设置所需的资源(例如:负载均衡器,静态公共IP地址,Cloud Service Provider上的路由)。

图片发布

图片发布

优点:

  • 轻松配置负载平衡和反向代理
  • NodePort管理无需用户干预即可完成
  • 不必管理K8s域之外的负载平衡设施

缺点:

  • 单个负载均衡器代理单个Services,因此是一种昂贵的方法
  • 实施细节有时不透明,需要进行手动调查才能理解和排除故障
  • 大多数时候都设置网络负载平衡器,因此,基于路径的路由和TLS / SSL终止之类的L7功能已无法使用
  • 云Services提供商通常需要一些时间才能完成预配负载均衡器

上面是不同的Service type,它们提供了不同的方法来将Service暴露给K8s集群的外部。但是,仅靠它们本身并不能使任务达到完整状态(例如NodePort仅导出端口,LoadBalancer不灵活)。因此,出现了将上述内容与更多应用程序级别功能结合在一起的某些“模式”。以下是两种这样的方法。

裸机Services负载均衡器模式

在K8s v1.1之前,裸机Services负载平衡器是解决上述LoadBalancer Service类型缺点的首选解决方案。这利用了NodePort和一组HAProxy Pod,它们充当其余Pod组的L7反向代理。解决方案大致如下。

  1. 单个容器的Pod包含一个HAProxy部署以及一个名为service_loadbalancer的附加二进制文件
  2. 此Pod部署为DaemonSet,其中每个节点仅调度一个Pod
  3. service_loadbalancer二进制不断手表K8S APIServices器和检索Services的细节。通过使用Services注释元数据,每个Services可以指示任何第三方负载平衡器将采用的负载平衡详细信息(TLS / SSL终端,虚拟主机名等)。
  4. 检索到详细信息后,它将重写HAProxy配置文件,backendfrontend使用每个Services的Pod IP地址填充和部分详细信息
  5. 写入HAProxy配置文件后,service_loadbalancer会对HAProxy进程进行软重新加载
  6. HAProxy公开端口80443。然后将这些作为NodePort暴露给外部流量
  7. 可以通过公共负载均衡器将NodePorts暴露在外部

图片发布

图片发布

在这种方法中,任何传入流量都按以下顺序到达Pod

  1. 流量通过公共IP地址路由到公共负载均衡器
  2. 负载均衡器将流量转发到NodePorts
  3. 一旦流量到达NodePorts HAProxy,便开始L7解构,并根据Services注释详细信息进行基于主机或路径的路由
  4. 一旦做出路由决策,流量将直接转发到Pod IP

请注意,一旦流量到达NodePorts(上面的#2),它就在覆盖网络内部。因此,可以基于任何L7决策来完成直接Pod IP地址之间的负载平衡。换句话说,Service K8s构造不参与负载平衡决策,只是在开始时提供了分组机制。这与迄今为止讨论的方法形成对比,在该方法中,负载平衡发生在K8s覆盖网络之外,并且流量通常被推送到Services虚拟IP。

在我个人使用此方法的故事中,选择此方法的主要驱动因素是其可定制性。一些用例涉及特定需求,例如基于主机和/或路径的路由的组合,相互认证等。使用这种方法(以及具有一定的Go编码能力),这些自定义易于实现。

优点:

  • 可以做出L7决策
  • 可以使用特定的负载均衡器功能,例如基于Cookie的会话粘性,这可能是Cloud Load Balancer方法无法实现的
  • 更好地控制应如何扩展负载平衡
  • 负载平衡详细信息通过K8s结构(例如Services注释)进行管理
  • 对于不同的用例,更具可定制性
  • 经济实惠,因为只需要为一个完整的K8s集群配置一个Cloud Load Balancer
  • 透明的配置更改和机制,因为在service_loadbalancer之后,涉及的更改是HAProxy特定的
  • service_loadbalancer在较短的时间间隔内获取更改时,更改会迅速传播

缺点:

  • 复杂的设置和故障排除
  • 如果节点或指定的相似性节点的数量受到限制,可能会导致单点故障
  • service_loadbalancer仅支持HAProxy(尽管理论上可以通过大量代码更改来支持其他反向代理)

在K8s v1.1之后,kubernetes/contrib存储库已将该文件夹标记service_loadbalancer为已弃用,从而支持HAProxy Ingress Controller。

将上述service_loadbalancer逻辑组合到轻量级反向代理中的traefik,在Services负载均衡器模式被弃用之后也随后弹出。有一个企业版,似乎建议量产。但是,我还没有亲自在生产中看到这一点。

入口

可以说,Ingress的概念和相关的Ingress Controller是从上面讨论的Bare Metal Service负载均衡器模式演变而来的。此方法仅在K8s v1.1之后可用。

在Service Load Balancer模式中,负载平衡和反向代理意图与Service声明绑定在一起,而实现与service_loadbalancer代码绑定在一起。注释是特定于实现的,并且不被视为标准。添加对新型负载均衡器的支持需要对现有代码进行多次更改。

Ingress将这些顾虑带到了自己的K8s API对象中。

入口是为了暴露从外部到Services代理的一组特定Pod的路由。为此,可以将诸如TLS终止,基于主机和/或基于路径的路由以及负载分配策略之类的负载均衡细节指定为rules。这与Service Load Balancer模式遵循的实现特定注释相反。入口规则是“入口规范”部分的组成部分,因此是K8s API的标准构造,而不是一些唯一的定制实现就可以发挥其含义的任意字符串。

入口(意图)的实现由入口控制器完成。这是合同的负载均衡器特定实现,应根据Ingress资源提供的详细信息配置给定的负载均衡器(例如:Nginx,HAProxy,AWS ALB)。metadata.annotations.kubernetes.io/ingress.class注释指定要用于特定Ingress资源的Ingress Controller 。此处的值可能是nginxgce或任何其他IngressController实现标识符。

在不同的Ingress Controller之间,此实现的详细信息可能有所不同。他们只需要遵守Ingress资源API定义的标准即可。

例如,Nginx Ingress Controller将Nginx代理部署为Pod,并根据集群中定义的Ingress资源对其进行配置。AWS ALB Ingress Controller会为AWS ALB设置Ingress规则的路由详细信息。对于GCE Ingress Controller来说也是如此,其中将配置GCP L7负载均衡器。AWS ALB和GCP Ingress Controller产生的外部负载均衡器都将通过暴露为NodePort类型的Services集群IP将流量转发到Pod。

图片发布

图片发布

这看起来类似于type: LoadBalancer上面讨论的Cloud Load Balancer()方法,但是Ingress和LoadBalancer类型Services之间存在关键差异。

  1. 在大多数情况下,云负载均衡器方法都在不控制L7构造的情况下配置网络负载均衡器。Ingress不会这样做。其最终的负载均衡器主要是L7。
  2. Cloud Load Balancer方法由Services声明规定。入口是一个独立的声明,不依赖于Services类型。
  3. 在前一种方法中,每个Services将只有一个Cloud Service Provider负载平衡器,而使用Ingress,可以使用单个负载平衡器管理多个Services和后端。

优点:

  • 调配外部负载均衡器的标准化方法
  • 支持HTTP和L7功能,例如基于路径的路由
  • K8s管理的负载均衡器行为
  • 可以使用单个负载均衡器管理多个Services
  • 可以轻松地插入不同的负载均衡器选项

缺点:

  • Ingress Controller的实现可能会出错(例如:我有相当一部分的GCP Ingress Controller没有正确地选择就绪性和活跃性探针来检测健康的后端)
  • 实施控制权在Ingress Controller的手中,这可能会限制某些自定义,否则可以使用Service Load Balancer模式来完成
  • K8的托管负载均衡器配置可能意味着通过以前可用的方式(也许在较早的部署架构中)可以较少地控制Cloud Service Provider负载均衡器。

总结

以上选项是目前可用的OOTB基本选项。但是,通过将这些方法或定制实现组合在一起,可以创建更多模式。

选择正确的部署方法时,可以将以下因素用作比较的基础。

成本

每种方法的基础架构和运营成本是多少?负载均衡器和相关资源(静态IP地址,存储,防火墙规则等)是否会存在多个实例,或者将它们压缩到最小可用数量?

使用特定方法进行操作是否意味着要雇用具有Go lang能力的新资源?还是仅使用YAML和K8s API的知识就可以管理?

复杂性和可定制性

该方法是否合理地解释了实现的内部运作方式?是否存在缺少足够文档的多个抽象,并使基础实现过于模糊?解决K8s Pod网络外部的客户端与其内部的后端之间的路径有多容易?

将项目/团队/组织特定的自定义纳入方法有多复杂?在将更改添加到“正式”存储库/图像注册表之前,是否必须将更改推向更广泛的项目才能获得批准?

延迟

进入K8s覆盖网络之后,到达实际后端之前,请求应经过多少路由跃点?这些跃点会引入可观的延迟吗?实施是否经过适当的负载测试以识别饱和点?

我已经看到使用Service Load Balancer实施进行的部署,其中HAProxy开始以较高的运行中请求计数重新生成,因为kubernetes/contrib存储库中提供的默认工件没有充分描述CPU和内存限制以及请求。

可执行度

允许修改作为该方法一部分创建的资源的自由度是多少?是否可以将旧的基础结构管理流程和方法与新方法一起用于K8领域之外创建的资源?还是会严重破坏Ops的任务?

例如,使用诸如Terraform之类的方法管理作为AWS ALB Ingress Controller的一部分创建的AWS ALB实例可能很棘手,因为Ingress Controller本身可能会将外部更改视为侵入性的。另一方面,如果ALB代理了基本的NodePort设置,则Terraform资源很可能会作为管理工件被使用。

归根结底,向K8s网络外部的客户端提供内部Services就是在Node专用网络和Pod网络之间建立桥梁。该网桥通常是NodePort。只要建立了此网桥,任何具有公共IP地址的外部负载平衡器都可以看到并收集该端口作为可转发的后端。除了Service type之外ClusterIP,这里讨论的所有其他方法都是关于如何管理此网桥(也称为NodePort)的。只要了解此概念,为的K8s集群找出反向代理策略是一项简单的任务。

本文为转载:

作者:chamila de alwis

原文: https://medium.com/@chamilad/load-balancing-and-reverse-proxying-for-kubernetes-Services-f03dd0efe80