在本系列的第一篇文章中,研究了kubernetes如何结合使用虚拟网络设备和路由规则,以容许在一个群集节点上运行的Pod与在另外一个群集节点上运行的Pod通讯,只要发送者知道接收者的Pod网络便可。 IP地址。若是您还不熟悉Pod的交流方式,那么在继续以前值得一读。集群中的Pod网络很简单,但仅凭其不足以建立持久性系统。那是由于kubernetes中的Pod是短暂的。您能够将Pod IP地址用做终结点,但不能保证该地址在下次从新建立Pod时不会更改,这可能因为多种缘由而发生。html
您可能已经意识到这是一个老问题,而且它有一个标准的解决方案:经过反向代理/负载均衡器运行流量。客户端链接到代理,代理负责维护将请求转发到的健康服务器列表。这对代理服务器提出了一些要求:代理服务器自己必须是耐用的而且可以抗故障;它必须具备能够转发到的服务器列表;而且它必须具备某种方式来了解特定服务器是否运行正常并可以响应请求。 kubernetes设计师以一种优雅的方式解决了这个问题,该方式创建在平台的基本功能之上,能够知足全部这三个需求,而且从一种称为服务的资源类型开始。python
在第一篇文章中,我展现了一个假设的集群,其中包含两个服务器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网络地址是什么:json
**$ 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
咱们能够经过建立一个简单的客户端容器来发出请求,而后查看输出来证实容器网络正在运行。segmentfault
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几乎能够确定会发生变化,而且客户端会中断。咱们经过建立服务来避免这种状况。api
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上接受请求。bash
$ 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地址。
分配测试服务的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:
在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。网桥很是笨拙,只是来回传递流量,所以网桥将数据包发送到主机/节点以太网接口。
本例中的主机/节点以太网接口位于网络10.100.0.0/24上,它也不知道地址为10.3.241.152的任何设备,所以一般再次发生的状况是,数据包将被转发到该接口的网关,图中所示的顶级路由器。相反,实际发生的是该数据包在飞行中被卡住并重定向到实时服务器Pod之一。
三年前,当我第一次开始使用kubernetes时,上图中发生的事情彷佛很是神奇。个人客户以某种方式可以链接到没有任何接口的地址,而且这些数据包在群集中的正确位置弹出。后来我了解到,这个谜题的答案是一款名为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扮演的角色。
在这种模式下,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规则同步。
最后,让咱们将上述全部内容与文章开头提到的关于可靠代理的要求进行比较。服务代理系统是否耐用?默认状况下,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时,我将更仔细地研究这两个问题。