注:本文基于Calico v3.20.1版本编写

1 关于Calico

flannel是overlay网络, 主要工作在L2(VXLAN),calico主要是L3,通过BGP路由协议在机器之间传送报文。

2 安装Calico(IPIP模式)

因为之前有安装flannel,因此需要先删除,

kubectl delete -f kube-flannel.yml

同时需要删除flannel.1和cni0这两个网络设备,

ip link delete cni0
ip link delete flannel.1

之后就可以根据官网安装了,

curl https://docs.projectcalico.org/manifests/calico.yaml -O
kubectl apply -f calico.yaml

是的,就是这么简单,然后就可以在机器上看到新生成的网络设备,

cali00dc074ce54: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1480
...
tunl0: flags=193<UP,RUNNING,NOARP>  mtu 1480

因为默认情况下,calico使用的是ipip隧道模式,这一点我们可以从calico的日志里看出,

2021-10-02 05:04:26.657 [INFO][9] startup/startup.go 651: CALICO_IPV4POOL_NAT_OUTGOING is true (defaulted) through environment variable
2021-10-02 05:04:26.657 [INFO][9] startup/startup.go 992: Ensure default IPv4 pool is created. IPIP mode: Always, VXLAN mode: Never
2021-10-02 05:04:26.718 [INFO][9] startup/startup.go 1002: Created default IPv4 pool (10.244.0.0/16) with NAT outgoing true. IPIP mode: Always, VXLAN mode: Never

所以多了一个tunl0设备,

[root@master home]# ip -d link show tunl0
9: tunl0@NONE: <NOARP,UP,LOWER_UP> mtu 1480 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1
    link/ipip 0.0.0.0 brd 0.0.0.0 promiscuity 0 
    ipip ...

与此同时,host上也多了两条通往node的路由记录,

[root@master calico]# route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         192.168.0.1     0.0.0.0         UG    0      0        0 ens33
10.244.104.0    192.168.0.112   255.255.255.192 UG    0      0        0 tunl0
10.244.166.128  192.168.0.111   255.255.255.192 UG    0      0        0 tunl0
...

3 Calico ipip模式工作原理

所谓ipip,也就是IP in IP,即在IP报文的基础上,又封装了一个IP头,和overlay网络相似。

k8s iptables 初始化_IPIP模式

我们以两个node上的两个容器为例,说一下报文的流动过程。

报文从Pod A发出,根据路由发往容器中的网关169.254.1.1,

[root@centos-bc2sm /]# route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         169.254.1.1     0.0.0.0         UG    0      0        0 eth0
169.254.1.1     0.0.0.0         255.255.255.255 UH    0      0        0 eth0

但是你会发现,host上并没有这设备,这就是calico独特之处。那报文到底是如何发送出去的呢?

我们先看下网关通向的二层地址,即mac地址,

[root@centos-bc2sm /]# arp -n
Address                  HWtype  HWaddress           Flags Mask            Iface
169.254.1.1              ether   ee:ee:ee:ee:ee:ee   C                     eth0

可以看到,容器中网关的arp地址是ee:ee:ee:ee:ee:ee,所以现在的问题就变成,哪个网卡的mac地址是这个。

我们在host上查看所有网卡,会发现,cali的所有网卡的mac地址都是ee:ee:ee:ee:ee:ee

[root@node2 ~]# ifconfig | grep -E "flags|ether"
cali0a4fde325ea: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1480
        ether ee:ee:ee:ee:ee:ee  txqueuelen 0  (Ethernet)
cali43af9e40d9b: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1480
        ether ee:ee:ee:ee:ee:ee  txqueuelen 0  (Ethernet)
cali61a9554ce92: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1480
...

另外,我们知道容器中的网卡用的是veth的pair虚拟网卡,一端连接到容器,另一端连着宿主机。所以我们需要查看下容器里网卡编号,

[root@centos-bc2sm /]# ethtool -S eth0
NIC statistics:
     peer_ifindex: 14

然后host中编号为14的即为另一端,

[root@node2 ~]# ip link show
...
14: cali0a4fde325ea@if4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1480 qdisc noqueue state UP mode DEFAULT 
    link/ether ee:ee:ee:ee:ee:ee brd ff:ff:ff:ff:ff:ff link-netnsid 4

