云原生環境網絡方案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是一致的。