Kubernetes11-网络原理

1、Kubernetes网络模型Kubernetes

  • 网络模型设计的一个基础原则是:每个Pod都拥有一个独立的IP地址,并假定所有Pod都在一个可以直接连通的、扁平的网络空间中
    • 所以不管它们是否运行在同一个Node(宿主机)中,都要求它们可以直接通过对方的IP进行访问。
    • 设计这个原则的原因是,用户不需要额外考虑如何建立Pod之间的连接,也不需要考虑如何将容器端口映射到主机端口等问题。
  • 实际上,在Kubernetes里,IP是以Pod为单位进行分配的一个Pod内部的所有容器共享一个网络堆栈(相当于一个网络命名空间,它们的IP地址、网络设备、配置等都是共享的)。按照这个网络原则抽象出来的为每个Pod都设置一个IP地址的模型也被称作IP-per-Pod模型
  • 由于Kubernetes的网络模型假设Pod之间访问时使用的是对方Pod的实际地址,所以一个Pod内部的应用程序看到的自己的IP地址和端口与集群内其他Pod看到的一样。它们都是Pod实际分配的IP地址。将IP地址和端口在Pod内部和外部都保持一致,也就不需要使用NAT来进行地址转换了。Kubernetes的网络之所以这么设计,主要原因就是可以兼容过去的应用。当然,我们使用Linux命令“ip addr show”也能看到这些地址,和程序看到的没有什么区别。所以这种IP-per-Pod的方案很好地利用了现有的各种域名解析和发现机制。
  • 为每个Pod都设置一个IP地址的模型还有另外一层含义,那就是同一个Pod内的不同容器会共享同一个网络命名空间,也就是同一个Linux网络协议栈。这就意味着同一个Pod内的容器可以通过localhost来连接对方的端口。这种关系和同一个VM内的进程之间的关系是一样的,看起来Pod内容器之间的隔离性减小了,而且Pod内不同容器之间的端口是共享的,就没有所谓的私有端口的概念了。如果你的应用必须要使用一些特定的端口范围,那么你也可以为这些应用单独创建一些Pod。反之,对那些没有特殊需要的应用,由于Pod内的容器是共享部分资源的,所以可以通过共享资源互相通信,这显然更加容易和高效。针对这些应用,虽然损失了可接受范围内的部分隔离性,却也是值得的。
  • IP-per-Pod模式和Docker原生的通过动态端口映射方式实现的多节点访问模式有什么区别呢?
    • 主要区别是后者的动态端口映射会引入端口管理的复杂性,而且访问者看到的IP地址和端口与服务提供者实际绑定的不同(因为NAT的缘故,它们都被映射成新的地址或端口了),这也会引起应用配置的复杂化。同时,标准的DNS等名字解析服务也不适用了,甚至服务注册和发现机制都将迎来挑战,因为在端口映射情况下,服务自身很难知道自己对外暴露的真实的服务IP和端口,外部应用也无法通过服务所在容器的私有IP地址和端口来访问服务。
  • 总的来说,IP-per-Pod模型是一个简单的兼容性较好的模型。从该模型的网络的端口分配、域名解析、服务发现、负载均衡、应用配置和迁移等角度来看,Pod都能够被看作一台独立的虚拟机或物理机
  • 按照这个网络抽象原则,Kubernetes对网络有什么前提和要求呢?
    • (1)所有容器都可以在不用NAT的方式下同别的容器通信。
    • (2)所有节点都可以在不用NAT的方式下同所有容器通信,反之亦然。
    • (2)容器的地址和别人看到的地址是同一个地址。
  • 这些基本要求意味着并不是只要两台机器都运行Docker,Kubernetes就可以工作了。具体的集群网络实现必须满足上述基本要求,原生的Docker网络目前还不能很好地支持这些要求。
  • 实际上,这些对网络模型的要求并没有降低整个网络系统的复杂度。如果你的程序原来在VM上运行,而那些VM拥有独立IP,并且它们之间可以直接透明地通信,那么Kubernetes的网络模型就和VM使用的网络模型一样。所以使用这种模型可以很容易地将已有的应用程序从VM或者物理机迁移到容器上。
  • 当然,谷歌设计Kubernetes的一个主要运行基础就是其公有云GCE,GCE默认支持这些网络要求。另外,常见的其他公有云服务商如亚马逊等,其公有云环境也支持这些网络要求。
  • 由于部署私有云的场景也非常普遍,所以在私有云中运行Kubernetes+Docker集群之前,需要自己搭建出符合Kubernetes要求的网络环境。有很多开源组件可以帮助我们打通Docker容器和容器之间的网络,实现满足Kubernetes要求的网络模型。当然,每种方案都有适合的场景,我们要根据自己的实际需要进行选择。在后面的章节中会对常见的开源方案进行介绍。
  • Kubernetes的网络依赖于Docker,Docker的网络又离不开Linux操作系统内核特性的支持,所以我们有必要先深入了解Docker背后的网络原理和基础知识。接下来一起深入学习必要的Linux网络知识。

2、Docker网络基础

  • Docker本身的技术依赖于近年来Linux内核虚拟化技术的发展,所以Docker对Linux内核的特性有很强的依赖。这里将Docker使用到的与Linux网络有关的主要技术进行简要介绍,这些技术有:网络命名空间(Network Namespace)、Veth设备对、网桥、ipatables和路由。

2.1、网络命名空间

  • 为了支持网络协议栈的多个实例,Linux在网络栈中引入了网络命名空间,这些独立的协议栈被隔离到不同的命名空间中。处于不同命名空间中的网络栈是完全隔离的,彼此之间无法通信,就好像两个“平行宇宙”。通过对网络资源的隔离,就能在一个宿主机上虚拟多个不同的网络环境。Docker正是利用了网络的命名空间特性,实现了不同容器之间的网络隔离。
  • 在Linux的网络命名空间中可以有自己独立的路由表及独立的iptables设置来提供包转发、NAT及IP包过滤等功能。
  • 为了隔离出独立的协议栈,需要纳入命名空间的元素有进程、套接字、网络设备等
    • 进程创建的套接字必须属于某个命名空间,套接字的操作也必须在命名空间中进行。
    • 同样,网络设备也必须属于某个命名空间。因为网络设备属于公共资源,所以可以通过修改属性实现在命名空间之间移动。当然,是否允许移动与设备的特征有关。
  • 让我们稍微深入Linux操作系统内部,看它是如何实现网络命名空间的,这也会对理解后面的概念有帮助。