因此,容器中的报文就通过cali0a4fde325ea到达宿主机。

然后根据宿主机的路由,报文发往tunl0

10.244.166.128  192.168.0.111   255.255.255.192 UG    0      0        0 tunl0

报文到达tunl0时,会再次封装一层ip报文头,把目标node的ip作为目的ip,然后通过ens33将报文发出。

对端node收到报文后,解开外层ip报文头,发现是ipip报文,会转交给tunl0, tunl0把内层ip报文头解开,查找路由,就发送给cali7dadcb96356,

10.244.166.140  0.0.0.0         255.255.255.255 UH    0      0        0 cali7dadcb96356

而这个cali7dadcb96356正是目标容器的veth pair网络设备的另一端。

[root@node1 ~]# ip link show cali7dadcb96356
14: cali7dadcb96356@if4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1480 qdisc noqueue state UP mode DEFAULT 
    link/ether ee:ee:ee:ee:ee:ee brd ff:ff:ff:ff:ff:ff link-netnsid 0

[root@centos-t854j /]# ethtool -S eth0
NIC statistics:
     peer_ifindex: 14

最终报文到达目标pod。

对于封装后的报文,我们可以通过tcpcump看下报文信息,

k8s iptables 初始化_k8s iptables 初始化_02

而对于同个node的情况,就不需要经过tunl0设备了,因为报文到达host上时,直接根据路由规则就可以转发到对应的cali设备。

4 几个疑问

4.1 容器内网关为169.254.1.1,为什么报文会到宿主机cali网络设备

我们通过arp命令查询mac缓存是逆推而来,而实际上,在发送报文时,会先通过arp广播报文,查询网关的mac地址,由于cali网络设备都开启了arp代理,因此在接收到arp请求时,就会响应该请求,把自己的mac地址回复给ARP广播请求发送者,这样报文就会发往该cali网络设备,同时也就有了这条arp缓存记录。

[root@node2 ~]# cat /proc/sys/net/ipv4/conf/cali0a4fde325ea/proxy_arp
1

所以,理论上容器内的网关设置为什么并不重要,最终由于arp代理,报文到会发送到宿主机的cali设备上。

4.2 为什么cali网络设备mac地址都为ee:ee:ee:ee:ee:ee

因为有一些系统无法在启动时给网络设备分配好mac地址,并且cali的mac地址并不会在实际的链路层报文中使用,因此calico将所有cali网络设备的mac地址都设置为全e,便于管理。

4.3 calico的cali网络设备和flannel的docker0网桥设备有和不同

flannel中所有容器都连接到docker0上,意味着所有容器都可以互通。而对于cali网络设备,每个容器连接到不同的host cali设备中,就能通过网络策略规则实现容器间的隔离,这就为多租户的场景提供了可能。

4.4 每个host是如何获取其他host的路由信息

这是通过BGP协议获取的,calico会在每个host上运行felix和bird进程,

[root@node2 ~]# ps aux | grep -iE "felix|bird"
root       4204  0.0  0.0   4236   400 ?        Ss   Oct08   0:00 runsv felix
root       4207  0.0  0.0   4236   400 ?        Ss   Oct08   0:00 runsv bird
root       4208  0.0  0.0   4236   400 ?        Ss   Oct08   0:00 runsv bird6
root       4213  4.7  2.4 1510880 45552 ?       Sl   Oct08 105:50 calico-node -felix
root       4412  0.0  0.0   1712   516 ?        S    Oct08   1:45 bird6 -R -s /var/run/calico/bird6.ctl -d -c /etc/calico/confd/config/bird6.cfg
root       4414  0.0  0.0   1828   780 ?        S    Oct08   1:51 bird -R -s /var/run/calico/bird.ctl -d -c /etc/calico/confd/config/bird.cfg

其中,felix进程负责监听etcd事件,获取路由、ARP 管理、ACL 管理和同步、状态上报等;而bird进程则负责把felix注入的路由信息通过BGP协议发送给其他机器,这样整个集群中都能获取到相关node的路由信息。


参考资料:

  1. https://docs.projectcalico.org/getting-started/kubernetes/self-managed-onprem/onpremises
  2. https://docs.projectcalico.org/reference/faq
  3. https://docs.projectcalico.org/reference/architecture/overview