03-K8S网络模型

容器跨主机网络的两种实现方式:UDP和VXLAN,有以下共同点:

  1. 用户的容器都是连接在docker0网桥上
  2. 网络插件在宿主机上创建一个特殊的设备,docker0与这个设备之间通过IP转发(路由表)进行协作
    1. UDP模式创建的是TUN设备
    2. VXLAN模式创建的是VETP设备
  3. 网络插件真正完成的是通过某种方法,把不同宿主机上的特殊设备连通,从而达到容器跨主机通信的目的

上述过程,也是kubernetes对容器网络的主要处理方式,kubernetes通过CNI接口,维护了一个单独的网桥(CNI网桥,cni0)来代替docker0。

以Flannel的VXLAN模式为例,在kubernetes环境下的工作方式如下图,只是把docker0换成cni0:

image

kubernetes为Flannel分配的子网范围是10.244.0.0/16,这个参数在部署的时候指定:

kubeadm init --pod-network-cidr=10.244.0.0/16

也可以在部署完成后,通过修改kube-controller-manager的配置文件来指定。

0.1. 例子

假设有两台宿主机,两个pod,pod1需要访问pod2

宿主机podIP地址
node1pod110.244.0.2
node2pod210.244.1.3

pod1的eth0网卡也是通过Veth Pair的方式连接在node1的cni0网桥上,所有pod1中的IP包会经过cni0网桥出现在宿主机上。

node1上的路由表如下:

# 在 Node 1 上
route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
...
10.244.0.0      0.0.0.0         255.255.255.0   U     0      0        0 cni0
10.244.1.0      10.244.1.0      255.255.255.0   UG    0      0        0 flannel.1
172.17.0.0      0.0.0.0         255.255.0.0     U     0      0        0 docker0
  1. IP包的目的IP地址是10.244.1.3,所以匹配第二条路由规则,这条规则指向本机的flannel.1设备进行处理。
  2. flannel.1处理完成后,要将IP包转发到网关,正是“隧道”另一端的VETP设备,即node2的flannel.1设备
  3. 接下来的处理流程和flannel VXLAN模式完全一样

CNI网桥只是接管所有CNI插件负责的(即kubernetes创建的pod)。

如果此时使用docker run单独启动一个容器,那么docker项目会把这个容器连接到docker0网桥上,所以这个容器的IP地址一定是属于docker0网桥的172.17.0.0/16网段。

kubernetes之所以要设置这样一个与docker0网桥几乎一样的CNI网桥,主要原因包括两个方面:

  1. kubernetes项目并没有使用docker的网络模型(CNM),所以不希望,也不具备配置docker0网桥的能力
  2. 这与kubernetes如何配置Pod,即Infra容器的Network Namespace密切相关

因为kubernetes创建一个Pod的第一步是创建并启动一个Infra容器,用来hold住这个pod的Network Namespace。所以CNI的设计思想是:kubernetes在启动Infra容器之后,就可以直接调用CNI网络组件,为这个Infra容器的Network Namespace配置符合预期的网络栈

一个Network Namespace的网络栈包括:网卡、回环设备、路由表、iptables。

0.2. Network Namespace网络栈的配置

首先需要部署和运行CNI插件。在部署kubernetes的时候,有一个步骤是安装kubernetes-cni包 ,它的目的就是在宿主机上安装CNI插件所需的基础可执行文件。安装完成后,可以在宿主机/opt/cni/bin/目录下看到它们,如下所示:

# 这些CNI的基础可执行文件,按功能可以分为三类
ls -al /opt/cni/bin/
total 73088

# 第一类,Main插件,用来创建具体网络设备的二进制文件
# Flannel Weave等项目,都属于“网桥”类型的CNI插件,具体实现时会调用二进制文件bridge
-rwxr-xr-x 1 root root  3890407 Aug 17  2017 bridge     # 网桥设备
-rwxr-xr-x 1 root root  3475802 Aug 17  2017 ipvlan
-rwxr-xr-x 1 root root  3026388 Aug 17  2017 loopback   # 回环设备
-rwxr-xr-x 1 root root  3520724 Aug 17  2017 macvlan
-rwxr-xr-x 1 root root  3877986 Aug 17  2017 ptp        # Veth Pair设备
-rwxr-xr-x 1 root root  3475750 Aug 17  2017 vlan

# 第二类,IPAM(IP Address Management)插件,负责分配IP地址的二进制文件
-rwxr-xr-x 1 root root  9921982 Aug 17  2017 dhcp       # 向DHCP服务器发起请求
-rwxr-xr-x 1 root root  2991965 Aug 17  2017 host-local # 使用预先配置的IP地址段进行分配