2.1.1、网络命名空间的实现

  • Linux的网络协议栈是十分复杂的,为了支持独立的协议栈,相关的这些全局变量都必须被修改为协议栈私有。最好的办法就是让这些全局变量成为一个Net Namespace变量的成员,然后为协议栈的函数调用加入一个Namespace参数。这就是Linux实现网络命名空间的核心。
  • 同时,为了保证对已经开发的应用程序及内核代码的兼容性,内核代码隐式地使用了命名空间中的变量。程序如果没有对命名空间有特殊需求,就不需要编写额外的代码,网络命名空间对应用程序而言是透明的。
  • 在建立了新的网络命名空间,并将某个进程关联到这个网络命名空间后,就出现了类似于如图7.1所示的内核数据结构,所有网站栈变量都被放入了网络命名空间的数据结构中。这个网络命名空间是其进程组私有的,和其他进程组不冲突。

  • 在新生成的私有命名空间中只有回环设备(名为“lo”且是停止状态),其他设备默认都不存在,如果我们需要,则要一一手工建立。Docker容器中的各类网络栈设备都是Docker Daemon在启动时自动创建和配置的。
  • 所有的网络设备(物理的或虚拟接口、桥等在内核里都叫作Net Device)都只能属于一个命名空间。当然,物理设备(连接实际硬件的设备)通常只能关联到root这个命名空间中。虚拟的网络设备(虚拟的以太网接口或者虚拟网口对)则可以被创建并关联到一个给定的命名空间中,而且可以在这些命名空间之间移动。
  • 前面提到,由于网络命名空间代表的是一个独立的协议栈,所以它们之间是相互隔离的,彼此无法通信,在协议栈内部都看不到对方。那么有没有办法打破这种限制,让处于不同命名空间的网络相互通信,甚至和外部的网络进行通信呢?答案就是“有,应用Veth设备对即可”。Veth设备对的一个重要作用就是打通互相看不到的协议栈之间的壁垒,它就像一条管子,一端连着这个网络命名空间的协议栈,一端连着另一个网络命名空间的协议栈。所以如果想在两个命名空间之间通信,就必须有一个Veth设备对。后面会介绍如何操作Veth设备对来打通不同命名空间之间的网络。

2.1.2、网络命名空间的操作

  • 可以使用Linux iproute2系列配置工具中的IP命令来操作网络命名空间。注意,这个命令需要由root用户运行。
ip netns list                        列出网络命名空间。此命令显示的是“/var/run/netns”中的所有网络命名空间。
ip netns add NAME                    添加网络命名空间
ip [-all] netns delete [NAME]        删除网络命名空间
ip [-all] netns exec [NAME] cmd …    在指定的网络命名空间中执行命令
ip netns set NAME NETNSID            给网络命名空间分配id
ip netns identify [PID]              查看进程的网络命名空间
ip netns monitor                     监控对网络命名空间的操作
ip netns pids NAME                   查找使用此网络命名空间并将其作为主要网络命名空间的进程。此命令会从/proc目录中遍历。
  • 创建一个命名空间:
]# ip netns add ns1
  • 在命名空间中执行命令:
]# ip netns exec ns1 ifconfig -a
lo: flags=8<LOOPBACK>  mtu 65536
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
  • 也可以先通过bash命令进入内部的shell界面,然后执行各种命令,最后使用exit退出该名称空间:
]# ip netns exec ns1 bash
]# ifconfig -a
lo: flags=8<LOOPBACK>  mtu 65536
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

]# exit
exit

2.1.3、网络命名空间的实用技巧

  • 可以在不同的网络命名空间之间转移设备。
    • 有些设备是可以转移的,比如Veth设备。
    • 有些设备是不可以转移的,比如lo设备、vxlan设备、ppp设备、bridge设备等。如果设备属性NETIF_F_ETNS_LOCAL为on,该设备就不能被转移到其他命名空间中。
  • 一个设备只能属于一个命名空间,所以转移后在这个命名空间中就看不到这个设备了。
  • 使用ethtool工具查看设备是否可以转移:
]# ethtool -k virbr0 | grep 'netns-local'
netns-local: on [fixed]
  • 将无法转移的设备移动到别的命名空间时,会得到无效参数的错误提示:
]# ip link set virbr0 netns ns1
RTNETLINK answers: Invalid argument

2.2、Veth设备对

  • 引入Veth设备对是为了在不同的网络命名空间之间通信,利用它可以直接将两个网络命名空间连接起来。由于要连接两个网络命名空间,所以Veth设备都是成对出现的,很像一对以太网卡,并且中间有一根直连的网线。既然是一对网卡,那么我们将其中一端称为另一端的peer。在Veth设备的一端发送数据时,它会将数据直接发送到另一端,并触发另一端的接收操作。
  • 整个Veth的实现非常简单,有兴趣的读者可以参考源代码“drivers/net/veth.c”的实现。如图7.2所示是Veth设备对的示意图。

2.2.1、操作Veth设备对的命令

  • 创建Veth设备对,然后连接不同的命名空间,并设置它们的地址,让它们通信。

1、创建Veth设备对

]# ip link add veth1 type veth peer name veth2

2、查看Veth设备对的信息,使用ip link show命令查看所有网络接口:

  • 有两个设备生成了,一个是veth1,它的peer是veth2。
]# ip link show
......
5: veth2@veth1: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 02:7d:50:46:ec:d1 brd ff:ff:ff:ff:ff:ff
6: veth1@veth2: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 92:f6:9b:b8:3f:9c brd ff:ff:ff:ff:ff:ff

3、将Veth设备对的一端veth2移动另一个命名空间ns1:

  • 可以看到将veth2移动到了名称空间ns1中。
]# ip link set veth2 netns ns1

]# ip link show
......
6: veth1@if5: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 92:f6:9b:b8:3f:9c brd ff:ff:ff:ff:ff:ff link-netnsid 0

]# ip netns exec ns1 ip link show
......
5: veth2@if6: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 02:7d:50:46:ec:d1 brd ff:ff:ff:ff:ff:ff link-netnsid 0

4、给Veth设备对分配IP地址

  • 现在还不可以通信,因为它们还没有任何地址。
//分配IP地址
]# ip addr add 10.2.2.1/24 dev veth1
]# ip netns exec ns1 ip addr add 10.2.2.2/24 dev veth2

//启动网卡
]# ip link set dev veth1 up
]# ip netns exec ns1 ip link set dev veth2 up

5、测试两个网卡的联通性

  • 两个网络命名空间可以互相通信了。
//veth1 ping veth2
]# ping -c 2 10.2.2.2
PING 10.2.2.2 (10.2.2.2) 56(84) bytes of data.
64 bytes from 10.2.2.2: icmp_seq=1 ttl=64 time=0.052 ms
64 bytes from 10.2.2.2: icmp_seq=2 ttl=64 time=0.104 ms

--- 10.2.2.2 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1000ms
rtt min/avg/max/mdev = 0.052/0.078/0.104/0.026 ms

//veth2 ping veth1
]# ip netns exec ns1 ping -c 2 10.2.2.1
PING 10.2.2.1 (10.2.2.1) 56(84) bytes of data.
64 bytes from 10.2.2.1: icmp_seq=1 ttl=64 time=0.083 ms
64 bytes from 10.2.2.1: icmp_seq=2 ttl=64 time=0.063 ms

--- 10.2.2.1 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1000ms
rtt min/avg/max/mdev = 0.063/0.073/0.083/0.010 ms
  • 至此,我们就能够理解Veth设备对的原理和用法了。在Docker内部,Veth设备对也是连通容器与宿主机的主要网络设备,离开它是不行的。

2.2.2、如何查看Veth设备对的对端

  • 一旦将Veth设备对的对端放入另一个命名空间,在本命名空间中就看不到它了。那么怎么知道这个Veth设备的对端在哪里呢,也就是说它到底连接到哪个命名空间呢?
  • 可以使用ethtool工具来查看(当网络命名空间特别多时,这可不是一件很容易的事情)。

1、查询Veth设备对端接口在设备列表中的序列号

  • 可以看出veth1对端的序列号是5,再查看哪个命名空间中有序列号是5的设备。
]# ethtool -S veth1
NIC statistics:
     peer_ifindex: 5

