Kubernetes 网络模型来龙去脉

Kubernetes 网络模型来龙去脉
文章图片

容器网络发端于Docker的网络 。 Docker使用了一个比较简单的网络模型 , 即内部的网桥加内部的保留IP 。 这种设计的好处在于容器的网络和外部世界是解耦的 , 无需占用宿主机的IP或者宿主机的资源 , 完全是虚拟的 。
它的设计初衷是:当需要访问外部世界时 , 会采用SNAT这种方法来借用Node的IP去访问外面的服务 。 比如容器需要对外提供服务的时候 , 所用的是DNAT技术 , 也就是在Node上开一个端口 , 然后通过iptable或者别的某些机制 , 把流导入到容器的进程上以达到目的 。
该模型的问题在于 , 外部网络无法区分哪些是容器的网络与流量、哪些是宿主机的网络与流量 。 比如 , 如果要做一个高可用的时候 , 172.16.1.1和172.16.1.2是拥有同样功能的两个容器 , 此时我们需要将两者绑成一个Group对外提供服务 , 而这个时候我们发现从外部看来两者没有相同之处 , 它们的IP都是借用宿主机的端口 , 因此很难将两者归拢到一起 。
Kubernetes 网络模型来龙去脉
文章图片

【Kubernetes 网络模型来龙去脉】在此基础上 , Kubernetes提出了这样一种机制:即每一个Pod , 也就是一个功能聚集小团伙应有自己的“身份证” , 或者说ID 。 在TCP协议栈上 , 这个ID就是IP 。
这个IP是真正属于该Pod的 , 外部世界不管通过什么方法一定要给它 。 对这个PodIP的访问就是真正对它的服务的访问 , 中间拒绝任何的变造 。 比如以10.1.1.1的IP去访问10.1.2.1的Pod , 结果到了10.1.2.1上发现 , 它实际上借用的是宿主机的IP , 而不是源IP , 这样是不被允许的 。 Pod内部会要求共享这个IP , 从而解决了一些功能内聚的容器如何变成一个部署的原子的问题 。
剩下的问题是我们的部署手段 。 Kubernetes对怎么实现这个模型其实是没有什么限制的 , 用underlay网络来控制外部路由器进行导流是可以的;如果希望解耦 , 用overlay网络在底层网络之上再加一层叠加网 , 这样也是可以的 。 总之 , 只要达到模型所要求的目的即可 。
Pod究竟如何上网
容器网络的网络包究竟是怎么传送的?
Kubernetes 网络模型来龙去脉
文章图片

我们可以从以下两个维度来看:
协议层次网络拓扑1.协议层次它和TCP协议栈的概念是相同的 , 需要从两层、三层、四层一层层地摞上去 , 发包的时候从右往左 , 即先有应用数据 , 然后发到了TCP或者UDP的四层协议 , 继续向下传送 , 加上IP头 , 再加上MAC头就可以送出去了 。 收包的时候则按照相反的顺序 , 首先剥离MAC的头 , 再剥离IP的头 , 最后通过协议号在端口找到需要接收的进程 。
2.网络拓扑一个容器的包所要解决的问题分为两步:第一步 , 如何从容器的空间(c1)跳到宿主机的空间(infra);第二步 , 如何从宿主机空间到达远端 。
我个人的理解是 , 容器网络的方案可以通过接入、流控、通道这三个层面来考虑 。
第一个是接入 , 就是说我们的容器和宿主机之间是使用哪一种机制做连接 , 比如Veth+bridge、Veth+pair这样的经典方式 , 也有利用高版本内核的新机制等其他方式(如mac/IPvlan等) , 来把包送入到宿主机空间;第二个是流控 , 就是说我的这个方案要不要支持NetworkPolicy , 如果支持的话又要用何种方式去实现 。 这里需要注意的是 , 我们的实现方式一定需要在数据路径必经的一个关节点上 。 如果数据路径不通过该Hook点 , 那就不会起作用;第三个是通道 , 即两个主机之间通过什么方式完成包的传输 。 我们有很多种方式 , 比如以路由的方式 , 具体又可分为BGP路由或者直接路由 。 还有各种各样的隧道技术等等 。 最终我们实现的目的就是一个容器内的包通过容器 , 经过接入层传到宿主机 , 再穿越宿主机的流控模块(如果有)到达通道送到对端 。 3.一个最简单的路由方案:Flannel-host-gw这个方案采用的是每个Node独占网段 , 每个Subnet会绑定在一个Node上 , 网关也设置在本地 , 或者说直接设在cni0这个网桥的内部端口上 。 该方案的好处是管理简单 , 坏处就是无法跨Node迁移Pod 。 就是说这个IP、网段已经是属于这个Node之后就无法迁移到别的Node上 。
Kubernetes 网络模型来龙去脉
文章图片