# 第三类,CNI社区维护的内置CNI插件
-rwxr-xr-x 1 root root  2814104 Aug 17  2017 flannel    # 为Flannel项目提供的CNI插件
-rwxr-xr-x 1 root root  3470464 Aug 17  2017 portmap    # 通过iptables配置端口映射的二进制文件
-rwxr-xr-x 1 root root  2605279 Aug 17  2017 sample     #
-rwxr-xr-x 1 root root  2808402 Aug 17  2017 tuning     # 通过sysctl调整网络设备参数的二进制文件

# bandwidth 使用Token Bucket Filter(TBF)来进行限流的二进制文件

从以上内容可以看出,实现一个kubernetes的容器网络方案,需要两部分工作,以Flannel为例:

  1. 实现网络方案本身,这部分需要编写flanneld进程里的主要逻辑,如创建和配置flannel.1设备,配置宿主机路由、配置ARP和FDB表里的信息
  2. 实现该网络方案对应的CNI插件,这部分主要是配置Infra容器里面的网络栈,并把它连接在CNI网桥上

Flannel项目对应的CNI插件已经内置在kubernetes项目中。其他项目如Weave、Calico等,需要安装插件,把对应的CNI插件的可执行文件放在/opt/cni/bin/目录下。对于Weave、Calico这样的网络方案来说,他们的DaemonSet只需要挂载宿主机的/opt/cni/bin/,就可以实现插件可执行文件的安装。

在宿主机上安装flanneld(网络方案本身),flanneld启动后会在每一台宿主机上生成它对应的CNI配置文件(是一个ConfigMap),从而告诉Kubernetes,这个集群要使用Flannel作为容器网络方案。CNI配置文件内容如下:

cat /etc/cni/net.d/10-flannel.conflist
{
  "name": "cbr0",
  "plugins": [
    {
      "type": "flannel",
      "delegate": {
        "hairpinMode": true,
        "isDefaultGateway": true
      }
    },
    {
      "type": "portmap",
      "capabilities": {
        "portMappings": true
      }
    }
  ]
}

在kubernetes中,处理容器网络相关的逻辑并不会在kubelet主干代码里执行,而是会在具体的CRI实现里完成。对于Docker项目来说,它的CRI实现是dockershim,在kubelet的代码中可以找到。所以dockershim会加载上述CNI配置文件。

目前,kubernetes不支持多个CNI插件混用,如果在CNI配置目录/etc/cni/net.d里面放置了多个CNI配置文件的话,dockershim只会加载按照字母顺序排序的第一个插件。不过CNI运行在一个CNI配置文件中,通过plugins字段,定义多个插件进行协作。上面的例子中,plugins字段指定了flannel和portmap两个插件。

dockershim会把CNI配置文件加载起来,并且把列表里的第一个插件(flannel)设置为默认插件,在后面的执行过程中,flannel和portmap插件会按照定义顺序被调用,从而依次完成“配置容器网络”和“配置端口映射”这两个操作。

0.3. CNI插件工作原理

当kubelet组件需要创建Pod的时候,第一个创建的一定是Infra容器。

  1. dockershim调用Docker API创建并启动Infra容器
  2. 执行SetUpPod方法,为CNI插件准备参数
  3. 调用CNI插件(/opt/cni/bin/flannel)为Infra容器配置网络

调用CNI插件需要为它准备的参数分为2部分:

  1. 设置环境变量
  2. dockershim从CNI配置文件里加载到的、默认插件的配置信息

0.3.1. 设置环境变量

由dockershim设置的一组CNI环境变量,其中最重要的环境变量是CNI_COMMAND,它的取值只有两种ADD/DEL。ADD和DEl操作是CNI插件唯一需要实现的两个方法。

  • ADD操作的含义:把容器添加到CNI网络里
  • DEL操作的含义:把容器从CNI网络里移除

对于网桥类型的CNI插件来说,这两个操作意味着把容器以Veth Pair的方式插到CNI网桥上或者从CNI网桥上拔出。

0.3.1.1. ADD操作

CNI的ADD操作需要的参数包括:

  1. 容器里网卡的名字eth0(CNI_IFNAME)
  2. Pod的Network Namespace文件的路径(CNI_NETNS)
  3. 容器的ID(CNI_CONTAINERID)

这些参数都属于上述环境变量里的内容。其中,Pod(Infra容器)的Network Namespace文件的路径是/proc/<容器进程的PID>/ns/net。在CNI环境变量里,还有一个叫作CNI_ARGS的参数,通过这个参数,CRI实现(如dockershim)就可以以key-value的格式,传递自定义信息给网络插件,这是用户自定义CNI协议的一个重要方法。

0.3.2. dockershim从CNI配置文件里加载默认插件的配置信息

配置信息在CNI中被叫作Network Configuration,dockershim会把Network Configuration以JSON格式,通过标准输入(stdin)的方式传递给Flannel CNI插件。

有了这两部分参数,Flannel CNI插件实现ADD操作的过程就很简单,需要在Flannel的CNI配置文件(/etc/cni/net.d/10-flannel.conflist)里有一个delegate字段:

...
     "delegate": {
        "hairpinMode": true,
        "isDefaultGateway": true
      }

