近期有个小需求,在不重建Container的前提下修改Pod结构中的Request值,限制仅可以调小。本以为很简单的一个需求,但实际花费了一天的时间才搞完,代码改动只有几行,但是在改完测试的过程中发现很多超出预期或者认知的现象,为了搞懂为什么会这样,又重新捋了捋kubelet源码。 在这件事结束之后也进行了反思,主要是有关源码阅读的,于是把这个过程和自己的感触以及后续的一些改进方法和计划记录下来。
过程
先看下这件事的过程,我们先忽略这个需求的合理性,直接分析技术实现。
首先,kubernetes本身并不支持修改Pod的资源属性,无论Request还是Limit,可以通过修改apiserver中的校验逻辑来放开此限制;
其次,如何保证在Request改变之后容器不重启?我们知道,kubelet会为每个container都计算出一个hash值,其中用到了container的所有属性,在调用docker api进行容器创建的时候会把这个值设置到容器的Label中,后续如果kubelet检测到新计算出的hash值与在运行的容器的hash值不同,则会进行容器的原地重启操作,这也是为什么修改container的Image会出发容器原地重启的原因。很明显,如果放开Request的修改,Request值变了之后也会导致新的hash值变化从而导致容器重建,与我们的期望不符。也有办法来解决:记录container创建时的Request值,计算的时候还是使用创建时的值,此值只有在container创建时会记录,后续不再更新。
测试场景是创建了Qos类型为Guaranteed 类型的Pod,放开了kube-apiserver对Request修改的限制,kubelet保持原生不动,调小其Request值,大家可以先尝试自己思考一下会发生什么?
那么在测试的时候遇到了什么问题呢?
首先,发现放开Request修改之后,如果改了Request的值,容器重启了(这一步符合预期),但是重启次数加2(这里其实是之前的一个盲点)
接着,继续修改Request值,容器依然重启(符合预期),但是此次重启次数只加了1
最后,通过查看Pod Cgroup目录,确认修改后的Request已经在Pod级别和Container级别分别生效,但是同时存在两个Pod的目录,类似如下
1 2 3 4 | # 修改之前的Pod对应的cgroup目录 /sys/fs/cgroup/cpu/kubepods/guaranteed/pod{uid} # 修改之后的Pod对应的cgroup目录 /sys/fs/cgroup/cpu/kubepods/burstable/pod{uid} |
---|
为什么同一个Pod会存在两个cgroup目录呢?
容器重启了,重启次数应该只加1,那为什么在第一步中加了2?
你可以在继续阅读之前先自己思考一下可能的原因。
问题分析
首先看为什么会有两个cgroup目录,需要先搞清楚cgroup目录是如何创建、如何删除的。
Cgorup创建
我们采用CgroupPerQos的方式进行管理,以cpu子系统为例,层级类似如下所示
- /sys/fs/cgroup/cpu
- kubepods
- guaranteed
- pod{uid}
- {containerid}
- {containerid}
- pod{uid}
- {containerid}
- {containerid}
- pod{uid}
- burstable
- bestaffort
- guaranteed
- kubepods
从创建者的角度分两种:kubelet创建的、docker创建的。其中container层由docker创建,container以上的pod层、qos层和root(kubepods)都是由kubelet创建的。那docker又是怎么知道容器的cgroup parent目录是谁呢?其实是kubelet在调用docker api时传给docker的一个参数,告诉了其cgroup parent路径,可以通过执行docker inspect {containerid} | grep -i cgroup
来查看每个container的cgroup parent路径。
那为什么会在两个qos目录下分别存在一个Pod目录呢?因为我们修改了Pod的Qos类型,触发了syncPod逻辑,里面会去根据Pod的qos类型进行cgroup目录判断,如果qos改变,则会把原Pod下的所有container全部杀掉,然后创建新的cgroup目录,再启动容器。这也就可以解释为什么在第一次修改Request之后Pod重启次数增加了2,因为pause容器也发生了重建。为什么要重建容器呢,因为整个pod的qos发生了变化,Pod内的所有容器需要在新的qos目录下重建其目录,但是kubelet没有去更新container的cgroup设置,而是采用重建的方式来实现。
为什么kubelet不直接去更新cgroup目录,而是重建容器呢?首先修改Request不仅影响cgroup,容器的oomscore也将受到影响,docker虽然提供了api来修改资源大小,但并没有提供相关的api去进行cgroup目录及oomscore等属性的修改,其次cgroup迁移是一个比较复杂的工作,迁移过程会出现部分历史数据丢失等问题,所以kubele直接采用重建的方式来解决这个问题。
Cgroup删除
经过分析Cgroup创建过程,重启两次的问题已经找到了答案。但为什么新的Pod cgroup目录创建出来之后,原有的目录没有被删除呢?这就需要搞清楚Pod Cgroup目录什么时候删除的,容器级别的cgroup目录是在容器被删除的时候删除的,这个很好理解,Pod级别的Cgroup目录是否也是在Pod删除时删除的呢?经过看代码发现并不是,Pod资源清理是一个异步的过程,定时监测Pod是否已经设置了deletionTimestamp
属性和容器的运行状态,只有设置了此属性的Pod才有可能被清理,清理的过程中包含挂在卷、Cgroup等资源,会一并清理。因为修改Request的请求是不会去给Pod设置deletionTimestamp
属性的,这就导致Pod级别的旧目录不会被删除,又因为新目录的创建,导致同时存在两个Pod级别的目录。
反思
综合看下来,这两个点都没有那么难,而且之前也做过kubelet定制开发,syncPod部分代码更是看过数次。那为什么花费了这么久的时间呢?
源码阅读的目的性
此前阅读源码的目的有几种,查问题、验证某些想法、探寻系统运行原理,还有一些人通过看源码来写blog、或者写源码分析之类的书。此前大部分场景是为了查问题、验证想法以及某些小功能的定制开发,可以聚焦到某些点或流程上,做完之后会对相关点印象比较深刻,但是相关性不大的地方就会很容易遗忘,或者说根本不会去刻意关注相关性不大的地方。偶尔会有想法去主动阅读源码,探求系统运行原理,实现方式,写blog等,但最终发现效果很差,因为在此过程中我们的目的性并不是真正的去理解系统,缺乏针对性。而且对于在还不了解系统运行原理的情况下想通过看源码去了解其原理就是本末倒置的事情。比如接手一个新项目或者其他人的项目的时候,如果没有相关背景,而且也没有项目文档,没有代码注释的情况,直接想通过代码去了解业务和系统运行原理是一件非常痛苦的事情。
思考的必要性
无论处于什么目的去看代码,需要有自己的思考,可以假设系统由自己设计,那会设计成什么样子,代码由自己实现,会写成什么样子。在设计过程中可能会遇到一些问题,带着问题再去看代码,去验证别人是如何设计并实现的,尤其是遇到和自己预期设计不一致的地方,可以进行对比,分析那种方案好,或者他这么设计是处于什么考虑,为什么这么实现。以这样的方式看代码要比没有目的性的走马观花式的浏览代码收获更多,印象更深刻。这里推荐一本书《思考,快与慢》,解释了人的大脑是如何工作的,可以通过本书了解到思考是一个怎样的过程。
后续计划
源码还是要去读的,后面会进行一些尝试,根据上面提到的阅读方式开始进行,即 思考系统运行方式 ==》自己设计系统实现 ==》带着问题读源码(验证想法) ==》思考与总结,试运行一段时间看看效果。