26

理解Kubernetes网络:ingress篇

 4 years ago
source link: https://studygolang.com/articles/25879
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

在本系列的 第一篇文章 中,我讲了Pod跨Kubernetes集群中的节点相互连接的网络。 第二篇 重点讲了服务网络如何为Pod提供负载平衡,以便群集内的客户端可以与它们可靠地通信。对于这第三篇也是最后一篇文章,我想以这些概念为基础来展示集群外的客户端如何使用同一服务网络连接到Pod。由于各种原因,这很可能是三篇涉及最多的,并且掌握前两篇关于pod和service的内容是理解本篇的的前提。

NvIfuy2.png!web

路由并不是负载均衡

在上一篇文章中,我们创建了一个具有两个Pod的部署,并为该服务分配了一个IP,称为“集群IP”,针对Pod的请求已发送到该IP。我将在此处继续根据该示例进行构建。回想一下,该服务的群集IP 10.3.241.152位于与Pod网络以及节点本身所在的网络不同的IP地址范围内。我称这个地址空间为“服务网络”,尽管它几乎不配名称,上面没有连接的设备,并且完全由路由规则组成。在示例中,我们展示了如何通过称为kube-proxy的kubernetes组件与名为netfilter的linux内核模块协作来实现该网络,以捕获和重新路由发送至集群IP的流量,从而将其发送至正常运行的Pod。

AfiaA3I.png!web

到目前为止,我们一直在谈论“连接”和“请求”,甚至是更加模棱两可的“流量”,但是要了解为什么kubernetes入口能够按其方式工作,我们需要更加具体。连接和请求在OSI的第4层(tcp)或第7层(http,rpc等)上运行。 Netfilter规则是路由规则,它们在第3层的IP数据包上运行。所有路由器(包括netfilter)或多或少地仅基于数据包中包含的信息来做出路由决策;通常它来自哪里,去哪里。因此,以第3层术语来描述此行为:netfilter处理到达节点eth0接口的目的地为10.3.241.152:80的每个服务的数据包,匹配为我们的服务建立的规则,并转发给一个健康的Pod。

显然,我们用来允许外部客户端调用Pod的任何机制都必须使用相同的路由基础结构。也就是说,那些外部客户端最终必须调用集群IP和端口,因为这是我们到目前为止所讨论的所有机器的“前端”,这使得我们不必在意pod的位置。在任何给定时间运行。但是,如何实现这一目标目前尚不明显。服务的群集IP仅可从节点的以太网接口访问。集群之外的任何人都不知道该范围内的地址该怎么做。我们如何将流量从公网IP端点转发到仅在数据包已经位于节点上之后才可访问的IP?

如果您想解决这个问题,您可能要做的事情之一就是使用iptables实用程序检查netfilter规则,如果您这样做,首先会发现似乎令人惊讶的内容:示例服务的规则不限于特定的原始网络。也就是说,来自节点以太网接口且目的地为10.3.241.152:80的任何数据包都将匹配并路由到Pod。那么,我们是否可以仅给该群集IP的客户端分配一个友好的域名,然后添加一条路由以将这些数据包发送到节点之一?

eQjEjqe.png!web

如果以这种方式进行设置,它将起作用。客户端调用群集IP,数据包沿路由到达节点,然后转发到Pod。在这一点上,您可能会有疑问,此解决方案存在一些严重的问题。首先是节点是短暂的,就像Pod一样。它们不像Pod那样短暂,但是它们可以迁移到新的虚拟机,集群可以伸缩,依此类推。在第3层数据包上运行的路由器不知道来自不健康状态的健康服务。他们希望路由中的下一跳保持稳定并可用。如果节点不可达,则路由将断开并在大多数情况下保持断开状态很长时间。即使路由很持久,您的所有外部流量都将流经单个节点,这可能不是最佳选择。

但是,我们必须以不依赖群集中任何单个节点的运行状况的方式引入客户端流量。如果没有对路由器进行积极的管理,使用路由进行路由的确没有可靠的方法,这正是kube-proxy在管理netfilter中的作用。对于设计人员来说,将kubernetes职责扩展到外部路由器的管理可能没有多大意义,特别是考虑到我们已经拥有成熟的工具来将客户端流量分配到一组机器上,这尤其有用。它们被称为负载平衡器,毫不奇怪,这是真正持久有效的kubernetes入口解决方案。