2、查看其他名称空间

  • 可以看出在名称空间ns1中有序号是5的设备。因此veth1的peer在ns1中。
]# for i in $(ip netns list | awk '{print $1}'); do echo "名称空间: $i"; ip netns exec $i ip link list | grep '5:'; done
名称空间: ns1
5: veth2@if6: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000

2.3、网桥

  • Linux可以支持多个不同的网络,它们之间能够相互通信,如何将这些网络连接起来并实现各网络中主机的相互通信呢?可以用网桥。
  • 网桥是一个二层的虚拟网络设备,把若干个网络接口“连接”起来,以使得网络接口之间的报文能够互相转发。
    • 网桥能够解析收发的报文,读取目标MAC地址的信息,和自己记录的MAC表结合,来决策报文的转发目标网络接口。为了实现这些功能,网桥会学习源MAC地址(二层网桥转发的依据就是MAC地址)。
    • 在转发报文时,网桥只需要向特定的网口进行转发,来避免不必要的网络交互。
    • 如果它遇到一个自己从未学习到的地址,就无法知道这个报文应该向哪个网络接口转发,就将报文广播给所有的网络接口(报文来源的网络接口除外)。
  • 在实际的网络中,网络拓扑不可能永久不变。设备如果被移动到另一个端口上,却没有发送任何数据,网桥设备就无法感知到这个变化,网桥还是向原来的端口转发数据包,在这种情况下数据就会丢失。所以网桥还要对学习到的MAC地址表加上超时时间(默认为5min)。如果网桥收到了对应端口MAC地址回发的包,则重置超时时间,否则过了超时时间后,就认为设备已经不在那个端口上了,它就会重新广播发送。
  • 在Linux的内部网络栈里实现的网桥设备,作用和上面的描述相同。过去Linux主机一般都只有一个网卡,现在多网卡的机器越来越多,而且有很多虚拟的设备存在,所以Linux的网桥提供了在这些设备之间互相转发数据的二层设备。
  • Linux内核支持网口的桥接(目前只支持以太网接口)。但是与单纯的交换机不同,交换机只是一个二层设备,对于接收到的报文,要么转发,要么丢弃。运行着Linux内核的机器本身就是一台主机,有可能是网络报文的目的地,其收到的报文除了转发和丢弃,还可能被送到网络协议栈的上层(网络层),从而被自己(这台主机本身的协议栈)消化,所以我们既可以把网桥看作一个二层设备,也可以把它看作一个三层设备

2.3.1、Linux网桥的实现

  • Linux内核是通过一个虚拟的网桥设备(Net Device)来实现桥接的。这个虚拟设备可以绑定若干个以太网接口设备,从而将它们桥接起来。如图7.3所示,这种Net Device网桥和普通的设备不同,最明显的一个特性是它还可以有一个IP地址。

  • 如图7.3所示,网桥设备br0绑定了eth0和eth1。
    • 对于网络协议栈的上层来说,只看的到br0就行。因为桥接是在数据链路层实现的,上层不需要关心桥接的细节,所以协议栈上层需要发送的报文被送到br0,网桥设备的处理代码判断报文该被转发到eth0还是eth1,或者两者皆转发;
    • 反过来,从eth0或从eth1接收到的报文被提交给网桥的处理代码,在这里会判断报文应该被转发、丢弃还是被提交到协议栈上层。
  • 而有时eth0、eth1也可能会作为报文的源地址或目的地址,直接参与报文的发送与接收,从而绕过网桥。

2.3.2、网桥的常用操作命令

  • Docker自动完成了对网桥的创建和维护。
  • 模拟docker网桥,如图所示。

//创建网桥
]# brctl addbr bridge0

//创建名称空间
]# ip netns add ns1
]# ip netns add ns2
]# ip netns add ns3
]# ip netns add ns4

//创建veth设备对
]# ip link add veth1 type veth peer name veth1_0
]# ip link add veth2 type veth peer name veth2_0
]# ip link add veth3 type veth peer name veth3_0
]# ip link add veth4 type veth peer name veth4_0

//将veth设备对的一端关联到网桥上(为网桥增加网口,在Linux中,一个网口其实就是一个物理网卡。)
]# brctl addif bridge0 veth1_0
]# brctl addif bridge0 veth2_0
]# brctl addif bridge0 veth3_0
]# brctl addif bridge0 veth4_0

//将veth设备对的一端转移到对应的名称空间中
]# ip link set veth1 netns ns1
]# ip link set veth2 netns ns2
]# ip link set veth3 netns ns3
]# ip link set veth4 netns ns4

//为各个名称空间中的veth设备端分配ip
]# ip netns exec ns1 ip addr add local 192.168.1.1/24 dev veth1
]# ip netns exec ns2 ip addr add local 192.168.1.2/24 dev veth2
]# ip netns exec ns3 ip addr add local 192.168.1.3/24 dev veth3
]# ip netns exec ns4 ip addr add local 192.168.1.4/24 dev veth4

//启动网桥和veth设备
]# ip link set bridge0 up

]# ip link set veth1_0 up
]# ip link set veth2_0 up
]# ip link set veth3_0 up
]# ip link set veth4_0 up

]# ip netns exec ns1 ip link set veth1 up
]# ip netns exec ns2 ip link set veth2 up
]# ip netns exec ns3 ip link set veth3 up
]# ip netns exec ns4 ip link set veth4 up

//互ping
]# ip netns exec ns1 ping -c 2 192.168.1.1
]# ip netns exec ns1 ping -c 2 192.168.1.2
]# ip netns exec ns1 ping -c 2 192.168.1.3
]# ip netns exec ns1 ping -c 2 192.168.1.4
  • 提示:若物理网卡作为网桥的一个网口,则此物理网卡将在链路层工作(是一个纯链路层设备),就不再需要IP地址了,可以取消物理网卡的IP,给网桥配置一个IP地址。

2.4、iptables和Netfilter

2.5、路由

  • Linux系统包含一个完整的路由功能。当IP层在处理数据发送或者转发时,会使用路由表来决定发往哪里。
    • 如果主机与目的主机直接相连,那么主机可以直接发送IP报文到目的主机,这个过程比较简单。例如,通过点对点的链接或网络共享。
    • 如果主机与目的主机没有直接相连,那么主机会将IP报文发送给对应的路由器,然后由路由器来决定往哪里发送IP报文。
  • 路由功能由IP层维护的一张路由表来实现。当主机收到数据报文时,它用此表来决策接下来应该做什么操作。
  • 当从网络侧接收到数据报文时,IP层首先会检查报文的IP地址是否与主机自身的地址相同。
    • 如果数据报文中的IP地址是主机自身的地址,那么报文将被发送到传输层相应的协议中。
    • 如果报文中的IP地址不是主机自身的地址,并且主机配置了路由功能,那么报文将被转发。否则,报文将被丢弃。
  • 路由表中的数据一般是以条目形式存在的。一个典型的路由表条目通常包含以下主要的条目项。
    • (1)目的IP地址:此字段表示目标的IP地址。这个IP地址可以是某主机的地址,也可以是一个网络地址。如果这个条目包含的是一个主机地址,那么它的主机ID将被标记为非零;如果这个条目包含的是一个网络地址,那么它的主机ID将被标记为零。
    • (2)下一个路由器的IP地址:这里采用“下一个”的说法,是因为下一个路由器并不总是最终的目的路由器,它很可能是一个中间路由器。条目给出的下一个路由器的地址用来转发在相应接口接收到的IP数据报文。
    • (3)标志:这个字段提供了另一组重要信息,例如,目的IP地址是一个主机地址还是一个网络地址。此外,从标志中可以得知下一个路由器是一个真实路由器还是一个直接相连的接口。
    • (4)网络接口规范:为一些数据报文的网络接口规范,该规范将与报文一起被转发。
  • 查看路由表
]# ip route list
//或者
]# route -n

