一、Kubernetes + Flannel
Kubernetes的网络模型假定了所有Pod都在一个可以直接连通的扁平的网络空间中,这在GCE(Google Compute Engine)里面是现成的网络模型,Kubernetes假定这个网络已经存在。而在私有云里搭建Kubernetes集群,就不能假定这个网络已经存在了。我们需要自己实现这个网络假设,将不同节点上的Docker容器之间的互相访问先打通,然后运行Kubernetes。
Flannel是CoreOS团队针对Kubernetes设计的一个网络规划服务,简单来说,它的功能是让集群中的不同节点主机创建的Docker容器都具有全集群唯一的虚拟IP地址。而且它还能在这些IP地址之间建立一个覆盖网络(Overlay Network),通过这个覆盖网络,将数据包原封不动地传递到目标容器内。
下面是一张它的网络原理图:
可以看到,Flannel首先创建了一个名为flannel0的网桥,而且这个网桥的一端连接docker0的网桥,另一端连接一个名为flanneld的服务进程。
Flanneld进程并不简单,它首先上连etcd,利用etcd来管理可分配的IP地址段资源,同时监控etcd中每个Pod的实际地址,并在内存中建立了一个Pod节点路由表;然后下连docker0和物理网络,使用内存中的Pod节点路由表,将docker0发给它的数据包包装起来,利用物理网络的连接将数据包投递到目标flanneld上,从而完成pod到pod之间的直接的地址通信。
Flannel之间的底层通信协议的可选余地有很多,比如UDP、VXlan、AWS VPC等等。只要能通到对端的Flannel就可以了。源Flannel封包,目标Flannel解包,最终docker0看到的就是原始的数据,非常透明,根本感觉不到中间Flannel的存在。
Flannel的安装配置网上讲的很多,在这里就不在赘述了。在这里注意一点,就是flannel使用etcd作为数据库,所以需要预先安装好etcd。
下面说说几个场景:
- 同一Pod内的网络通信。在同一个Pod内的容器共享同一个网络命名空间,共享同一个Linux协议栈。所以对于网络的各类操作,就和它们在同一台机器上一样,它们可以用localhost地址直接访问彼此的端口。其实这和传统的一组普通程序运行的环境是完全一样的,传统的程序不需要针对网络做特别的修改就可以移植了。这样做的结果是简单、安全和高效,也能减少将已经存在的程序从物理机或者虚拟机移植到容器下运行的难度。
- Pod1到Pod2的网络,分两种情况。Pod1与Pod2不在同一台主机与Pod1与Pod2在同一台主机。
- 先说Pod1与Pod2不在同一台主机。Pod的地址是与docker0在同一个网段的,但docker0网段与宿主机网卡是两个完全不同的IP网段,并且不同Node之间的通信只能通过宿主机的物理网卡进行。将Pod的IP和所在Node的IP关联起来,通过这个关联让Pod可以互相访问。
- Pod1与Pod2在同一台主机。Pod1和Pod2在同一台主机的话,由Docker0网桥直接转发请求到Pod2,不需要经过Flannel。
- Pod到Service的网络。创建一个Service时,相应会创建一个指向这个Service的域名,域名规则为{服务名}.{namespace}.svc.{集群名称}。之前Service IP的转发由iptables和kube-proxy负责,目前基于性能考虑,全部为iptables维护和转发。iptables则由kubelet维护。Service仅支持UDP和TCP协议,所以像ping的ICMP协议是用不了的,所以无法ping通Service IP。
- Pod到外网。Pod向外网发送请求,查找路由表, 转发数据包到宿主机的网卡,宿主网卡完成路由选择后,iptables执行Masquerade,把源IP更改为宿主网卡的IP,然后向外网服务器发送请求。
- 集群外部访问Pod或Service
由于Pod和Service是Kubernetes集群范围内的虚拟概念,所以集群外的客户端系统无法通过Pod的IP地址或者Service的虚拟IP地址和虚拟端口号访问到它们。为了让外部客户端可以访问这些服务,可以将Pod或Service的端口号映射到宿主机,以使得客户端应用能够通过物理机访问容器应用。
总结:Flannel实现了对Kubernetes网络的支持,但是它引入了多个网络组件,在网络通信时需要转到flannel0网络接口,再转到用户态的flanneld程序,到对端后还需要走这个过程的反过程,所以也会引入一些网络的时延损耗。另外Flannel默认的底层通信协议是UDP。UDP本身是非可靠协议,虽然两端的TCP实现了可靠传输,但在大流量、高并发应用场景下还需要反复调试,确保不会出现传输质量的问题。特别是对网络依赖重的应用,需要评估对业务的影响。
二、基于Docker Libnetwork的网络定制容器跨主机的网络通信,主要实现思路有两种:二层VLAN网络和Overlay网络。
- 二层VLAN网络的解决跨主机通信的思路是把原先的网络架构改造为互通的大二层网络,通过特定网络设备直接路由,实现容器点到点的之间通信。
- Overlay网络是指在不改变现有网络基础设施的前提下,通过某种约定通信协议,把二层报文封装在IP报文之上的新的数据格式。
Libnetwork是Docker团队将Docker的网络功能从Docker核心代码中分离出去,形成一个单独的库。 Libnetwork通过插件的形式为Docker提供网络功能。 使得用户可以根据自己的需求实现自己的Driver来提供不同的网络功能。
Libnetwork所要实现的网络模型基本是这样的: 用户可以创建一个或多个网络(一个网络就是一个网桥或者一个VLAN ),一个容器可以加入一个或多个网络。 同一个网络中容器可以通信,不同网络中的容器隔离。这才是将网络从docker分离出去的真正含义,即在创建容器之前,我们可以先创建网络(即创建容器与创建网络是分开的),然后决定让容器加入哪个网络。
Libnetwork实现了5种网络模式:
- bridge:Docker默认的容器网络驱动,Container通过一对veth pair链接到docker0网桥上,由Docker为容器动态分配IP及配置路由、防火墙等。
- host:容器与主机共享同一Network Namespace。
- null:容器内网络配置为空,需要用户手动为容器配置网络接口及路由。
- remote:Docker网络插件的实现,Remote driver使得Libnetwork可以通过HTTP Resful API 对接第三方的网络方案,类似于SocketPlane的SDN方案只要实现了约定的HTTP URL处理函数以及底层的网络接口配置方法,就可以替代Docker原生的网络实现。
- overlay:Docker原生的跨主机多子网网络方案。
Docker自身的网络功能比较简单,不能满足很多复杂的应用场景。因此,有很多开源项目用来改善Docker的网络功能,如Pipework、Weave、SocketPlane等。
举例:网络配置工具PipeworkPipework是一个简单易用的Docker容器网络配置工具。由200多行shell脚本实现。通过使用IP、brctl、ovs-vsctl等命令来为Docker容器配置自定义的网桥、网卡、路由等。有如下功能:
- 支持使用自定义的Linux Bridge、veth pair为容器提供通信。
- 支持使用MacVLAN设备将容器连接到本地网络。
- 支持DHCP获取容器的IP。
- 支持Open vSwitch。
- 支持VLAN划分。
Pipework简化了在复杂场景下对容器连接的操作命令,为我们配置复杂的网络拓扑提供了一个强有力的工具。对于一个基本应用而言,Docker的网络模型已经很不错了。然而,随着云计算和微服务的兴起,我们不能永远停留在使用基本应用的级别上,我们需要性能更好且更灵活的网络功能。Pipework是个很好的网络配置工具,但Pipework并不是一套解决方案,我们可以利用它提供的强大功能,根据自己的需求添加额外的功能,帮助我们构建自己的解决方案。
OVS跨主机多子网网络方案OVS的优势是,作为开源虚拟交换机软件,它相对成熟和稳定,而且支持各类网络隧道协议,经过了OpenStack等项目的考验。这个网上很多,就不再赘述了。
三、Kubernetes集成CalicoCalico是一个纯3层的数据中心网络方案,而且无缝集成像OpenStack这种IaaS云架构,能够提供可控的VM、容器、裸机之间的IP通信。
通过将整个互联网的可扩展IP网络原则压缩到数据中心级别,Calico在每一个计算节点利用Linux Kernel实现了一个高效的vRouter来负责数据转发,而每个vRouter通过BGP协议负责把自己上运行的workload的路由信息像整个Calico网络内传播——小规模部署可以直接互联,大规模下可通过指定的BGP route reflector来完成。这样保证最终所有的workload之间的数据流量都是通过IP路由的方式完成互联的。
Calico节点组网可以直接利用数据中心的网络结构(无论是L2或者L3),不需要额外的NAT,隧道或者Overlay Network。
Calico基于iptables还提供了丰富而灵活的网络Policy,保证通过各个节点上的ACLs来提供Workload的多租户隔离、安全组以及其他可达性限制等功能。
Calico有两种布署方案,一般集群都配有SSL证书和非证书的情况。
- 第一种无HTTPS连接etcd方案,HTTP模式部署即没有证书,直接连接etcd
- 第二种HTTPS连接etcd集群方案,加载etcd https证书模式,有点麻烦
总结:目前Kubernetes网络最快的第一就是Calico,第二种稍慢Flannel,根据自己的网络环境条件来定。 Calico作为一款针对企业级数据中心的虚拟网络工具,借助BGP、路由表和iptables,实现了一个无需解包封包的三层网络,并且有调试简单的特点。虽然目前还有些小缺陷,比如stable版本还无法支持私有网络,但希望在后面的版本中会更加强大。
四、应用容器IP固定(参考网上资料)Docker 1.9开始支持Contiv Netplugin,Contiv带来的方便是用户可以根据实例IP直接进行访问。
Docker 1.10版本支持指定IP启动容器,并且由于部分数据库应用对实例IP固定有需求,有必要研究容器IP固定方案的设计。
在默认的Kubernetes + Contiv的网络环境下,容器Pod的IP网络连接是由Contiv Network Plugin来完成的,Contiv Master只实现了简单的IP地址分配和回收,每次部署应用时,并不能保证Pod IP不变。所以可以考虑引入新的Pod层面的IPAM(IP地址管理插件),以保证同一个应用多次发生部署时,Pod IP始终是不变的。
作为Pod层面的IPAM,可以把这一功能直接集成在Kubernetes里。Pod作为Kubernetes的最小调度单元,原有的Kubernetes Pod Registry(主要负责处理所有与Pod以及Pod subresource相关的请求:Pod的增删改查,Pod的绑定及状态更新,exec/attach/log等操作)并不支持在创建Pod时为Pod分配IP,Pod IP是通过获取Pod Infra Container的IP来获取的,而Pod Infra Container的IP即为Contiv动态分配得来的。
在原有Kubernetes代码基础上,修改Pod结构(在PodSpec中加入PodIP)并重写了Pod Registry同时引入了两个新的资源对象:
- Pod IP Allocator:Pod IP Allocator是一个基于etcd的IP地址分配器,主要实现Pod IP的分配与回收。Pod IP Allocator通过位图记录IP地址的分配情况,并且将该位图持久化到etcd;
- Pod IP Recycler:Pod IP Recycler是一个基于etcd的IP地址回收站,也是实现PodConsistent IP的核心。Pod IP Recycler基于RC全名(namespace + RC name)记录每一个应用曾经使用过的IP地址,并且在下一次部署的时候预先使用处于回收状态的IP。Pod IP Recycler只会回收通过RC创建的Pod的IP,通过其他controller或者直接创建的Pod的IP并不会记录,所以通过这种方式创建的Pod的IP并不会保持不变;同时Pod IP Recycle检测每个已回收IP对象的TTL,目前设置的保留时间为一天。
这里对kubelet也需要进行改造,主要包括根据Pod Spec中指定IP进行相关的容器创建(docker run加入IP指定)以及Pod删除时释放IP操作。
Pod的创建在PaaS里主要有两种情形:
- 应用的第一次部署及扩容,这种情况主要是从IP pool中随机分配;
- 应用的重新部署:在重新部署时,已经释放的IP已根据RC全名存放于IP Recycle列表中,这里优先从回收列表中获取IP,从而达到IP固定的效果。
另外为了防止IP固定方案中可能出现的问题,在Kubernetes中加入了额外的REST API:包括对已分配IP的查询,手动分配/释放IP。
容器IP固定方案已测试评估中,运行基本上没问题,但稳定性有待提升。主要表现在有时不能在预期时间内停止旧Pod,从而无法释放IP造成无法复用(初步原因是由于Docker偶尔的卡顿造成无法在规定时间内停止容器),可以手动去修复。但从长期考虑,IP固定方案还需要加强稳定性并根据具体需求进行优化。
总结:目前已有多种支持Kubernetes的网络方案,比如Flannel、Calico、华为的Canal、Weave Net等。因为它们都实现了CNI规范,用户无论选择哪种方案,得到的网络模型都一样,即每个Pod都有独立的 IP,可以直接通信。区别在于不同方案的底层实现不同,有的采用基于VXLAN的Overlay实现,有的则是Underlay,性能上有区别,再有就是是否支持Network Policy了。
Q&A
Q:今天讲了很多方法解决容器网络的问题,似乎可以从集群外部或集群内部可以直接访问容器的IP,但是在集群外部访问容器IP的场景有吗?我觉得容器的IP不应该暴露出来,从外部不能直接访问容器的IP,因为容器的IP可能是变化的。有的时候使用RC之类的。我的问题是能不能从集群外部通过clusterip或node-port ip来访问基于容器的业务呢?如果可以的话,今天介绍的Flanel或Calico的价值是什么呢,这两种方案,我感觉都是直接访问容器的IP地址,那么如果某个容器出问题,重启之后,肯定要换IP,那这个时候怎么办呢?另外,集群内容器到容器的访问,到底是直接访问对端容器的IP,还是访问cluster-ip,谢谢。
A:集群内容器到容器的访问,可以直接通过localhost加端口即可。如果是同一主机上的Pod,由于它们在同一个docker0网桥上,地址段相同,可以直接通信。如果是不同主机上的Pod,则需要跨越物理机上的物理网卡,通过宿主机的IP转到docker0上再到对应的Pod里的某个容器。不管是Flanel或Calico,都有各自的应用场景。Flanel不是直接访问容器IP的,它还有自己的flanel0网桥,Calico节点组网可以直接利用数据中心的网络结构(支持L2或者 L3),可以直接通信。从外部可以直接访问容器的IP,需要做容器固定IP,针对某些特定应用比如数据库。