K8s的网络主要需要解决4个问题:

  1. 高度耦合的容器间的通信问题
  2. Pod与Pod之间的通信(本文)
  3. Pod和Service之间的通信
  4. 外部系统和Service之间的通信

上篇文章讨论了第1个问题,本文继续讲述第2个问题:Pod与Pod之间的通信。

k8s网络模型

pod是k8s中最小的部署单元,不同pod可能会分布在不同机器上面,解决了Pod之间的通信问题,其实也就解决了跨主机容器通信的问题。而k8s在这方面的设计上考虑的不仅要解决通信问题,还要以一种对应用层使用更友好的方式。所以便提出了k8s网络模型:集群中的每个Pod都要有自己的IP,这个模型也称之为“IP-per-pod”。同时,k8s还要求所有实现该模型的网络实现都满足以下要求:

  1. 一个节点上的pod可以和所有节点上面的Pod以非NAT的方式进行通信;
  2. 一个节点上的代理(比如系统守护、kubelet)可以和该节点上面的所有Pod通信;
  3. 运行在主机网络中的pod也可以和所有节点上面的Pod以非NAT的方式进行通信。

这3个要求基本将集群内所有Pod/容器的网络直接打通了,整个集群的容器网络将完全扁平化,而且还没有使用NAT这种方式(NAT会带来性能损耗,限制集群规模;而且因为替换了源地址,很难追溯,不利于排错)。可以看到这个IP-per-pod(其实也是IP-per-container,只不过同一个Pod内的容器IP相同)的模型以及3个要求都非常简单清楚,而且从网络通信的角度看,当实现了这个模型以后,其实每个Pod相当于传统意义上的一个虚拟机或者物理机了,原来在虚拟机或者物理机部署的应用几乎可以不用做任何网络层面的适配就可以迁移到Pod里面。而这个模型规定的是一个结果,但实现这个结果有很多种方式,如果没有一个统一的标准(特别是如何使用),大家都各自按自己想法去实现,对于最终的使用者来说将是一场噩梦。所以容器网络的标准便产生了,只是中间发生了一些有趣的故事。

CNM、CNI和k8s的恩怨情仇

如该系列第一篇文章所述,早期的docker提供的网络非常简单:只有基于“veth-pair+linux bridge”单机容器通信。后来随着容器的快速发展,容器跨主机通信的需求越来越迫切,于是Docker公司收购了一家做SDN的初创公司SocketPlane专门研究跨主机容器通信方案,并最终提出了一套容器网络标准CNM(Container Network Model),并且将网络部分的代码从原来的docker核心代码中剥离出来,形成了一个单独的项目libnetwork,该项目便是第一个实现CNM的项目,并且作为docker容器内置的网络插件。没过多久,另外一个做容器的公司CoreOS也提出了另外一套容器网络标准CNI(Container Network Interface),相比于CNM,CNI更加通用、简单。

而就在Docker CNM发布的时候,Kubernetes内部已经实现了一个很初级的网络插件Kubenet,这个插件非常简单,也只有单机容器网络的能力,要实现容器跨机通信,需要依赖一些云厂商提供的cloud provider,或者使用一些其它网络插件(比如flannel)。也就是当时容器网络有3个势力:k8s的kubenet、Docker的CNM、CoreOS的CNI。一方面k8s的kubenet相比于CNM和CNI还处于非常早期(alpha版本),而CNM和CNI都已经相对成熟,尤其是CNM,已经发布正式版了;另一方面,k8s社区也想更多的借助开源的力量,所以便决定从CNM或者CNI中选择一个作为k8s的容器网络标准。起初k8s社区是比较倾向于使用更加成熟一些的CNM,但由于一些技术和非技术的原因,最终却选择了CoreOS的CNI作为k8s的容器网络标准。至于其中的缘由,k8s创始人之一 Tim Hockin还专门发布了一篇文章:Why Kubernetes doesn’t use libnetwork。简单概括一下:

  • 技术方面:最主要的技术原因是CNM在设计之初就是结合docker去设计的,所以做了一些可能只有docker才有的假设,这使得要想将CNM作为一个通用容器的网络标准比较困难。而CNI却没有这些问题,虽然也是基于CoreOS自己的容器运行时rkt进行设计,但却更加开放、灵活,可以和其它容器运行时使用,是一个通用的容器网络标准,而且也的确比CNM更加简单,这也正符合Kubernetes的设计理念:简单。也比较符合UNIX的设计理念:doing one thing well。
  • 非技术方面:k8s开发者觉得Docker公司并没有打算让CNM变得开放、独立。他们给libnetwork提了很多让CNM更加独立的PR和优化建议,都没有采纳。

更多细节可以看原文。在k8s没有站CNI的队之前,很多公司都还在开发符合CNM的插件,但在k8s选择CNI之后,一些大公司马上也选择了CNI,比如像Cloud Foundry、Mesos这些比较大的容器平台。如今CNI也已经是CNCF的一员,并且CNCF也致力于将CNI变为工业标准,CNI可以说是前途一片光明。

最后说些题外话:站在现在来看,个人觉得Docker公司其实挺“悲壮”的。作为一个创业公司,在短短几年间让容器在IT界“家喻户晓”,成为业界的明星、宠儿。同时,设计的容器镜像分层技术更是一笔宝贵的财富,包括Docker Hub。然而由于是基于开源的运作方式,一时很难找到商业变现的途径。而后来发现可以在容器编排方面实现商业变现,却无奈碰到了k8s这种神一样的对手。现在k8s已经成为容器编排领域的事实标准(docker swarm以其简单性也还有少量的市场),甚至是云时代的操作系统。随着CRI、CNI等厂商独立的标准成熟,Docker的统治地位越来越被削弱。现在看来,docker最有价值的部分似乎就剩下Docker Hub和镜像了。对于容器行业来说,也许Google这样的巨头推出的k8s更加符合需求,更能促进行业的开放、发展。但从创业的角度来说,在巨头面前,docker这样的小创业公司的命运又不得让人唏嘘。