这个方案的精髓在于route表的设置 , 如上图所示 。 接下来为大家一一解读一下 。
第一条很简单 , 我们在设置网卡的时候都会加上这一行 。 就是指定我的默认路由是通过哪个IP走掉 , 默认设备又是什么;第二条是对Subnet的一个规则反馈 。 就是说我的这个网段是10.244.0.0 , 掩码是24位 , 它的网关地址就在网桥上 , 也就是10.244.0.1 。 这就是说这个网段的每一个包都发到这个网桥的IP上;第三条是对对端的一个反馈 。 如果你的网段是10.244.1.0(上图右边的Subnet) , 我们就把它的Host的网卡上的IP(10.168.0.3)作为网关 。 也就是说 , 如果数据包是往10.244.1.0这个网段发的 , 就请以10.168.0.3作为网关 。再来看一下这个数据包到底是如何跑起来的?
假设容器(10.244.0.2)想要发一个包给10.244.1.3 , 那么它在本地产生了TCP或者UDP包之后 , 再依次填好对端IP地址、本地以太网的MAC地址作为源MAC以及对端MAC 。 一般来说本地会设定一条默认路由 , 默认路由会把cni0上的IP作为它的默认网关 , 对端的MAC就是这个网关的MAC地址 。 然后这个包就可以发到桥上去了 。 如果网段在本桥上 , 那么通过MAC层的交换即可解决 。
这个例子中我们的IP并不属于本网段 , 因此网桥会将其上送到主机的协议栈去处理 。 主机协议栈恰好找到了对端的MAC地址 。 使用10.168.0.3作为它的网关 , 通过本地ARP探查后 , 我们得到了10.168.0.3的MAC地址 。 即通过协议栈层层组装 , 我们达到了目的 , 将Dst-MAC填为右图主机网卡的MAC地址 , 从而将包从主机的eth0发到对端的eth0上去 。
所以大家可以发现 , 这里有一个隐含的限制 , 上图中的MAC地址填好之后一定是能到达对端的 , 但如果这两个宿主机之间不是二层连接的 , 中间经过了一些网关、一些复杂的路由 , 那么这个MAC就不能直达 , 这种方案就是不能用的 。 当包到达了对端的MAC地址之后 , 发现这个包确实是给它的 , 但是IP又不是它自己的 , 就开始Forward流程 , 包上送到协议栈 , 之后再走一遍路由 , 刚好会发现10.244.1.0/24需要发到10.244.1.1这个网关上 , 从而到达了cni0网桥 , 它会找到10.244.1.3对应的MAC地址 , 再通过桥接机制 , 这个包就到达了对端容器 。
大家可以看到 , 整个过程总是二层、三层 , 发的时候又变成二层 , 再做路由 , 就是一个大环套小环 。 这是一个比较简单的方案 , 如果中间要走隧道 , 则可能会有一条vxlantunnel的设备 , 此时就不填直接的路由 , 而填成对端的隧道号 。
Service究竟如何工作
Service其实是一种负载均衡(LoadBalance)的机制 。
我们认为它是一种用户侧(ClientSide)的负载均衡 , 也就是说VIP到RIP的转换在用户侧就已经完成了 , 并不需要集中式地到达某一个NGINX或者是一个ELB这样的组件来进行决策 。
Kubernetes 网络模型来龙去脉
文章图片

它的实现是这样的:首先是由一群Pod组成一组功能后端 , 再在前端上定义一个虚IP作为访问入口 。 一般来说 , 由于IP不太好记 , 我们还会附赠一个DNS的域名 , Client先访问域名得到虚IP之后再转成实IP 。 Kube-proxy则是整个机制的实现核心 , 它隐藏了大量的复杂性 。 它的工作机制是通过apiserver监控Pod/Service的变化(比如是不是新增了Service、Pod)并将其反馈到本地的规则或者是用户态进程 。
一个LVS版的Service我们来实际做一个LVS版的Service 。 LVS是一个专门用于负载均衡的内核机制 。 它工作在第四层 , 性能会比用iptable实现好一些 。
假设我们是一个Kube-proxy , 拿到了一个Service的配置 , 如下图所示:它有一个ClusterIP , 在该IP上的端口是9376 , 需要反馈到容器上的是80端口 , 还有三个可工作的Pod , 它们的IP分别是10.1.2.3,10.1.14.5,10.1.3.8 。
Kubernetes 网络模型来龙去脉
文章图片

它要做的事情就是:
Kubernetes 网络模型来龙去脉
文章图片

