K8S的网络接口CNI及灵雀云的实践
K8S的网络模型
我们从底层网络来看,分为三个层面。首先是Pod之间的多个容器的网络互通。我们知道,K8S的Pod可以由多个容器组成,这个层面网络互通是比较简单的,因为所有的容器都是共享一个网卡,可以直接通信。
第二个,一台虚拟机上多个容器之间的网络是如何通信的。这块儿其实也比较好解决,例如Docker会搭一个网桥,让上面所有的东西、网卡接到网桥上,他们之间的网络就可以互通。Docker默认服务会创建一个Docker0的网桥,其它主流的像Calico、Flannel的模式也是类似的。这种方式的实现也是现在的主流。
第三种是比较难的,Docker一开始就没有做好,是跨主机的Pod之间的网络通信。对于K8S来说,这个网卡如何分配其实K8S是没有定义的。不同Pod跨主机网络之间如何通信、如何打通网络,K8S不管,是交给第三方实现的。在这块,是有很多工作可以做的。
容器网络设计给运维人员带来的困惑
现在我们再想一些问题,容器网络设计会给传统运维工作带来怎样的困惑?
传统运维工作强调对IP要有很强的管控。容器时代,Pod需不需要有固定的IP?Pod重启之后,IP是否不变?其实K8S没有规定,而且从大部分主流的实现来看,容器IP是可以变的。既然容器的IP是会变的,就会带来一个很直接的问题,我想访问这个服务怎么访问?之前两位同事讲过很多,我不做很细的介绍。
这个大致的原理是,尽管底层Pod IP不断变化,但是我会给你提供固定的域名或者DNS的方式,或者固定的Cluster IP的方式等等。不管Pod方式如何变,总会给你固定的方式,通过固定的方式可以访问到一直变化的IP,这些方式都是通过Kube-proxy、iptables、Kube-dns实现。
我们实际测的时候发现,K8S的服务发现功能比较少,随着访问量越来越大,会发现它的性能有很多问题,还有稳定性也有很严重的问题,而且它的稳定性缺陷是它从设计当初就很难避免。我们正在改进这些设计缺陷问题。
为此很多人就会问,说你的容器IP是一直变的,怎么管?但容器就是这样的,容器生命周期很短,不断创建、不断消失,IP肯定不断变。容器的好处是,你可以在这台机器上挂,在另外一台机器也可以。但运维人员首先觉得IP这样飘不好。对于运维来说,网络方面是很重要的资源,要对IP进行强管控,服务来回飘,会让他的安全感下降很多。
运维服务有很多基于IP的东西,有流量和突发的监控,如果你服务的IP一直变化,通过这个IP它很难用到这个服务,相当于IP的监控就没有意义,因为根本不知道IP流量上去了是哪个服务的,很难对应到这个事。
还有是对于IP安全策略没有办法做。如果以前IP是固定的,iptables或者网络底层防火墙,这样的东西都是很好做的,可以很好的进行服务之间的安全访问限制。如果IP都是变的,这个事情就变得没有办法做了。
第四是定位分析和数据处理问题。我们见到,有的客户把网络正常在跑的所有流量镜像一份,每天对复制的流量进行分析,看你有没有违规操作或者有什么恶意攻击,或者哪些行为不正常,这样他们通过IP再定位服务。如果IP是变的,相当于把他们很多能做的事情去掉了。
还有一些特殊的软件,他们的license是根据网卡来的,就是说他的license是根据网卡的IP加上Mac计算出来的。这种情况下相当于IP变了,license失效,软件跑不起来。
还有你的IP不断变化,实际中也有问题,有很多服务和软件就是基于IP部署的,基于IP相互发现的,最典型的是Etcd这样的。如果你是用K8S或者其他的容器,IP是变的,这个东西怎么填都是很难的事情。
K8S有它的解决方案,Headless Server和 Stateful Set可以做这个事情。但是这个事情在我看来有两个问题,它引入两个不好理解的概念,如果大家对K8S比较关心,第一次看,我觉得你看好半天才能明白它干什么,它为什么这么设计。它的核心理想是你的Pod IP是变的,然后给Pod一个顺序,给每个Pod一个标志符,etcd1、2、3,给他们三个人每个人用k8s Service+域名,你在里面不用填IP,填对应的三个域名就可以。但域名有cash缓存的问题,底层Pod IP失效,域名的缓存没有失效就会造成研判时间的不一致,我觉得靠Service做这件事情也是有问题的,并不能完美的解决这个问题。
为什么Docker、K8S都没有做固定IP的事情?
刚才说了IP不固定的影响,做容器之前大家可能没有想这个事,可能从一开始就认为IP是不固定的,我们想一下如果IP是固定的有什么影响,其实好像还可以用,并且可能变得更简单。比如Cluster IP或者DNS它们的映射由于IP是固定的可能变得更简单了。如果容器的IP固定,运维传统的基于IP的监控也可以做了,因为对他们来说没有影响,和原来是一样的,对他们的接受程度也会更高一点。
我们很多服务发现的方式,可能之前通过K8S的Sevrice来进行服务发现,既然IP固定,我们可以跳过,直接用IP服务发现就可以,没有必要让你的运维理解很复杂的概念。大家会问什么是Cluster IP,什么是Pod,这些还是有理解成本,如果提供原生的方式也是不错的。
可能想到一个问题,既然固定IP还是有好处的,为什么Docker还是K8S都没有做这个事情呢?我觉得一开始Docker没有想好这个事情怎么做。最开始Docker就是一个单机的工具,没有想做得很复杂,最早Docker的网络和存储都是很弱的,它根本没有往这方面想。
还有一点,在我理解就是理念之争。到底服务应该做成有状态的还是无状态的?有Docker之后大家认为容器应该是跑无状态的东西,而不是有状态的东西,但是网络这块是有状态的。我们之前说有状态想到的是存储,认为存储才是有状态的服务,其实网络也是状态的一部分。大家可以想一下,如果Pod里面的服务访问外面,给外部留下的信息是某个IP访问我,所以对外的状态对你来说,IP其实也是你的一个状态,所以说网络其实也是状态的一部分,如果要做有状态这个事情就变得很难。
但是能不能做呢?可以做,我们做了一些尝试,做的方式是我们自定义CNI插件。
如何自定义CNI插件?
CNI,这个概念大家平时听的少一点。它的全称是Container Network Interface,它注重给你的容器提供一套标准的网络的接口。现在包括K8S等都是通过CNI这种方式实现的。CNI 是 CNCF 的一个项目,如果你对比其它项目,CNI应该是里面最简单的。如果你对这个项目有兴趣,从这里开始是比较好的,可能几百行或者上千行的代码就到头了。一个是CNI的标准,包括CNI接口和库,怎么快速实现CNI,还有CNI提供的网络插件。
介绍一下Kubernetes和CNI之间是如何交互的,给大家讲完之后,大家可以比较容易上手,自己实现自己的网络。
这是Kubernetes的基本参数,大家可以看到。红色的标起来的是两个选项。CNI在我看来是有点奇怪的实现。一般的时候一个程序和另一个程序交互,大概是HTTP REST或者是RPC或者TCP的通信,但是CNI和Kubernetes的交互方式是通过二进制文件的方式,它会调用二进制文件传一个参数来做这样的事情。第二张是CNI实现的组件,他们都是以二进制的形式放在目录下面。左下角是CNI的配置,是最简单的,参数很少,还有一些其他比较复杂的。其实比较重要的是type和IPAM。
大概讲一下它交互的流程,是通过创建Pod和销毁Pod两个事件来触发的。创建Pod的时候,它会去调CNI,它会根据这个文件会先调用macvlan,创建一个网卡,然后再分配IP,都是调里面的DHCP和macvlan二进制文件。创建网卡,构建IP,把这个网卡放到Pod network namespace,相当于完成Pod网卡的创建。还有,Pod删除的时候,可能调用二进制文件释放掉,然后删除网卡,完成网卡的销毁。大概的交互比较简单,就这两个,实现的时候也是实现这两个方法就可以。
最下面是main函数,其实有一个库,可以方便实现,照着这个写就可以,需要实现的是下面的函数就可以。函数其它的都不太用着重关注,都是做错误处理。最主要关注上面的,它是在NetNSpath里面创建的一块link,删除的时候再把这块网卡删除,这是最简单的设备创建。如果大家对网络比较熟,或者对自己的网络有特殊的要求或者定制化的需求,其实在里面实现自己的逻辑就可以,整体来说它的逻辑还是比较简单的。而且还有一个功能是多个命令是可以串联的,比如我刚才举的例子,macvlan和DHCP是串联的,macvlan先创建网卡,然后再调DHCP,分配IP,他们之间有一个result的返回,通过result进行数据结构的传输。我们看一下result就可以知道网络还可以做什么别的事情。
这是官方定义的API标准,其实我们也是可以在里面扩充的。上面是interfaces,Kubernetes这个网络做得实在太简单,包括容器中的很多东西都是太简单了,可能一个pod里面有一个网卡设一个IP就完了。其实一个pod里面有多个网卡的需求,一个数据流,一个控制流,这种是可以做的。在它的interfaces里面可以看到有内置的网卡和mac地址,因为我们碰到有些客户,你的mac是可信域才可以做安全策略,这需要你要自定义的mac。再往下,网卡是不是有多个IP,而不是常用的网络,一个容器的一个IP。再往下可以定义这个容器的路由表,包括路由的顺序等等都是可以定义的。再往下是DNS server,容器里面的DNS都可以自定义,容器通信网络这块有很多可以做的事情,如果大家想做这方面,我觉得机会还是很大的。现在的网络说实在的,尽管实现很多,但是都还是蛮弱的。
容器网络方面已经有一些第三方实现,比较知名的有flannel、calico、weave……。下面两项其实是它官方已经实现的,看起来分两大类:一是网卡生成的方式,比如bridge、ipvlan、macvlan、loopback、ptp、vlan。第二大块是IPAM。我不知道大家是否理解IPAM,IPAM的意思是IP该怎么分配。官方实现的有两种,一种是DHCP,一种是host-local。
灵雀云如何通过IPAM做到精确管控IP?
回到之前说的我们要做固定IP,大家可以很容易联想IPAM这块做一些东西,因为这个IPAM是针对IP的,如果要做的话,先看一下现有的两种有没有问题,或者现有的两种怎么实现。DHCP大家比较熟悉,就是自动分配IP,它的问题是没有办法精细管控IP。因为当需要通过主机发再发DHCP的广播再获得IP,相当于容器IP和宿主机的IP混在一起,很难精确分配到某个Pod使某个IP,因为你不知道这些信息。这在实际生产环境用得很少。因为IP管控要求很严格,不可能自己计算IP,我们家里的wifi或者办公才会用到,企业级业务碰到的机会都很少。
host-local,每台机器上都有这个文件,这个文件上规定每台机器能生成一些IP。可以看一下配置文件的格式。其实这两个参数比较重要,一个是rangeStart和rangeEnd,就是每台机器分配的范围是多少。这种分配方式有一个很严重的问题, Calico 和 Flannel都有,就是每台机器都是固定的,这给每台机器划分了一个网关,相当于你Pod是在机器上的,因为IP是很不灵活,很难实现IP的漂移。而且分配IP比较困难,新增机器要重新定义,这台机器用多少IP,要考虑IP段的释放,很不灵活,显然没有办法实现固定IP的事情。
为什么这个事情感觉这么难?我觉得也是K8S设计理念的问题,我们可以看到K8S管了很多东西,管了Pod、configmap、Service等,你会发现它没有管你的IP。理论上来说,IP对你的服务或者对你的集群来说是很重要的资源,但是K8S里面IP不是它的一等应用,甚至你根本没有一个跟容器相关的资源在开发里面找到,所以使得你的IPAM变得很难。你要么通过K8S完全无关的东西来实现,要么通过锁死的方式实现,这其实都是因为你没有把IP资源看得特别重,你只是当做可以随机分配的,所以它设置的理念就是你的IP我不关心,该是什么就是什么,所以导致这样的结果。
最后讲一下我们做的,我们把IP当做很重要的资源,进行单独的管理。我们会专门实现IPAM的组件,实现网段的添加、删除,可以在K8S管理网段,给某个业务或者某几个用户分配一个网关,然后对IP进行网关设置,路由设置,和DNS设置。有了网段之后,有IP的添加删除,会有很重要的资源,该用哪些IP,哪些IP是可以用的,都是可以用管理员指定的,而不是像K8S随机划一个网段,还有权限、配额,之后就可以顺利创建服务了。
现在创建K8S的Deploy的时候需要申明用哪几个IP。如果IP信息传输到IPAM的组件,这个IPAM就是一个数据库,这个数据库就会显示哪些IP可以使用,哪些应该被哪个服务使用,把这个记录下来。在K8S启动Pod,CNI里面再实现一套逻辑,把IPAM的逻辑实现了,那套做的东西会进入IPAM的数据库里面找,如果用就占用这个,别人不能再用,就可以实现每个POD有自己的IP,这个IP是你之前想要的。如果在删除的时候,CNI会把IP释放掉,说明这个IP不会再被这个服务使用,别的服务可以使用这个IP。我们如果想创建etcd的服务,就不用再写一个东西,你只要做你想要的IP就可以,很大的简化你的工作量,而且实现我之前说的传统的运维,IP的监控、管理的东西。