容器网络模型需要解决容器 IP 地址的管理和容器之间的互相通信。其中容器 IP 地址的管理包括容器 IP 地址的分配与回收,容器之间的相互通信包括同一主机容器之间和跨主机容器之间通信两种场景。本文将基于 Docker 容器网络模型,介绍主流容器网络模型的原理与实现。

Docker 网络模式

docker run 创建 Docker 容器时,可以用 --net 选项指定容器的网络模式,Docker 有以下 5 种网络模式:

网络模式 简介
bridge 为每一个容器分配、设置 IP 等,并将容器连接到一个 docker0 虚拟网桥,默认为该模式。
host 容器将不会虚拟出自己的网卡,配置自己的 IP 等,而是使用宿主机的 IP 和端口。
none 容器有独立的 Network namespace,但并没有对其进行任何网络设置,如分配 veth pair 和网桥连接,IP 等。
container 新创建的容器不会创建自己的网卡和配置自己的 IP,而是和一个指定的容器共享 IP、端口范围等。
overlay 通过 overlay 隧道实现容器网络

bridge network

bridge 桥接网络是 docker 默认的网络模型,如果我们在创建容器的时候不指定网络模型,则默认使用bridge。bridge 网络模型可以解决单宿主机上的容器之间的通信以及容器服务对外的暴露,实现原理也很简单:

可以看到,bridge 网络模型主要依赖于大名鼎鼎的 docker0 网桥以及 veth 虚拟网络设备对实现。关于 bridge 网桥,更多可以参考 bridge

通过之前笔记对于 linux 虚拟网络设备的了解,我们知道 veth 设备对对于从一个设备发出的数据包,会直接出现在另一个网络设备上,即使不在一个 netns 网络命名空间中,所以将 veth 设备对实际上是连接不同 netns 网络命名空间的”网线”,docker0 网桥设备充当不同容器网络的网关。事实上,我们一旦而当以 bridge 网络模式创建容器时,会自动创建相应的 veth 设备对,其中一端连接到 docker0 网桥,另外一端连接到容器网络的 eth0 虚拟网卡。

首先我们在安装了 docker 的宿主机上查看网桥设备 docker0 和路由规则:

1
2
3
4
5
6
# ip link show docker0
4: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default
    link/ether 02:42:59:c8:67:c0 brd ff:ff:ff:ff:ff:ff
# ip route ls
...
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1

然后使用默认的 bridge 网络模式创建一个容器,并查看宿主机端的 veth 设备对:

1
2
3
4
5
6
# docker run -d --name mynginx  nginx:latest
# ip link show type veth
11: veth42772d8@if10: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP mode DEFAULT group default
    link/ether e2:a3:89:76:14:f3 brd ff:ff:ff:ff:ff:ff link-netnsid 0
# bridge link
11: veth42772d8 state UP @if10: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 master docker0 state forwarding priority 32 cost 2

可以看到新的 veth 设备对的一端veth42772d8已经连接到docker0网桥,那么另外一端呢?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# ls /var/run/docker/netns/
62fd67d9ef3e  default
# nsenter --net=/var/run/docker/netns/62fd67d9ef3e ip link show type veth
10: eth0@if11: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
# nsenter --net=/var/run/docker/netns/62fd67d9ef3e ip addr show  type veth
10: eth0@if11: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
       valid_lft forever preferred_lft forever

正如我们设想的那样,veth 设备的另外一端处于新的 netns 网络命名空间62fd67d9ef3e中,并且 IP 地址为172.17.0.2/16,与docker0处于同一子网中。

Note: 如果我们创建了映射到/var/run/docker/netns/的符号链接/var/run/netns,就不用使用nsenter命令或者进入容器内部查看 veth 设备对的另外一端,直接使用如下iproute2工具包

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# ip netns show
62fd67d9ef3e (id: 0)
default
# ip netns exec 62fd67d9ef3e ip link show type veth
10: eth0@if11: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
# ip netns exec 62fd67d9ef3e ip addr show type veth
10: eth0@if11: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
       valid_lft forever preferred_lft forever