第1步 , 绑定VIP到本地(欺骗内核);首先需要让内核相信它拥有这样的一个虚IP , 这是LVS的工作机制所决定的 , 因为它工作在第四层 , 并不关心IP转发 , 只有它认为这个IP是自己的才会拆到TCP或UDP这一层 。 在第一步中 , 我们将该IP设到内核中 , 告诉内核它确实有这么一个IP 。 实现的方法有很多 , 我们这里用的是iproute直接加local的方式 , 用Dummy哑设备上加IP的方式也是可以的 。
第2步 , 为这个虚IP创建一个IPVS的virtualserver;告诉它我需要为这个IP进行负载均衡分发 , 后面的参数就是一些分发策略等等 。 virtualserver的IP其实就是我们的ClusterIP 。
第3步 , 为这个IPVSservice创建相应的realserver 。我们需要为virtualserver配置相应的realserver , 就是真正提供服务的后端是什么 。 比如说我们刚才看到有三个Pod , 于是就把这三个的IP配到virtualserver上 , 完全一一对应过来就可以了 。 Kube-proxy工作跟这个也是类似的 。 只是它还需要去监控一些Pod的变化 , 比如Pod的数量变成5个了 , 那么规则就应变成5条 。 如果这里面某一个Pod死掉了或者被杀死了 , 那么就要相应地减掉一条 。 又或者整个Service被撤销了 , 那么这些规则就要全部删掉 。 所以它其实做的是一些管理层面的工作 。
啥?负载均衡还分内部外部
最后我们介绍一下Service的类型 , 可以分为以下4类 。
1.ClusterIP集群内部的一个虚拟IP , 这个IP会绑定到一堆服务的GroupPod上面 , 这也是默认的服务方式 。 它的缺点是这种方式只能在Node内部也就是集群内部使用 。
2.NodePort供集群外部调用 。 将Service承载在Node的静态端口上 , 端口号和Service一一对应 , 那么集群外的用户就可以通过:的方式调用到Service 。
3.LoadBalancer给云厂商的扩展接口 。 像阿里云、亚马逊这样的云厂商都是有成熟的LB机制的 , 这些机制可能是由一个很大的集群实现的 , 为了不浪费这种能力 , 云厂商可通过这个接口进行扩展 。 它首先会自动创建NodePort和ClusterIP这两种机制 , 云厂商可以选择直接将LB挂到这两种机制上 , 或者两种都不用 , 直接把Pod的RIP挂到云厂商的ELB的后端也是可以的 。
4.ExternalName摈弃内部机制 , 依赖外部设施 , 比如某个用户特别强 , 他觉得我们提供的都没什么用 , 就是要自己实现 , 此时一个Service会和一个域名一一对应起来 , 整个负载均衡的工作都是外部实现的 。
下图是一个实例 。 它灵活地应用了ClusterIP、NodePort等多种服务方式 , 又结合了云厂商的ELB , 变成了一个很灵活、极度伸缩、生产上真正可用的一套系统 。
Kubernetes 网络模型来龙去脉
文章图片

首先我们用ClusterIP来做功能Pod的服务入口 。 大家可以看到 , 如果有三种Pod的话 , 就有三个ServiceClusterIP作为它们的服务入口 。 这些方式都是Client端的 , 如何在Server端做一些控制呢?
首先会起一些Ingress的Pod(Ingress是K8s后来新增的一种服务 , 本质上还是一堆同质的Pod) , 然后将这些Pod组织起来 , 暴露到一个NodePort的IP , K8s的工作到此就结束了 。
任何一个用户访问23456端口的Pod就会访问到Ingress的服务 , 它的后面有一个Controller , 会把ServiceIP和Ingress的后端进行管理 , 最后会调到ClusterIP , 再调到我们的功能Pod 。 前面提到我们去对接云厂商的ELB , 我们可以让ELB去监听所有集群节点上的23456端口 , 只要在23456端口上有服务的 , 就认为有一个Ingress的实例在跑 。
整个的流量经过外部域名的一个解析跟分流到达了云厂商的ELB , ELB经过负载均衡并通过NodePort的方式到达Ingress , Ingress再通过ClusterIP调用到后台真正的Pod 。 这种系统看起来比较丰富 , 健壮性也比较好 。 任何一个环节都不存在单点的问题 , 任何一个环节也都有管理与反馈 。
本文总结
本节课的主要内容就到此为止了 , 这里为大家简单总结一下:
大家要从根本上理解Kubernetes网络模型的演化来历 , 理解PerPodPerIP的用心在哪里;网络的事情万变不离其宗 , 按照模型从4层向下就是发包过程 , 反正层层剥离就是收包过程 , 容器网络也是如此;Ingress等机制是在更高的层次上(服务端口)方便大家部署集群对外服务 , 通过一个真正可用的部署实例 , 希望大家把Ingress+ClusterIP+PodIP等概念联合来看 , 理解社区出台新机制、新资源对象的思考 。


    推荐阅读