在本系列的第一篇文章中,研究了kubernetes如何结合使用虚拟网络设备和路由规则,以允许在一个群集节点上运行的Pod与在另一个群集节点上运行的Pod通信,只要发送者知道接收者的Pod网络即可。 IP地址。如果您还不熟悉Pod的交流方式,那么在继续之前值得一读。集群中的Pod网络很简单,但仅凭其不足以创建持久性系统。那是因为kubernetes中的Pod是短暂的。您可以将Pod IP地址用作终结点,但不能保证该地址在下次重新创建Pod时不会更改,这可能由于多种原因而发生。

您可能已经意识到这是一个老问题,并且它有一个标准的解决方案:通过反向代理/负载均衡器运行流量。客户端连接到代理,代理负责维护将请求转发到的健康服务器列表。这对代理服务器提出了一些要求:代理服务器本身必须是耐用的并且能够抗故障;它必须具有可以转发到的服务器列表;并且它必须具有某种方式来了解特定服务器是否运行正常并能够响应请求。 kubernetes设计师以一种优雅的方式解决了这个问题,该方式建立在平台的基本功能之上,可以满足所有这三个需求,并且从一种称为服务的资源类型开始。

m-01.png

Services

在第一篇文章中,我展示了一个假设的集群,其中包含两个服务器Pod,并描述了它们如何跨节点通信。在这里,我想以该示例为基础来描述kubernetes服务如何在一组服务器Pod之间实现负载平衡,从而允许客户端Pod独立且持久地运行。要创建服务器容器,我们可以使用如下部署:

kind: Deployment
apiVersion: extensions/v1beta1
metadata:
  name: service-test
spec:
  replicas: 2
  selector:
    matchLabels:
      app: service\_test\_pod
  template:
    metadata:
      labels:
        app: service\_test\_pod
    spec:
      containers:
      - name: simple-http
        image: python:2.7
        imagePullPolicy: IfNotPresent
        command: \["/bin/bash"\]
        args: \["-c", "echo \\"<p>Hello from $(hostname)</p>\\" > index.html; python -m SimpleHTTPServer 8080"\]
        ports:
        - name: http
          containerPort: 8080

部署创建了两个非常简单的http服务器pod,它们在端口8080上以其运行的Pod的主机名进行响应。使用kubectl apply创建此部署后,我们可以看到Pod在集群中运行,并且我们还可以查询到查看他们的Pod网络地址是什么:

**$ kubectl apply -f test-deployment.yaml  
deployment "service-test" created

$ kubectl get pods  
service-test-6ffd9ddbbf-kf4j2    1/1    Running    0    15s  
service-test-6ffd9ddbbf-qs2j6    1/1    Running    0    15s

$ kubectl get pods --selector=app=service_test_pod -o jsonpath='{.items[*].status.podIP}'  
10.0.1.2 10.0.2.2

我们可以通过创建一个简单的客户端容器来发出请求,然后查看输出来证明容器网络正在运行。

apiVersion: v1
kind: Pod
metadata:
  name: service-test-client1
spec:
  restartPolicy: Never
  containers:
  - name: test-client1
    image: alpine
    command: ["/bin/sh"]
    args: ["-c", "echo 'GET / HTTP/1.1\r\n\r\n' | nc 10.0.2.2 8080"]

创建此容器后,命令将运行至完成,容器将进入“已完成”状态,然后可以使用 kubectl logs 检索输出:

**$ kubectl logs service-test-client1  
HTTP/1.0 200 OK  
<!-- blah -->

<p>Hello from service-test-6ffd9ddbbf-kf4j2</p>

此示例中没有任何内容显示客户端Pod在哪个节点上创建,但是由于Pod网络,无论客户端Pod在群集中的哪个位置运行,它都可以到达服务器Pod并获得响应。但是,如果服务器Pod死掉并重新启动,或重新安排到其他节点,则其IP几乎可以肯定会发生变化,并且客户端会中断。我们通过创建服务来避免这种情况。

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

服务是一种Kubernetes资源,可导致将代理配置为将请求转发到一组Pod。将接收流量的Pod集合由选择器确定,该选择器与创建Pod时分配给Pod的标签匹配。创建服务后,我们可以看到已为其分配了IP地址,并将在端口80上接受请求。

$ kubectl get service service-test  
NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE  
service-test 10.3.241.152 <none> 80/TCP 11s

可以将请求直接发送到服务IP,但是最好使用解析为IP地址的主机名。幸运的是,kubernetes提供了一个内部群集DNS,用于解析服务名称,并且在客户端pod稍作更改的情况下,我们可以使用它:

apiVersion: v1
kind: Pod
metadata:
  name: service-test-client2
spec:
  restartPolicy: Never
  containers:
  - name: test-client2
    image: alpine
    command: ["/bin/sh"]
    args: ["-c", "echo 'GET / HTTP/1.1\r\n\r\n' | nc service-test 8080"]

此Pod运行完成后,输出显示服务已将请求转发到服务器Pod之一。

$ kubectl logs service-test-client2 
HTTP/1.0 200 OK  
<!-- blah -->

