一篇文章围绕一张图,讲述一个主题。今天聊聊K8s的网络模型。
网络是K8s的核心部分,但想要理解它是如何工作的却有点难度。
K8s定义了一个网络模型,目的是给Pod,service等使用者提供一个简单、一致的网络视图和使用体验,对它们屏蔽宿主机环境的网络拓扑的同时,也屏蔽网络模型实现上的细节。
本文尝试从K8s网络模型角度,以鸟瞰视角瞧瞧K8s典型的各种网络通信方案。既然是鸟瞰图,就不要在乎细节。多年搬砖、掉坑、爬坑的经验告诉我,无论是游历一个新地方还是学一个新东西,首要的是抓大放小。这倒不是说细节不重要,只是带着全局的思维研究一个东西时,不容易迷路也不容易掉入细节的深井。天平的左边是全局,右边是细节,在全局和细节之间,我们最终会找到一个平衡点,但现在让我们从把天平偏向左边开始。
K8s网络模型
简单来说,Kubernetes引入的网络模型提出了下列基本要求。只要满足了这些要求,即可成为一个K8s网络方案供应商。
- 每个Pod都有自己单独的IP地址
- Pod内部的所有容器共享Pod的IP地址且可以相互自由通信
- 一个Node上的容器可以使用IP地址和其它Node上面的容器通信,且不需要通过NAT 注意:这里只是说Pods之间跨Node通信时不可以用NAT,但是Pod访问其它实体比如google.com时可以使用NAT
- 如果Pod使用宿主机网络环境,那么跨Node的容器间可以使用IP地址进行通信,且不需要通过NAT 像Linux这种可以直接让Pod使用宿主机网络环境的平台,跨Node的容器间间通信也不可以通过NAT
- 一个Node上面的agent(比如system daemon, kubelet等)可以使用IP地址和位于该Node上面的所有容器通信,且不需要通过NAT
- Pod之间容器通信所涉及到的隔离问题通过Network policy解决
这种类型的网络模型也被称作为"扁平网络"。下图展示了这样的扁平网络。同时它也画出了宿主机环境既可能是二层互通的也可能是三层可达的这样一个事实。
图:K8s扁平网络模型
我们来细品这些要求。容器之间可以通过IP通信,且不能NAT,至少说明以下两点:
- 不能NAT意味着Pod自己看自己的IP和别人(宿主机上面的agent或者其它Pod)看到自己的IP是一样的,对,一眼看穿、看懂对方的那种。而与此对应的是,如有NAT在捣鬼的话,当企业内部的机器访问躲在Nginx后面的服务时,二者相互看不清对方的本来面目。
- 容器之间IP互通,也就间接要求了宿主机之间是三层可达的。为什么呢?如果是宿主机环境是二层网络,那么天生就是三层可达的,但如果二层不通的话,就需要三层可达,不然从一个Pod发出的数据不是被憋死在宿主机上面了吗?对于在阿里云或腾讯云上租借VM作为Node来搭建K8s集群这样一个典型的使用场景来讲,更方便的姿势是直接将这些租借的VM置于同一个subnet。
Pod可以被当成是有独立、唯一IP地址的VM或者主机。此时Pod内的容器就很像VM或主机上的进程,它们都跑在相同的network namespace里面且共享同一个IP地址。当需要把运营环境从VM或主机迁移到Kubernetes时,采用这个模型会使得迁移前后,无论是RD还是OP对网络部分的理解相对一致,平滑地过度而不至于出现剧烈的变化。
另外,因为K8s网络隔离是通过network policy完成的,而不是基于网络拓扑,这样便于理解和维护。
可以看到K8s网络模型只是要求了容器间可以直接用IP地址自由地通信,但并没有强制要求Pod IP在K8s网络边界之外也可以路由。是的,K8s说"我只要扁平,剩下的我不管"。说到"K8s网络边界"也就引出了一个重要的概念:K8s网络和宿主机网络。
宿主机网络:组成K8s集群的各个Node之间在没有安装K8s前就已经存在的网络拓扑。比如通过subnet将所有的Node放到一个LAN里面,或通过VLAN将其分属不同的子网但三层可通信,甚至让它们分布在同一个Region但不同的Zone里面。
K8s网络:特点是扁平化,可以直接使用宿主机网络,也可以在每个Node上以一个bridge为网关管理该Node上的所有Pod。
K8s网络和宿主机网络之间是有明显的边界存在的,当容器在跨Node间通信时,traffic会在这两个环境间来回穿梭跳跃。当我们审视K8s集群相关的traffic时,比较好的方式是提醒自己:traffic目前在什么位置?是在K8s网络内部还是已经流出K8s网络到了宿主机网络环境?如果我们把出入K8s集群的traffic(以太帧)看成是导弹,把K8s网络看成大气层,而把宿主机网络看成太空的话,那么这个以太帧的流动轨迹就和德国火箭科学家桑格尔提出的再入弹跳式弹道有点类似。
图:钱学森—桑格尔弹道
一般会将宿主机网络称为Underyling network,在其之上的K8s网络方案虽然看起来变化多端,但实现方式无外乎以下几种模式:
- Overlay networks模式
- 直接路由Pod IP模式
- Underlay模式
CNI
在CNI标准出来之前的两个月,Docker公司主持起草了一个叫CNM(Container Network Model)规范。但与CNI的开放性相比,Docker坚持CNM只能基于 Docker 来设计,可对于K8s而言,Docker只是它编排大业里面可选择的众多容器引擎之一而已,因而容器网络作为K8s最基础也是最重要的部分绝不可能绑定在Docker身上。于是K8s在经过一番研究以及挣扎之后,毅然决定放弃CNM,自立CNI。
我们今天依旧可以重温Kubernetes Network SIG 的Leader、Google 的工程师蒂姆·霍金(Tim Hockin)于2016年所写的文章:为何K8s不使用CNM https://kubernetes.io/blog/2016/01/why-kubernetes-doesnt-use-libnetwork/。行文中充满了所提Feature Request被Docker工程师无情拒绝时的无耐。其中提到一个非常有意思的细节:很多提交给Docker的问题,都被Docker RD以"working as intended"为由关闭。
时至今日,回望来时路,在当时弃CNM而扶持CNI是一件挺冒险的事情,但如今再看CNCF landscape "Cloud Native Network"部分,看看那众多支持CNI的网络方案,用"百花齐放"和"百家争鸣"来形容绝不为过。
图:CNCF landscape "Cloud Native Network"
典型的CNI实现方案
K8s内建了一个kubenet,它可以支持一些基本的网络连接。但更普遍的使用方式是用第三方的网络方案。只要它满足CNI(Container Network Interface) 规范就可以以插件的方式在K8s环境使用。
CNI插件的种类多种多样,但关键的功能不外乎以下两个:
- 网络插件,主要负责将Pod插入到K8s网络或从K8s网络删除。
- IP地址管理(IPAM, IP Address Management)插件,主要负责为Pod分配IP地址,并在Pod被销毁的时候回收IP。 相较于传统的on-premise 自购服务器部署服务或者云VM形式部署方式,以Pod为单位的部署方式使得微服务的横向扩展(scale out)、横向收缩(scale in)的程度空前剧烈,这就要求IPAM插件能够快速地响应Pod的这些变化。 顺便说一句,也正因为变化如此动荡、如此剧烈,使得传统的基于静态IP来配置安全策略的方法显得略为力不从心。
由于CNI的开放性和网络模式的简单性,可供选择的网络方案看起来多种多样,下面是几种在业界颇为流行的实现:
- Flannel VXLAN
- Flannel host-gw
- Calico IP in IP
- Calib BGP
我们从K8s网络的traffic离开宿主机时对宿主机环境二层、三层网络连通要求和是否需要封包两种角度来分别看看这几种实现。
我做了一个表格,方便你对比查看。
要求宿主机二层连通的方案
如果宿主机环境二层是通的,比如所有的宿主机都在同一个局域网内,则表示主机之间通信直接通过二层交换机转发数据包即可。
Flannel host-gw方案,属于"直接路由Pod IP"模式。它虽然是三层网络方案,但要求集群宿主机之间是必须是二层连通的。这看起来比较奇怪,其实非常好理解。这个方案得以正常工作的核心,是为每个容器的 IP 地址,找到它所对应的“下一跳”的网关。既然它将目标宿主机设为网关,那么要求网关和本机在二层是可达的也就显得不是那么奇怪了。
BGP 的全称是 Border Gateway Protocol,即:边界网关协议。Calico BGP方案实际上将集群里的所有Node,都当作是边界路由器来处理,且在Calico看来,每个Node都是一个自制系统(AS)的网关。这些Node一起组成了一个全连通的网络,互相之间通过 BGP 协议交换路由规则。
这些Node之间理所当然应该是三层可达的。但是实际转发workload traffic的时候,出现了一个问题,下面这条路由规则里的下一跳地址是17.168.0.3,可是如果它对应的 Node X 跟 Node 1 却根本不在一个子网里,则没办法通过二层网络把 IP 包发送到下一跳地址。
10.244.1.3/24 via 17.168.0.3 eth0
这就是为什么Calico BGP方案也要求二层是连通的。
说到这里,说个题外话:我们需要区分目的IP和next-hop IP的区别。 目的IP是数据包的最终目的地,在整个数据包传输过程中它是不会改变的。 而next-hop IP则随着数据包在internet上被网络设备中继时不断地修改,具体而言每个网路设备的路由规则会通过next-hop IP来决定网络包的dest MAC地址,也即目前数据包在我手上,下一个我要转交的那个人。
类似地,Flannel VXLAN也要求二层连通。
作为一般规则,当K8s网络的traffic离开宿主机时,如果下一跳或者网关是集群主机的IP地址,也即dest MAC是集群主机的MAC地址时,就需要宿主机环境二层是能直接连通的。
要求宿主机三层可达的方案
宿主机环境二层不连通,但三层是可达的,是另一个常见的场景,比如宿主机分别分布在不通的VLAN,主机之间通过三层交换机(路由器)转发数据包通信。
Calico IP in IP方案只需要宿主机环境三层可达即可。
图:IP in IP封包(图片选自https://en.wikipedia.org/wiki/IP_in_IP)
作为一般规则,当K8s网络的traffic离开宿主机时,如果下一跳或者网关不是集群主机的IP地址,就不需要二层连通,此时只要三层IP可达即可把以太帧路由到终点。
Flannel host-gw方案和Calico BGP方案均要求三层可达。
Overlay networks模式
对于K8s来说,使用Overlay network方案有下面的三大好处:
- 在Node间传递pod之间的网络流量时不用关心underlying network是什么
- 不用关心通信双方的Pod分别在哪个Node上
- underlying network也不需要关心Pod的IP地址
但这个世界上没有绝对的好事,Overlay networks虽然用得很舒服,但它涉及到封包(Encapsulation),性能损失大概20%-30%。
作为一般规则,当K8s网络的traffic途径宿主机网络中的路由器时不可以被正常路由的话,就需要考虑封包。典型的封包方案有VXLAN和IP-in-IP两种。
Flannel VXLAN使用的即为下图所示的VXLAN封包,也叫做Layer 2 over UDP。
图:VXLAN封包
Calico IP in IP封包示意图如前文所示。也叫做IP payload in outer IP package。
直接路由Pod IP模式
作为一般规则,如果K8s网络的traffic途径宿主机网络中的路由器可以被正常路由的话,就可以采用直接路由Pod IP方案。
Flannel host-gw和Calico BGP使用了这种模式,这种模式不需要将K8s网络的traffic封包,与VXLAN和IP in IP相比,性能损失大概只有10%。