CNI

# 浅谈容器网络 所谓“网络栈”,就包括了:网卡(Network Interface)、回环设备(Loopback Device)、路由表(Routing Table)和 iptables 规则。对于一个进程来说,这些要素,其实就构成了它发起和响应网络请求的基本环境。 需要指出的是,作为一个容器,它可以声明直接使用宿主机的网络栈(–net=host),即:不开启 Network Namespace,比如: ``` $ docker run –d –net=host --name nginx-host nginx ``` 在这种情况下,这个容器启动后,直接监听的就是宿主机的 80 端口。 像这样直接使用宿主机网络栈的方式,虽然可以为容器提供良好的网络性能,但也会不可避免地引入共享网络资源的问题,比如端口冲突。所以,在大多数情况下,我们都希望容器进程能使用自己 Network Namespace 里的网络栈,即:拥有属于自己的 IP 地址和端口。 这时候,一个显而易见的问题就是:这个被隔离的容器进程,该如何跟其他 Network Namespace 里的容器进程进行交互呢? 为了理解这个问题,你其实可以把每一个容器看做一台主机,它们都有一套独立的“网络栈”。 如果你想要实现两台主机之间的通信,最直接的办法,就是把它们用一根网线连接起来;而如果你想要实现多台主机之间的通信,那就需要用网线,把它们连接在一台交换机上。 在 Linux 中,能够起到虚拟交换机作用的网络设备,是网桥(Bridge)。它是一个工作在数据链路层(Data Link)的设备,主要功能是根据 MAC 地址学习来将数据包转发到网桥的不同端口(Port)上。 而为了实现上述目的,Docker 项目会默认在宿主机上创建一个名叫 docker0 的网桥,凡是连接在 docker0 网桥上的容器,就可以通过它来进行通信。可是,我们又该如何把这些容器“连接”到 docker0 网桥上呢? 这时候,我们就需要使用一种名叫 Veth Pair 的虚拟设备了。Veth Pair 设备的特点是:它被创建出来后,总是以两张虚拟网卡(Veth Peer)的形式成对出现的。并且,从其中一个“网卡”发出的数据包,可以直接出现在与它对应的另一张“网卡”上,哪怕这两个“网卡”在不同的 Network Namespace 里。 这就使得 Veth Pair 常常被用作连接不同 Network Namespace 的“网线”。比如,现在我们启动了一个叫作 nginx-1 的容器: ``` $ docker run –d --name nginx-1 nginx ``` 然后进入到这个容器中查看一下它的网络设备: ``` # 在宿主机上 $ docker exec -it nginx-1 /bin/bash # 在容器里 root@2b3c181aecf1:/# ifconfig eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500 inet 172.17.0.2 netmask 255.255.0.0 broadcast 0.0.0.0 inet6 fe80::42:acff:fe11:2 prefixlen 64 scopeid 0x20<link> ether 02:42:ac:11:00:02 txqueuelen 0 (Ethernet) RX packets 364 bytes 8137175 (7.7 MiB) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 281 bytes 21161 (20.6 KiB) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536 inet 127.0.0.1 netmask 255.0.0.0 inet6 ::1 prefixlen 128 scopeid 0x10<host> 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 $ route Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface default 172.17.0.1 0.0.0.0 UG 0 0 0 eth0 172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 eth0 ``` 可以看到,这个容器里有一张叫作 eth0 的网卡,它正是一个 Veth Pair 设备在容器里的这一端。(Veth Pair设备,相当于虚拟的网线,把docker内网卡和宿主机上的docker0网桥绑定一起,是成对出现的) 通过 route 命令查看 nginx-1 容器的路由表,我们可以看到,这个 eth0 网卡是这个容器里的默认路由设备;所有对 172.17.0.0/16 网段的请求,也会被交给 eth0 来处理(第二条 172.17.0.0 路由规则)。 而这个 Veth Pair 设备的另一端,则在宿主机上。你可以通过查看宿主机的网络设备看到它,如下所示: ``` # 在宿主机上 $ ifconfig ... docker0 Link encap:Ethernet HWaddr 02:42:d8:e4:df:c1 inet addr:172.17.0.1 Bcast:0.0.0.0 Mask:255.255.0.0 inet6 addr: fe80::42:d8ff:fee4:dfc1/64 Scope:Link UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 RX packets:309 errors:0 dropped:0 overruns:0 frame:0 TX packets:372 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:0 RX bytes:18944 (18.9 KB) TX bytes:8137789 (8.1 MB) veth9c02e56 Link encap:Ethernet HWaddr 52:81:0b:24:3d:da inet6 addr: fe80::5081:bff:fe24:3dda/64 Scope:Link UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 RX packets:288 errors:0 dropped:0 overruns:0 frame:0 TX packets:371 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:0 RX bytes:21608 (21.6 KB) TX bytes:8137719 (8.1 MB) $ brctl show bridge name bridge id STP enabled interfaces docker0 8000.0242d8e4dfc1 no veth9c02e56 ``` 通过 ifconfig 命令的输出,你可以看到,nginx-1 容器对应的 Veth Pair 设备,在宿主机上是一张虚拟网卡。它的名字叫作 veth9c02e56。并且,通过 brctl show 的输出,你可以看到这张网卡被“插”在了 docker0 上。 这时候,如果我们再在这台宿主机上启动另一个 Docker 容器,比如 nginx-2: ``` $ docker run –d --name nginx-2 nginx $ brctl show bridge name bridge id STP enabled interfaces docker0 8000.0242d8e4dfc1 no veth9c02e56 vethb4963f3 ``` 你就会发现一个新的、名叫 vethb4963f3 的虚拟网卡,也被“插”在了 docker0 网桥上。这时候,如果你在 nginx-1 容器里 ping 一下 nginx-2 容器的 IP 地址(172.17.0.3),就会发现同一宿主机上的两个容器默认就是相互连通的,当你在 nginx-1 容器里访问 nginx-2 容器的 IP 地址(比如 ping 172.17.0.3)的时候,这个目的 IP 地址会匹配到 nginx-1 容器里的第二条路由规则。可以看到,这条路由规则的网关(Gateway)是 0.0.0.0,这就意味着这是一条直连规则,即:凡是匹配到这条规则的 IP 包,应该经过本机的 eth0 网卡,通过二层网络直接发往目的主机。 而要通过二层网络到达 nginx-2 容器,就需要有 172.17.0.3 这个 IP 地址对应的 MAC 地址。所以 nginx-1 容器的网络协议栈,就需要通过 eth0 网卡发送一个 ARP 广播,来通过 IP 地址查找对应的 MAC 地址。 备注:ARP(Address Resolution Protocol),是通过三层的 IP 地址找到对应的二层 MAC 地址的协议。 我们前面提到过,这个 eth0 网卡,是一个 Veth Pair,它的一端在这个 nginx-1 容器的 Network Namespace 里,而另一端则位于宿主机上(Host Namespace),并且被“插”在了宿主机的 docker0 网桥上。 一旦一张虚拟网卡被“插”在网桥上,它就会变成该网桥的“从设备”。从设备会被“剥夺”调用网络协议栈处理数据包的资格,从而“降级”成为网桥上的一个端口。而这个端口唯一的作用,就是接收流入的数据包,然后把这些数据包的“生杀大权”(比如转发或者丢弃),全部交给对应的网桥。 所以,在收到这些 ARP 请求之后,docker0 网桥就会扮演二层交换机的角色,把 ARP 广播转发到其他被“插”在 docker0 上的虚拟网卡上。这样,同样连接在 docker0 上的 nginx-2 容器的网络协议栈就会收到这个 ARP 请求,从而将 172.17.0.3 所对应的 MAC 地址回复给 nginx-1 容器。 有了这个目的 MAC 地址,nginx-1 容器的 eth0 网卡就可以将数据包发出去。 ![image.png](https://cos.easydoc.net/97954506/files/leobdsq7.png) 需要注意的是,在实际的数据传递时,上述数据的传递过程在网络协议栈的不同层次,都有 Linux 内核 Netfilter 参与其中。所以,如果感兴趣的话,你可以通过打开 iptables 的 TRACE 功能查看到数据包的传输过程,具体方法如下所示: ``` # 在宿主机上执行 $ iptables -t raw -A OUTPUT -p icmp -j TRACE $ iptables -t raw -A PREROUTING -p icmp -j TRACE ``` 通过上述设置,你就可以在 /var/log/syslog 里看到数据包传输的日志了。 熟悉了 docker0 网桥的工作方式,你就可以理解,在默认情况下,被限制在 Network Namespace 里的容器进程,实际上是通过 Veth Pair 设备 + 宿主机网桥的方式,实现了跟同其他容器的数据交换。 与之类似地,当你在一台宿主机上,访问该宿主机上的容器的 IP 地址时,这个请求的数据包,也是先根据路由规则到达 docker0 网桥,然后被转发到对应的 Veth Pair 设备,最后出现在容器里。这个过程的示意图,如下所示: ![image.png](https://cos.easydoc.net/97954506/files/leobh5wj.png) 同样地,当一个容器试图连接到另外一个宿主机时,比如:ping 10.168.0.3,它发出的请求数据包,首先经过 docker0 网桥出现在宿主机上。然后根据宿主机的路由表里的直连路由规则(10.168.0.0/24 via eth0)),对 10.168.0.3 的访问请求就会交给宿主机的 eth0 处理。 所以接下来,这个数据包就会经宿主机的 eth0 网卡转发到宿主机网络上,最终到达 10.168.0.3 对应的宿主机上。当然,这个过程的实现要求这两台宿主机本身是连通的。这个过程的示意图,如下所示: ![image.png](https://cos.easydoc.net/97954506/files/leobiog8.png) 所以说,当你遇到容器连不通“外网”的时候,你都应该先试试 docker0 网桥能不能 ping 通,然后查看一下跟 docker0 和 Veth Pair 设备相关的 iptables 规则是不是有异常,往往就能够找到问题的答案了。 不过,在最后一个“Docker 容器连接其他宿主机”的例子里,你可能已经联想到了这样一个问题:如果在另外一台宿主机(比如:10.168.0.3)上,也有一个 Docker 容器。那么,我们的 nginx-1 容器又该如何访问它呢? 这个问题,其实就是容器的“跨主通信”问题。在 Docker 的默认配置下,一台宿主机上的 docker0 网桥,和其他宿主机上的 docker0 网桥,没有任何关联,它们互相之间也没办法连通。所以,连接在这些网桥上的容器,自然也没办法进行通信了。 如果我们通过软件的方式,创建一个整个集群“公用”的网桥,然后把集群里的所有容器都连接到这个网桥上,不就可以相互通信了吗?这样一来,我们整个集群里的容器网络就会类似于下图所示的样子: ![image.png](https://cos.easydoc.net/97954506/files/leobk18s.png) 可以看到,构建这种容器网络的核心在于:我们需要在已有的宿主机网络上,再通过软件构建一个覆盖在已有宿主机网络之上的、可以把所有容器连通在一起的虚拟网络。所以,这种技术就被称为:Overlay Network(覆盖网络)。 而这个 Overlay Network 本身,可以由每台宿主机上的一个“特殊网桥”共同组成。比如,当 Node 1 上的 Container 1 要访问 Node 2 上的 Container 3 的时候,Node 1 上的“特殊网桥”在收到数据包之后,能够通过某种方式,把数据包发送到正确的宿主机,比如 Node 2 上。而 Node 2 上的“特殊网桥”在收到数据包后,也能够通过某种方式,把数据包转发给正确的容器,比如 Container 3。 甚至,每台宿主机上,都不需要有一个这种特殊的网桥,而仅仅通过某种方式配置宿主机的路由表,就能够把数据包转发到正确的宿主机上。 # CNI ## CNI Kubernetes网络模型设计的基础原则是: ●所有的Pod能够不通过NAT就能相互访问。 ●所有的节点能够不通过NAT就能相互访问。 ●容器内看见的IP地址和外部组件看到的容器IP是一样的。 Kubernetes的集群里,IP 地址是以Pod为单位进行分配的,每个Pod都拥有一个独立的IP地址。一个Pod内部的所有容器共享一个网络栈,即宿主机上的一个网络命名空间,包括它们的IP地址、网络设备、配置等都是共享的。 也就是说,Pod里面的所有容器能通过localhost:port来连接对方。在Kubernetes中,提供了一个轻量的通用容器网络接口CNI ( Container Network Interface),专门用于设置和删除容器的网络连通性。容器运行时通过CNI调用网络插件来完成容器的网络设置。 ### CNI插件运行机制 ![image.png](https://cos.easydoc.net/97954506/files/l1u960gv.png) ### CNI的运行机制 关于容器网络管理,容器运行时一般需要配置两个参数--cni-bin-dir和--cni-conf-dir。有一种特殊情况,kubelet 内置的Docker作为容器运行时,是由kubelet来查找CNI插件的,运行插件来为容器设置网络,这两个参数应该配置在kubelet处: ●cni-bin-dir:网络插件的可执行文件所在目录。默认是/opt/cni/bin。 ●cni-conf-dir: 网络插件的配置文件所在目录。默认是/etc/cni/net.d. ### CNI插件设计考量 ![image.png](https://cos.easydoc.net/97954506/files/l1u987qp.png) ![image.png](https://cos.easydoc.net/97954506/files/l1u98igy.png) ## 打通主机层网络 CNI插件外,Kubernetes还需要标准的CNI插件lo,最低版本为0.2.0版本。网络插件除支持设置和清理Pod网络接口外,该插件还需要支持Iptables。如果Kube-proxy工作在Iptables模式,网络插件需要确保容器流量能使用lptables转发。 例如,如果网络插件将容器连接到Linux网桥,必须将net/ bridge/bridge-nf-call-iptables参数sysctl设置为1,网桥上数据包将遍历Iptables规则。 如果插件不使用Linux桥接器(而是类似Open vSwitch或其他某种机制的插件),则应确保容器流量被正确设置了路由。 ## CNI插件 ContainerNetworking组维护了一些CNI插件,包括网络接口创建的bridge、ipvlan、 loopback、macvlan、ptp、host-device 等,IP 地址分配的DHCP、host-local 和static,其他的Flannel、tunning、portmap、firewall 等。 社区还有些第三方网络策略方面的插件,例如Calico、Cilium 和Weave等。可用选项的多样性意味着大多数用户将能够找到适合其当前需求和部署环境的CNI插件,并在情况变化时迅捷转换解决方案。 ### flannel Flannel是由CoreOS开发的项目,是CNI插件早期的入门产品,简单易用。 Flannel使用Kubernetes集群的现有etcd集群来存储其状态信息,从而不必提供专用的数据存储,只需要在每个节点上运行flanneld来守护进程。 每个节点都被分配一个子网,为该节点上的Pod分配IP地址。 同一主机内的Pod可以使用网桥进行通信,而不同主机上的Pod将通过flanneld将其流量封装在UDP数据包中,以路由到适当的目的地。 封装方式默认和推荐的方法是使用VxLAN,因为它具有良好的性能,并且比其他选项要少些人为干预。 虽然使用VxLAN之类的技术封装的解决方案效果很好,但缺点就是该过程使流量跟踪变得困难。 ![image.png](https://cos.easydoc.net/97954506/files/l1uc5362.png) ### Calico Calico以其性能、灵活性和网络策略而闻名,不仅涉及在主机和Pod之间提供网络连接,而且还涉及网络安全性和策略管理。 对于同网段通信,基于第3层,Calico 使用BGP路由协议在主机之间路由数据包,使用BGP路由协议也意味着数据包在主机之间移动时不需要包装在额外的封装层中。 对于跨网段通信,基于IPinIP使用虚拟网卡设备tunl0,用一个IP数据包封装另一个IP数据包,外层IP数据包头的源地址为隧道入口设备的IP地址,目标地址为隧道出口设备的IP地址。 网络策略是Calico最受欢迎的功能之一,使用ACLS协议和kube-proxy来创建iptables过滤规则,从而实现隔离容器网络的目的。 此外,Calico 还可以与服务网格lstio集成,在服务网格层和网络基础结构层.上解释和实施集群中工作负载的策略。这意味着你可以配置功能强大的规则,以描述Pod应该如何发送和接收流量,提高安全性及加强对网络环境的控制。 Calico属于完全分布式的横向扩展结构,允许开发人员和管理员快速和平稳地扩展部署规模。对于性能和功能(如网络策略)要求高的环境,Calico 是一个不错选择。 ![image.png](https://cos.easydoc.net/97954506/files/l1uc8e1o.png) ![image.png](https://cos.easydoc.net/97954506/files/l1ucki6b.png) Calico是一种容器之间互通的网络方案。在虚拟化平台中,比如OpenStack,docker等都需要实现workloads之间互联,但同时也需要对容器做隔离控制。而在多数的虚拟化平台实现中,通常都使用二层隔离技术来实现容器的网络,这些网络技术有一些弊端,比如需依赖VLAN,bridge和隧道等技术,其中bridge带来复杂性,VLAN隔离和tunnel隧道在拆包或者加包头市,则消耗更多的资源并对物理环境也有要求。随着网络规模的增大,整体会更加复杂。 Calico 把主机当做Internet中的路由器,使用BGP同步路由,并使用iptables来做安全访问策略。 设计思路:Calico不使用隧道或NAT来实现转发,而是巧妙地把所有的二三层流量转换成三层流量,并通过主机上路由配置完成夸主机转发。 ### k8s 组网方案对比 flannel方案︰需要在每个节点上把发向容器的数据包进行封装后,再用隧道将封装后的数据包发送到运行着目标Pod的node节点上。目标node节点再负责去掉封装,将去除封装的数据包发送到目标Pod 上。数据通信性能则大受影响。 Overlay方案∶在下层主机网络的上层,基于隧道封装机制,搭建层叠网络,实现跨主机的通信,Overlay无疑是架构最简单清晰的网络实现机制,但数据通信性能则大受影响。 Calico方案:在k8s多个网路解决方案中选择了延迟表现最好的calico方案。 总结:Flannel网络非常类似于Docker网络的Overlay驱动,基于二层的层叠网络。 层叠网络的优势: 1. 对底层网络依赖较少,不管底层是物理网络还是虚拟网络,对层叠网络的配置管理影响较少; 2. 配置简单,逻辑清晰,易于理解和学习,非常适用于开发测试等对网络性能要求不高的场景。 层叠网络的劣势: 1. 网络封装是一种传输开销,对网络性能会有影响,不适用于对网络性能要求高的生产场景; 2. 由于对底层网络结构缺乏了解,无法做到真正有效的流量工程控制,也会对网络性能产生影响;3.某些情况下也不能完全做到与下层网络无关,例如隧道封装会对网络的MTU限制产生影响。