3、Docker的网络实现

  • 标准的Docker支持以下4类网络模式。
    • host模式:使用--net=host指定。
    • container模式:使用--net=container:NAME_or_ID指定。
    • none模式:使用--net=none指定。
    • bridge模式:使用--net=bridge指定,为默认设置。
  • 在Kubernetes管理模式下通常只会使用bridge模式,所以本节只介绍在bridge模式下Docker是如何支持网络的。
  • 在bridge模式下,Docker Daemon第1次启动时会创建一个虚拟的网桥,默认的名称是docker0,然后按照RPC1918的模型在私有网络空间中给这个网桥分配一个子网。针对由Docker创建的每一个容器,都会创建一个虚拟的以太网设备(Veth设备对),其中一端关联到网桥上,另一端使用Linux的网络命名空间技术,映射到容器内的eth0设备,然后从网桥的地址段内给eth0接口分配一个IP地址。
  • 如图7.6所示就是Docker的默认桥接网络模型。

  • 其中ip1是网桥的IP地址,Docker Daemon会在几个备选地址段里给它选一个地址,通常是以172开头的一个地址。这个地址和主机的IP地址是不重叠的。ip2是Docker在启动容器时,在这个地址段选择的一个没有使用的IP地址分配给容器。相应的MAC地址也根据这个IP地址,在02:42:ac:11:00:00和02:42:ac:11:ff:ff的范围内生成,这样做可以确保不会有ARP冲突。
  • 启动后,Docker还将Veth对的名称映射到eth0网络接口。ip3就是主机的网卡地址。
  • 在一般情况下,ip1、ip2和ip3是不同的IP段,所以在默认不做任何特殊配置的情况下,在外部是看不到ip1和ip2的。
  • 这样做的结果就是,在同一台机器内的容器之间可以相互通信,不同主机上的容器不能相互通信,实际上它们甚至有可能在相同的网络地址范围内(不同主机上的docker0的地址段可能是一样的)。
  • 为了让它们跨节点互相通信,就必须在主机的地址上分配端口,然后通过这个端口路由或代理到容器上。这种做法显然意味着一定要在容器之间小心谨慎地协调好端口的分配,或者使用动态端口的分配技术。在不同应用之间协调好端口分配是十分困难的事情,特别是集群水平扩展时。而动态的端口分配也会带来高度复杂性,例如:每个应用程序都只能将端口看作一个符号(因为是动态分配的,所以无法提前设置)。
  • 而且API Server要在分配完后,将动态端口插入配置的合适位置,服务也必须能互相找到对方等。这些都是Docker的网络模型在跨主机访问时面临的问题。

3.1、查看Docker启动后的系统情况

  • 我们已经知道,Docker网络在bridge模式下Docker Daemon启动时创建docker0网桥,并在网桥使用的网段为容器分配IP。让我们看看实际的操作。
  • 在启动Docker Daemon并且还没有启动任何容器时,查看网络协议栈、iptables和route。

1、查看网络协议栈

  • 可以看到,Docker创建了docker0网桥。

2、查看iptables规则

  • 可以看到,Docker添加了iptables规则。

  • 对这些规则的说明如下。
    • (1)在NAT表前两条匹配后,都会继续执行DOCKER链,而此时DOCKER链为空,所以前两条只是做了一个框架,并没有实际效果。
    • (2)NAT表第3条的含义是,若本地发出的数据包不是发往docker0的,即是发往主机之外的设备的,则都需要进行动态地址修改(MASQUERADE),将源地址从容器的地址(172段)修改为宿主机网卡的IP地址,之后就可以发送给外面的网络了。
    • (3)FILTER表中,第7条是说,如果接收到的数据包属于以前已经建立好的连接,那么允许直接通过。这样接收到的数据包自然又走回docker0,并中转到相应的容器。
    • (4)在FILTER表中,第8条也是一个框架,因为后继的DOCKER链是空的。
    • (4)在FILTER表中,第9条是说,docker0发出的包,如果需要Forward到非docker0的本地IP地址的设备,则是允许的。这样,docker0设备的包就可以根据路由规则中转到宿主机的网卡设备,从而访问外面的网络。
    • (5)FILTER表中,第10条是说,docker0的包还可以被中转给docker0本身,即连接在docker0网桥上的不同容器之间的通信也是允许的。

3、查看route

4、核心转发功能

  • 除了这些的设置,Linux的ip_forward功能也被Docker Daemon打开了:
]# cat /proc/sys/net/ipv4/ip_forward
1

3.2、查看容器启动后的情况(容器无端口映射)

  • 启动一个容器
    • 宿主机器上的Netfilter和路由表都没有变化,说明在不进行端口映射时,Docker的默认网络是没有特殊处理的
]# docker container run --name b1 -it --rm busybox:1.28

1、查看网络协议栈

  • 可以看到添加了一个网络接口。
    • 宿主机上的Veth对已经建立,并连接到容器内。

2、查看iptables规则

  • 可以看到iptables规则没有变化。

3、查看route

  • 可以看到route规则没有变化。

4、查看容器的网路协议栈和路由

  • 进入容器内,查看网络栈和路由:
    • 可以看到,默认停止的回环设备lo已经被启动,外面宿主机连接进来的Veth设备也被命名成了eth0,并且已经配置了地址172.17.0.2。路由信息表包含一条到docker0的子网路由和一条到docker0的默认路由。
/ # ip addr list
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
12: eth0@if13: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue 
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
       valid_lft forever preferred_lft forever
/ # ip route
default via 172.17.0.1 dev eth0 
172.17.0.0/16 dev eth0 scope link  src 172.17.0.2 

3.3、查看容器启动后的情况(容器有端口映射)

  • 启动一个带端口映射的容器
]# docker container run --name b1 -it --rm -p 8081:8082 busybox:1.28

1、查看网络协议栈

2、查看iptables规则

3、查看route

  • 从新增的规则可以看出,Docker服务在NAT和FILTER两个表内添加的两个DOCKER子链都是给端口映射用的。
    • 在本例中把外面宿主机的8081端口映射到容器的8082端口。通过前面的分析我们知道,无论是宿主机接收到的还是宿主机本地协议栈发出的,目标地址是本地IP地址的包都会经过NAT表中的DOCKER子链。Docker为每一个端口映射都在这个链上增加了到实际容器目标地址和目标端口的转换。
    • 经过这个DNAT的规则修改后的IP包,会重新经过路由模块的判断进行转发。由于目标地址和端口已经是容器的地址和端口,所以数据自然就被转发到docker0上,从而被转发到对应的容器内部。
    • 当然在Forward时,也需要在DOCKER子链中添加一条规则,如果目标端口和地址是指定容器的数据,则允许通过。
  • 在Docker按照端口映射的方式启动容器时,主要的不同就是上述iptables部分。而容器内部的路由和网络设备,都和不做端口映射时一样,没有任何变化。

