这次聊聊docker、k8s、mesos+k8s的网络,了解一下容器、pod和服务间是怎样通信的。

Docker的网络模型

Docker默认使用桥接网络。Docker daemon启动时,会在主机上启动一个名为docker0的网桥接口。当容器启动的时候,自动分配一对VETH设备,一个连接到docker0上,另一个连接到容器内部的eth0里,于是同一台主机上的容器便能够跨越网络的命名空间,经由主机相互通信。可是不同的主机上的容器就不是那么简单的了。有一种办法是把容器的端口映射到主机的某个端口上去,这样其他主机上的容器可以通过访问这个主机端口的方式实现跨主机通信。Docker 1.6版本之后发布了一个覆盖网络overlay network。当使用这个覆盖网络的时候,它便能实现容器的跨主机通信和隔离。

Kubernetes的网络模型

Kubernetes把每个pod当成是一个节点,在这个pod内的所有容器的网络命名空间是共享的,也就意味着它们共享着一个IP地址。Pod内的容器可以用localhost相互通信,与其他容器通信时,直接使用其他pod的IP地址就可以了。如果把每个pod当成一个虚拟机,这样的设计方式是再正常不过的了。用户无需再去考虑pod间的通信问题,也不用考虑pod和主机端口映射的问题了。从这样的易用性出发,kubernetes对网络有三个要求:

  • 所有的容器可以在不使用NAT的情况下相互通信
  • 所有的主机和容器可以在不使用NAT的情况下相互通信
  • 容器自己的IP和外部看它的IP是一样的

Kubernetes项目启动的时候,docker还只提供了桥接网络。所以单纯地安装docker和kubernetes并不能够满足kubernetes对网络的要求。常见的公有云如GCE、AWS的基础设施都是默认满足网络要求的。私有云的话,一个方法是直接路由,也就是在所有主机的路由表增加其他主机的docker0网桥。但是,增删主机时所有节点都需要重新配置,非常麻烦。另一种方法使用覆盖网络来实现容器跨主机互通,操作相对容易一些。有不少人使用Flannel,也有人使用Open vSwitchWeaveCalico。就flannel来说,它的网络传输如下图:

Flannel会在每个主机上运行一个叫做flanneld的代理,它通过etcd保证所有主机上的容器都不会出现重复IP,并能通过物理网络将数据包投递到目标节点的flanneld去。有兴趣的话可以参考《一篇文章带你了解Flannel》。大部分通过覆盖网络实现跨主机容器互通的方案是工作在L2层,在性能上是有些损耗的,但是Calico略微有点不一样。它直接工作在L3层,没有封包解包的损耗,所以性能上影响很小。所付出的代价就是它仅能支持TCP、UDP、ICMP等协议,不过通常来说也足够了。Open vSwitch功能强大,但是配置也比较麻烦。

虽然docker 1.9版正式宣布内置的覆盖网络可以使用在产品环境了,但是kubernetes并不打算支持docker的覆盖网络,据说技术上最主要的原因是kubernetes并不仅仅是为docker这一种容器技术服务的,kubernetes既不想再引入一个键值存储,也不愿意把自己的键值存储暴露给docker用。当然如果用户自己把docker需要的键值存储管理起来,docker自己的覆盖网络还是能够工作的。非技术上的原因是kubernetes认为docker不够开放,都是因为利益啊。详情可以参考这篇文章

Kubernetes的服务

Kubernetes是怎么做到一个pod内的容器只有一个IP地址的呢?假如我们启动一个包含俩容器的pod,然后到启动pod的那台虚拟机查看,就会发现除了这两个容器之外,kubernetes另外还启动了一个叫做gcr.io/google_containers/pause的容器。Pod自身的两个容器都是通过在docker run时,net指定使用pause容器网络的办法,把自己需要的端口由pause容器暴露出来的。如下图所示:

当我们通过replication controller,把pod暴露为服务的时候,kubernetes会通过etcd,为这个服务分配一个唯一的虚拟IP。这样做的好处是:避免了用户自己定义的端口有可能重复的问题。每个kube-proxy都会往iptables里写一条关于service虚拟IP的规则。当内部用户使用这个服务的时候,这个虚拟IP便会把流量导入到kube-proxy监听的某个端口上,由kube-proxy使用轮询或基于客户端IP的会话保持的方式,决定最终来提供服务的pod。外部用户由于没有kube-proxy,是不能访问这个服务的,除非我们把服务通过负载均衡或者NodePort的方式暴露出去,本文就不再赘述了。

Kubernetes-Mesos的网络模型

Mesos使用最基本的Docker网络模型,也就是主机内共享的桥接网络。这点跟kubernetes的要求是矛盾的。所以当一个pod的端点(endpoint),也就是以pod的IP:端口的格式暴露出来的时候,由于pod的IP并不能被其他主机所访问,就会导致通信出问题。于是Kubernetes-Mesos开发组想了一个权宜之计:把端点以主机IP:端口的格式暴露出来。由于主机的IP是在集群内是都能访问的,所以只要开放端口,通信就能正常工作。如果用户没有指定主机的端口,那就随机分配一个。这个策略是可以通过指定scheduler和controller-manager的启动参数-host-port-endpoints=false(默认为true)来绕过去的,也就是能恢复到kubernetes默认的网络方案去,不过我们就得自己来想办法实现容器的跨主机通信了。