内容简介:作者:陈绥来源:UCloud(ID:
作者:陈绥
来源:UCloud(ID: ucloud_tech )
UK8S是UCloud推出的Kubernetes容器云产品,完全兼容原生API,为用户提供一站式云上Kubernetes服务。我们团队自研了CNI(Container Network Interface)网络插件,深度集成VPC, 使UK8S容器应用拥有与云主机间等同的网络性能 (目前最高可达10Gb/s, 100万pps),并打通容器和物理云/托管云的网络。过程中,我们解决了开源kubelet创建多余Sandbox容器导致Pod IP莫名消失的问题,确保CNI插件正常运行,并准备将修复后的kubelet源码提交给社区。
深度集成VPC的网络方案
按照我们的设想,开发者可以在UK8S上部署、管理、扩展容器化应用,无需关心Kubernetes集群自身的搭建及维护等运维类工作。UK8S完全兼容原生的Kubernetes API, 以UCloud 公有云资源为基础, 通过自研的插件整合打通了ULB、UDisk、EIP等公有云网络和存储产品,为用户提供一站式云上Kubernetes服务。
其中VPC既保障网络隔离,又提供灵活的IP地址定义等,是用户对网络的必备需求之一。UK8S研发团队经过考察后认为,UCloud基础网络平台具有原生、强大的底层网络控制能力,令我们能抛开Overlay方案,把VPC的能力上移到容器这一层,通过VPC的能力去实现控制和转发。 UK8S每创建一个Pod都为其申请一个VPC IP并通过VethPair配置到Pod上,再配置策略路由。 原理如下图所示。
此方案具有以下优势:
-
无Overlay,网络性能高。50台Node下的测试数据表明,容器与容器之间的网络性能,相对于云主机与云主机之间,只有轻微差异(小包场景下,pps 会有 3~5% 损耗),而且Pod网络性能各项指标(吞吐量,包量,延迟等)不会随着节点规模增大而削减。而Flannel UDP,VXLan模式和Calico IPIP的模式存在明显的性能消耗。
-
Pod能直通公有云和物理云。对于使用公有云和物理云的用户而言,业务上K8S少了一层障碍,多了一份便利。而Flannel的host gw模式下,容器无法访问公有云和物理云主机。
而CNI的工作流程如下所示。
创建Pod网络过程:
删除Pod网络过程:
Pod IP 消失问题的排查与解决
为了测试CNI插件的稳定性,测试同学在UK8S上部署了一个CronJob,每分钟运行一个Job任务,一天要运行1440个任务。该CronJob定义如下:
apiVersion: batch/v1beta1
kind: CronJob
metadata:
name: hello
spec:
schedule: "*/1 * * * *"
jobTemplate:
spec:
template:
spec:
containers:
- name: hello
image: busybox
args:
- /bin/sh
- -c
- date; echo Hello from the Kubernetes cluster
restartPolicy: OnFailure
每运行一次Job都要创建一个Pod, 每创建一个Pod,CNI插件需要申请一次VPC IP,当Pod被销毁时,CNI插件需要释放该VPC IP。 因此理论上,通过该CronJob每天需要进行1440次申请VPC IP和释放VPC IP操作。
然而,经过数天的测试统计,发现通过该CronJob,集群每天申请IP次数高达2500以上, 而释放的的IP次数也达到了1800。申请和释放次数都超过了1440,而且申请次数超过了释放次数,意味着,部分分配给Pod的VPC IP被无效占用而消失了。
CNI: 待删除的IP去哪儿了?
仔细分析CNI插件的运行日志,很快发现,CNI在执行拆除SandBox网络动作(CNI_COMMAND=DEL)中,存在不少无法找到Pod IP的情况。由于UK8S 自研的CNI查找Pod IP依赖正确的Pod网络名称空间路径(格式:/proc/10001/net/ns),而kubelet传给CNI的NETNS环境变量参数为空字符串,因此,CNI无法获取待释放的VPC IP,这是造成IP泄露的直接原因,如下图所示。
问题转移到kubelet, 为什么kubelet会传入一个空的CNI_NETNS环境变量参数给CNI插件?
随后跟踪kubelet的运行日志,发现不少Job Pod创建和销毁的时候,生成了一个额外的Sandbox容器。Sandbox容器是k8s pod中的Infra容器,它是Pod中第一个创建出来的容器,用于创建Pod的网络名称空间和初始化Pod网络,例如调用CNI分配Pod IP,下发策略路由等。它执行一个名为pause的进程,这个进程绝大部分时间处于Sleep状态,对系统资源消耗极低。奇怪的是,当任务容器busybox运行结束后,kubelet为Pod又创建了一个新的Sandbox容器,创建过程中自然又进行了一次CNI ADD调用,再次申请了一次VPC IP。
回到UK8S CNI,我们再次分析重现案例日志。这一次有了更进一步的发现,所有kubelet传递给NETNS参数为空字符串的情形都发生在kubelet试图销毁Pod中第二个Sandbox的过程中。反之,kubelet试图销毁第二个Sandbox时,给CNI传入的NETNS参数也全部为空字符串。
到这里,思路似乎清晰了不少,所有泄露的VPC IP都是来自第二个Sandbox容器。因此,我们需要查清楚两个问题:
1. 为什么会出现第二个Sandbox容器?
2. 为什么kubelet在销毁第二个Sandbox容器时,给CNI传入了不正确的NETNS参数?
第二个Sandbox: 我为何而生?
在了解的第二个Sandbox的前世今生之前,需要先交待一下kubelet运行的基本原理和流程。
kubelet是kubernetes集群中Node节点的工作进程。当一个Pod被kube-sheduler成功调度到Node节点上后, kubelet负责将这个Pod创建出来,并把它所定义的各个容器启动起来。kubelet也是按照控制器模式工作的,它的工作核心是一个控制循环,源码中称之为syncLoop,这个循环关注并处理以下事件:
-
Pod更新事件,源自API Server;
-
Pod生命周期(PLEG)变化, 源自Pod本身容器状态变化, 例如容器的创建,开始运行,和结束运行;
-
kubelet本身设置的周期同步(Sync)任务;
-
Pod存活探测(LivenessProbe)失败事件;
-
定时的清理事件(HouseKeeping)。
在上文描述的CronJob任务中, 每次运行Job任务都会创建一个Pod。这个Pod的生命周期中,理想情况下,需要经历以下重要事件:
1. Pod被成功调度到某个工作节点,节点上的Kubelet通过Watch APIServer感知到创建Pod事件,开始创建Pod流程;
2. kubelet为Pod创建Sandbox容器,用于创建Pod网络名称空间和调用CNI插件初始化Pod网络,Sandbox容器启动后,会触发第一次kubelet PLEG(Pod Life Event Generator)事件。
3. 主容器创建并启动,触发第二次PLEG事件。
4. 主容器date命令运行结束,容器终止,触发第三次PLEG事件。
5. kubelet杀死Pod中残余的Sandbox容器。
6. Sandbox容器被杀死,触发第四次PLEG事件。
其中3和4由于时间间隔短暂,可能被归并到同一次PLEG事件(kubelet每隔1s进行一次PLEG事件更新)。
然而,在我们观察到的所有VPC IP泄露的情况中,过程6之后“意外地”创建了Pod的第二个Sandbox容器,如下图右下角所示。在我们对Kubernetes的认知中,这不应该发生。
对kubelet源码(1.13.1)抽丝剥茧
前文提到,syncLoop循环会监听PLEG事件变化并处理之。而PLEG事件,则来源kubelet内部的一个pleg relist定时任务。kubelet每隔一秒钟执行一次relist操作,及时获取容器的创建,启动,容器,删除事件。
relist的主要责任是通过CRI来获取Pod中所有容器的实时状态,这里的容器被区分成两大类:Sandbox容器和非Sandbox容器,kubelet通过给容器打不同的label来识别之。CRI是一个统一的容器操作gRPC接口,kubelet对容器的操作,都要通过CRI请求来完成,而Docker,Rkt等容器项目则负责实现各自的CRI实现,Docker的实现即为dockershim,dockershim负责将收到的CRI请求提取出来,翻译成Docker API发给Docker Daemon。
relist通过CRI请求更新到Pod中Sandbox容器和非Sandbox容器最新状态,然后将状态信息写入kubelet的缓存podCache中,如果有容器状态发生变化,则通过pleg channel通知到syncLoop循环。对于单个pod,podCache分配了两个数组,分别用于保存Sandbox容器和非Sandbox容器的最新状态。
syncLoop收到pleg channel传来事件后,进入相应的sync同步处理流程。对于PLEG事件来说,对应的处理函数是HandlePodSyncs。这个函数开启一个新的pod worker goroutine,获取pod最新的podCache信息,然后进入真正的同步操作:syncPod函数。
syncPod将podCache中的pod最新状态信息(podStatus)转化成Kubernetes API PodStatus结构。这里值得一提的是,syncPod会通过podCache里各个容器的状态,来计算出Pod的状态(getPhase函数),比如Running,Failed或者Completed。然后进入Pod容器运行时同步操作:SyncPod函数,即将当前的各个容器状态与Pod API定义的SPEC期望状态做同步。下面源码流程图可以总结上述流程。
SyncPod: 我做错了什么?
SyncPod首先计算Pod中所有容器的当前状态与该Pod API期望状态做对比同步。这一对比同步分为两个部分:
-
检查podCache中的Sandbox容器的状态是否满足此条件:Pod中有且只有一个Sandbox容器,并且该容器处于运行状态,拥有IP。如不满足,则认为该Pod需要重建Sandbox容器。如果需要重建Sandbox容器,Pod内所有容器都需要销毁并重建。
-
检查podCache中非Sandbox容器的运行状态,保证这些容器处于Pod API Spec期望状态。例如,如果发现有容器主进程退出且返回码不为0,则根据Pod API Spec中的RestartPolicy来决定是否重建该容器。
回顾前面提到的关键线索:所有的VPC IP泄露事件,都源于一个意料之外的Sandbox容器,被泄露的IP即为此Sandbox容器的IP。刚才提到,SyncPod函数中会对Pod是否需要重建Sandbox容器进行判定,这个意外的第二个Sandbox容器是否和这次判定有关呢? 凭kubelet的运行日志无法证实该猜测,必须修改源码增加日志输出。重新编译kubelet后,发现第二个Sandbox容器确实来自SyncPod函数中的判定结果。进一步确认的是,该SyncPod调用是由第一个Sandbox容器被kubelet所杀而导致的PLEG触发的。
那为什么SyncPod在第一个Sandbox容器被销毁后认为Pod需要重建Sandbox容器呢?进入判定函数podSandboxChanged仔细分析。
podSandboxChanged获取了podCache中Sandbox容器结构体实例,发现第一个Sandbox已经被销毁,处于NOT READY状态,于是认为pod中已无可用的Sandbox容器,需要重建之,源码如下图所示。
注意本文前面我们定位的CronJob yaml配置, Job模板里的restartPolicy被设置成了OnFailure。SyncPod完成Sandbox容器状态检查判定后,认为该Pod需要重建Sandbox容器,再次检查Pod的restartPolicy为OnFailure后,决定重建Sandbox容器,对应源码如下。
可以看出kubelet在第一个Sandbox容器死亡后触发的SyncPod操作中,只是简单地发现唯一的Sandbox容器处于NOT READY状态,便认为Pod需要重建Sandbox,忽视了Job的主容器已经成功结束的事实。
事实上,在前面syncPod函数中通过podCache计算API PodStatus Phase的过程中,kubelet已经知道该Pod处于Completed状态并存入apiPodStatus变量中作为参数传递给SyncPod函数。如下图所示。
Job已经进入Completed状态,此时不应该重建Sandbox容器。而SyncPod函数在判定Sandbox是否需要重建时, 并没有参考调用者syncPod传入的apiPodStatus参数,甚至这个参数是被忽视的。
第二个Sandbox容器的来源已经水落石出,解决办法也非常简单,即kubelet不为已经Completed的Pod创建Sandbox,具体代码如下所示。
重新编译kubelet并更新后,VPC IP泄露的问题得到解决。
下图可以总结上面描述的第二个Sandbox容器诞生的原因。
事情离真相大白还有一段距离。还有一个问题需要回答:
为什么kubelet在删除第二个Sandbox容器的时候, 调用CNI拆除容器网络时,传入了不正确的NETNS环境变量参数?
失去的NETNS
还记得前面介绍kubelet工作核心循环syncLoop的时候,里面提到的定期清理事件(HouseKeeping)吗?HouseKeeping是一个每隔2s运行一次的定时任务,负责扫描清理孤儿Pod,删除其残余的Volume目录并停止该Pod所属的Pod worker goroutine。HouseKeeping发现Job Pod进入Completed状态后,会查找该Pod是否还有正在运行的残余容器,如有则请理之。由于第二个Sandbox容器依然在运行,因此HouseKeeping会将其清理,其中的一个步骤是清理该Pod所属的cgroup,杀死该group中的所有进程,这样第二个Sandbox容器里的pause进程被杀,容器退出。
已经死亡的第二个Sandbox容器会被kubelet里的垃圾回收循环接管,它将被彻底停止销毁。然而由于之前的Housekeeping操作已经销毁了该容器的cgroup, 网络名称空间不复存在,因此在调用CNI插件拆除Sandbox网络时,kubelet无法获得正确的NETNS参数传给CNI,只能传入空字符串。
到此,问题的原因已经确认。
问题解决
一切水落石出后,我们开始着手解决问题。为了能确保找到所删除的Pod对应的VPC IP,CNI需要在ADD操作成功后,将PodName,Sandbox容器ID,NameSpace,VPC IP等对应关联信息进行额外存储。这样当进入DEL操作后,只需要通过kubelet传入的PodName,Sandbox容器ID和NameSpace即可找到VPC IP,然后通过UCloud 公有云相关API删除之,无需依赖NETNS操作。
考虑到问题的根因是出现在kubelet源码中的SyncPod函数,UK8S团队也已修复kubelet相关源码并准备提交patch给Kubernetes社区。
写在最后
Kubernetes依然是一个高速迭代中的开源项目,生产环境中会不可用避免遇见一些异常现象。UK8S研发团队在学习理解Kubernetes各个组件运行原理的同时,积极根据现网异常现象深入源码逐步探索出问题根因,进一步保障UK8S服务的稳定性和可靠性,提升产品体验。
2019年内UK8S还将支持节点弹性伸缩(Cluster AutoScaler)、物理机资源、GPU资源、混合云和ServiceMesh等一系列特性,敬请期待。
5月28日,UCLOUD用户大会暨 Think in Cloud 2019 北京将于北京富力万丽酒店举办,下午技术专场为大家呈现Kubernetes、Serverless等技术主题演讲,现在可免费报名,名额有限,赶紧报名吧!
以上所述就是小编给大家介绍的《Kubernetes源码探疑:Pod IP泄露排查及解决》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Web Caching
Duane Wessels / O'Reilly Media, Inc. / 2001-6 / 39.95美元
On the World Wide Web, speed and efficiency are vital. Users have little patience for slow web pages, while network administrators want to make the most of their available bandwidth. A properly design......一起来看看 《Web Caching》 这本书的介绍吧!