3.4、Docker的网络局限

  • 我们从Docker对Linux网络协议栈的操作可以看到,Docker一开始没有考虑到多主机互联的网络解决方案。
  • Docker一直以来的理念都是“简单为美”,几乎所有尝试Docker的人都被它“用法简单,功能强大”的特性所吸引,这也是Docker迅速走红的一个原因。
  • 我们都知道,虚拟化技术中最为复杂的部分就是虚拟化网络技术,即使是单纯的物理网络部分,也是一个门槛很高的技能领域,通常只被少数网络工程师所掌握,所以我们可以理解结合了物理网络的虚拟网络技术有多难。在Docker之前,所有接触过OpenStack的人都对其网络问题讳莫如深,Docker明智地避开这个“雷区”,让其他专业人员去用现有的虚拟化网络技术解决Docker主机的互联问题,以免让用户觉得Docker太难,从而放弃学习和使用Docker。
  • Docker成名以后,重新开始重视网络解决方案,收购了一家Docker网络解决方案公司—Socketplane,原因在于这家公司的产品广受好评,但有趣的是Socketplane的方案就是以Open vSwitch为核心的,其还为Open vSwitch提供了Docker镜像,以方便部署程序。之后,Docker开启了一个宏伟的虚拟化网络解决方案—Libnetwork,如图7.7所示是其概念图。
  • 这个概念图没有了IP,也没有了路由,已经颠覆了我们的网络常识,对于不怎么懂网络的大多数人来说,它的确很有诱惑力,未来是否会对虚拟化网络的模型产生深远冲击,我们还不得而知,但它仅仅是Docker官方当前的一次“尝试”。
  • 针对目前Docker的网络实现,Docker使用的Libnetwork组件只是将Docker平台中的网络子系统模块化为一个独立库的简单尝试,离成熟和完善还有一段距离。

4、Kubernetes的网络实现

  • 在实际的业务场景中,业务组件之间的关系十分复杂,特别是随着微服务理念逐步深入人心,应用部署的粒度更加细小和灵活。为了支持业务应用组件的通信,Kubernetes网络的设计主要致力于解决以下问题。
    • (1)容器到容器之间的直接通信。
    • (2)抽象的Pod到Pod之间的通信。
    • (3)Pod到Service之间的通信。
    • (4)集群外部与内部组件之间的通信。
  • 其中第3条、第4条在之前的章节里都有所讲解,本节对更为基础的第1条与第2条进行深入分析和讲解。

4.1、容器到容器的通信

  • 同一个Pod内的容器(Pod内的容器是不会跨宿主机的)共享同一个网络命名空间,共享同一个Linux协议栈。所以对于网络的各类操作,就和它们在同一台机器上一样,它们甚至可以用localhost地址访问彼此的端口。
    • 这么做的结果是简单、安全和高效,也能减小将已经存在的程序从物理机或者虚拟机移植到容器下运行的难度。其实,在容器技术出来之前,大家早就积累了如何在一台机器上运行一组应用程序的经验,例如,如何让端口不冲突,以及如何让客户端发现它们等。
  • 我们来看一下Kubernetes是如何利用Docker的网络模型的。
    • 如图7.8中的阴影部分所示,在Node上运行着一个Pod实例(有两个容器)。容器1和容器2共享一个网络的命名空间,共享一个命名空间的结果就是它们好像在一台机器上运行,它们打开的端口不会有冲突,可以直接使用Linux的本地IPC进行通信(例如消息队列或者管道)。其实,这和传统的一组普通程序运行的环境是完全一样的,传统程序不需要针对网络做特别的修改就可以移植了,它们之间的互相访问只需要使用localhost就可以。例如,如果容器2运行的是MySQL,那么容器1使用localhost:3306就能直接访问这个运行在容器2上的MySQL了。

4.2、Pod之间的通信

  • 每一个Pod都有一个真实的全局IP地址,同一个Node内的不同Pod之间可以直接采用对方Pod的IP地址通信,而且不需要采用其他发现机制,例如DNS、Consul或者etcd。
  • Pod容器既有可能在同一个Node上运行,也有可能在不同的Node上运行,所以通信也分为两类:
    • 同一个Node内Pod之间的通信
    • 不同Node上Pod之间的通信

4.2.1、同一个Node内Pod之间的通信

  • 同一个Node内两个Pod之间的关系,如图7.9所示。

  • 可以看出,Pod1和Pod2都是通过Veth连接到同一个docker0网桥上的,它们的IP地址IP1、IP2都是从docker0的网段上动态获取的,它们和网桥本身的IP3是同一个网段的。
  • 另外,在Pod1、Pod2的Linux协议栈上,默认路由都是docker0的地址,也就是说所有非本地地址的网络数据,都会被默认发送到docker0网桥上,由docker0网桥直接中转。
  • 综上所述,由于它们都关联在同一个docker0网桥上,地址段相同,所以它们之间是能直接通信的。

4.2.2、不同Node上Pod之间的通信

  • Pod的地址是与docker0在同一个网段的。
    • 我们知道docker0网段与宿主机网卡是两个完全不同的IP网段,并且不同Node之间的通信只能通过宿主机的物理网卡进行,因此要想实现不同Node上Pod容器之间的通信,就必须想办法通过主机的这个IP地址进行寻址和通信。
    • 另一方面,这些动态分配且藏在docker0之后的所谓“私有”IP地址也是可以找到的。Kubernetes会记录所有正在运行的Pod的IP分配信息,并将这些信息保存在etcd中(作为Service的Endpoint)。这些私有IP信息对于Pod到Pod的通信也是十分重要的,因为我们的网络模型要求Pod到Pod使用私有IP进行通信。所以首先要知道这些IP是什么。
  • 之前提到,Kubernetes的网络对Pod的地址是平面的和直达的,所以这些Pod的IP规划也很重要,不能有冲突。只要没有冲突,我们就可以想办法在整个Kubernetes的集群中找到它。
  • 综上所述,要想支持不同Node上Pod之间的通信,就要满足两个条件:
    • (1)在整个Kubernetes集群中对Pod的IP分配进行规划,不能有冲突;
    • (2)找到一种办法,将Pod的IP和所在Node的IP关联起来,通过这个关联让Pod可以互相访问。
  • 根据条件1的要求,我们需要在部署Kubernetes时对docker0的IP地址进行规划,保证每个Node上的docker0地址都没有冲突。我们可以在规划后手工配置到每个Node上,或者做一个分配规则,由安装的程序自己去分配占用。例如,Kubernetes的网络增强开源软件Flannel就能够管理资源池的分配。
  • 根据条件2的要求,Pod中的数据在发出时,需要有一个机制能够知道对方Pod的IP地址挂在哪个具体的Node上。也就是说先要找到Node对应宿主机的IP地址,将数据发送到这个宿主机的网卡,然后在宿主机上将相应的数据转发到具体的docker0上。一旦数据到达宿主机Node,则那个Node内部的docker0便知道如何将数据发送到Pod。如图7.10所示。
  • 在图7.10中,IP1对应的是Pod1,IP2对应的是Pod2。Pod1在访问Pod2时,首先要将数据从源Node的eth0发送出去,找到并到达Node2的eth0。即先是从IP3到IP4的递送,之后才是从IP4到IP2的递送。

  • 在谷歌的GCE环境中,Pod的IP管理(类似docker0)、分配及它们之间的路由打通都是由GCE完成的。Kubernetes作为主要在GCE上面运行的框架,它的设计是假设底层已经具备这些条件,所以它分配完地址并将地址记录下来就完成了它的工作。在实际的GCE环境中,GCE的网络组件会读取这些信息,实现具体的网络打通。
  • 而在实际生产环境中,因为安全、费用、合规等种种原因,Kubernetes的客户不可能全部使用谷歌的GCE环境,所以在实际的私有云环境中,除了需要部署Kubernetes和Docker,还需要额外的网络配置,甚至通过一些软件来实现Kubernetes对网络的要求。做到这些后,Pod和Pod之间才能无差别地进行透明通信。
  • 为了达到这个目的,开源界有不少应用增强了Kubernetes、Docker的网络,在后面的章节中会介绍几个常用的组件及其组网原理。