要使用负载均衡器将客户端流量分发到群集中的节点,我们需要客户端可以连接到的公共IP,并且我们需要负载均衡器可以将请求转发到的节点本身上的地址。由于上述原因,我们无法轻松地使用服务网络(集群IP)在网关路由器和节点之间创建稳定的静态路由。唯一可用的其他地址是节点的以太网接口连接到的网络,在此示例中为10.100.0.0/24。网关路由器已经知道如何将数据包发送到这些接口,并且从负载平衡器发送到路由器的连接将到达正确的位置。但是,如果客户希望通过端口80连接到我们的服务,我们不能只是将数据包发送到节点接口上的该端口。

3QrYJvF.png!web

失败的原因显而易见。没有进程在监听10.100.0.3:80(或者如果是错误的监听),我们希望使用的netfilter规则将拦截我们的请求并将其定向到Pod与该目标地址不匹配。它们仅与服务网络上位于10.3.241.152:80的群集IP匹配。因此,当这些数据包到达该接口时,它们将无法传递,并且内核会以ECONNREFUSED进行响应。这给我们留下了一个难题:设置netfilter来转发数据包的网络很难从网关路由到节点,而容易路由的网络却不是netfilter转发的网络。解决此问题的方法是在这些网络之间建立一座桥梁,而这正是kubernetes对NodePort所做的工作。

NodePort Services

我们在上一篇文章中创建的示例服务未指定类型,因此采用默认类型ClusterIP。还有两种其他类型的服务可添加其他功能,而接下来重要的一种类型是NodePort。这是作为NodePort服务的示例服务。

kind: Service
apiVersion: v1
metadata:
  name: service-test
spec:
  type: NodePort
  selector:
    app: service_test_pod
  ports:
  - port: 80
    targetPort: http

类型为NodePort的服务是具有附加功能的ClusterIP服务:可以在节点的IP地址以及服务网络上分配的群集IP上访问。完成此操作的方法非常简单:当kubernetes创建NodePort服务时,kube-proxy会在30000–32767范围内分配一个端口,并在每个节点的eth0接口上打开该端口(因此称为“ NodePort”)。与此端口的连接将转发到服务的群集IP。如果我们在上面创建服务并运行kubectl get svc service-test,我们将看到已为其分配的NodePort。

$ kubectl get svc service-test  
NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE  
service-test 10.3.241.152 <none> 80:32213/TCP 1m

在这种情况下,我们的服务被分配了NodePort 32213。这意味着我们现在可以连接到示例集群中任一节点上的服务,地址为10.100.0.2:32213或10.100.0.3:32213,流量将转发到该服务。有了这一部分,我们现在有了一个完整的管道,可以平衡外部客户端对集群中所有节点的请求。

YVzeu2m.png!web

在上图中,客户端通过公共IP地址连接到负载均衡器,负载均衡器选择一个节点并以10.100.0.3:32213连接到该节点,kube-proxy接收此连接并将其转发到群集IP上的服务。 10.3.241.152:80,此时请求与netfilter规则匹配,并在10.0.2.2:8080上重定向到服务器Pod。似乎有些复杂,而且在某些方面,但是很难想到一个更简单的解决方案,它可以维护Pod和服务网络提供的所有出色功能。

这种机制并非没有问题。使用NodePorts可将服务公开给非标准端口上的客户端。这通常不是问题,因为负载平衡器可以公开常规端口并向最终用户屏蔽NodePort。但是在某些情况下(例如Google Cloud上的内部负载平衡),您将不得不向上游传播NodePort。 NodePorts也是一种有限的资源,尽管2768对于甚至最大的群集来说也足够了。对于大多数应用程序,您可以让kubernetes随机选择端口,但是如果需要,您也可以显式设置它们。最后,在请求中保留源IP方面存在一些限制。您可以参考该主题上的文档文章,以获取有关如何管理这些问题的信息。

NodePort是所有外部流量进入kubernetes集群的基本机制。但是,它们本身并不是一个完整的解决方案。由于上述原因,无论客户是内部客户还是来自公共网络的用户,总是需要在集群前需要某种负载均衡器。该平台的设计师意识到了这一点,并提供了两种不同的方式从kubernetes本身指定负载均衡器配置,因此接下来让我们快速看一下。

LoadBalancer Services 和 Ingress

这最后两个概念是kubernetes执行的更复杂的功能之一,但我不会在这些功能上花费很多时间,因为它们实际上并没有改变我们刚才所说的任何内容。如上所述,所有外部流量最终都会通过NodePort进入群集。设计人员本来可以停在那里的,只是让您担心公用IP和负载平衡器,甚至在某些情况下(例如在裸机上运行或在家庭实验室中运行),这是您要做的。但是在支持API驱动的网络资源配置的环境中,kubernetes使得可以在一处定义所有内容。