模拟一下 bridge 网络模型的实现,基本的网络拓扑图如下所示:

  1. 首先创建两个 netns 网络命名空间:

    1
    2
    3
    4
    5
    6
    
    # ip netns add netns_A
    # ip netns add netns_B
    # ip netns
    netns_B
    netns_A
    default
    
  2. 在 default 网络命名空间中创建网桥设备 mybr0,并分配 IP 地址172.18.0.1/16使其成为对应子网的网关:

    1
    2
    3
    4
    5
    6
    7
    8
    
    # ip link add name mybr0 type bridge
    # ip addr add 172.18.0.1/16 dev mybr0
    # ip link show mybr0
    12: mybr0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/ether ae:93:35:ab:59:2a brd ff:ff:ff:ff:ff:ff
    # ip route
    ...
    172.18.0.0/16 dev mybr0 proto kernel scope link src 172.18.0.1
    
  3. 接下来,创建 veth 设备对并连接在第一步创建的两个 netns 网络命名空间:

    1
    2
    3
    4
    5
    6
    7
    
    # ip link add vethA type veth peer name vethpA
    # ip link show vethA
    14: vethA@vethpA: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether da:f1:fd:19:6b:4a brd ff:ff:ff:ff:ff:ff
    # ip link show vethpA
    13: vethpA@vethA: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 86:d6:16:43:54:9e brd ff:ff:ff:ff:ff:ff
    
  4. 将上一步创建的 veth 设备对的一端vethA连接到mybr0网桥并启动:

    1
    2
    3
    4
    
    # ip link set dev vethA master mybr0
    # ip link set vethA up
    # bridge link
    14: vethA state LOWERLAYERDOWN @vethpA: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 master mybr0 state disabled priority 32 cost 2
    
  5. 将 veth 设备对的另一端vethpA放到 netns 网络命名空间netns_A中并配置 IP 启动:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    # ip link set vethpA netns netns_A
    # ip netns exec netns_A ip link set vethpA name eth0
    # ip netns exec netns_A ip addr add 172.18.0.2/16 dev eth0
    # ip netns exec netns_A ip link set eth0 up
    # ip netns exec netns_A ip addr show type veth
    13: eth0@if14: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 86:d6:16:43:54:9e brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 172.18.0.2/16 scope global eth0
       valid_lft forever preferred_lft forever
    
  6. 现在就可以验证从netns_A网络命名空间中访问mybr0网关:

    1
    2
    3
    4
    5
    6
    7
    8
    
    # ip netns exec netns_A ping -c 2 172.18.0.1
    PING 172.18.0.1 (172.18.0.1) 56(84) bytes of data.
    64 bytes from 172.18.0.1: icmp_seq=1 ttl=64 time=0.096 ms
    64 bytes from 172.18.0.1: icmp_seq=2 ttl=64 time=0.069 ms
    
    --- 172.18.0.1 ping statistics ---
    2 packets transmitted, 2 received, 0% packet loss, time 1004ms
    rtt min/avg/max/mdev = 0.069/0.082/0.096/0.016 m
    
  7. 若想要从从netns_A网络命名空间中非172.18.0.0/16的地址,就需要增加一条默认默认路由:

    1
    2
    3
    4
    
    # ip netns exec netns_A ip route add default via 172.18.0.1
    # ip netns exec netns_A ip route
    default via 172.18.0.1 dev eth0
    172.18.0.0/16 dev eth0 proto kernel scope link src 172.18.0.2
    

