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

阅读数:1911 2019 年 9 月 12 日 08:00

浅聊几种主流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/
exit 0
fi
NAMESPACE_FILE=/var/run/docker/netns/${NAMESPACE}
if [[ ! -f $NAMESPACE_FILE ]]; then
NAMESPACE_FILE=$(docker inspect -f "{{.NetworkSettings.SandboxKey}}" $NAMESPACE 2>/dev/null)
fi
if [[ ! -f $NAMESPACE_FILE ]]; then
echo "Cannot open network namespace '$NAMESPACE': No such file or directory"
exit 1
fi
shift
if [[ $# -lt 1 ]]; then
echo "No command specified"
exit 1
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 UNKNOWN group default 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
354: eth0@if355: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group 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 UP group 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
for namespace in $($DOCKER_NETNS_SCRIPT); do
printf "\e[1;31m%s: \e[0m\n" $namespace
$DOCKER_NETNS_SCRIPT $namespace ip -c -o link
printf "\n"
done
else
for namespace in $($DOCKER_NETNS_SCRIPT); do
if $DOCKER_NETNS_SCRIPT $namespace ip -c -o link | grep -Pq "^$IFINDEX: "; then
printf "\e[1;31m%s: \e[0m\n" $namespace
$DOCKER_NETNS_SCRIPT $namespace ip -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 mode DEFAULT 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 网络:

复制代码
docker network create -d overlay --subnet 10.20.0.0/16 overlay

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

复制代码
docker run -d --name busybox --net overlay busybox sleep 36000

容器列表如下:

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

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

我们发现容器有两个 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-1 ip r
default via 172.18.0.1 dev eth1
10.20.0.0/16 dev eth0 scope link src 10.20.0.3
172.18.0.0/16 dev eth1 scope link src 172.18.0.2

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

复制代码
# docker exec busybox-node-1 ip link show eth1
77: eth1@if78: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
link/ether 02:42:ac:12:00:02 brd ff:ff:ff:ff:ff:ff
# ./find_links.sh 78
default:
78: vethf2de5d4@if77: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker_gwbridge state UP mode DEFAULT group default
link/ether 2e:6a:94:6a:09:c5 brd ff:ff:ff:ff:ff:ff link-netnsid 1

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

复制代码
# brctl show
bridge name bridge id STP enabled interfaces
docker0 8000.02427406ba1a no
docker_gwbridge 8000.0242bb868ca3 no vethf2de5d4

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 -s 172.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-1 ip link show eth0
75: eth0@if76: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1450 qdisc noqueue
link/ether 02:42:0a:14:00:03 brd ff:ff:ff:ff:ff:ff
# ./find_links.sh 76
1-19c5d1a7ef:
76: veth0@if75: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master br0 state UP mode DEFAULT group default \ link/ether 6a:ce:89:a2:89:4a brd ff:ff:ff:ff:ff:ff link-netnsid 1

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 mode DEFAULT 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 name bridge id STP enabled interfaces
br0 8000.6ace89a2894a no veth0
vxlan0

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

复制代码
./docker_netns.sh 1-19c5d1a7ef ip -c -d link show vxlan0
74: vxlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master br0 state UNKNOWN mode DEFAULT 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 4789 proxy l2miss l3miss ttl inherit ageing 300 udpcsum noudp6zerocsumtx noudp6zerocsumrx
...

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

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

图中 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:

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

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

复制代码
./docker_netns.sh 2-19c5d1a7ef ip -d -o link show vxlan0 | grep proxy_arp

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

复制代码
# ./docker_netns.sh 3-19c5d1a7ef ip neigh
10.20.0.3 dev vxlan0 lladdr 02:42:0a:14:00:03 PERMANENT
10.20.0.4 dev vxlan0 lladdr 02:42:0a:14:00:04 PERMANENT

记录中的 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.sh 3-19c5d1a7ef bridge fdb
...
02:42:0a:14:00:04 dev vxlan0 dst 192.168.1.245 link-netnsid 0 self permanent
02:42:0a:14:00:03 dev vxlan0 dst 192.168.1.254 link-netnsid 0 self 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-range 172.111.222.0/24 192.168.1.68 192.168.1.254 192.168.1.245

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

复制代码
# node-1
docker run -d --name busybox-node-1 --net weave busybox sleep 3600
# node-2
docker run -d --name busybox-node-2 --net weave busybox sleep 3600
# node-3
docker run -d --name busybox-node-3 --net weave busybox sleep 3600

在容器中我们相互 ping:

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

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

另外查看容器的路由:

复制代码
# docker exec -t -i busybox-node-$NODE ip r
default via 172.18.0.1 dev eth0
172.18.0.0/16 dev eth0 scope link src 172.18.0.2
172.111.222.0/24 dev ethwe0 scope link src 172.111.222.128
224.0.0.0/4 dev 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 mode DEFAULT 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 name bridge id STP enabled interfaces
weave 8000.d2939d07704b no vethwe-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 mode DEFAULT 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> mtu 1376 qdisc noqueue
master datapath state UP mode DEFAULT group default
link/ether f6:74:e9:0b:30:6d brd ff:ff:ff:ff:ff:ff promiscuity 1
veth
openvswitch_slave addrgenmode eui64 numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535

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
port 0: datapath (internal)
port 1: vethwe-datapath
port 2: 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> mtu 65535 qdisc noqueue
master datapath state UNKNOWN mode DEFAULT group default qlen 1000
link/ether d2:21:db:c1:9b:28 brd ff:ff:ff:ff:ff:ff promiscuity 1
vxlan id 0 srcport 0 0 dstport 6784 nolearning ttl inherit ageing 300 udpcsum noudp6zerocsumtx udp6zerocsumrx external
openvswitch_slave addrgenmode eui64 numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535

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

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

四、简单优雅的 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容器:

复制代码
docker run -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> mtu 8951 qdisc noqueue state UP mode DEFAULT group default
link/ether 02:42:28:0f:2b:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0 promiscuity 0
veth addrgenmode eui64 numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535

和 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 mode DEFAULT 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 name bridge id STP enabled interfaces
docker0 8000.0242d6f8613e no veth1cfe340
vethd1fae9d
docker_gwbridge 8000.024257f32054 no

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

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

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

复制代码
# ip r
default via 192.168.1.1 dev eth0 proto dhcp src 192.168.1.68 metric 100
40.15.26.0/24 via 40.15.26.0 dev flannel.1 onlink
40.15.43.0/24 dev docker0 proto kernel scope link src 40.15.43.1
40.15.56.0/24 via 40.15.56.0 dev flannel.1 onlink
...

我们只关心 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> mtu 8951 qdisc noqueue state UNKNOWN mode DEFAULT group default
link/ether 0e:08:23:57:14:9a brd ff:ff:ff:ff:ff:ff promiscuity 0
vxlan id 1 local 192.168.1.68 dev eth0 srcport 0 0 dstport 8472 nolearning ttl inherit ageing 300
udpcsum noudp6zerocsumtx noudp6zerocsumrx addrgenmode eui64 numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535

可见 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:38 dev flannel.1 dst 192.168.1.245 self permanent
da:17:1b:07:d3:70 dev flannel.1 dst 192.168.1.254 self 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 subnet in $(etcdctl ls /coreos.com/network/subnets); do etcdctl 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 网络实现原理简化如图:

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

可见除了增加或者减少 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 地址欺骗功能,如图:

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

使用 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
default via 192.168.1.1 dev eth0 proto dhcp src 192.168.1.68 metric 100
40.15.26.0/24 via 192.168.1.254 dev eth0
40.15.43.0/24 dev docker0 proto kernel scope link src 40.15.43.1
40.15.56.0/24 via 192.168.1.245 dev 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:

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

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

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

复制代码
iptables -I FORWARD --dest 40.15.0.0/16 -j ACCEPT
iptables -I FORWARD --src 40.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 关联的路由表如图:

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

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

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

由此可见,每增加一个 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 的容器,如图:

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

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

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

这是因为每个 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 服务,再次查看两个路由表:

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

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

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

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

从中我们发现,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 虚拟机可以直接与容器互通。

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

不过需要注意的是,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 如下:

复制代码
for host in $(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-1 197.19.38.128/26
int32bit-docker-2 197.19.186.192/26
int32bit-docker-3 197.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

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

相互 ping 通没有问题。

5.2 Calico 容器内部网络

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

复制代码
# ./docker_netns.sh busybox ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default 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
2: tunl0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN group default qlen 1000
link/ipip 0.0.0.0 brd 0.0.0.0
14: cali0@if15: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether ee:ee:ee:ee:ee:ee brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 197.19.38.136/32 brd 197.19.38.136 scope global cali0
valid_lft forever preferred_lft forever
# ./docker_netns.sh busybox ip r
default via 169.254.1.1 dev cali0
169.254.1.1 dev cali0 scope link

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

  • 所有容器的 MAC 地址都是 ee:ee:ee:ee:ee:ee
  • 网关地址是 169.254.1.1,然而我找尽了所有的 namespaces 也没有找到这个 IP。

这两个问题在 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 包:

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

可以看到容器网卡的对端 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 | grep 197.19
197.19.26.0/26 via 192.168.1.245 dev eth0 proto bird
blackhole 197.19.38.128/26 proto bird
197.19.38.139 dev calia2656637189 scope link
197.19.38.140 dev calie889861df72 scope link
197.19.186.192/26 via 192.168.1.254 dev 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 network ls | grep calico
ad7ca8babf01 calico-net-1 calico global
5eaf3984f69d calico-net-2 calico global

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

复制代码
docker run -d --name busybox-3 --net calico-net-2 busybox sleep 36000
# docker exec busybox-3 ip a
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
2: tunl0@NONE: <NOARP> mtu 1480 qdisc noop qlen 1000
link/ipip 0.0.0.0 brd 0.0.0.0
24: cali0@if25: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
link/ether ee:ee:ee:ee:ee:ee brd ff:ff:ff:ff:ff:ff
inet 197.19.38.141/32 brd 197.19.38.141 scope global cali0
valid_lft forever preferred_lft forever
# ip r | grep 197.19
197.19.26.0/26 via 192.168.1.245 dev eth0 proto bird
blackhole 197.19.38.128/26 proto bird
197.19.38.139 dev calia2656637189 scope link
197.19.38.140 dev calie889861df72 scope link
197.19.38.141 dev calib12b038e611 scope link
197.19.186.192/26 via 192.168.1.254 dev eth0 proto bird

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

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

我们发现虽然两个跨网络的容器分配的 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" -j cali-from-wl-dispatch
-A cali-FORWARD -o cali+ -m comment --comment "cali:UtJ9FnhBnFbyQMvU" -j cali-to-wl-dispatch
-A cali-FORWARD -i cali+ -m comment --comment "cali:Tt19HcSdA5YIGSsw" -j ACCEPT
-A cali-FORWARD -o cali+ -m comment --comment "cali:9LzfFCvnpC5_MYXm" -j ACCEPT
...

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

复制代码
# iptables-save -t filter | grep -- '-A cali-to-wl-dispatch'
-A cali-to-wl-dispatch -o calia2656637189 -m comment --comment "cali:TFwr8sfMnFH3BUla" -g cali-tw-calia2656637189
-A cali-to-wl-dispatch -o calib12b038e611 -m comment --comment "cali:ZbRb0ozg-GGeUfRA" -g cali-tw-calib12b038e611
-A cali-to-wl-dispatch -o calie889861df72 -m comment --comment "cali:5OoGv50NzX0sKdMg" -g cali-tw-calie889861df72
-A cali-to-wl-dispatch -m comment --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 -m comment --comment "cali:259EHpBvnovN8_q6" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A cali-tw-calia2656637189 -m comment --comment "cali:YLokMEiVkZggfg9R" -m conntrack --ctstate INVALID -j DROP
-A cali-tw-calia2656637189 -m comment --comment "cali:pp8a6fGxqaALtRK5" -j MARK --set-xmark 0x0/0x1000000
-A cali-tw-calia2656637189 -m comment --comment "cali:bgw2sCtlIfZjhXLA" -j cali-pri-calico-net-1
-A cali-tw-calia2656637189 -m comment --comment "cali:1Z2NvhoS27pP03Ll" -m comment --comment "Return if profile accepted" -m mark --mark 0x1000000/0x1000000 -j RETURN
-A cali-tw-calia2656637189 -m comment --comment "cali:mPb8hORsTXeVt7yC" -m comment --comment "Drop if no profiles matched" -j DROP

其中第 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 -m comment --comment "cali:Gvse2HBGxQ9omCdo" -m set --match-set cali4-s:VFoIKKR-LOG_UuTlYqcKubo src -j MARK --set-xmark 0x1000000/0x1000000
-A cali-pri-calico-net-1 -m comment --comment "cali:0vZpvvDd_5bT7g_k" -m mark --mark 0x1000000/0x1000000 -j RETURN

规则很简单,只要 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 hashsize 1024 maxelem 1048576
Size in memory: 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:

复制代码
docker run -d --name busybox-node-4 --net calico-net-1 busybox sleep 36000
docker exec busybox-node-4 ping -c 1 -w 1 197.19.38.144
PING 197.19.38.144 (197.19.38.144): 56 data bytes
64 bytes from 197.19.38.144: seq=0 ttl=62 time=0.539 ms
--- 197.19.38.144 ping statistics ---
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 | grep 197.19
197.19.26.0/26 via 192.168.1.245 dev eth0 proto bird
blackhole 197.19.38.128/26 proto bird
197.19.38.142 dev cali459cc263d36 scope link
197.19.38.143 dev cali6d0015b0c71 scope link
197.19.38.144 dev calic8e5fab61b1 scope link
197.19.65.128/26 via 192.168.0.33 dev tunl0 proto bird onlink
197.19.186.192/26 via 192.168.1.254 dev 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 mode DEFAULT group default qlen 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/ip remote 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

评论

发布