云原生环境网络方案1 --- 容器网络模型与K8S网络模型
K8S系统将网络方面的功能托管给了第三方插件来完成,本文将简略描述其基本通讯原理以及常见的几种网络方案。
Docker网络通讯模型
Docker容器的网络设置有4种形式,去除None和共享Namespace之外,其实只有两种网络即:
主机网络
Bridge网络
其中Bridge是Docker容器的默认网络。
在任何安装了Docker的宿主机环境上,我们都能发现系统中新增了一个名为Docker0的网桥设备。
$ brctl show
bridge name bridge id STP enabled interfaces
docker0 8000.024204b14ad5 no veth9d63d04 vethbf226b3
这个名为Docker0的网桥是默认用来给Docker容器通信用的。在容器创建过程中,默认会产生一对虚拟网口veth,一头连在Docker0上,另外一头连接在容器内的eth0上。如下图:
我们查看一下这个eth0的ip地址
$ sudo docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
157545493a1b nginx:latest "/docker-en.…" 26 minutes ago Up 26 minutes 0.0.0.0:80->80/tcp nginx_demo
$ sudo docker container inspect 157545493a1b
...
"Gateway": "172.18.0.1",
"GlobalIPv6Address": "",
"GlobalIPv6PrefixLen": 0,
"IPAddress": "172.18.0.2",
"IPPrefixLen": 16,
"IPv6Gateway": "",
"MacAddress": "02:42:ac:12:00:03",
"Networks": {
"bridge": {
"IPAMConfig": null,
"Links": null,
"Aliases": null,
"NetworkID": "91397797f09b069b9472a638861e21e9c0e861c14d2374cec9184997b52a2ba1",
"EndpointID": "8e8230218df826a1005625a09398785833497d79af2c69b0b82db37eaa0798a2",
"Gateway": "172.18.0.1",
"IPAddress": "172.18.0.2",
"IPPrefixLen": 16,
"IPv6Gateway": "",
"GlobalIPv6Address": "",
"GlobalIPv6PrefixLen": 0,
"MacAddress": "02:42:ac:12:00:03",
"DriverOpts": null
}
}
可以看到这个容器的eth0的IP地址被设置为了172.18.0.2
,而网关被设置为了172.18.0.1
。那这个网关的地址是谁的呢?
$ ifconfig
docker0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 172.18.0.1 netmask 255.255.0.0 broadcast 172.18.255.255
inet6 fe80::42:4ff:feb1:4ad5 prefixlen 64 scopeid 0x20<link>
ether 02:42:04:b1:4a:d5 txqueuelen 0 (Ethernet)
RX packets 494147 bytes 23247538 (23.2 MB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 886037 bytes 1319084427 (1.3 GB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
在宿主机中使用ifconfig
命令,我们可以看到一个名为docker0的设备其网络地址为172.18.0.1
,正是容器设置里的网关地址。在Linux环境中,可以为网桥配置网络地址,其实质上是一个名为Docker0的虚拟网卡设备插在Docker0网桥上,做为一个可参与路由的节点:
$ route -n
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 172.17.1.1 0.0.0.0 UG 0 0 0 ens160
172.17.1.0 0.0.0.0 255.255.255.0 U 0 0 0 ens160
172.18.0.0 0.0.0.0 255.255.0.0 U 0 0 0 docker0
如果此时,我再启动另外一个容器,这个容器的eth0的地址为172.18.0.3
,那这两个容器中,相互是可以ping通的。那么他们之间的网络拓扑如下图:
此时,数据包还是只能在宿主机内部传输,我们启动的这个容器是一个nginx web服务器,从上面的容器信息输出可以看到0.0.0.0:80->80/tcp
,其实现上是通过NAT进行的,从刚才的输出可以看到,宿主机的80端口被映射到了容器的80端口上。查看一下宿主机的NAT表,我们可以看到这个映射:
$ sudo iptables -L -n -t nat
...
Chain POSTROUTING (policy ACCEPT)
target prot opt source destination
MASQUERADE all -- 172.18.0.0/16 0.0.0.0/0
MASQUERADE tcp -- 172.18.0.2 172.18.0.2 tcp dpt:80
Chain DOCKER (2 references)
target prot opt source destination
RETURN all -- 0.0.0.0/0 0.0.0.0/0
DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:80 to:172.18.0.2:80
可以看到,Docker网络模型的设计,采用了私网地址+Linux 网桥的方法与外部外诺做解耦,流量进入宿主机时,使用DNAT方式映射端口,容器网络内部出去则需要做SNAT借用宿主机的网络地址。由于使用了大量的NAT映射,大规模网络出现问题的情况下,定位是非常困难的。
另外一种使用主机网络的方案,和直接跑一个程序在宿主机上是完全一致的,本文不做赘述,Docker的视角实际上只着眼于宿主机内部的网络通讯,跨宿主机通讯的场景实际上并没有考虑的。这样的考量应当是对多宿主机组成的容器云进行调度和编排的方案需要考虑的,比如K8S。
K8S跨宿主机网络通讯模型
上一节提到的是同一个宿主机内部容器间的通信,本节描述跨宿主机通讯的几种方式。
目前,跨宿主机通讯在网络层面一般有两种即:
- 基于隧道的宿主机间通讯
- 基于路由的宿主机间通讯
这两种方式又被称为overlayer和underlayer,其区别在于容器网络和宿主机网络是否在同一层面。
基于隧道的宿主机通讯
其中基于隧道的宿主机间通讯可能有多种类型,目前最常见的是基于VxLAN的隧道,这也是虚拟化网络的常规方案。以flannel的VxLAN方案为例。
在每台宿主机上,会有一个VxLAN的VTEP虚拟设备flannel.1,其负责与其他VTEP设备打通,构成2层VxLAN虚拟交换网络。以上图为例,当宿主机2加入到网络中时,宿主机1上会有如下路由信息:
$ route -n
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
...
172.18.1.0 172.18.1.0 255.255.255.0 UG 0 0 0 flannel.1
其中172.18.1.0是宿主机2的VTEP设备的网络地址。宿主机1上所有发往该网段的网络包会发往VTEP设备flannel.1,然后走VxLAN隧道发往宿主机2。
flannel的隧道方案除了VxLAN以外还有一种基于存UDP的早期方案,由于性能问题,目前已被废弃。
基于路由的宿主机通讯
上述基于隧道的宿主机间通讯,由于多了两次拆解包的过程,性能会有所下降。据有关数据表明,网络传输效率下降约20%-30%。除了这种通讯方式之外,还有一种基于路由的跨宿主机通信方式。例如除了上节提到的基于VxLAN隧道的方案外,flannel还有一种基于host-gateway的3层路由方案。其原理比较简单,即将各个宿主机网络进行子网划分,并在每台宿主机上设置相同的路由,类似以下:
$ ip route
...
172.18.0.0/24 via 172.17.1.41 dev ens160
172.18.1.0/24 via 172.17.1.42 dev ens160
利用Linux的路由功能实现容器的跨宿主机通信。剩下的问题就是如何保证各个宿主机上的路由表一致了。不同的插件方案使用的解决方案是不同的。例如flannel使用了etcd来存储同步路由信息,而Calico则使用了BGP来实现这一点。除此以外,Calico方案也不会在宿主机上建立任何网桥设备,这一点与flannel有很大区别。在Calico方案组veth-pair的另一端不会被插入到任何网桥设备中,而只是放在宿主机的网络命名空间内。
172.18.2.2 dev cali3863f3 scope link
宿主机上会有类似以上的路由记录,将相关数据包发送至虚拟设备cali3863f3上。
与flannel方案相比,由于没了虚拟网桥连接同一宿主机内各个容器,Calico方案需要在宿主机的路由表中添加多得多的路由表项。
基于路由的宿主机通信要求各个宿主机在二层网络层面是直连互通的,对底层网络有一定的要求。
常见容器网络插件
由于k8s将网络这部分开放给社区,目前CNI这部分的插件较多,常见的有如下几种:
- Flannel,目前最普遍的实现,同时支持overlayer(UDP,VxLAN)和underlayer(Host-gw)的网络后端实现。
- Calico,基于BGP的underlayer方案,功能丰富,对底层网络有一定要求。
- Cilium,基于eBPF和XDP的高性能Overlayer方案
- Kube-Route,采用BGP提供网络直连,集成基于LVS的负载均衡能力
- WeaveNet,采用UDP封装实现的L2 Overlayer方案,支持用户态(慢,可加密)和内核态(快,无加密)两种实现
总结
宿主机内部的容器间网络通讯有基于网桥的,基于宿主机网络的。跨宿主机容器间网络通讯有Overlayer和Underlayer两种,区别在于容器间通讯是否和宿主机间通讯属于同一层面。
现有普适性最强的方案是Flannel,同时具备Overlayer和Underlayer的方案,为了统一架构,宿主机内部通讯使用了Linux虚拟网桥方案。需要注意的是,Flannel方案不支持K8S的NetworkPolicy,需要其他CNI插件配合,比如Calico。
Calico方案是目前比较成熟的Underlayer方案,其采用了BGP来同步多宿主机的路由表。
本文讨论的是网络层的解决方案,着眼于从网络层面联通各个容器的网络,至于访问控制,熔断,限流等内容是在其他组件比如Service Proxy,Service Mesh,API Gateway来实现的,不在本文讨论范围内。
最后,我们回顾一下K8S网络模型的3原则:
- 任意两个容器(或者POD-共享网络命名空间的一组容器)之间可以直接通讯,无需显式NAT。
- 宿主机与容器(POD)之间也是可以直接通讯,无需显式NAT。
- 容器(POD)看到自己的IP和其他人看到它的IP是一致的。