Kubernetes网络之Services

meShell · 2019-08-12

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

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

Services

在第一篇文章中,我展示了一个带有两个服务器pod的假设集群,并描述了它们如何跨节点进行通信。在这里,我希望以该示例为基础来描述kubernetes服务如何在一组服务器pod中实现负载平衡,从而允许客户端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

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


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"]

创建此pod后,命令将运行完成,pod将进入”已完成”状态,然后可以使用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要死并重新启动,或者被重新安排到另一个节点,那么它的IP几乎肯定会改变,客户端也会崩溃。我们通过创建服务来避免这种情况。


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

Service是一种kubernetes资源,可以将代理配置为将请求转发到一组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 80"]

在此pod运行完成后,输出显示该服务将请求转发到其中一个服务器pod。


$ kubectl logs service-test-client2
HTTP/1.0 200 OK
<!-- blah -->
<p>Hello from service-test-6ffd9ddbbf-kf4j2</p>

您可以继续运行客户端,并且您将看到来自两个服务器窗格的响应,每个服务器窗格获得大约50%的请求。如果您的目标是了解其实际工作原理,那么一个好的起点就是我们的服务所分配的IP地址。

service网络

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


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容器引擎中运行,则可以执行以下操作:

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

此地址空间指定的网络称为”service network”。每个“ClusterIP”类型的服务都将在此网络上分配一个IP地址。还有其他类型的服务,我将在下一篇文章中讨论其中的几个,但是ClusterIP是默认的,它意味着“将为服务分配一个可从群集中的任何pod访问的IP地址”。您可以通过使用服务名称运行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网络的路由规则)和三个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。网桥非常愚蠢,只是来回传递流量,因此网桥将数据包发送到主机/节点以太网接口。

kubernetes-service-network

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

kubernetes-service-network

三年前,当我第一次开始使用kubernetes时,上图中发生的事情看起来非常神奇。不知何故,我的客户端能够连接到没有与之关联的接口的地址,并且这些数据包在群集中的正确位置弹出。我后来才知道这个谜团的答案是一个叫做kube-proxy的软件。

kube-proxy

与kubernetes中的所有内容一样,服务只是一种资源,是中央数据库中的记录,它描述了如何配置一些软件来执行某些操作。事实上,服务会影响集群中几个组件的配置和行为,但是这里重要的一个是上面描述的魔法,它是kube-proxy。你们中的许多人将根据名称大致了解这个组件的功能,但是有一些关于kube-proxy的东西使它与像haproxy或者linkerd这样的典型反向代理完全不同。

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

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

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

kubernetes-service-network

在此模式下,kube-proxy在本地主机接口上打开一个端口(上例中为10400)以侦听对测试服务的请求,插入netfilter规则以将发往服务IP的数据包重新路由到其自己的端口,并转发这些请求端口8080上的pod。这就是对10.3.241.152:80的请求神奇地成为对10.0.2.2:8080的请求的方式。鉴于netfilter的功能,所有这些都需要使所有服务都能用于kube-proxy以打开端口并为该服务插入正确的netfilter规则,以响应来自主api服务器的更改的通知集群。

这个故事还有一点点扭曲。我在上面提到过,由于编组数据包,用户空间代理很昂贵。在kubernetes1.2版本中kube-proxy获得了在iptables模式下运行的能力。在这种模式下,kube-proxy通常不再是集群间连接的代理,而是委托netfilter检测绑定服务IP的数据包并将其重定向到pods,所有这些都发生在内核空间中。在这种模式下,kube-proxy的工作或多或少局限于保持netfilter规则同步。

kubernetes-service-network

为了结束,让我们将上述所有内容与帖子开头为可靠代理设置的要求进行比较。服务代理系统是否经久耐用?默认情况下,kube-proxy作为systemd单元运行,因此如果失败则会重新启动。在Google Container Engine中,它作为由守护进程控制的pod运行。这将是未来的默认值,可能是版本1.9。作为用户空间代理,kube-proxy仍然代表单点连接失败。在iptables模式下运行时,从尝试连接的本地pod的角度来看,系统非常耐用,因为如果节点已启动,则netfilter也是如此。

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

所有这些都增加了一个高度可用的集群范围的工具,用于在pod之间代理请求,同时允许pod自身随着集群需求的变化而变化。然而,该系统并非没有缺点。最基本的一点是,它只能用于发起集群内部的请求,即从一个pod到另一个pod的请求。另一个是netfilter规则工作方式的结果:对于从集群外部到达的请求,规则会混淆源IP。这引起了一些争论,正在积极考虑解决方案。当我们在本系列的最后一篇文章中讨论入口时,我会更仔细地研究这两个问题。

翻译:https://medium.com/google-cloud/understanding-kubernetes-networking-services-f0cb48e4cc82?