<p>Hello from service-test-6ffd9ddbbf-kf4j2</p>

您可以继续运行客户端Pod,并且会看到两个服务器Pod的响应,每个服务器窗格大约收到50%的请求。如果您的目标是了解它是如何工作的,那么最好的出发点是为我们的服务分配的IP地址。

Service网络

分配测试服务的IP表示网络上的地址,您可能已经注意到,该网络与Pod所在的网络不同。

thing IP network  
----- -- -------  
pod1 10.0.1.2 10.0.0.0/14  
pod2 10.0.2.2 10.0.0.0/14  
service 10.3.241.152 10.3.240.0/20

它也与节点所在的专用网络不同,下面将更加清楚。在第一篇文章中,我注意到Pod网络地址范围未通过kubectl公开,因此您需要使用提供程序特定的命令来检索此集群属性。服务网络地址范围也是如此。如果您在Google Container Engine中运行,则可以执行以下操作:

$ gcloud container clusters describe test | grep servicesIpv4Cidr  
servicesIpv4Cidr: 10.3.240.0/20

由该地址空间指定的网络称为“服务网络”。每个“ ClusterIP”类型的服务都将在该网络上分配一个IP地址。还有其他类型的服务,在下一篇有关Ingress的文章中,我将讨论其中的几个。但是ClusterIP是默认的,这意味着“将为该服务分配一个IP地址,该IP地址可以从群集中的任何Pod访问。”您可以通过运行带有服务名称的kubectl describe services命令来查看服务的类型。

$ kubectl describe services service-test  

Name: service-test  
Namespace: default  
Labels: <none>  
Selector: app=service\_test\_pod  
Type: ClusterIP  
IP: 10.3.241.152  
Port: http 80/TCP  
Endpoints: 10.0.1.2:8080,10.0.2.2:8080  
Session Affinity: None  
Events: <none>

像Pod网络一样,服务网络是虚拟的,但是它在某些有趣的方面不同于Pod网络。考虑Pod网络地址范围10.0.0.0/14。如果您要查看组成集群中节点的主机,列出网桥和接口,则将看到在该网络上配置了地址的实际设备。这些是每个Pod的虚拟以太网接口,以及将它们彼此连接以及与外界连接的桥梁。

现在查看服务网络10.3.240.0/20。您可以通过ifconfig令自己高兴,并且在该网络上找不到配置有地址的任何设备。您可以在连接所有节点的网关上检查路由规则,而找不到该网络的任何路由。服务网络不存在,至少不作为连接接口存在。但是正如我们在上面看到的那样,当我们以某种方式向该网络上的IP发出请求时,该请求又将其发送给了在Pod网络上运行的服务器Pod。那是怎么发生的?让我们跟随一个包看看。

想象一下,我们上面运行的命令在测试集群中创建了以下Pod:

m-02.png

在IP网络通常配置有路由,这样,当接口由于本地不存在具有指定地址的设备而无法将数据包传递到其目的地时,会将其转发到其上游网关。因此,在此示例中,看到数据包的第一个接口是客户端Pod内的虚拟以太网接口。该接口位于Pod网络10.0.0.0/14上,并且不知道地址为10.3.241.152的任何设备,因此它将数据包转发到其网关即网桥cbr0。网桥很笨,只是来回传递流量,我们有两个节点,连接它们的网关(也具有Pod网络的路由规则)和三个Pod:节点1上的客户端Pod,节点1上的服务器Pod和节点2上的另一个服务器Pod。客户端使用DNS名称service-test向服务发出http请求。群集DNS系统将该名称解析为服务群集IP 10.3.241.152,客户端Pod最终创建了一个http请求,该请求导致一些数据包使用该IP在目标字段中发送。

IP网络通常配置有路由,这样,当接口由于本地不存在具有指定地址的设备而无法将数据包传递到其目的地时,会将其转发到其上游网关。因此,在此示例中,看到数据包的第一个接口是客户端Pod内的虚拟以太网接口。该接口位于Pod网络10.0.0.0/14上,并且不知道地址为10.3.241.152的任何设备,因此它将数据包转发到其网关即网桥cbr0。网桥非常笨拙,只是来回传递流量,因此网桥将数据包发送到主机/节点以太网接口。

m-03.png

本例中的主机/节点以太网接口位于网络10.100.0.0/24上,它也不知道地址为10.3.241.152的任何设备,因此通常再次发生的情况是,数据包将被转发到该接口的网关,图中所示的顶级路由器。相反,实际发生的是该数据包在飞行中被卡住并重定向到实时服务器Pod之一。

m-04.png

三年前,当我第一次开始使用kubernetes时,上图中发生的事情似乎非常神奇。我的客户以某种方式能够连接到没有任何接口的地址,并且这些数据包在群集中的正确位置弹出。后来我了解到,这个谜题的答案是一款名为kube-proxy的软件。

kube-proxy