Note: 如果你此时尝试去 ping 其他的公网地址,eg. google.com,是 ping 不通的,是因为 ping 的出去的数据包(ICMP 包)的源地址没有做源地址转换(snat),导致 ICMP 包有去无回;Docker 是通过设置iptables实现源地址转换的。

  1. 接下来,按照上述步骤创建连接defaultnetns_B网络命名空间 veth 设备对:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    
    # ip link add vethB type veth peer name vethpB
    # ip link set dev vethB master mybr0
    # ip link set vethB up
    # ip link set vethpB netns netns_B
    # ip netns exec netns_B ip link set vethpB name eth0
    # ip netns exec netns_B ip addr add 172.18.0.3/16 dev eth0
    # ip netns exec netns_B ip link set eth0 up
    # ip netns exec netns_B ip route add default via 172.18.0.1
    # ip netns exec netns_B ip add show eth0
    15: eth0@if16: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 0e:2f:c6:de:fe:24 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 172.18.0.3/16 scope global eth0
       valid_lft forever preferred_lft forever
    # ip netns exec netns_B ip route show
    default via 172.18.0.1 dev eth0
    172.18.0.0/16 dev eth0 proto kernel scope link src 172.18.0.3
    
  2. 默认情况下把 Linux 会把网桥设备 bridge 的FORWORD功能禁用,所以在netns_A里面是 ping 不通netns_B的,需要额外增加一条 iptables 规则:

    1
    
    # iptables -A FORWARD -i mybr0 -j ACCEPT
    
  3. 现在就可以验证两个 netns 网络命名空间之间可以互通:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    
    # ip netns exec netns_A ping -c 2 172.18.0.3
    PING 172.18.0.3 (172.18.0.3) 56(84) bytes of data.
    64 bytes from 172.18.0.3: icmp_seq=1 ttl=64 time=0.091 ms
    64 bytes from 172.18.0.3: icmp_seq=2 ttl=64 time=0.093 ms
    
    --- 172.18.0.3 ping statistics ---
    2 packets transmitted, 2 received, 0% packet loss, time 1027ms
    rtt min/avg/max/mdev = 0.091/0.092/0.093/0.001 ms
    # ip netns exec netns_B ping -c 2 172.18.0.2
    PING 172.18.0.2 (172.18.0.2) 56(84) bytes of data.
    64 bytes from 172.18.0.2: icmp_seq=1 ttl=64 time=0.259 ms
    64 bytes from 172.18.0.2: icmp_seq=2 ttl=64 time=0.078 ms
    
    --- 172.18.0.2 ping statistics ---
    2 packets transmitted, 2 received, 0% packet loss, time 1030ms
    rtt min/avg/max/mdev = 0.078/0.168/0.259/0.091 ms
    

实际上,此时两个 netns 网络命名空间处于同一个子网中,所以网桥设备mybr0还是在二层(数据链路层)起到的作用,只需要对方的 MAC 地址就可以访问。

但是如果需要从两个 netns 网络命名空间访问其他网段的地址,这个时候就需要设置默认网桥设备mybr0充当的默认网关地址就发挥作用了:来自于两个 netns 网络命名空间的数据包发现目标 IP 地址并不是本子网地址,于是发给网关mybr0,此时网桥设备mybr0其实工作在三层(IP 网络层),它收到数据包之后,查看本地路由与目标 IP 地址,寻找下一跳的地址。

当然,如果需要从两个 netns 网络命名空间访问其他公网地址.eg. google.com,需要这是 iptables 来做源地址转换,这里就不细细展开来说。

bridge 网络模型主要用于解决同一主机间的容器相互访问以及容器对外暴露服务的问题,并没有涉及到怎么解决跨主机容器之间互相访问的问题。

host network

  • host 网络模式需要在创建容器时通过参数 --net host 或者 --network host 指定;
  • 采用 host 网络模式的 Docker Container,可以直接使用宿主机的 IP 地址与外界进行通信,若宿主机的 eth0 是一个公有 IP,那么容器也拥有这个公有 IP。同时容器内服务的端口也可以使用宿主机的端口,无需额外进行 NAT 转换;
  • host 网络模式可以让容器共享宿主机网络栈,这样的好处是外部主机与容器直接通信,但是容器的网络缺少隔离性。

该模式将禁用 Docker 容器的网络隔离。因为容器共享了宿主机的网络命名空间,直接暴露在公共网络中。因此,你需要通过端口映射(port mapping)来进行协调。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
$ docker run -it --name box2 --net host busybox
/ # ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue
    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
2: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq qlen 1000
    link/ether 52:54:00:be:23:7a brd ff:ff:ff:ff:ff:ff
    inet 9.134.218.214/22 brd 9.134.219.255 scope global eth1
       valid_lft forever preferred_lft forever
3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue
    link/ether 02:42:23:66:94:56 brd ff:ff:ff:ff:ff:ff
    inet 192.168.10.1/24 brd 192.168.10.255 scope global docker0
       valid_lft forever preferred_lft forever
$ ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue
    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
2: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq qlen 1000
    link/ether 52:54:00:be:23:7a brd ff:ff:ff:ff:ff:ff
    inet 9.134.218.214/22 brd 9.134.219.255 scope global eth1
       valid_lft forever preferred_lft forever
3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue
    link/ether 02:42:23:66:94:56 brd ff:ff:ff:ff:ff:ff
    inet 192.168.10.1/24 brd 192.168.10.255 scope global docker0
       valid_lft forever preferred_lft forever

我们可以从上例中看到:容器和宿主机具有相同的 IP 地址9.134.218.214。 在下图中,我们可以看到:当使用 host 模式网络时,容器实际上继承了宿主机的 IP 地址。该模式比 bridge 模式更快(因为没有路由开销),但是它将容器直接暴露在公共网络中,是有安全隐患的。

host 网络模式下,容器完全复用复用宿主机的网络设备以及协议栈,容器的 IP 就是主机的 IP,这样只要宿主机主机能通信,容器也就自然能通信。

为了暴露容器服务,每个容器需要占用宿主机上的一个端口,通过这个端口和外界通信。所以,就需要手动维护端口的分配,不要使不同的容器服务运行在一个端口上,正因为如此,这种容器网络模型很难被推广到生产环境。

overlay network

解决跨主机通信的可行方案主要是让容器配置与宿主机不一样的 IP 地址,往往是在现有二层或三层网络之上再构建起来一个独立的 overlay 网络,这个网络通常会有自己独立的 IP 地址空间、交换或者路由的实现。但是由于容器有自己独立配置的 IP 地址,underlay 平面的底层网络设备如交换机、路由器等完全不感知这些 IP 的存在,也就导致容器的 IP 不能直接路由出去实现跨主机通信。

为了解决容器独立 IP 地址间的访问问题,主要有以下两个思路:

  1. 修改底层网络设备配置,加入容器网络 IP 地址的管理,修改路由器网关等,该方式主要和 SDN 结合
  • 采用这种方案的比如有 Calico BGP
  1. 完全不修改底层网络设备配置,复用原有的 underlay 平面网络,解决容器跨主机通信,主要有如下两种方式:
    • 隧道传输(Overlay): 将容器的数据包封装到原主机网络的三层或者四层数据包中,然后使用主机网络的 IP 或者 TCP/UDP 传输到目标主机,目标主机拆包后再转发给目标容器。Overlay 隧道传输常见方案包括 VxLAN、IPIP 等,目前使用 Overlay 隧道传输技术的主流容器网络有 Flannel 等。
    • 修改主机路由:把容器网络加到主机路由表中,把主机网络设备当作容器网关,通过路由规则转发到指定的主机,实现容器的三层互通。目前通过路由技术实现容器跨主机通信的网络如 Flannel host-gw、Calico 等。

Docker 原生支持 overlay 网络来解决容器间的跨主机通信问题,事实上,对于 Docker 原生支持的 overlay 网络,Laurent Bernaille 在 DockerCon 2017 上详细剖析了它的实现原理,甚至还有从头开始一步步实现 Docker 的 overlay 网络的实践教程 123

在这里我就只是大致介绍一下 Docker 原生支持的 overlay 网络模型的大致原理:

从上面的网络模型图可以看出,对于 docker 原生的 overlay 网络来说,处理容器对外访问的南北流量个容器之间相互访问的东西流量分别使用不同的 Linux 网络设备:

  • 南北流量:类似于 bridge 网络模型,通过主机的网桥设备充当网关,然后使用 veth 设备对分别连接主机网桥和容器内网卡设备,最后通过主机网卡发送接收对外的数据包,需要注意的是,对外数据包需要做地址转化 nat
  • 东西流量:另外在主机上单独增加一个网桥设备,然后使用 veth 设备对分别连接主机网桥和容器内网卡设备,同时主机内网桥设备还绑定了 vxlan 设备,vxlan 设备将跨主机的容器数据包封装成 vxlan 数据包发送到目标主机,然后解封装后转发给对应的容器。