5、Pod和Service网络实战

  • Docker给我们带来了不同的网络模式,Kubernetes也以一种不同的方式来解决这些网络模式的挑战,但其方式有些难以理解,特别是对于刚开始接触Kubernetes的网络的开发者来说。我们在前面学习了Kubernetes、Docker的理论,本节将通过一个完整的实验,从部署一个Pod开始,一步一步地部署那些Kubernetes的组件,来剖析Kubernetes在网络层是如何实现及工作的。
  • 这里使用虚拟机来完成实验。如果要部署在物理机器上或者云服务商的环境中,则涉及的网络模型很可能稍微有所不同。不过,从网络角度来看,Kubernetes的机制是类似且一致的。
  • 好了,来看看我们的实验环境,如图7.11所示。

  • 查看两个node节点的route(路由):
//node1的pod网段:10.10.2.1/24
10.10.1.0/24 via 10.10.1.0 dev flannel.1 onlink 
10.10.2.0/24 dev cni0 proto kernel scope link src 10.10.2.1

//node2的pod网段:10.10.1.1/24
10.10.1.0/24 dev cni0 proto kernel scope link src 10.10.1.1 
10.10.2.0/24 via 10.10.2.0 dev flannel.1 onlink 
  • 创建service和pod
]# cat test-busybox.yaml
apiVersion: v1
kind: Service
metadata:
  name: test-busybox-svc
  namespace: default
  labels:
    app: test-busybox-svc
spec:
  type: NodePort
  ports:
  - port: 54321
    targetPort: 54321
    nodePort: 54321
    protocol: TCP
    name: http
  selector:
    app: test-busybox-pod
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: test-busybox-deployment
  namespace: default
spec:
  selector:
    matchLabels:
      app: test-busybox-pod
  replicas: 2
  template:
    metadata:
      labels:
        app: test-busybox-pod
    spec:
      imagePullSecrets:
       - name: svharborwx
      containers:
      - name: test-busybox-container
        image: busybox:1.28
        imagePullPolicy: IfNotPresent
        command: ["/bin/sleep", "10000000000"]
        ports:
        - containerPort: 54321
  • 查看变化
//查看service
]# kubectl get service -A
NAMESPACE     NAME               TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)                  AGE
default       test-busybox-svc   NodePort    10.20.139.128   <none>        54321:54321/TCP          4m54s

//查看pod
]# kubectl get pods -A -o wide
NAMESPACE      NAME                                       READY   STATUS    RESTARTS   AGE     IP          NODE          NOMINATED NODE   READINESS GATES
default        test-busybox-deployment-5dd4f8dc7f-qxnhr   1/1     Running   0          6m49s   10.10.1.6   k8s-node2     <none>           <none>
default        test-busybox-deployment-5dd4f8dc7f-x5cns   1/1     Running   0          6m49s   10.10.2.5   k8s-node1     <none>           <none>

//在iptables中自动添加的规则
-A KUBE-NODEPORTS -p tcp -m comment --comment "default/test-busybox-svc:http" -m tcp --dport 54321 -j KUBE-MARK-MASQ
-A KUBE-NODEPORTS -p tcp -m comment --comment "default/test-busybox-svc:http" -m tcp --dport 54321 -j KUBE-SVC-RL6YOI6CKNXY4IQL
-A KUBE-SEP-FFGFMESX4SUR5H4Y -s 10.10.2.5/32 -m comment --comment "default/test-busybox-svc:http" -j KUBE-MARK-MASQ
-A KUBE-SEP-FFGFMESX4SUR5H4Y -p tcp -m comment --comment "default/test-busybox-svc:http" -m tcp -j DNAT --to-destination 10.10.2.5:54321
-A KUBE-SEP-ZZTL3GGA5VIPT4OH -s 10.10.1.6/32 -m comment --comment "default/test-busybox-svc:http" -j KUBE-MARK-MASQ
-A KUBE-SEP-ZZTL3GGA5VIPT4OH -p tcp -m comment --comment "default/test-busybox-svc:http" -m tcp -j DNAT --to-destination 10.10.1.6:54321
-A KUBE-SERVICES ! -s 10.10.0.0/16 -d 10.20.139.128/32 -p tcp -m comment --comment "default/test-busybox-svc:http cluster IP" -m tcp --dport 54321 -j KUBE-MARK-MASQ
-A KUBE-SERVICES -d 10.20.139.128/32 -p tcp -m comment --comment "default/test-busybox-svc:http cluster IP" -m tcp --dport 54321 -j KUBE-SVC-RL6YOI6CKNXY4IQL
-A KUBE-SVC-RL6YOI6CKNXY4IQL -m comment --comment "default/test-busybox-svc:http" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-ZZTL3GGA5VIPT4OH
-A KUBE-SVC-RL6YOI6CKNXY4IQL -m comment --comment "default/test-busybox-svc:http" -j KUBE-SEP-FFGFMESX4SUR5H4Y

6、CNI网络模型

  • 随着容器技术在企业生产系统中的逐步落地,用户对容器云的网络特性要求也越来越高。跨主机容器间的网络互通已经成为基本要求,更高的要求包括容器固定IP地址、一个容器多个IP地址、多个子网隔离、ACL控制策略、与SDN集成等。
  • 目前主流的容器网络模型主要有:
    • Docker公司提出的Container Network Model(CNM)模型
    • CoreOS公司提出的Container Network Interface(CNI)模型

6.1、CNM模型

  • CNM模型是由Docker公司提出的容器网络模型,现在已经被CiscoContiv、Kuryr、Open Virtual Networking(OVN)、Project Calico、VMware、Weave和Plumgrid等项目所采纳。另外,Weave、ProjectCalico、Kuryr和Plumgrid等项目也为CNM提供了网络插件的具体实现。
  • CNM模型主要通过Network Sandbox、Endpoint和Network这3个组件进行实现,如图7.17所示。
    • Network Sandbox:容器内部的网络栈,包括网络接口、路由表、DNS等配置的管理。Sandbox可用Linux网络命名空间、FreeBSDJail等机制进行实现。一个Sandbox可以包含多个Endpoint。
    • Endpoint:用于将容器内的Sandbox与外部网络相连的网络接口。可以使用veth对、Open vSwitch的内部port等技术进行实现。一个Endpoint仅能够加入一个Network。
    • Network:可以直接互连的Endpoint的集合。可以通过Linux网桥、VLAN等技术进行实现。一个Network包含多个Endpoint。