最简单且应该作为首选的方法是第三种类型的kubernetes服务,称为 LoadBalancer 服务。假设您在支持API驱动的网络资源配置的GCP或AWS之类的环境中运行,LoadBalancer类型的服务具有NodePort服务的所有功能以及构建完整的入口路径的能力。

kind: Service
apiVersion: v1
metadata:
  name: service-test
spec:
  type: LoadBalancer
  selector:
    app: service_test_pod
  ports:
  - port: 80
    targetPort: http

如果我们在Google Kubernetes Engine上删除并重新创建示例服务,我们很快就会通过kubectl get svc service-test看到已经分配了外部IP。

$ kubectl get svc service-test  
NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE  
openvpn 10.3.241.52 35.184.97.156 80:32213/TCP 5m

我说“很快”,尽管分配外部IP可能要花几分钟,但这不足为奇,因为必须调拨大量资源。例如,在GCP上,这要求系统创建外部IP,转发规则,目标代理,后端服务以及可能的实例组。分配IP后,您可以通过它连接到服务,为其分配一个域名并将其分发给客户端。只要不破坏和重新创建服务(很少有充分的理由这样做),IP便不会更改。

LoadBalancer类型的服务有一些限制。您无法将lb配置为终止https流量。您无法进行虚拟主机或基于路径的路由,因此您不能使用单个负载平衡器以任何实用的方式代理多个服务。这些限制导致在版本1.2中增加了一个单独的kubernetes资源,用于配置负载均衡器,称为Ingress。 LoadBalancer服务全都涉及扩展单个服务以支持外部客户端。相比之下,Ingress是一个单独的资源,可以更加灵活地配置负载均衡器。 Ingress API支持TLS终止,虚拟主机和基于路径的路由。它可以轻松地设置负载均衡器来处理多个后端服务。

Ingress API的内容太多,无法在此处进行详细介绍,因为如上所述,它与Ingress在网络级别的实际工作方式无关。该实现遵循基本的kubernetes模式:资源类型和用于管理该类型的控制器。在这种情况下,资源是一个入口,其中包括对网络资源的请求。这就是我们的测试服务的Ingress资源的外观。

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: test-ingress
  annotations:
    kubernetes.io/ingress.class: "gce"
spec:
  tls:
    - secretName: my-ssl-secret
  rules:
  - host: testhost.com
    http:
      paths:
      - path: /*
        backend:
          serviceName: service-test
          servicePort: 80

Ingress控制器负责通过将环境中的资源驱动到必要状态来满足此请求。使用Ingress时,您可以将服务创建为NodePort类型,并让Ingress控制器确定如何获取到节点的流量。有针对GCE负载平衡器,AWS弹性负载平衡器以及流行代理(例如nginx和haproxy)的入口控制器实现。请注意,在某些环境中,将Ingress资源与LoadBalancer服务混合使用可能会引起一些细微问题。这些都可以轻松解决,但总的来说,最好将Ingress用于甚至简单的服务。

HostPort 和 HostNetwork

我想谈的最后两件事实际上更多地是有趣的好奇心而不是有用的工具。实际上,我建议它们在99.99%的用例中是反模式的,任何使用这两种方法的实现都应进行自动设计审查。我考虑过将它们完全排除在外,但是它们是某种侵入的途径,因此值得一看一下它们的作用。

其中第一个是HostPort。这是容器的属性(在ContainerPort结构中声明),当设置为给定的整数端口号时,该端口将在节点上打开并直接转发到容器。没有代理,端口仅在运行容器的节点上打开。在平台的早期,在添加DaemonSets和StatefulSets之前,这是一个技巧,可以用来确保在任何给定节点上仅运行一个类型的容器。例如,我曾经通过将HostPort设置为9200并指定与节点数相同的副本数来实现Elasticsearch集群。现在,这将被视为骇人听闻的骇客,除非您要实施kubernetes系统组件,否则您极不可能希望设置HostPort。

在kubernetes的上下文中,其中的第二个甚至更奇怪,这就是pod的HostNetwork属性。设置为true时,与docker run的--network = host参数具有相同的效果。这会导致Pod中的所有容器都使用节点的网络名称空间,即它们都可以访问eth0并直接在此接口上打开端口。我认为建议您永远不需要这样做是很困难的。如果您有一个用例,那么您很可能已经是kubernetes的贡献者,不需要我的任何帮助。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK