浅聊几种主流 Docker 网络的实现原理

一、容器网络简介

容器网络主要解决两大核心问题:一是容器的 IP 地址分配,二是容器之间的相互通信。本文重在研究第二个问题并且主要研究容器的跨主机通信问题。

实现容器跨主机通信的最简单方式就是直接使用 host 网络,这时由于容器 IP 就是宿主机的 IP,复用宿主机的网络协议栈以及 underlay 网络,原来的主机能通信,容器也就自然能通信,然而带来的最直接问题就是端口冲突问题。

因此通常容器会配置与宿主机不一样的属于自己的 IP 地址。由于是容器自己配置的 IP,underlay 平面的底层网络设备如交换机、路由器等完全不感知这些 IP 的存在,也就导致容器的 IP 不能直接路由出去实现跨主机通信。

要解决如上问题实现容器跨主机通信,主要有如下两个思路:

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

本文接下来将详细介绍目前主流容器网络的实现原理。

在开始正文内容之前,先引入两个后续会一直使用的脚本:

第一个脚本为 docker_netns.sh :

复制代码

#!/bin/bash

NAMESPACE=$1

if[[ -z$NAMESPACE]];then
ls -1 /var/run/docker/netns/
exit0
fi

NAMESPACE_FILE=/var/run/docker/netns/${NAMESPACE}

if[[ ! -f$NAMESPACE_FILE]];then
NAMESPACE_FILE=$(docker inspect -f"{{.NetworkSettings.SandboxKey}}"$NAMESPACE2>/dev/null)
fi

if[[ ! -f$NAMESPACE_FILE]];then
echo"Cannot open network namespace '$NAMESPACE': No such file or directory"
exit1
fi

shift