6.2、CNI模型

  • CNI是由CoreOS公司提出的另一种容器网络规范,现在已经被Kubernetes、rkt、Apache Mesos、Cloud Foundry和Kurma等项目采纳。另外,Contiv Networking, Project Calico、Weave、SR-IOV、Cilium、Infoblox、Multus、Romana、Plumgrid和Midokura等项目也为CNI提供网络插件的具体实现。图7.18描述了容器运行环境与各种网络插件通过CNI进行连接的模型。

 

  • CNI定义的是容器运行环境与网络插件之间的简单接口规范,通过一个JSON Schema定义CNI插件提供的输入和输出参数。一个容器可以通过绑定多个网络插件加入多个网络中。
  • 本节将对Kubernetes如何实现CNI模型进行详细说明。

1、CNI规范概述

  • CNI提供了一种应用容器的插件化网络解决方案,定义对容器网络进行操作和配置的规范,通过插件的形式对CNI接口进行实现。CNI是由rkt Networking Proposal发展而来的,试图提供一种普适的容器网络解决方案。CNI仅关注在创建容器时分配网络资源,和在销毁容器时删除网络资源,这使得CNI规范非常轻巧、易于实现,得到了广泛的支持。
  • 在CNI模型中只涉及两个概念:容器和网络。
    • 容器(Container):是拥有独立Linux网络命名空间的环境,例如使用Docker或rkt创建的容器。关键之处是容器需要拥有自己的Linux网络命名空间,这是加入网络的必要条件。
    • 网络(Network):表示可以互连的一组实体,这些实体拥有各自独立、唯一的IP地址,可以是容器、物理机或者其他网络设备(比如路由器)等。
  • 对容器网络的设置和操作都通过插件(Plugin)进行具体实现,CNI插件包括两种类型:CNI Plugin和IPAM(IP Address Management)Plugin。CNI Plugin负责为容器配置网络资源,IPAM Plugin负责对容器的IP地址进行分配和管理。IPAM Plugin作为CNI Plugin的一部分,与CNI Plugin一起工作。

2、CNI Plugin插件详解

  • CNI Plugin包括3个基本接口的定义:添加(ADD)、删除(DELETE)、检查(CHECK)和版本查询(VERSION)。这些接口的具体实现要求插件提供一个可执行的程序,在容器网络添加或删除时进行调用,以完成具体的操作。
    • (1)添加:将容器添加到某个网络。主要过程为在ContainerRuntime创建容器时,先创建好容器内的网络命名空间(NetworkNamespace),然后调用CNI插件为该netns进行网络配置,最后启动容器内的进程。
    • (2)删除:容器销毁时将容器从某个网络中删除。
    • (3)检查:检查容器网络是否正确设置。
    • (4)版本查询:查询网络插件支持的CNI规范版本号

3、IPAM Plugin插件详解

  • 为了减轻CNI Plugin对IP地址管理的负担,在CNI规范中设置了一个新的插件专门用于管理容器的IP地址(还包括网关、路由等信息),被称为IPAM Plugin。通常由CNI Plugin在运行时自动调用IPAM Plugin完成容器IP地址的分配。
  • IPAM Plugin负责为容器分配IP地址、网关、路由和DNS,典型的实现包括host-local和dhcp。与CNI Plugin类似,IPAM插件也通过可执行程序完成IP地址分配的具体操作。IPAM可执行程序也处理传递给CNI插件的环境变量和通过标准输入(stdin)传入的网络配置参数。

4、多网络插件

  • 在很多情况下,一个容器需要连接多个网络,CNI规范支持为一个容器运行多个CNI Plugin来实现这个目标。多个网络插件将按照网络配置列表中的顺序执行,并将前一个网络配置的执行结果传递给后面的网络配置。

6.3、在Kubernetes中使用网络插件

  • Kubernetes目前支持两种网络插件的实现。
    • CNI插件:根据CNI规范实现其接口,以与插件提供者进行对接。
    • kubenet插件:使用bridge和host-local CNI插件实现一个基本的cbr0。
  • 为了在Kubernetes集群中使用网络插件,需要在kubelet服务的启动参数上设置下面两个参数。
    • --network-plugin-dir:kubelet启动时扫描网络插件的目录。
    • --network-plugin:网络插件名称,对于CNI插件,设置为cni即可,无须关注--network-plugin-dir的路径。对于kubenet插件,设置为kubenet,目前仅实现了一个简单的cbr0 Linux网桥。
  • 在设置--network-plugin="cni"时,kubelet还需设置下面两个参数。
    • --cni-conf-dir:CNI插件的配置文件目录,默认为/etc/cni/net.d。该目录下配置文件的内容需要符合CNI规范。
    • --cni-bin-dir:CNI插件的可执行文件目录,默认为/opt/cni/bin。
  • 目前已有多个开源项目支持以CNI网络插件的形式部署到Kubernetes集群中,进行Pod的网络设置和网络策略的设置,包括Calico、Canal、Cilium、Contiv、Flannel、Romana、Weave Net等。

7、Kubernetes网络策略

  • 为了实现细粒度的容器间网络访问隔离策略,Kubernetes从1.3版本开始,由SIG-Network小组主导研发了Network Policy机制,目前已升级为networking.k8s.io/v1稳定版本。Network Policy的主要功能是对Pod间的网络通信进行限制和准入控制,设置方式为将Pod的Label作为查询条件,设置允许访问或禁止访问的客户端Pod列表。目前查询条件可以作用于Pod和Namespace级别。
  • 为了使用Network Policy,Kubernetes引入了一个新的资源对象NetworkPolicy,供用户设置Pod间网络访问的策略。但仅定义一个网络策略是无法完成实际的网络隔离的,还需要一个策略控制器(PolicyController)进行策略的实现。策略控制器由第三方网络组件提供,目前Calico、Cilium、Kube-router、Romana、Weave Net等开源项目均支持网络策略的实现。
  • Network Policy的工作原理如图7.19所示,policy controller需要实现一个API Listener,监听用户设置的NetworkPolicy定义,并将网络访问规则通过各Node的Agent进行实际设置(Agent则需要通过CNI网络插件实现)。

7.1、网络策略配置说明

  • 网络策略的设置主要用于对目标Pod的网络访问进行限制,在默认情况下对所有Pod都是允许访问的,在设置了指向Pod的NetworkPolicy网络策略之后,到Pod的访问才会被限制。
  • 下面通过一个例子对NetworkPolicy资源对象的使用进行说明:
    • podSelector:用于定义该网络策略作用的Pod范围,本例的选择条件为包含“role=db”标签的Pod。
    • policyTypes:网络策略的类型,包括ingress和egress两种,用于设置目标Pod的入站和出站的网络限制。
    • ingress:定义允许访问目标Pod的入站白名单规则,满足from条件的客户端才能访问ports定义的目标Pod端口号。
      • -from:对符合条件的客户端Pod进行网络放行,规则包括基于客户端Pod的Label、基于客户端Pod所在的Namespace的Label或者客户端的IP范围。
      • -ports:允许访问的目标Pod监听的端口号。
    • egress:定义目标Pod允许访问的“出站”白名单规则,目标Pod仅允许访问满足to条件的服务端IP范围和ports定义的端口号。
      • -to:允许访问的服务端信息,可以基于服务端Pod的Label、基于服务端Pod所在的Namespace的Label或者服务端IP范围。
      • -ports:允许访问的服务端的端口号。
apiVersion:  networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: test-network-policy
  namespace: default