需要注意的是,虽然跨主机的两个容器是通过 Overlay 通信的,但容器自己不能感知,因为它们只认为彼此都在一个子网中,只需要知道对方的 MAC 地址,可以通过 ARP 协议广播学习获取 IP 与 MAC 地址转换。当然通过 VXLAN 隧道广播 ARP 包理论上也没有问题,问题是该方案将导致广播包过多,广播的成本会很大。

Docker 给出的方案是通过ARP 代理+静态配置解决 ARP 广播问题,容器的地址信息保存到到 KV 数据库 etcd 中。这样就可以通过静态配置的方式填充 IP 和 MAC 地址表(neigh 表)替换使用 ARP 广播的方式,所以 vxlan 设备还负责本地容器的 ARP 代理:

1
2
3
4
# ip link show vxlan0 | grep proxy_arp
# ip neigh
192.168.0.103 dev vxlan0 lladdr 02:42:0a:14:00:03 PERMANENT
192.168.0.104 dev vxlan0 lladdr 02:42:0a:14:00:04 PERMANENT

上面 neign 信息中的PERMANENT代表静态配置而不是通过学习获取的,而192.168.0.103192.168.0.104是另外两个容器的 IP 地址。每当有新的容器创建时,Docker 通过通知节点更新本地 neigh ARP 表。

另外,容器之间的数据包最终还是通过 VXLAN 隧道传输的,因此需要知道数据包的目标容器在哪个 Node 节点。当 Node 数量达到一定数量级之后,如果采用和 ARP 一样的广播洪泛的方式学习,那么显然同样存在性能问题,实际上也很少使用这种方案,在硬件 SDN 中通常使用 BGP EVPN 技术实现 VXLAN 的控制平面,而 Docker 解决的办法和 ARP 类似,通过静态配置的方式填充 VTEP(VXLAN Tunnel Endpoint)表,我们可以查看容器网络 namespace 的转发表(Forward database,简称 fdb):

1
2
3
4
# bridge fdb
...
82:fa:1d:48:14:04 dev vxlan0 dst 10.0.0.10 link-netnsid 0 self permanent
82:fa:1d:48:14:05 dev vxlan0 dst 10.0.0.11 link-netnsid 0 self permanent

上面的转发表信息表示 MAC 地址82:fa:1d:48:14:04的对端 VTEP 地址为10.0.0.10,而82:fa:1d:48:14:04的对端 VTEP 地址为10.0.0.10,permanent 说明这两条转发表记录都是静态配置的,而这些数据来源依然是 KV 数据库 etcd,这些 VTEP 地址为容器所在的主机的 IP 地址。


Docker 容器网络模型中,我介绍了各种容器网络的实现原理,然而真正将容器技术发扬光大的是 Kubernetes 容器编排平台。Kubernetes 通过整合规模庞大的容器实例形成集群,这些容器实例可能运行在异构的底层网络环境中,如何保证这些容器间的互通是实际生产环境中首要考虑的问题之一。本文将介绍在 k8s 环境下,网络模型的基本原理和主流 k8s 容器网络方案。

none network

  • none 网络模式是指禁用网络功能,只有 lo 接口 local 的简写,代表 127.0.0.1,即 localhost 本地环回接口。在创建容器时通过参数 --net none 或者 --network none 指定;
  • None 网络模式即不为 Docker Container 创建任何的网络环境,容器内部就只能使用 loopback 网络设备,不会再有其他的网络资源。可以说 none 模式为 Docke Container 做了极少的网络设定,但是俗话说得好“少即是多”,在没有网络配置的情况下,作为 Docker 开发者,才能在这基础做其他无限多可能的网络定制开发。这也恰巧体现了 Docker 设计理念的开放。
1
2
3
4
5
6
$ docker run -it --name box5 --net none busybox
/ # ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue
    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

macvlan network

CNM vs CNI

关于容器网络,Docker 与 Kubernetes 分别提出了不同的规范标准:

  • Docker 采用的 CNM 模型
  • Kunernetes 支持的 CNI 模型

K8s 网络要求