Delegate字段的意思是,CNI插件并不会自己做事,而是调用Delegate指定的某种CNI内置插件来完成。对于Flannel来说,它调用的Delegate插件,就是CNI bridge插件。

所以说,dockershim对Flannel CNI插件的调用,其实只是走个过程,Flannel CNI插件唯一需要做的,就是对dockershim传来的Network Configuration进行补充,如将Delegate的Type字段设置为bridge,将Delegate的IPAM字段设置为host-local等。

经过Flannel CNI插件的补充后,完整的Delegate字段如下:

{
    "hairpinMode":true,
    "ipMasq":false,
    "ipam":{
        "routes":[
            {
                "dst":"10.244.0.0/16"
            }
        ],
        "subnet":"10.244.1.0/24",
        "type":"host-local"
    },
    "isDefaultGateway":true,
    "isGateway":true,
    "mtu":1410,
    "name":"cbr0",
    "type":"bridge"
}

其中,ipam字段里的信息,比如10.244.1.0/24,读取自Flannel在宿主机上生成的Flannel配置文件,即宿主机上/run/flannel/subnet.env文件。接下来Flannel CNI插件就会调用CNI bridge插件,也就是执行/opt/cni/bin/bridge二进制文件。

这一次调用CNI bridge插件需要两部分参数的第一部分,就是CNI环境变量,并没有变化,所以它里面的CNI_COMMAND参数的值还是“ADD”。

第二部分Network Configuration正是上面补充好的Delegate字段。Flannel CNI插件会把Delegate字段的内容以标准输入的方式传递给CNI bridge插件。Flannel CNI插件还会把Delegate字段以JSON文件的方式,保存在/var/lib/cni/flannel目录下,这是给删除容器调用DEL操作时使用。

有了两部分参数,CNI bridge插件就可以代表Flannel,将容器加入到CNI网络里,这一步与容器Network Namespace密切相关。

  1. 首先,CNI bridge插件会在宿主机上检查CNI网桥是否存在。如果没有的话,那就创建它。相当于在宿主机上执行如下操作:
# 在宿主机上
ip link add cni0 type bridge
ip link set cni0 up
  1. 接下来,CNI bridge插件通过Infra容器的Network Namespace文件,进入到这个Network Namespace里面,然后创建一对Veth Pair设备。
  2. 然后,把这个Veth Pair的其中一端,移动到宿主机上,相当于在容器里执行如下命令:
# 创建一对 Veth Pair 设备。其中一个叫作 eth0,另一个叫作 vethb4963f3
ip link add eth0 type veth peer name vethb4963f
# 启动 eth0 设备
ip link set eth0 u
# 将 Veth Pair 设备的另一端(vethb4963f3设备)放到宿主机(Host Namespace)里
ip link set vethb4963f3 netns $HOST_N
# 通过 Host Namespace,启动宿主机上的 vethb4963f3 设备
ip netns exec $HOST_NS ip link set vethb4963f3 up
  1. CNI bridge 插件就可以把vethb4963f3设备连接到CNI网桥上。这相当于在宿主机上执行如下命令:
# 在宿主机上
ip link set vethb4963f3 master cni0

在将vethb4963f3设备连接在CNI网桥之后,CNI bridge插件还会为它设置Hairpin Mode(发夹模式),因为在默认情况下,网桥设备是不允许一个数据包从一个端口进来后再从这个端口发出去。开启发夹模式取消这个限制。这个特性主要用在容器需要通过NAT(端口映射)的方式,自己访问自己的场景。这样这个集群Pod才可以通过它自己的Service访问到自己。

  1. CNI bridge插件会调用CNI ipam插件,从ipam.subnet字段规定的网段里为容器分配一个可用的IP地址。然后,CNI bridge插件就会把这个IP地址添加到容器的eth0网卡上,同时为容器设置默认路由,这相当于执行如下命令:
# 在容器里
ip addr add 10.244.0.2/24 dev eth0
ip route add default via 10.244.0.1 dev eth0
  1. 最后,CNI bridge插件会为CNI网桥添加IP地址,相当于在宿主机上执行:
# 在宿主机上
ip addr add 10.244.0.1/24 dev cni0

执行完上述操作后,CNI插件会把容器的IP地址等信息返回给dockershim,然后被kubelet添加到Pod的status字段。至此,CNI插件的ADD方法就宣告结束,接下来的流程就是容器跨主机通信的过程。

0.4. 总结

kubernetes CNI网络模型:

  1. 所有容器都可以直接使用IP地址与其他容器通信,无需使用NAT
  2. 所有宿主机都可以直接使用IP地址与所有容器通信,而无需使用NAT,反之亦然
  3. 容器自己“看到”的自己的IP地址,和别人(宿主机或容器)看到的地址是完全一样的

容器和容器通,容器和宿主机通,并且直接基于容器和宿主机的IP地址来进行通信。

上次修改: 14 April 2020