spec:
  podSelector:
    matchLabels:
      role: db
  policyTypes:
  - Ingress
  - Egress
  ingress:
  - from:
    - ipBlock:
        cidr: 172.17.0.0/16
        except:
        - 172.17.1.0/24
    - namespacesSelector:
        matchLabels:
          project: mopoject
    - podSelector:
        matchLabels:
          role: frontend
    ports:
    - protocol: TCP
      port: 6379
  egress:
  - to:
    - ipBlock:
        cidr: 10.0.0.0/24
      ports:
      - protocol: TCP
        port: 5978
  • 如上示例的最终效果如下:
    • 该网络策略作用于Namespace“default”中含有“role=db”Label的全部Pod。
    • 允许与目标Pod在同一个Namespace中的包含“role=frontend”Label的客户端Pod访问目标Pod。
    • 允许属于包含“project=myproject”Label的Namespace的客户端Pod访问目标Pod。
    • 允许从IP地址范围“172.17.0.0/16”的客户端Pod访问目标Pod,但是不包括IP地址范围“172.17.1.0/24”的客户端。
    • 允许目标Pod访问IP地址范围“10.0.0.0/24”并监听5978端口的服务。
  • 注意:关于namespaceSelector和podSelector的说明:在from或to的配置中,namespaceSelector和podSelector可以单独设置,也可以组合配置。如果仅配置podSelector,则表示与目标Pod属于相同的Namespace,而组合设置则可以设置Pod所属的Namespace,例如:
- from:
    - namespacesSelector:
        matchLabels:
          project: mopoject
    - podSelector:
        matchLabels:
          role: frontend

7.2、在Namespace级别设置默认的网络策略

  • 在Namespace级别还可以设置一些默认的全局网络策略,以方便管理员对整个Namespace进行统一的网络策略设置。
  • 默认禁止任何客户端访问该Namespace中的所有Pod:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  • 默认允许任何客户端访问该Namespace中的所有Pod:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-all
spec:
  podSelector: {}
  ingress:
  - {}
  policyTypes:
  - Ingress
  • 默认禁止该Namespace中的所有Pod访问外部服务:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny
spec:
  podSelector: {}
  policyTypes:
  - Egress
  • 默认允许该Namespace中的所有Pod访问外部服务:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-all
spec:
  podSelector:{}
  egress:
  - {}
  policyTypes:
  - Egress
  • 默认禁止任何客户端访问该Namespace中的所有Pod,同时禁止访问外部服务:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  - Egress

8、开源的网络组件

  • Kubernetes的网络模型假定了所有Pod都在一个可以直接连通的扁平网络空间中。这在GCE里面是现成的网络模型,Kubernetes假定这个网络已经存在。而在私有云里搭建Kubernetes集群,就不能假定这种网络已经存在了。我们需要自己实现这个网络假设,将不同节点上的Docker容器之间的互相访问先打通,然后运行Kubernetes。
  • 目前已经有多个开源组件支持容器网络模型。本节介绍几个常见的网络组件及其安装配置方法,包括Flannel、Open vSwitch、直接路由和Calico。

8.1、Flannel

  • Flannel之所以可以搭建Kubernetes依赖的底层网络,是因为它能实现以下两点。
    • (1)它能协助Kubernetes,给每一个Node上的Docker容器都分配互相不冲突的IP地址。
    • (2)它能在这些IP地址之间建立一个覆盖网络(OverlayNetwork),通过这个覆盖网络,将数据包原封不动地传递到目标容器内。
  • 现在,通过图7.20来看看Flannel是如何实现这两点的。

  • 可以看到,Flannel首先创建了一个名为flannel0的网桥,而且这个网桥的一端连接docker0网桥另一端连接一个叫作flanneld的服务进程
  • flanneld进程并不简单,它上连etcd,利用etcd来管理可分配的IP地址段资源,同时监控etcd中每个Pod的实际地址,并在内存中建立了一个Pod节点路由表;它下连docker0和物理网络,使用内存中的Pod节点路由表,将docker0发给它的数据包包装起来,利用物理网络的连接将数据包投递到目标flanneld上,从而完成Pod到Pod之间的直接地址通信。
  • Flannel之间的底层通信协议的可选技术包括UDP、VxLan、AWSVPC等多种方式。通过源flanneld封包、目标flanneld解包,最终docker0收到的就是原始的数据,对容器应用来说是透明的,感觉不到中间Flannel的存在。
  • 我们看一下Flannel是如何做到为不同Node上的Pod分配的IP不产生冲突的。其实想到Flannel使用了集中的etcd存储就很容易理解了。它每次分配的地址段都在同一个公共区域获取,这样大家自然能够互相协调,不产生冲突了。而且在Flannel分配好地址段后,后面的事情是由Docker完成的,Flannel通过修改Docker的启动参数将分配给它的地址段传递进去:
--bip=172.17.18.1/24
  • 通过这些操作,Flannel就控制了每个Node上的docker0地址段的地址,也就保障了所有Pod的IP地址在同一个水平网络中且不产生冲突了。
  • Flannel完美地实现了对Kubernetes网络的支持,但是它引入了多个网络组件,在网络通信时需要转到flannel0网络接口,再转到用户态的flanneld程序,到对端后还需要走这个过程的反过程,所以也会引入一些网络的时延损耗。
  • 另外,Flannel模型默认采用了UDP作为底层传输协议,UDP本身是非可靠协议,虽然两端的TCP实现了可靠传输,但在大流量、高并发的应用场景下还需要反复测试,确保没有问题。

8.2、Calico容器网络

  • Calico是一个基于BGP的纯三层的网络方案,与OpenStack、Kubernetes、AWS、GCE等云平台都能够良好地集成。
    • Calico在每个计算节点都利用Linux Kernel实现了一个高效的vRouter来负责数据转发。每个vRouter都通过BGP1协议把在本节点上运行的容器的路由信息向整个Calico网络广播,并自动设置到达其他节点的路由转发规则。
    • Calico保证所有容器之间的数据流量都是通过IP路由的方式完成互联互通的。
    • Calico节点组网时可以直接利用数据中心的网络结构(L2或者L3),不需要额外的NAT、隧道或者Overlay Network,没有额外的封包解包,能够节约CPU运算,提高网络效率,如图7.24所示。

  • Calico在小规模集群中可以直接互联,在大规模集群中可以通过额外的BGP route reflector来完成,如图7.25所示。

  • 此外,Calico基于iptables还提供了丰富的网络策略,实现了Kubernetes的Network Policy策略,提供容器间网络可达性限制的功能。Calico的系统架构如图7.26所示。

  • Calico的主要组件如下:
    • Felix:Calico Agent,运行在每个Node上,负责为容器设置网络资源(IP地址、路由规则、iptables规则等),保证跨主机容器网络互通。
    • etcd:Calico使用的后端存储。
    • BGP Client:负责把Felix在各Node上设置的路由信息通过BGP协议广播到Calico网络。
    • Route Reflector:通过一个或者多个BGP Route Reflector来完成大规模集群的分级路由分发。
    • CalicoCtl:Calico命令行管理工具。
  • IP Pool可以使用两种模式:BGP或IPIP。
    • 使用IPIP模式时,设置CALICO_IPV4POOL_IPIP="always"
    • 使用BGP模式时,设置CALICO_IPV4POOL_IPIP="off"
  • 启用IPIP模式时,Calico将在各Node上创建一个名为tunl0的虚拟网络接口。

  • BGP模式则直接使用物理机作为虚拟路由器(vRouter),不再创建额外的tunnel。
#                                                                                                                       #
posted @ 2022-06-06 14:00  麦恒  阅读(90)  评论(0编辑  收藏  举报