Kubernetes 对容器技术做了更多的抽象,其中最重要的一点是提出 Pod 的概念,Pod 是 Kubernetes 资源调度的基本单元,我们可以简单地认为 Pod 是容器的一种延伸扩展,从网络的角度来看,Pod 必须满足以下条件:

  • 每一个 Pod 都有一个独特的 IP 地址,所有 Pod 都在一个可以直接连通的、扁平的网络空间中
  • 同一个 Pod 内的所有容器共享同一个 netns 网络命名空间

基于这样的基本要求,我们可以知道:

  • 同一个 Pod 内的所有容器之间共享端口,可直接通过 localhost+端口来访问
  • 由于每个 Pod 有单独的 IP,所以不需要考虑容器端口与主机端口映射以及端口冲突问题

事实上,Kubernetes 进一步确定了对一个合格集群网络的基本要求:

  • 任意两个 Pod 之间其实是可以直接通信的,无需显式地使用 NAT 进行地址的转换
  • 任意集群节点 node 与任意 Pod 之间是可以直接通信的,无需使用明显的地址转换,反之亦然
  • 任意 Pod 看到自己的 IP 跟别人看见它所用的 IP 是一样的,中间不能经过地址转换

也就是说,必须同时满足以上三点的网络模型才能适用于 kubernetes,事实上,在早期的 Kubernetes 中,并没有什么网络标准,只是提出了以上基本要求,只有满足这些要求的网络才可以部署 Kubernetes,基于这样的底层网络假设,Kubernetes 设计了 Pod-Deployment-Service的经典三层服务访问机制。直到 1.1 发布,Kubernetes 才开始采用全新的 CNI 网络标准。

Pod 网络模型

要了解 kubernetes 网络模型的实现原理,我们就要从单个 Pod 入手,事实上,一旦熟悉了单个 Pod 的网络模型,就会发现 kubernetes 网络模型基本遵循和容器网络模型一样的原理。

Pod 启动的时候先创建 pause容器生成对应的 netns 网络命名空间,然后其他容器共享 pause容器创建的网络命名空间。而对于单个容器的网络模型我们之前也介绍过,主要就是通过 docker0网桥设备与 veth 设备对连接不同的容器网络命名空间,由此,我们可以得到如下图所示的单个 Pod 网络模型的创建过程:

可以看到,同一个 Pod 里面的其他容器共享 pause容器创建的网络命名空间,也就是说,所有的容器共享相同的网络设备,路由表设置,服务端口等信息,仿佛是在同一台机器上运行的不同进程,所以这些容器之间可以直接通过 localhost与对应的端口通信;对于集群外部的请求,则通过 docker0网桥设备充当的网关,同时通过 iptables 做地址转换。我们会发现,这其实就是对当个容器的 bridge 网络模型的扩展。

主流网络方案

上一小节我们知道单个 Pod 的网络模型是容器网络模型的扩展,但是 Pod 与 Pod 之间的是怎么相互通信的呢?这其实与容器之间相互通信非常类似,也分为同一个主机上的 Pod 之间与跨主机的 Pod 之间两种。

如容器网络模型一样,对于统一主机上的 Pod 之间,通过 docker0网桥设备直接二层(数据链路层)网络上通过 MAC 地址直接通信:

而跨主机的 Pod 之间的相互通信也主要有以下两个思路:

  1. 修改底层网络设备配置,加入容器网络 IP 地址的管理,修改路由器网关等,该方式主要和 SDN(Software define networking)结合。
  2. 完全不修改底层网络设备配置,复用原有的 underlay 平面网络,解决容器跨主机通信,主要有如下两种方式:
  • 隧道传输(Overlay): 将容器的数据包封装到原主机网络的三层或者四层数据包中,然后使用主机网络的 IP 或者 TCP/UDP 传输到目标主机,目标主机拆包后再转发给目标容器。Overlay 隧道传输常见方案包括 Vxlan、ipip 等,目前使用 Overlay 隧道传输技术的主流容器网络有 Flannel 等;

  • 修改主机路由:把容器网络加到主机路由表中,把主机网络设备当作容器网关,通过路由规则转发到指定的主机,实现容器的三层互通。目前通过路由技术实现容器跨主机通信的网络如 Flannel host-gw、Calico 等;

参考资料