if[[$#-lt 1 ]];then
echo"No command specified"
exit1
fi

nsenter --net=${NAMESPACE_FILE}$@

该脚本通过指定容器 id、name 或者 namespace 快速进入容器的 network namespace 并执行相应的 shell 命令。

如果不指定任何参数,则列举所有 Docker 容器相关的 network namespaces。

复制代码

# ./docker_netns.sh # list namespaces
4-a4a048ac67
abe31dbbc394
default

# ./docker_netns.sh busybox ip addr # Enter busybox namespace
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWNgroup defaultqlen 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
354: eth0@if355: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UPgroup default
link/ether 02:42:c0:a8:64:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 192.168.100.2/24 brd 192.168.100.255 scope global eth0
valid_lft forever preferred_lft forever
356: eth1@if357: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UPgroup default
link/ether 02:42:ac:12:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 1
inet 172.18.0.2/16 brd 172.18.255.255 scope global eth1
valid_lft forever preferred_lft forever

另一个脚本为 find_links.sh

复制代码

#!/bin/bash

DOCKER_NETNS_SCRIPT=./docker_netns.sh
IFINDEX=$1
if[[ -z$IFINDEX]]; then
fornamespacein$($DOCKER_NETNS_SCRIPT);do
printf"\e[1;31m%s: \e[0m\n"$namespace
$DOCKER_NETNS_SCRIPT$namespaceip-c -o link
printf"\n"
done
else
fornamespacein$($DOCKER_NETNS_SCRIPT);do
if$DOCKER_NETNS_SCRIPT$namespaceip-c -o link | grep -Pq"^$IFINDEX: "; then
printf"\e[1;31m%s: \e[0m\n"$namespace
$DOCKER_NETNS_SCRIPT$namespaceip-c -o link | grep -P"^$IFINDEX: ";
printf"\n"
fi
done
fi

该脚本根据 ifindex 查找虚拟网络设备所在的 namespace:

复制代码

# ./find_links.sh 354
abe31dbbc394:
354: eth0@if355: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP modeDEFAULT group default
link/ether 02:42:c0:a8:64:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0

该脚本的目的是方便查找 veth 的对端所在的 namespace 位置。如果不指定 ifindex,则列出所有 namespaces 的 link 设备。

二、Docker 原生的 Overlay

Laurent Bernaille 在 DockerCon2017 上详细介绍了 Docker 原生的 Overlay 网络实现原理,作者还总结了三篇干货文章一步一步剖析 Docker 网络实现原理,最后还教大家一步一步从头开始手动实现 Docker 的 Overlay 网络,这三篇文章为:

  • Deep dive into docker overlay networks part 1
  • Deep dive into docker overlay networks part 2
  • Deep dive into docker overlay networks part 3

建议感兴趣的读者阅读,本节也大量参考了如上三篇文章的内容。

2.1 Overlay 网络环境

测试使用两个 Node 节点:

Node 名 主机 IP
node-1 192.168.1.68
node-2 192.168.1.254

首先创建一个 overlay 网络:

复制代码

dockernetworkcreate -d overlay --subnet 10.20.0.0/16 overlay

在两个节点分别创建两个 busybox 容器:

复制代码

dockerrun-d--name busybox --netoverlay busyboxsleep36000

容器列表如下:

Node 名 主机 IP 容器 IP
node-1 192.168.1.68 10.20.0.3/16
node-2 192.168.1.254 10.20.0.2/16

我们发现容器有两个 IP,其中 eth0 10.20.0.0/16 为我们创建的 Overlay 网络 ip,两个容器能够互相 ping 通。而不在同一个 node 的容器 IP eth1 都是 172.18.0.2,因此 172.18.0.0/16 很显然不能用于跨主机通信,只能用于单个节点容器通信。

2.2 容器南北流量

这里的南北流量主要是指容器与外部通信的流量,比如容器访问互联网。

我们查看容器的路由:

复制代码

# docker exec busybox-node-1ip r
defaultvia172.18.0.1dev eth1
10.20.0.0/16dev eth0 scope link src10.20.0.3
172.18.0.0/16dev eth1 scope link src172.18.0.2

由此可知容器默认网关为 172.18.0.1,也就是说容器是通过 eth1 出去的:

复制代码

# docker exec busybox-node-1ip link show eth1
77:eth1@if78: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu1500qdisc noqueue
link/ether02:42:ac:12:00:02brd ff:ff:ff:ff:ff:ff
# ./find_links.sh78
default:
78:vethf2de5d4@if77: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu1500qdisc noqueue master docker_gwbridge state UP mode DEFAULT groupdefault
link/ether2e:6a:94:6a:09:c5 brd ff:ff:ff:ff:ff:ff link-netnsid1

通过 find_links.sh 脚本查找 ifindex 为 78 的 link 在默认 namespace 中,并且该 link 的 master 为 docker_gwbridge ,也就是说该设备挂到了 docker_gwbridge bridge。

复制代码

# brctl show
bridge namebridgeid STP enabled interfaces
docker0 8000.02427406ba1ano
docker_gwbridge 8000.0242bb868ca3novethf2de5d4

172.18.0.1 正是 bridge docker_gwbridge 的 IP,也就是说 docker_gwbridge 是该节点的所有容器的网关。

由于容器的 IP 是 172.18.0.0/16 私有 IP 地址段,不能出公网,因此必然通过 NAT 实现容器 IP 与主机 IP 地址转换,查看 iptables nat 表如下:

复制代码

# iptables-save -t nat | grep --'-A POSTROUTING'
-A POSTROUTING -s172.18.0.0/16! -o docker_gwbridge -j MASQUERADE

由此可验证容器是通过 NAT 出去的。

我们发现其实容器南北流量用的其实就是 Docker 最原生的 bridge 网络模型,只是把 docker0 换成了 docker_gwbridge 。如果容器不需要出互联网,创建 Overlay 网络时可以指定 --internal 参数,此时容器只有一个 Overlay 网络的网卡,不会创建 eth1。

2.3 容器东西向流量

容器东西流量指容器之间的通信,这里特指跨主机的容器间通信。

显然容器是通过 eth0 实现与其他容器通信的:

复制代码

# docker exec busybox-node-1ip link show eth0
75:eth0@if76: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu1450qdisc noqueue
link/ether02:42:0a:14:00:03brd ff:ff:ff:ff:ff:ff

# ./find_links.sh76
1-19c5d1a7ef:
76:veth0@if75: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu1450qdisc noqueue master br0 state UP mode DEFAULT groupdefault\ link/ether6a:ce:89:a2:89:4a brd ff:ff:ff:ff:ff:ff link-netnsid1

eth0 的对端设备 ifindex 为 76,通过 find_links.sh 脚本查找 ifindex 76 在 1-19c5d1a7ef namespace 下,名称为 veth0 ,并且 master 为 br0,因此 veth0 挂到了 br0 bridge 下。

通过 docker_netns.sh 脚本可以快速进入指定的 namespace 执行命令:

复制代码

# ./docker_netns.sh 1-19c5d1a7ef ip link show veth0
76: veth0@if75: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master br0 state UP modeDEFAULT group default
link/ether 6a:ce:89:a2:89:4a brd ff:ff:ff:ff:ff:ff link-netnsid 1

# ./docker_netns.sh 1-19c5d1a7ef brctl show
bridge namebridgeid STP enabled interfaces
br0 8000.6ace89a2894anoveth0
vxlan0

可见除了 veth0,bridge 还绑定了 vxlan0:

复制代码

./docker_netns.sh 1-19c5d1a7efip-c -d link show vxlan0
74: vxlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master br0 state UNKNOWN modeDEFAULT group default
link/ether 96:9d:64:39:76:4e brd ff:ff:ff:ff:ff:ff link-netnsid 0 promiscuity 1
vxlan id 256 srcport 0 0 dstport 4789proxyl2miss l3miss ttl inherit ageing 300 udpcsum noudp6zerocsumtx noudp6zerocsumrx
...

vxlan0 是一个 VxLan 虚拟网络设备,因此可以推断 Docker Overlay 是通过 vxlan 隧道实现跨主机通信的。这里直接引用 Deep dive into docker overlay networks part 1 的图:

图中 192.168.0.0/16 对应前面的 10.20.0.0/16 网段。

2.4 ARP 代理

如前面所述,跨主机的两个容器虽然是通过 Overlay 通信的,但容器自己却不知道,他们只认为彼此都在一个二层中(同一个子网),或者说大二层。我们知道二层是通过 MAC 地址识别对方的,通过 ARP 协议广播学习获取 IP 与 MAC 地址转换。当然通过 Vxlan 隧道广播 ARP 包理论上也没有问题,问题是该方案将导致广播包过多,广播的成本会很大。

和 OpenStack Neutron 的 L2 Population 原理一样,Docker 也是通过 ARP 代理 + 静态配置解决 ARP 广播问题。我们知道,虽然 Linux 底层除了通过自学习方式外无法知道目标 IP 的 MAC 地址是什么,但是应用却很容易获取这些信息,比如 Neutron 的数据库中就保存着 Port 信息,Port 中就有 IP 和 MAC 地址。Docker 也一样会把 endpoint 信息保存到 KV 数据库中,如 etcd:

有了这些数据完全可以实现通过静态配置的方式填充 IP 和 MAC 地址表(neigh 表) 替换使用 ARP 广播的方式。因此 vxlan0 还负责了本地容器的 ARP 代理:

复制代码

./docker_netns.sh 2-19c5d1a7efip-d -o link show vxlan0 | grep proxy_arp

而 vxlan0 代理回复时直接查找本地的 neigh 表回复即可,而本地 neigh 表则是 Docker 静态配置,可查看 Overlay 网络 namespaced neigh 表:

复制代码

# ./docker_netns.sh3-19c5d1a7ef ip neigh
10.20.0.3dev vxlan0 lladdr02:42:0a:14:00:03PERMANENT
10.20.0.4dev vxlan0 lladdr02:42:0a:14:00:04PERMANENT

记录中的 PERMANENT 说明是静态配置而不是通过学习获取的,IP 10.20.0.3、10.20.0.4 正是另外两个容器的 IP 地址。

每当有新的容器创建时,Docker 通过 Serf 以及 Gossip 协议通知节点更新本地 neigh ARP 表。

2.5 VTEP 表静态配置

前面介绍的 ARP 代理属于 L2 层问题,而容器的数据包最终还是通过 Vxlan 隧道传输的,那自然需要解决的问题是这个数据包应该传输到哪个 node 节点?如果只是两个节点,创建 vxlan 隧道时可以指定本地 ip(local IP) 和对端 IP(remote IP) 建立点对点通信,但实际上显然不可能只有两个节点。

我们不妨把 Vxlan 出去的物理网卡称为 VTEP(VXLAN Tunnel Endpoint),它会有一个可路由的 IP,即 Vxlan 最终封装后的外层 IP。通过查找 VTEP 表决定数据包应该传输到哪个 remote VTEP:

容器 MAC 地址 Vxlan ID Remote VTEP
02:42:0a:14:00:03 256 192.168.1.254
02:42:0a:14:00:04 256 192.168.1.245

VTEP 表和 ARP 表类似,也可以通过广播洪泛的方式学习,但显然同样存在性能问题,实际上也很少使用这种方案。在硬件 SDN 中通常使用 BGP EVPN 技术实现 Vxlan 的控制平面。

而 Docker 解决的办法和 ARP 类似,通过静态配置的方式填充 VTEP 表,我们可以查看容器网络 namespace 的转发表 (Forward database,简称 fdb),

复制代码

./docker_netns.sh3-19c5d1a7ef bridge fdb
...
02:42:0a:14:00:04dev vxlan0 dst192.168.1.245link-netnsid0self permanent
02:42:0a:14:00:03dev vxlan0 dst192.168.1.254link-netnsid0self permanent
...

可见 MAC 地址 02:42:0a:14:00:04 的对端 VTEP 地址为 192.168.1.245, 而 02:42:0a:14:00:03 的对端 VTEP 地址为 192.168.1.254, 两条记录都是 permanent ,即静态配置的,而这些数据来源依然是 KV 数据库,endpoint 中 locator 即为容器的 node IP。

2.6 总结

容器使用 Docker 原生 Overlay 网络默认会创建两张虚拟网卡,其中一张网卡通过 bridge 以及 NAT 出容器外部,即负责南北流量。另一张网卡通过 Vxlan 实现跨主机容器通信,为了减少广播,Docker 通过读取 KV 数据静态配置 ARP 表和 FDB 表,容器创建或者删除等事件会通过 Serf 以及 Gossip 协议通知 Node 更新 ARP 表和 FDB 表。

三、和 Docker Overlay 差不多的 Weave

weave 是 weaveworks 公司提供的容器网络方案,实现上和 Docker 原生 Overlay 网络有点类似。

初始化三个节点 192.168.1.68、192.168.1.254、192.168.1.245 如下:

复制代码

weave launch --ipalloc-range172.111.222.0/24192.168.1.68192.168.1.254192.168.1.245

分别在三个节点启动容器:

复制代码

# node-1
docker run -d --name busybox-node-1--net weave busybox sleep3600
# node-2
docker run -d --name busybox-node-2--net weave busybox sleep3600
# node-3
docker run -d --name busybox-node-3--net weave busybox sleep3600

在容器中我们相互 ping:

从结果发现,Weave 实现了跨主机容器通信,另外我们容器有两个虚拟网卡,一个是 Docker 原生的桥接网卡 eth0,用于南北通信,另一个是 Weave 附加的虚拟网卡 ethwe0,用于容器跨主机通信。

另外查看容器的路由:

复制代码

# docker exec -t -i busybox-node-$NODE ip r
defaultvia172.18.0.1dev eth0
172.18.0.0/16dev eth0 scope link src172.18.0.2
172.111.222.0/24dev ethwe0 scope link src172.111.222.128
224.0.0.0/4dev ethwe0 scope link

其中 224.0.0.0/4 是一个组播地址,可见 Weave 是支持组播的,参考 Container Multicast Networking: Docker & Kubernetes | Weaveworks.

我们只看第一个容器的 ethwe0,VETH 对端 ifindex 为 14:

复制代码

# ./find_links.sh 14
default:
14: vethwl816281577@if13: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1376 qdisc noqueue
master weave state UP modeDEFAULT group default
link/ether de:12:50:59:f0:d9 brd ff:ff:ff:ff:ff:ff link-netnsid 0

可见 ethwe0 的对端在 default namespace 下,名称为 vethwl816281577 ,该虚拟网卡桥接到 weave bridge 下:

复制代码

# brctl show weave
bridge namebridgeid STP enabled interfaces
weave 8000.d2939d07704bnovethwe-bridge
vethwl816281577

weave bridge 下除了有 vethwl816281577 ,还有 vethwe-bridge :

复制代码

# ip link show vethwe-bridge
9: vethwe-bridge@vethwe-datapath: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1376 qdisc noqueue
master weave state UP modeDEFAULT group default
link/ether 0e:ee:97:bd:f6:25 brd ff:ff:ff:ff:ff:ff

可见 vethwe-bridgevethwe-datapath 是一个 VETH 对,我们查看对端 vethwe-datapath :

复制代码

# ip -d link show vethwe-datapath
8: vethwe-datapath@vethwe-bridge: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu1376qdisc noqueue
master datapath state UP mode DEFAULT groupdefault
link/ether f6:74:e9:0b:30:6d brd ff:ff:ff:ff:ff:ff promiscuity1
veth
openvswitch_slave addrgenmode eui64 numtxqueues1numrxqueues1gso_max_size65536gso_max_segs65535

vethwe-datapath 的 master 为 datapath ,由 openvswitch_slave 可知 datapath 应该是一个 openvswitch bridge,而 vethwe-datapath 挂到了 datapath 桥下,作为 datapath 的 port。

为了验证,通过 ovs-vsctl 查看:

复制代码

# ovs-vsctl show
96548648-a6df-4182-98da-541229ef7b63
ovs_version:"2.9.2"

使用 ovs-vsctl 发现并没有 datapath 这个桥。官方文档中 fastdp how it works 中解释为了提高网络性能,没有使用用户态的 OVS,而是直接操纵内核的 datapath。使用 ovs-dpctl 命令可以查看内核 datapath:

复制代码

# ovs-dpctl show
system@datapath:
lookups: hit:109 missed:1508 lost:3
flows: 1
masks: hit:1377 total:1 hit/pkt:0.85
port0: datapath (internal)
port1: vethwe-datapath
port2: vxlan-6784 (vxlan:packet_type=ptap)

可见 datapath 类似于一个 OVS bridge 设备,负责数据交换,该设备包含三个 port:

  • port 0: datapath (internal)
  • port 1: vethwe-datapath
  • port 2: vxlan-6784

除了 vethwe-datapath ,还有一个 vxlan-6784 ,由名字可知这是一个 vxlan:

复制代码

# ip -d link show vxlan-6784
10: vxlan-6784: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu65535qdisc noqueue
master datapath state UNKNOWN mode DEFAULT groupdefaultqlen1000
link/ether d2:21:db:c1:9b:28brd ff:ff:ff:ff:ff:ff promiscuity1
vxlan id0srcport00dstport6784nolearning ttl inherit ageing300udpcsum noudp6zerocsumtx udp6zerocsumrxexternal
openvswitch_slave addrgenmode eui64 numtxqueues1numrxqueues1gso_max_size65536gso_max_segs65535

最后 Weave 的网络流量图如下:

四、简单优雅的 Flannel

4.1 Flannel 简介

Flannel 网络是目前最主流的容器网络之一,同时支持 overlay(如 vxlan)和路由 (如 host-gw)两种模式。

Flannel 和 Weave 以及 Docker 原生 overlay 网络不同的是,后者的所有 Node 节点共享一个子网,而 Flannel 初始化时通常指定一个 16 位的网络,然后每个 Node 单独分配一个独立的 24 位子网。由于 Node 都在不同的子网,跨节点通信本质为三层通信,也就不存在二层的 ARP 广播问题了。

另外,我认为 Flannel 之所以被认为非常简单优雅的是,不像 Weave 以及 Docker Overlay 网络需要在容器内部再增加一个网卡专门用于 Overlay 网络的通信,Flannel 使用的就是 Docker 最原生的 bridge 网络,除了需要为每个 Node 配置 subnet(bip) 外,几乎不改变原有的 Docker 网络模型。

4.2 Flannel Overlay 网络

我们首先以 Flannel Overlay 网络模型为例,三个节点的 IP 以及 Flannel 分配的子网如下:

Node 名 主机 IP 分配的子网
node-1 192.168.1.68 40.15.43.0/24
node-2 192.168.1.254 40.15.26.0/24
node-3 192.168.1.245 40.15.56.0/24

在三个集成了 Flannel 网络的 Node 环境下分别创建一个 busybox 容器:

复制代码

dockerrun-d--name busybox busybox:latest sleep 36000

容器列表如下:

Node 名 主机 IP 容器 IP
node-1 192.168.1.68 40.15.43.2/24
node-2 192.168.1.254 40.15.26.2/24
node-3 192.168.1.245 40.15.56.2/24

查看容器 namespace 的网络设备:

复制代码

# ./docker_netns.sh busybox ip -d -c link
416:eth0@if417: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu8951qdisc noqueue state UP mode DEFAULT groupdefault
link/ether02:42:28:0f:2b:02brd ff:ff:ff:ff:ff:ff link-netnsid0promiscuity0
veth addrgenmode eui64 numtxqueues1numrxqueues1gso_max_size65536gso_max_segs65535

和 Docker bridge 网络一样只有一张网卡 eth0,eth0 为 veth 设备,对端的 ifindex 为 417.

我们查找下 ifindex 417 的 link 信息:

复制代码

# ./find_links.sh 417
default:
417: veth1cfe340@if416: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 8951 qdisc noqueue master docker0 state UP modeDEFAULT group default
link/ether 26:bd:de:86:21:78 brd ff:ff:ff:ff:ff:ff link-netnsid 0

可见 ifindex 417 在 default namespace 下,名称为 veth1cfe340 并且 master 为 docker0,因此挂到了 docker0 的 bridge 下。

复制代码

# brctl show
bridge namebridgeid STP enabled interfaces
docker0 8000.0242d6f8613enoveth1cfe340
vethd1fae9d
docker_gwbridge 8000.024257f32054no

和 Docker 原生的 bridge 网络没什么区别,那它是怎么解决跨主机通信的呢?

实现跨主机通信,要么 Overlay 隧道封装,要么静态路由,显然 docker0 没有看出有什么 overlay 的痕迹,因此只能通过路由实现了。

不妨查看下本地路由如下:

复制代码

# ip r
defaultvia192.168.1.1dev eth0 proto dhcp src192.168.1.68metric100
40.15.26.0/24via40.15.26.0dev flannel.1onlink
40.15.43.0/24dev docker0 proto kernel scope link src40.15.43.1
40.15.56.0/24via40.15.56.0dev flannel.1onlink
...

我们只关心 40.15 开头的路由,忽略其他路由,我们发现除了 40.15.43.0/24 直接通过 docker0 直连外,其他均路由转发到了 flannel.1 。而 40.15.43.0/24 为本地 Node 的子网,因此在同一宿主机的容器直接通过 docker0 通信即可。

我们查看 flannel.1 的设备类型:

复制代码

413: flannel.1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu8951qdisc noqueue state UNKNOWN mode DEFAULT groupdefault
link/ether0e:08:23:57:14:9a brd ff:ff:ff:ff:ff:ff promiscuity0
vxlan id1local192.168.1.68dev eth0 srcport00dstport8472nolearning ttl inherit ageing300
udpcsum noudp6zerocsumtx noudp6zerocsumrx addrgenmode eui64 numtxqueues1numrxqueues1gso_max_size65536gso_max_segs65535

可见 flannel.1 是一个 Linux Vxlan 设备,其中 .1 为 VNI 值,不指定默认为 1。

由于不涉及 ARP 因此不需要 proxy 参数实现 ARP 代理,而本节点的容器通信由于在一个子网内,因此直接 ARP 自己学习即可,不需要 Vxlan 设备学习,因此有个 nolearning 参数。

flannel.1 如何知道对端 VTEP 地址呢?我们依然查看下转发表 fdb:

复制代码

bridge fdb | grep flannel.1
4e:55:ee:0a:90:38dev flannel.1dst192.168.1.245self permanent
da:17:1b:07:d3:70dev flannel.1dst192.168.1.254self permanent

其中 192.168.1.245、192.168.1.254 正好是另外两个 Node 的 IP,即 VTEP 地址,而 4e:55:ee:0a:90:38 以及 da:17:1b:07:d3:70 为对端的 flannel.1 设备的 MAC 地址,由于是 permanent 表,因此可推测是由 flannel 静态添加的,而这些信息显然可以从 etcd 获取:

复制代码

# for subnetin$(etcdctlls/coreos.com/network/subnets);doetcdctl get $subnet;done
{"PublicIP":"192.168.1.68","BackendType":"vxlan","BackendData":{"VtepMAC":"0e:08:23:57:14:9a"}}
{"PublicIP":"192.168.1.254","BackendType":"vxlan","BackendData":{"VtepMAC":"da:17:1b:07:d3:70"}}
{"PublicIP":"192.168.1.245","BackendType":"vxlan","BackendData":{"VtepMAC":"4e:55:ee:0a:90:38"}}

因此 Flannel 的 Overlay 网络实现原理简化如图:

可见除了增加或者减少 Node,需要 Flannel 配合配置静态路由以及 fdb 表,容器的创建与删除完全不需要 Flannel 干预,事实上 Flannel 也不需要知道有没有新的容器创建或者删除。

4.3 Flannel host-gw 网络

前面介绍 Flannel 通过 Vxlan 实现跨主机通信,其实 Flannel 支持不同的 backend,其中指定 backend type 为 host-gw 支持通过静态路由的方式实现容器跨主机通信,这时每个 Node 都相当于一个路由器,作为容器的网关,负责容器的路由转发。

需要注意的是,如果使用 AWS EC2,使用 Flannel host-gw 网络需要禁用 MAC 地址欺骗功能,如图:

使用 OpenStack 则最好禁用 Neutron 的 port security 功能。

同样地,我们在三个节点分别创建 busybox 容器,结果如下:

Node 名 主机 IP 容器 IP
node-1 192.168.1.68 40.15.43.2/24
node-2 192.168.1.254 40.15.26.2/24
node-3 192.168.1.245 40.15.56.2/24

我们查看 192.168.1.68 的本地路由:

复制代码

# ip r
defaultvia192.168.1.1dev eth0 proto dhcp src192.168.1.68metric100
40.15.26.0/24via192.168.1.254dev eth0
40.15.43.0/24dev docker0 proto kernel scope link src40.15.43.1
40.15.56.0/24via192.168.1.245dev eth0
...

我们只关心 40.15 前缀的路由,发现 40.15.26.0/24 的下一跳为 192.168.1.254,正好为 node2 IP,而 40.15.43.0/24 的下一跳为本地 docker0,因为该子网就是 node 所在的子网,40.15.56.0/24 的下一跳为 192.168.1.245,正好是 node3 IP。可见,Flannel 通过配置静态路由的方式实现容器跨主机通信,每个 Node 都作为路由器使用。

host-gw 的方式相对 Overlay 由于没有 vxlan 的封包拆包过程,直接路由就过去了,因此性能相对要好。不过正是由于它是通过路由的方式实现,每个 Node 相当于是容器的网关,因此每个 Node 必须在同一个 LAN 子网内,否则跨子网由于链路层不通导致无法实现路由导致 host-gw 实现不了。

4.4 Flannel 利用云平台路由实现跨主机通信

前面介绍的 host-gw 是通过修改主机路由表实现容器跨主机通信,如果能修改主机网关的路由当然也是没有问题的,尤其是和 SDN 结合方式动态修改路由。

目前很多云平台均实现了自定义路由表的功能,比如 OpenStack、AWS 等,Flannel 借助这些功能实现了很多公有云的 VPC 后端,通过直接调用云平台 API 修改路由表实现容器跨主机通信,比如阿里云、AWS、Google 云等,不过很可惜官方目前好像还没有实现 OpenStack Neutron 后端。

下面以 AWS 为例,创建了如下 4 台 EC2 虚拟机:

  • node-1: 197.168.1.68/24
  • node-2: 197.168.1.254/24
  • node-3: 197.168.1.245/24
  • node-4: 197.168.0.33/24

注意第三台和其余两台不在同一个子网。

三台 EC2 均关联了 flannel-role,flannel-role 关联了 flannel-policy,policy 的权限如下:

复制代码

{
"Version":"2012-10-17",
"Statement": [
{
"Sid":"VisualEditor0",
"Effect":"Allow",
"Action": [
"ec2:DescribeInstances",
"ec2:CreateRoute",
"ec2:DeleteRoute",
"ec2:ModifyInstanceAttribute",
"ec2:DescribeRouteTables",
"ec2:ReplaceRoute"
],
"Resource":"*"
}
]
}

即 EC2 实例需要具有修改路由表等相关权限。

之前一直很疑惑 AWS 的 role 如何与 EC2 虚拟机关联起来的。换句话说,如何实现虚拟机无需配置 Key 和 Secretd 等认证信息就可以直接调用 AWS API,通过 awscli 的 --debug 信息可知 awscli 首先通过 metadata 获取 role 信息,再获取 role 的 Key 和 Secret:

关于 AWS 如何知道调用 metadata 的是哪个 EC2 实例,可参考之前的文章 OpenStack 虚拟机如何获取 metadata.

另外所有 EC2 实例均禁用了 MAC 地址欺骗功能(Change Source/Dest Check),安全组允许 flannel 网段 40.15.0.0/16 通过,另外增加了如下 iptables 规则:

复制代码

iptables -I FORWARD --dest40.15.0.0/16-j ACCEPT
iptables -I FORWARD --src40.15.0.0/16-j ACCEPT

flannel 配置如下:

复制代码

# etcdctl get /coreos.com/network/config | jq .
{
"Network":"40.15.0.0/16",
"Backend": {
"Type":"aws-vpc"
}
}

启动 flannel,自动为每个 Node 分配 24 位子网,网段如下:

Node 名 主机 IP 容器 IP
node-1 192.168.1.68 40.15.16.0/24
node-2 192.168.1.254 40.15.64.0/24
node-3 192.168.1.245 40.15.13.0/24
node-4 192.168.0.33 40.15.83.0/24

我们查看 node-1、node-2、node-3 关联的路由表如图:

node-4 关联的路由表如图:

由此可见,每增加一个 Flannel 节点,Flannel 就会调用 AWS API 在 EC2 实例的子网关联的路由表上增加一条记录,Destination 为该节点分配的 Flannel 子网,Target 为该 EC2 实例的主网卡。

在 4 个节点分别创建一个 busybox 容器,容器 IP 如下:

Node 名 主机 IP 容器 IP
node-1 192.168.1.68 40.15.16.2/24
node-2 192.168.1.254 40.15.64.2/24
node-3 192.168.1.245 40.15.13.2/24
node-4 192.168.0.33 40.15.83.2/24

所有节点 ping node-4 的容器,如图:

我们发现所有节点都能 ping 通 node-4 的容器。但是 node-4 的容器却 ping 不通其余容器:

这是因为每个 Node 默认只会添加自己所在路由的记录。node-4 没有 node-1 ~ node-3 的路由信息,因此不通。

可能有人会问,node1 ~ node3 也没有 node4 的路由,那为什么能 ping 通 node4 的容器呢?这是因为我的环境中 node1 ~ node3 子网关联的路由是 NAT 网关,node4 是 Internet 网关,而 NAT 网关的子网正好是 node1 ~ node4 关联的子网,因此 node1 ~ node3 虽然在自己所在的 NAT 网关路由没有找到 node4 的路由信息,但是下一跳到达 Internet 网关的路由表中找到了 node4 的路由,因此能够 ping 通,而 node4 找不到 node1 ~ node3 的路由,因此都 ping 不通。

以上只是默认行为,Flannel 可以通过 RouteTableID 参数配置 Node 需要更新的路由表,我们只需要增加如下两个子网的路由如下:

复制代码

.
{
"Network":"40.15.0.0/16",
"Backend": {
"Type":"aws-vpc",
"RouteTableID": [
"rtb-0686cdc9012674692",
"rtb-054dfd5f3e47102ae"
]
}
}

重启 Flannel 服务,再次查看两个路由表:

我们发现两个路由表均添加了 node1 ~ node4 的 Flannel 子网路由。

此时四个节点的容器能够相互 ping 通。

从中我们发现, aws-vpc 解决了 host-gw 不能跨子网的问题 ,Flannel 官方也建议如果使用 AWS,推荐使用 aws-vpc 替代 overlay 方式,能够获取更好的性能:

When running within an Amazon VPC, we recommend using the aws-vpc backend which, instead of using encapsulation, manipulates IP routes to achieve maximum performance. Because of this, a separate flannel interface is not created.  The biggest advantage of using flannel AWS-VPC backend is that the AWS knows about that IP. That makes it possible to set up ELB to route directly to that container.

另外,由于路由是添加到了主机网关上,因此只要关联了该路由表,EC2 实例是可以从外面直接 ping 通容器的,换句话说,同一子网的 EC2 虚拟机可以直接与容器互通。

不过需要注意的是,AWS 路由表默认最多支持 50 条路由规则,这限制了 Flannel 节点数量,不知道 AWS 是否支持增加配额功能。另外目前最新版的 Flannel v0.10.0 好像对 aws-vpc 支持有点问题,再官方修复如上问题之前建议使用 Flannel v0.8.0 版本。

五、黑科技最多的 Calico

5.1 Calico 环境配置

Calico 和 Flannel host-gw 类似都是通过路由实现跨主机通信, 区别在于 Flannel 通过 flanneld 进程逐一添加主机静态路由实现,而 Calico 则是通过 BGP 实现节点间路由规则的相互学习广播。

这里不详细介绍 BGP 的实现原理,仅研究容器是如何通信的。

创建了 3 个节点的 calico 集群,ip pool 配置如下:

复制代码

# calicoctl get ipPool -o yaml
- apiVersion:v1
kind:ipPool
metadata:
cidr:197.19.0.0/16
spec:
ipip:
enabled:true
mode:cross-subnet
nat-outgoing:true
- apiVersion:v1
kind:ipPool
metadata:
cidr:fd80:24e2:f998:72d6::/64
spec:{}

Calico 分配的 ip 如下:

复制代码

forhostin$(etcdctl --endpoints $ENDPOINTS ls /calico/ipam/v2/host/);do
etcdctl --endpoints $ENDPOINTS ls $host/ipv4/block | awk -F'/''{sub(/-/,"/",$NF)}{print $6,$NF}'
done | sort

int32bit-docker-1197.19.38.128/26
int32bit-docker-2197.19.186.192/26
int32bit-docker-3197.19.26.0/26

由此可知,Calico 和 Flannel 一样,每个节点分配一个子网,只不过 Flannel 默认分 24 位子网,而 Calico 分的是 26 位子网。

三个节点分别创建 busybox 容器:

Node 名 主机 IP 容器 IP
node-1 192.168.1.68 197.19.38.136
node-2 192.168.1.254 197.19.186.197
node-3 192.168.1.245 197.19.26.5/24

相互 ping 通没有问题。

5.2 Calico 容器内部网络

我们查看容器的 link 设备以及路由:

复制代码

# ./docker_netns.sh busybox ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu65536qdisc noqueue state UNKNOWN groupdefaultqlen1000
link/loopback00:00:00:00:00:00brd00:00:00:00:00:00
inet127.0.0.1/8scope host lo
valid_lft forever preferred_lft forever
2:tunl0@NONE: <NOARP> mtu1480qdisc noop state DOWN groupdefaultqlen1000
link/ipip0.0.0.0brd0.0.0.0
14:cali0@if15: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu1500qdisc noqueue state UP groupdefault
link/ether ee:ee:ee:ee:ee:ee brd ff:ff:ff:ff:ff:ff link-netnsid0
inet197.19.38.136/32brd197.19.38.136scope global cali0
valid_lft forever preferred_lft forever

# ./docker_netns.sh busybox ip r
defaultvia169.254.1.1dev cali0
169.254.1.1dev cali0 scope link

有如下几点个人感觉很神奇:

ee:ee:ee:ee:ee:ee

这两个问题在 Calico 官方的 faq 中有记录#1 Why do all cali* interfaces have the MAC address ee:ee:ee:ee:ee:ee?、#2 Why can’t I see the 169.254.1.1 address mentioned above on my host?。

针对第一个问题,官方认为不是所有的内核都能支持自动分配 MAC 地址,所以干脆 Calico 自己指定 MAC 地址,而 Calico 完全使用三层路由通信,MAC 地址是什么其实无所谓,因此直接都使用 ee:ee:ee:ee:ee:ee

第二个问题,回顾之前的网络模型,大多数都是把容器的网卡通过 VETH 连接到一个 bridge 设备上,而这个 bridge 设备往往也是容器网关,相当于主机上多了一个虚拟网卡配置。Calico 认为容器网络不应该影响主机网络,因此容器的网卡的 VETH 另一端没有经过 bridge 直接挂在默认的 namespace 中。而容器配的网关其实也是假的,通过 proxy_arp 修改 MAC 地址模拟了网关的行为,所以网关 IP 是什么也无所谓,那就直接选择了 local link 的一个 ip,这还节省了容器网络的一个 IP。我们可以抓包看到 ARP 包:

可以看到容器网卡的对端 calia2656637189 直接代理回复了 ARP,因此出去网关时容器的包会直接把 MAC 地址修改为 06:66:26:8e:b2:67 , 即伪网关的 MAC 地址。

有人可能会说那如果在同一主机的容器通信呢?他们应该在同一个子网,容器的 MAC 地址都是一样那怎么进行二层通信呢?仔细看容器配置的 IP 掩码居然是 32 位的,那也就是说跟谁都不在一个子网了,也就不存在二层的链路层直接通信了。

5.3 Calico 主机路由

前面提到 Calico 通过 BGP 动态路由实现跨主机通信,我们查看主机路由如下,其中 197.19.38.139、197.19.38.140 是在本机上的两个容器 IP:

复制代码

# ip r | grep197.19
197.19.26.0/26via192.168.1.245dev eth0 proto bird
blackhole197.19.38.128/26proto bird
197.19.38.139dev calia2656637189 scope link
197.19.38.140dev calie889861df72 scope link
197.19.186.192/26via192.168.1.254dev eth0 proto bird

我们发现跨主机通信和 Flannel host-gw 完全一样,下一跳直接指向 hostIP,把 host 当作容器的网关。不一样的是到达宿主机后,Flannel 会通过路由转发流量到 bridge 设备中,再由 bridge 转发给容器,而 Calico 则为每个容器的 IP 生成一条明细路由,直接指向容器的网卡对端。因此如果容器数量很多的话,主机路由规则数量也会越来越多,因此才有了路由反射,这里不过多介绍。

里面还有一条 blackhole 路由,如果来的 IP 是在 host 分配的容器子网 197.19.38.128/26 中,而又不是容器的 IP,则认为是非法地址,直接丢弃。

5.4 Calico 多网络支持

在同一个集群上可以同时创建多个 Calico 网络:

复制代码

# docker networkls| grep calico
ad7ca8babf01 calico-net-1 calicoglobal
5eaf3984f69d calico-net-2 calicoglobal

我们使用另一个 Calico 网络 calico-net-2 创建一个容器:

复制代码

docker run -d --name busybox-3--net calico-net-2busybox sleep36000
# docker exec busybox-3ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu65536qdisc noqueue qlen1000
link/loopback00:00:00:00:00:00brd00:00:00:00:00:00
inet127.0.0.1/8scope host lo
valid_lft forever preferred_lft forever
2:tunl0@NONE: <NOARP> mtu1480qdisc noop qlen1000
link/ipip0.0.0.0brd0.0.0.0
24:cali0@if25: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu1500qdisc noqueue
link/ether ee:ee:ee:ee:ee:ee brd ff:ff:ff:ff:ff:ff
inet197.19.38.141/32brd197.19.38.141scope global cali0
valid_lft forever preferred_lft forever

# ip r | grep197.19
197.19.26.0/26via192.168.1.245dev eth0 proto bird
blackhole197.19.38.128/26proto bird
197.19.38.139dev calia2656637189 scope link
197.19.38.140dev calie889861df72 scope link
197.19.38.141dev calib12b038e611 scope link
197.19.186.192/26via192.168.1.254dev eth0 proto bird

我们发现在同一个主机不在同一个网络的容器 IP 地址在同一个子网,那不是可以通信呢?

我们发现虽然两个跨网络的容器分配的 IP 在同一个子网,但居然实现了隔离。

如果使用诸如 vxlan 的 overlay 网络,很好猜测是怎么实现隔离的,无非就是使用不同的 VNI。但 Calico 没有使用 overlay,直接使用路由通信,而且不同网络的子网还是重叠的,它是怎么实现隔离的呢。

要在同一个子网实现隔离,我们猜测实现方式只能是逻辑隔离,即通过本地防火墙如 iptables 实现。

查看了下 Calico 生成的 iptables 规则发现太复杂了,各种包 mark。由于决定包的放行或者丢弃通常是在 filter 表实现,而不是发往主机的自己的包应该在 FORWARD 链中,因此我们直接研究 filter 表的 FORWARD 表。

复制代码

# iptables-save -t filter | grep -- '-A FORWARD'
-A FORWARD -m comment--comment"cali:wUHhoiAYhphO9Mso"-j cali-FORWARD
...

Calico 把 cali-FORWARD 子链挂在了 FORWARD 链上,comment 中的一串看起来像随机字符串 cali:wUHhoiAYhphO9Mso 不知道是干嘛的。

复制代码

# iptables-save -t filter | grep -- '-A cali-FORWARD'
-A cali-FORWARD -i cali+ -m comment --comment"cali:X3vB2lGcBrfkYquC"-jcali-from-wl-dispatch
-A cali-FORWARD -o cali+ -m comment --comment"cali:UtJ9FnhBnFbyQMvU"-jcali-to-wl-dispatch
-A cali-FORWARD -i cali+ -m comment --comment"cali:Tt19HcSdA5YIGSsw"-jACCEPT
-A cali-FORWARD -o cali+ -m comment --comment"cali:9LzfFCvnpC5_MYXm"-jACCEPT
...

cali+ 表示所有以 cali 为前缀的网络接口,即容器的网卡对端设备。由于我们只关心发往容器的流量方向,即从 caliXXX 发往容器的流量,因此我们只关心条件匹配的 -o cali+ 的规则,从如上可以看出所有从 cali+ 出来的流量都跳转到了 cali-to-wl-dispatch 子链处理,其中 wl 是 workload 的缩写,workload 即容器。

复制代码

# iptables-save -tfilter| grep-- '-A cali-to-wl-dispatch'
-A cali-to-wl-dispatch -o calia2656637189 -mcomment--comment "cali:TFwr8sfMnFH3BUla" -g cali-tw-calia2656637189
-A cali-to-wl-dispatch -o calib12b038e611 -mcomment--comment "cali:ZbRb0ozg-GGeUfRA" -g cali-tw-calib12b038e611
-A cali-to-wl-dispatch -o calie889861df72 -mcomment--comment "cali:5OoGv50NzX0sKdMg" -g cali-tw-calie889861df72
-A cali-to-wl-dispatch -mcomment--comment "cali:RvicCiwAy9cIEAKA" -m comment --comment "Unknown interface" -j DROP

从子链名字也可以看出 cali-to-wl-dispatch 是负责流量的分发的,即根据具体的流量出口引到具体的处理流程子链,从 X 出来的,由 cali-tw-X 处理,从 Y 出来的,由 cali-tw-Y 处理,依次类推,其中 twto workload 的简写。

我们假设是发往 busybox 197.19.38.139 这个容器的,对应的主机虚拟设备为 calia2656637189 ,则跳转子链为 cali-tw-calia2656637189

复制代码

# iptables-save-t filter | grep -- '-A cali-tw-calia2656637189'
-A cali-tw-calia2656637189 -mcomment --comment"cali:259EHpBvnovN8_q6"-mconntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A cali-tw-calia2656637189 -mcomment --comment"cali:YLokMEiVkZggfg9R"-mconntrack --ctstate INVALID -jDROP
-A cali-tw-calia2656637189 -mcomment --comment"cali:pp8a6fGxqaALtRK5"-jMARK--set-xmark 0x0/0x1000000
-A cali-tw-calia2656637189 -mcomment --comment"cali:bgw2sCtlIfZjhXLA"-j cali-pri-calico-net-1
-A cali-tw-calia2656637189 -mcomment --comment"cali:1Z2NvhoS27pP03Ll"-mcomment --comment"Return if profile accepted"-mmark--mark0x1000000/0x1000000 -jRETURN
-A cali-tw-calia2656637189 -mcomment --comment"cali:mPb8hORsTXeVt7yC"-mcomment --comment"Drop if no profiles matched"-jDROP

其中第 1、2 条规则在深入浅出 OpenStack 安全组实现原理中介绍过,不再赘述。

第三条规则注意使用的是 set-xmark 而不是 set-mark ,为什么不用 set-mark ,这是由于 set-mark 会覆盖原来的值。而 set-xmark value/netmask ,表示 X=(X&(~netmask))^value--set-xmark0x0/0x1000000 的意思就是把 X 的第 25 位重置为 0,其他位保留不变。

这个 mark 位的含义我在官方中没有找到,在 Calico 网络的原理、组网方式与使用这篇文章找到了相关资料:

node 一共使用了 3 个标记位,0x7000000 对应的标记位

0x1000000: 报文的处理动作,置 1 表示放行,默认 0 表示拒绝

0x2000000: 是否已经经过了 policy 规则检测,置 1 表示已经过

0x4000000: 报文来源,置 1,表示来自 host-endpoint

即第 25 位表示报文的处理动作,为 1 表示通过,0 表示拒绝,第 5、6 条规则也可以看出第 25 位的意义,匹配 0x1000000/0x1000000 直接 RETRUN,不匹配的直接 DROP。

因此第 3 条规则的意思就是清空第 25 位标志位重新评估,谁来评估呢?这就是第 4 条规则的作用,根据虚拟网络设备 cali-XXX 所处的网络跳转到指定网络的子链中处理,由于 calia2656637189 属于 calico-net-1,因此会跳转到 cali-pri-calico-net-1 子链处理。

我们观察 cali-pri-calico-net-1 的规则:

复制代码

# iptables-save-t filter | grep -- '-A cali-pri-calico-net-1'
-A cali-pri-calico-net-1 -mcomment --comment"cali:Gvse2HBGxQ9omCdo"-mset--match-setcali4-s:VFoIKKR-LOG_UuTlYqcKubo src -jMARK--set-xmark 0x1000000/0x1000000
-A cali-pri-calico-net-1 -mcomment --comment"cali:0vZpvvDd_5bT7g_k"-mmark--mark0x1000000/0x1000000 -jRETURN

规则很简单,只要 IP 在 cali4-s:VFoIKKR-LOG_UuTlYqcKubo 在这个 ipset 集合中就设置 mark 第 25 位为 1,然后 RETURN,否则如果 IP 不在 ipset 中则直接 DROP(子链的默认行为为 DROP)。

复制代码

# ipset list cali4-s:VFoIKKR-LOG_UuTlYqcKubo
Name: cali4-s:VFoIKKR-LOG_UuTlYqcKubo
Type: hash:ip
Revision:4
Header: family inet hashsize1024maxelem1048576
Sizeinmemory:280
References:1
Number of entries:4
Members:
197.19.38.143
197.19.26.7
197.19.186.199
197.19.38.144

到这里终于真相大白了,Calico 是通过 iptables + ipset 实现多网络隔离的,同一个网络的 IP 会加到同一个 ipset 集合中,不同网络的 IP 放到不同的 ipset 集合中,最后通过 iptables 的 set 模块匹配 ipset 集合的 IP,如果 src IP 在指定的 ipset 中则允许通过,否则 DROP。

5.5 Calico 跨网段通信

我们知道 Flannel host-gw 不支持 Node 主机跨网段,Calico 是否支持呢,为此我增加了一个 node-4(192.168.0.33/24),显然和其他三个 Node 不在同一个子网。

在新的 Node 中启动一个 busybox:

复制代码

dockerrun-d --name busybox-node-4 --net calico-net-1 busybox sleep 36000
docker exec busybox-node-4ping-c 1 -w 1 197.19.38.144
PING 197.19.38.144 (197.19.38.144): 56 data bytes
64 bytesfrom197.19.38.144:seq=0ttl=62time=0.539 ms

--- 197.19.38.144pingstatistics ---
1 packets transmitted, 1 packets received, 0% packet loss
round-trip min/avg/max = 0.539/0.539/0.539 ms

验证发现容器通信时没有问题的。

查看 node-1 路由:

复制代码

# ip r | grep197.19
197.19.26.0/26via192.168.1.245dev eth0 proto bird
blackhole197.19.38.128/26proto bird
197.19.38.142dev cali459cc263d36 scope link
197.19.38.143dev cali6d0015b0c71 scope link
197.19.38.144dev calic8e5fab61b1 scope link
197.19.65.128/26via192.168.0.33dev tunl0 proto bird onlink
197.19.186.192/26via192.168.1.254dev eth0 proto bird

和其他路由不一样的是,我们发现 197.19.65.128/26 是通过 tunl0 出去的:

复制代码

# ip -d link show tunl0
5: tunl0@NONE: <NOARP,UP,LOWER_UP> mtu 1440 qdisc noqueue state UNKNOWN modeDEFAULT group defaultqlen 1000
link/ipip 0.0.0.0 brd 0.0.0.0 promiscuity 0
ipip any remote any local any ttl inherit nopmtudisc addrgenmode eui64 numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
# ip -d tunnel show
tunl0: any/ipremote any local any ttl inherit nopmtudisc

由此可知,如果节点跨网段,则 Calico 通过 ipip 隧道传输,相当于走的是 overlay。

对比 Flannel host-gw,除了静态与 BGP 动态路由配置的区别,Calico 还通过 iptables + ipset 解决了多网络支持问题,通过 ipip 隧道实现了节点跨子网通信问题。

另外,某些业务或者 POD 需要固定 IP,比如 POD 从一个节点迁移到另一个节点保持 IP 不变,这种情况下可能导致容器的 IP 不在节点 Node 上分配的子网范围内,Calico 可以通过添加一条 32 位的明细路由实现,Flannel 不支持这种情况。

因此相对来说 Calico 实现的功能相对要多些,但是,最终也导致 Calico 相对 Flannel 要复杂得多,运维难度也较大,光一堆 iptables 规则就不容易理清了。

六、与 OpenStack 网络集成的 Kuryr

Kuryr 是 OpenStack 中一个较新的项目,其目标是“Bridge between container framework networking and storage models to OpenStack networking and storage abstractions.”, 即实现容器与 OpenStack 的网络集成,该方案实现了与虚拟机、裸机相同的网络功能和互通,比如多租户、安全组等。

网络模型和虚拟机基本一样,唯一区别在于虚拟机是通过 TAP 设备直接挂到虚拟机设备中的,而容器则是通过 VETH 连接到容器的 namespace。

复制代码

vm Container whatever
| | |
tapX tapY tapZ
| | |
| | |
qbrX qbrY qbrZ
| | |
---------------------------------------------

| br-int(OVS) |
---------------------------------------------
|
----------------------------------------------
| br-tun(OVS) |
----------------------------------------------

Kuryr 在我之前的文章 OpenStack 容器服务 Zun 初探与原理分析详细介绍过,这里不再赘述。

本文转载自公众号 int32bit(ID:int32bit)。

原文链接:

https://mp.weixin.qq.com/s/-L_2qPpFmc85lMmVUi_UCQ

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章