不过不管是CNI还是CNM,都没有涉及网络策略(Network Policy),但这对于平台的网络是非常重要的,特别是公有云和混合云。所以k8s提出了Network Policies,一些比较新的符合CNI的插件,一般也会实现网络策略的功能。本系列是介绍k8s网络的,所以本文就不再介绍CNM模型了,以后单独介绍吧。有兴趣的看这里CNM design

CNI介绍

CNI的完整介绍可参见官方文档:CNI SPEC v0.4.0,要更好的理解CNI,最好是结合CNI plugin介绍,不过本文就先不深入介绍了,这里先挑一些比较通用的知识简单介绍。

架构

从概念上说,CNI标准和CNM类似,都是在容器运行时和具体的网络之间设计了一层抽象,对上层提供统一接口,屏蔽下层的网络差异和细节,如下图(图片来自网络)所示:

cni-layer

具体的网络细节都由下层的插件实现。

从功能上说,CNI就是定义了创建网络(ADD)和删除网络(DEL)两个接口,各个插件来实现这两个接口。然后CRI在创建容器时创建网络,并调用ADD接口将容器加入该网络;在容器销毁时调用DEL将容器从网络中删除,必要时删除这个网络。如下图(图片来自网络):

cni-architecture

相比于CNM,CNI首先在接口设计上就很简化,只有上述2个核心接口(随着发展,又增加了一些接口,但核心的还是这两个)。另外,在配置文件方面CNI也很简单:CNM需要一个分布式key-value来存储其网络配置,而CNI的配置就是一个JSON文件,存储在本地。更多的细节就不介绍了,英文好建议直接看我前面附的官方SPEC,喜欢看中文的可以看这篇翻译文章:CNI - Container Network Interface(容器网络接口)

如何在k8s中使用CNI

CNI标准规定实现CNI的插件必须是一个可以被容器管理系统(比如k8s、rkt)调用的可执行文件。所以从最终使用者的角度而言,在k8s中使用CNI就是准好好这个可执行文件,外加一个JSON格式的配置。Kubelet和CNI规定了2个默认的路径:

  • /etc/cni/net.d:默认的CNI配置文件存储目录。如果要修改默认目录,在kubelet启动参数中通过--cni-conf-dir指定。如果指定目录下有多个配置文件,则使用文件名字典序的第一个。
  • /opt/cni/bin:默认的CNI插件存储目录。如果要修改默认目录,在kubelet启动参数中通过--cni-bin-dir指定。k8s要求CNI插件的最低版本为0.2.0。

随便查看一个使用了CNI的k8s集群节点(kubelet的启动参数中有--network-plugin=cni),都可以看到这两个目录。比如我的两个k8s集群:

# 集群1,使用的是Flannel插件
➜  ~ ls /opt/cni/bin/
bandwidth  bridge  dhcp  firewall  flannel  host-device  host-local  ipvlan  loopback  macvlan  portmap  ptp  sbr  static  tuning  vlan
➜  ~ ls /etc/cni/net.d/
10-flannel.conflist
➜  ~ cat /etc/cni/net.d/10-flannel.conflist
{
  "name": "cbr0",
  "cniVersion": "0.3.1",
  "plugins": [
    {
      "type": "flannel",
      "delegate": {
        "hairpinMode": true,
        "isDefaultGateway": true
      }
    },
    {
      "type": "portmap",
      "capabilities": {
        "portMappings": true
      }
    }
  ]
}


# 集群2,使用的是Calico插件
➜  ~ ls /opt/cni/bin
bandwidth  calico       dhcp      flannel      host-local  loopback  portmap  sbr     tuning
bridge     calico-ipam  firewall  host-device  ipvlan      macvlan   ptp      static  vlan
➜  ~ ls /etc/cni/net.d
10-calico.conflist  calico-kubeconfig
➜  ~ cat /etc/cni/net.d/10-calico.conflist
{
  "name": "k8s-pod-network",
  "cniVersion": "0.3.1",
  "plugins": [
    {
      "type": "calico",
      "log_level": "info",
      "datastore_type": "kubernetes",
      "nodename": "epimoni",
      "mtu": 1440,
      "ipam": {
          "type": "calico-ipam"
      },
      "policy": {
          "type": "k8s"
      },
      "kubernetes": {
          "kubeconfig": "/etc/cni/net.d/calico-kubeconfig"
      }
    },
    {
      "type": "portmap",
      "snat": true,
      "capabilities": {"portMappings": true}
    },
    {
      "type": "bandwidth",
      "capabilities": {"bandwidth": true}
    }
  ]
}

其中的name字段就是网络名称,type字段就是实现对应功能的二进制文件名,可以看到一个完整的网络功能往往需要调用多个不同的插件,特别是对于那些功能不完整的插件,比如上面的10-calico.conflist中不仅使用了calico实现基本的k8s网络模型,还使用了portmap提供端口映射服务,使用bandwidth提供带宽控制功能。可见这种机制是非常简单灵活的,可以根据需求进行组合配置。

总结

本篇主要介绍了k8s采用的网络标准CNI,但也主要是介绍了比较通用的部分,并没有详细介绍每个CNI Plugin在k8s里面的具体使用,以及各个插件是如何实现k8s的网络模型的。这部分每个不同的插件是不一样的,而且比较细节化,后面单独分几篇文章来介绍常用的CNI插件,比如flannel、calico。