就像kubernetes中的所有内容一样,服务只是一种资源,是中央数据库中的一条记录,它描述了如何配置一些软件来执行某些操作。实际上,一项服务会影响集群中多个组件的配置和行为,但是在这里很重要的一项服务(使上述魔术得以实现的一项服务)是kube-proxy。你们中的许多人都会基于名称对该组件的作用有一个大致的了解,但是关于kube-proxy的某些事情使其与典型的反向代理(如haproxy或linkerd)大不相同。

代理的一般行为是通过两个打开的连接在客户端和服务器之间传递流量。客户端将入站连接到服务端口,代理将出站连接到服务器。由于所有此类代理都在用户空间中运行,因此这意味着数据包在每次通过代理的过程中都会被封送到用户空间并返回内核空间。最初,kube-proxy只是作为这样的用户空间代理实现的,但有所不同。代理需要一个接口,既可以侦听客户端连接,又可以用于连接到后端服务器。节点上唯一可用的接口是:a)主机的以太网接口;或b)Pod网络上的虚拟以太网接口。

为什么不在这些网络之一上使用地址?我没有任何相关知识,但是我想我很早就知道在项目中这样做会复杂化那些网络的路由规则,这些规则旨在满足Pod和节点的需求,这两个都是短暂的集群中的实体。服务显然需要它们自己的,稳定的,无冲突的网络地址空间,而虚拟IP系统最有意义。但是,正如我们指出的,该网络上没有实际的设备。您可以在路由规则,防火墙过滤器等中使用伪装的网络,但实际上无法在端口上侦听或通过不存在的接口打开连接。

Kubernetes使用Linux内核的一个名为netfilter的功能和一个名为iptables的用户空间接口来解决这个问题。这篇已经很长的帖子没有足够的空间来介绍它的确切工作方式。如果您想了解更多信息,netfilter pages是一个很好的起点。netfilter是基于规则的数据包处理引擎。它在内核空间中运行,并在生命周期的各个点查看每个数据包。它根据规则对数据包进行匹配,并在找到匹配的规则时采取指定的操作。它可以采取的许多措施之一是将数据包重定向到另一个目的地。没错,netfilter是内核空间代理。下面说明了当kube-proxy作为用户空间代理运行时netfilter扮演的角色。

m-05.png

在这种模式下,kube-proxy在本地主机接口上打开一个端口(在上面的示例中为10400),以侦听对test-service的请求,插入netfilter规则以将发往服务IP的数据包重新路由到其自己的端口,并将这些数据包转发。对端口8080上的Pod的请求。这就是对10.3.241.152:80的请求如何神奇地变为对10.0.2.2:8080的请求。鉴于netfilter的功能,使所有服务都能正常工作所需要的一切就是让kube-proxy打开一个端口并为该服务插入正确的netfilter规则,以响应来自主api服务器的通知更改。

这个故事还有一点曲折。上面我提到,由于封送数据包,用户空间代理很昂贵。在kubernetes 1.2中,kube-proxy获得了在iptables模式下运行的能力。在这种模式下,kube-proxy通常不再充当集群间连接的代理,而是委托netfilter进行检测绑定到服务IP的数据包并将它们重定向到Pod的工作,所有这些操作都发生在内核空间中。在这种模式下,kube-proxy的工作或多或少地局限于保持netfilter规则同步。

m-06.png

最后,让我们将上述所有内容与文章开头提到的关于可靠代理的要求进行比较。服务代理系统是否耐用?默认情况下,kube-proxy作为systemd单元运行,因此如果失败,它将重新启动。在Google Container Engine中,它作为由守护程序控制的容器运行。这将是将来的默认设置,可能是1.9版。作为用户空间代理,kube-proxy仍然代表连接失败的单点。当以iptables模式运行时,从尝试尝试连接的本地Pod的角度来看,该系统具有很高的持久性,因为如果节点启动,则netfilter也是如此。

服务代理是否知道可以处理请求的健康服务器容器?如上所述,kube-proxy侦听主api服务器以了解集群中的更改,其中包括对服务和端点的更改。当它收到更新时,它使用iptables使netfilter规则保持同步。创建新服务并填充其端点时,kube-proxy获取通知并创建必要的规则。同样,删除服务时,它也会删除规则。针对端点的运行状况检查由kubelet(在每个节点上运行的另一个组件)执行,当发现不正常的端点时,kubelet通过api服务器通知kube-proxy,并编辑netfilter规则以删除此端点,直到它再次恢复健康为止。

所有这些加在一起构成了一个高度可用的集群范围的设施,用于在Pod之间代理请求,同时允许Pod本身随着集群需求的变化而来来往往。但是,该系统并非没有缺点。最基本的是,它仅适用于针对群集内部发出的请求(即从一个Pod到另一个Pod的请求)进行描述。另一个原因是netfilter规则工作方式的结果:对于来自群集外部的请求,规则会混淆原始IP。这引起了一些辩论,解决方案正在积极考虑中。当我们在本系列的最后一篇文章中讨论Ingress时,我将更仔细地研究这两个问题。


iyacontrol
1.4k 声望2.7k 粉丝

专注kubernetes,devops,aiops,service mesh。