本文由 CNCF + Alibaba 云原生技术公开课 整理而来
Kubernetes 网络模型演化
- Docker 网络:
容器网络发端于 Docker 的网络。Docker 使用了一个比较简单的网络模型,即内部的网桥加内部的保留 IP。这样设计的好处在于容器的网络和外部世界是解耦的,无需占用宿主机的 IP 或者宿主机的资源,完全是虚拟的。
它的设计初衷是:当需要访问外部世界时,会采用 SNAT 这种方法来借用 Node
的 IP 去访问外面的服务。比如容器需要对外提供服务的时候,所用的是 DNAT 技术,也就是在 Node
上开一个端口,然后通过 iptable 或者别的某些机制,把流量导入到容器的进程上以达到目的。
该模型的问题在于,外部网络无法区分哪些是容器的网络与流量、哪些是宿主机的网络与流量。比如,如果要做一个高可用的时候,172.16.1.1 和 172.16.1.2 是拥有同样功能的两个容器,此时我们需要将两者绑成一个 Group 对外提供服务,而此时从外部看来两者没有相同之处,它们的 IP 都是借用宿主机的端口,因此很难将两者归拢到一起。
- Kubernetes 网络:
在此基础上,Kubernetes 提出了这样一种机制:即每一个 Pod,也就是一个功能聚集小团伙应有自己的“身份证”,或者说 ID。在 TCP 协议栈上,这个 ID 就是 IP。
这个 IP 是真正属于该 Pod
的,外部世界不管通过什么方法一定要给它。对这个 Pod
IP 的访问就是真正对它的服务的访问,中间拒绝任何的变造。比如以 10.1.1.1 的 IP 去访问 10.1.2.1 的 Pod,结果到了 10.1.2.1 上发现,它实际上借用的是宿主机的 IP,而不是源 IP,这样是不被允许的。Pod
内部会要求共享这个 IP,从而解决了一些功能内聚的容器如何变成一个部署的原子的问题。
剩下的问题就是部署手段。Kubernetes 对怎么实现这个模型其实是没有什么限制的,用 underlay
网络来控制外部路由器进行导流是可以的;如果希望解耦,用 overlay
网络在底层网络之上再加一层叠加网,这样也是可以的。总之,只要达到模型所要求的目的即可。
Pod 如何上网
容器网络的网络包究竟是怎么传送的?
可以从两个维度来看:
协议层次 网络拓扑
- 协议层次:
它和 TCP 协议栈的概念是相同的,需要从两层、三层、四层一层层地摞上去,发包的时候从右往左,即先有应用数据,然后发到了 TCP 或者 UDP 的四层协议,继续向下传送,加上 IP 头,再加上 MAC 头就可以送出去了。收包的时候则按照相反的顺序,首先剥离 MAC 的头,再剥离 IP 的头,最后通过协议号在端口找到需要接收的进程。
- 网络拓扑:
一个容器的包所要解决的问题分为两步:第一步,如何从容器的空间 (c1) 跳到宿主机的空间 (infra);第二步,如何从宿主机空间到达远端。
个人的理解是,容器网络的方案可以通过接入、流控、通道这三个层面来考虑。
接入,就是说容器和宿主机之间是使用哪一种机制做连接,比如 Veth + bridge、Veth + pair 这样的经典方式, 也有利用高版本内核的新机制等其他方式(如 mac/IPvlan 等),来把包送入到宿主机空间; 流控,就是说方案要不要支持 Network Policy,如果支持的话又要用何种方式去实现。这里需要注意的是,实现方式一定需要在数据路径必经的一个关节点上。 如果数据路径不通过该 Hook 点,那就不会起作用; 通道,即两个主机之间通过什么方式完成包的传输。有很多种方式,比如以路由的方式,具体又可分为 BGP 路由或者直接路由,还有各种各样的隧道技术等等。 最终实现的目的就是一个容器内的包通过容器,经过接入层传到宿主机,再穿越宿主机的流控模块(如果有)到达通道送到对端。
Service 如何工作
Service
其实是一种负载均衡 (Load Balance) 的机制。
可以认为它是一种用户侧(Client Side) 的负载均衡,也就是说 VIP 到 RIP 的转换在用户侧就已经完成了,并不需要集中式地到达某一个 NGINX 或者是一个 ELB 这样的组件来进行决策。
Service
的实现是这样的:首先是由一群 Pod
组成一组功能后端,再在前端上定义一个虚 IP 作为访问入口。一般来说,由于 IP 不太好记,因此还会附赠一个 DNS 的域名,Client 先访问域名得到虚 IP 之后再转成实 IP。Kube-Proxy
则是整个机制的实现核心,它隐藏了大量的复杂性。它的工作机制是通过 ApiServer
监控 Pod/Service
的变化(比如是不是新增了 Service
、Pod
)并将其反馈到本地的规则或者是用户态进程。
Service 类型
Service
类型可以分为以下 4 类。
ClusterIP
:
Kubernetes 集群内部的一个虚拟 IP,这个 IP 会绑定到一堆服务的 Group Pod
上面,这也是默认的服务方式。它的缺点是这种方式只能在 Node
内部也就是集群内部使用。
NodePort
:
供集群外部调用。类似于容器的端口映射,将 Service
承载在 Node
的静态端口上,端口号和 Service
一一对应,那么集群外的用户就可以通过 的方式调用到 Service
。
LoadBalancer
:
给云厂商的扩展接口。像阿里云、亚马逊这样的云厂商都是有成熟的 LB 机制的,这些机制可能是由一个很大的集群实现的,为了不浪费这种能力,云厂商可通过这个接口进行扩展。它首先会自动创建 NodePort
和 ClusterIP
这两种机制,云厂商可以选择直接将 LB 挂到这两种机制上,或者两种都不用,直接把 Pod
的 RIP 挂到云厂商的 ELB 的后端也是可以的。
ExternalName
:
摈弃内部机制,依赖外部设施,比如某个用户特别强,他觉得内部提供的都没什么用,就是要自己实现,此时一个 Service
会和一个域名一一对应起来,整个负载均衡的工作都是外部实现的。
下图是一个实例。它灵活地应用了 ClusterIP
、NodePort
等多种服务方式,又结合了云厂商的 ELB,变成了一个很灵活、极度伸缩、生产上真正可用的一套系统。
首先用 ClusterIP
来做功能 Pod
的服务入口。可以看到,如果有三种 Pod
的话,就有三个 Service ClusterIP
作为它们的服务入口。这些方式都是 Client 端的,如何在 Server 端做一些控制呢?
首先会起一些 Ingress
的 Pod
(Ingress
是 Kubernetes 新增的一个资源对象,本质上还是一堆同质的 Pod
),然后将这些 Pod
组织起来,暴露到一个 NodePort
的 IP,Kubernetes 的工作到此就结束了。
任何一个用户访问 23456 端口的 Pod
就会访问到 Ingress
的服务,它的后面有一个 Controller
,会把 Service IP
和 Ingress
的后端进行管理,最后会调到 ClusterIP
,再调到功能 Pod
。前面提到去对接云厂商的 ELB,可以让 ELB 去监听所有集群节点上的 23456 端口,只要在 23456 端口上有服务的,就认为有一个 Ingress
的实例在跑。
整个的流量经过外部域名的一个解析跟分流到达了云厂商的 ELB,ELB 经过负载均衡并通过 NodePort
的方式到达 Ingress
,Ingress
再通过 ClusterIP
调用到后台真正的 Pod
。这种系统看起来比较丰富,健壮性也比较好。任何一个环节都不存在单点的问题,任何一个环节也都有管理与反馈。