第6章 简洁的服务模型
在CNCF(云原生计算基金会)对云原生的定义中,容器、微服务、服务网格、不可变基础设施和声明式API被作为云原生的代表性技术。其中的容器、不可变基础设施和声明式API的作用是建立一个自动化的、容错性强的应用承载平台;而微服务和服务网格的作用则是为应用提供优秀的内部模块间的互通机制和对外服务接口机制。
Kubernetes作为云原生的操作系统,实现了一套默认的服务机制。这套机制的特点是足够健壮且足够简单,基本上能够满足大多数业务需求,用户可以在这套机制的基础上搭建自己的微服务环境。
虽然Kubernetes的服务机制上手比较容易,但是在对云上海量问题的处理过程中,我们发现大多数工程师对服务机制,或者说机制背后的原理不是很清楚,这会严重影响工程师的运维开发效率。
本章以Kubernetes服务为主题,希望通过深入的解释,可以让读者理解服务的本质及实现原理。
6.1 服务的本质是什么
从概念上来讲,Kubernetes集群的服务,其实就是负载均衡或反向代理。这跟阿里云的负载均衡产品有很多类似的地方。和负载均衡一样,服务有它的IP地址以及前端端口,同时服务后面会挂载多个容器组作为其“后端服务器”,这些“后端服务器”有自己的IP地址以及监听端口,如图6-1所示。
图6-1 Kubernetes服务的本质
当这样的负载均衡和后端的架构与Kubernetes集群结合的时候,我们可以想到的最直观的实现方式,就是集群中某一个节点专门做负载均衡(类似Linux虚拟服务器)的角色(见图6-2),而其他节点则用来承载后端容器组。
这样的实现方法有一个巨大的缺陷,就是单点问题。Kubernetes集群是Google多年来自动化运维实践的结晶,这样的实现显然与其自动化运维的哲学是相背离的。
6.2 自带通信员
边车(sidecar)模式是微服务领域的核心概念。边车模式换一个通俗一点的说法,就是自带通信员模式。熟悉服务网格的读者肯定对它很熟悉了,但是可能很少有人注意到,其实Kubernetes集群原始服务的实现,也是基于边车模式的,如图6-3所示。
图6-2 集群节点实现负载均衡
图6-3 服务本质上是边车模式
在Kubernetes集群中,服务的实现实际上是为每一个集群节点部署了一个反向代理sidecar。而所有对集群服务的访问,都会被节点上的反向代理转换成对服务后端容器组的访问。基本上,节点和这些sidecar的关系如图6-4所示。
图6-4 节点和服务关系图
6.3 让服务照进现实
我们在前面两节中看到了,Kubernetes集群的服务本质上是负载均衡,即反向代理;同时我们知道了,在具体实现中,这个反向代理,并不是部署在集群某一个节点上的,而是作为集群节点的sidecar部署在每个节点上的。
在这里让服务“照”进反向代理这个“现实”的,是Kubernetes集群的一个控制器,即kube-proxy。简单来说,kube-proxy作为部署在集群节点上的控制器,通过集群API Server监听着集群状态变化。当有新的服务被创建的时候,kube-proxy会把集群服务的状态、属性,翻译成反向代理的配置,整个过程如图6-5所示。
图6-5 Kubernetes服务框架图
那剩下的问题就是反向代理(即图6-5中的Proxy)的实现了。
6.4 基于Netfilter的实现
Kubernetes集群节点实现服务反向代理的方法目前主要有三种,即userspace、iptables以及ipvs。本章以阿里云Flannel集群网络为范本,仅对基于iptables的服务实现做深入讨论。
6.4.1 过滤器框架
现在我们来设想一种场景。我们有一间屋子,这间屋子的水管有一个入口和一个出口。从入口进入的水是不能直接饮用的,因为有杂质,而我们期望从出口流出的水可以直接饮用。为了达到目的,我们切开水管,在中间加一个杂质过滤器,如图6-6所示。
图6-6 有杂质过滤功能的水管
过了几天,我们的需求变了,我们不仅要求从屋子里流出来的水可以直接饮用,还希望水是热水。所以我们不得不再在水管上增加一个切口,并增加一个“温度过滤器”,即加热器。改变后的状态如图6-7所示。
图6-7 有杂质过滤和加热功能的水管
很明显,这种切开水管增加新功能的方法是很原始的,因为需求可能随时会变。我们甚至很难保证,在经过一年半载之后,这根水管还能找得到可以被切开的地方。所以我们需要重新设计一个方案。
首先我们不能随便切开水管,所以我们要把水管的切口固定下来。以上面的场景为例,我们确保水管只能有一个切口。其次,我们抽象出水的两种变化:物理变化和化学变化,从而可以设计两种处理方式。修改后的结构如图6-8所示。
图6-8 过滤器框架
基于以上的设计,如果我们需要过滤杂质,就可以在化学变化这个功能模块里增加一条过滤杂质的规则(仅用于说明模型,实际水处理过程中,过滤杂质涉及化学变化和物理变化);如果我们需要增加温度的话,就可以在物理变化这个功能模块里增加一条加热的规则。这种过滤器框架显然比切水管的方式要优秀很多。
设计这个框架,我们主要做了两件事情,一个是固定水管切口位置,另外一个是抽象并设计出两种水处理方式。理解了这两件事情之后,我们可以来看一下iptables,或者更准确的名称——Netfilter的工作原理。
Netfilter实际上就是一个过滤器框架。Netfilter在网络包收发及路由的“管道”上,一共“切”了5个口,分别是PREROUTING、FORWARD、POSTROUTING、INPUT以及OUTPUT,同时Netfilter定义了包括NAT、Filter在内的若干个网络包处理方式。Netfilter框架如图6-9所示。
图6-9 Netfilter框架图
需要注意的是,Routing 和FORWARD 很大程度上增加了以上Netfilter的复杂程度,如果我们不考虑Routing和FORWARD,那么Netfilter会变得和我们的水过滤器框架一样简单。
6.4.2 节点网络大图
现在我们参考图6-10所示的Kubernetes集群节点网络全貌。横向来看,节点上的网络环境被分割成不同的网络命名空间,包括主机网络命名空间和Pod网络命名空间;纵向来看,每个网络命名空间包括完整的网络栈:从应用到协议栈,再到网络设备。
在网络设备这一层,我们通过cni0虚拟网桥组建出系统内部的一个虚拟局域网。Pod网络通过Veth对连接到这个虚拟局域网内,cni0虚拟局域网通过主机路由以及网口eth0与外部通信。
在网络协议栈这一层,我们可以通过在Netfilter过滤器框架上编程,来实现集群节点的反向代理。
实现反向代理,归根结底就是做DNAT,即把发送给集群服务IP地址和端口的数据包,修改成发给具体容器组的IP地址和端口。参考图6-9中的Netfilter过滤器框架,我们知道,在Netfilter里,可以通过在PREROUTING、OUTPUT以及POSTROUTING三个位置加入NAT规则,来改变数据包的源地址或目的地址。
图6-10 Kubernetes集群节点网络全貌
因为这里需要做的是DNAT,需要改变目的地址,这样的修改必须在路由(Routing)之前发生以保证数据包可以被路由正确处理,所以实现反向代理的规则,需要被加到PREROUTING和OUTPUT两个位置。
其中,PREROUTING的规则用来处理从Pod访问服务的流量。数据包从Pod网络Veth发送到cni0之后,进入主机协议栈,首先会经过Netfilter PREROUTING的处理,所以发给服务的数据包,会在这个位置做DNAT。经过DNAT处理之后,数据包的目的地址变成另外一个Pod的地址,从而经过主机路由转发到eth0,发送给正确的集群节点。
而添加在OUTPUT这个位置的DNAT规则,则用来处理从主机网络发给服务的数据包,原理也是类似的,即在经过路由之前修改目的地址,以方便路由转发。
6.4.3 升级过滤器框架
在“过滤器框架”一节,我们看到Netfilter是一个过滤器框架。Netfilter在数据“管道”上“切”了5个口,分别在这5个口上做了一些数据包处理工作。虽然固定切口位置以及网络包处理方式分类已经极大地优化了过滤器框架,但是有一个关键的问题,就是我们还是得在管道上做修改以满足新的功能。换句话说,这个框架没有做到管道和过滤功能两者的彻底解耦。
为了实现管道和过滤功能两者的解耦,Netfilter用了表这个概念。表就是Netfilter的过滤中心,其核心功能是过滤方式的分类(表),以及每种过滤方式中过滤规则的组织(链),如图6-11所示。
图6-11 Netfilter是典型的过滤器框架
把管道和过滤功能解耦之后,所有对数据包的处理都变成了对表的配置。而管道上的5个切口,仅仅变成了流量的出入口,负责把流量发送到过滤中心,并把处理之后的流量沿着管道继续传送下去。
Netfilter把表中的规则组织成链。表中有针对每个管道切口的默认链,也有我们自己加入的自定义链。默认链是数据的入口,默认链可以通过跳转到自定义链来完成一些复杂的功能。这里允许增加自定义链的好处是显然的。为了完成一个复杂的过滤功能,比如实现Kubernetes集群节点的反向代理,我们可以使用自定义链来使我们的规则模块化。
6.4.4 用自定义链实现服务的反向代理
集群服务的反向代理,实际上就是利用自定义链,模块化地实现了数据包的DNAT转换。KUBE-SERVICE是整个反向代理的入口链,其对应所有服务的总入口;KUBE-SVC-XXXX链是具体某一个服务的入口链。KUBE-SERVICE链会根据服务IP地址,跳转到具体服务的KUBE-SVC-XXXX链。KUBE-SEP-XXXX 链代表着某一个具体Pod 的地址和端口,即Endpoint,具体服务链KUBE-SVC-XXXX会按照一定的负载均衡算法跳转到Endpoint链。其整体结构如图6-12所示。
图6-12 用自定义链实现服务的反向代理
而如前文中提到的,因为这里需要做的是DNAT,即改变目的地址,这样的修改必须在路由之前发生以保证数据包可以被路由正确处理,所以KUBE-SERVICE会被PREROUTING和OUTPUT两个默认链所调用。
6.5 总结
读完本章后,大家应该对Kubernetes集群服务的概念以及实现有了更深层次的认识。我们需要把握三个要点:
- 服务本质上是负载均衡。
- 服务负载均衡的实现采用了与服务网格类似的边车模式,而不是LVS类型的独占模式。
- kube-proxy本质上是一个集群控制器。
除此之外,我们思考了过滤器框架的设计,并在此基础上,理解了使用iptables实现的服务负载均衡的原理。
图像说明
本章包含以下图像(位置对应原文页码):
- 图6-1 (Page 90)
- 图6-2, 图6-3, 图6-4 (Page 91-92)
- 图6-5 (Page 93)
- 图6-6, 图6-7, 图6-8 (Page 94-95)
- 图6-9 (Page 96)
- 图6-10 (Page 97)
- 图6-11 (Page 98)
- 图6-12 (Page 100-101) 这些图像展示了Kubernetes服务模型、Netfilter框架、节点网络拓扑以及iptables链结构等核心概念。