我是一堆 K8s 控制器。
你可能会疑惑为什么是一堆,因为我不是一个人,我只是众多控制器中的一员,你也可以把我看成是众多控制器的集合。我的职责就是监控集群内资源的实际状态,一旦发现其与期望的状态不相符,就采取行动使其符合期望状态。
想当初,Kubernetes 老大哥创造我时,只是打算让我用控制循环简单维护下资源的状态。但我后来的发展,远远超出了他的想象。
1. 控制循环#
所谓控制循环就是一个用来调节系统状态的周期性操作,在 Kubernetes 中也叫调谐循环(Reconcile Loop)。我的手下控制着很多种不同类型的资源,比如 Pod,Deployment,Service 等等。就拿 Deployment
来说吧,我的控制循环主要分为三步:
- 从
API Server
中获取到所有属于该 Deployment 的 Pod,然后统计一下它们的数量,即它们的实际状态。 - 检查 Deployment 的
Replicas
字段,看看期望状态是多少个 Pod。 - 将这两个状态做比较,如果期望状态的 Pod 数量比实际状态多,就创建新 Pod,多几个就创建几个新的;如果期望状态的 Pod 数量比实际状态少,就删除旧 Pod,少几个就删除几个旧的。
然而好景不长,我收到了 Kubernetes 掌门人(看大门的) API Server
的抱怨:“你访问我的次数太频繁了,非常消耗我的资源,我连上厕所的时间都没有了!”
我仔细一想,当前的控制循环模式确实有这个缺陷——访问 API Server
的次数太频繁了,容易被老大反感。
所以我决定,找一个小弟。
2. Informer#
这次我招的小弟叫 Informer
,它分担一部分我的任务,具体的做法是这样的:由 Informer
代替我去访问 API Server,而我不管是查状态还是对资源进行伸缩都和 Informer 进行交接。而且 Informer 不需要每次都去访问 API Server,它只要在初始化的时候通过 LIST API
获取所有资源的最新状态,然后再通过 WATCH API
去监听这些资源状态的变化,整个过程被称作 ListAndWatch
。
而 Informer 也不傻,它也有一个助手叫 Reflector
,上面所说的 ListAndWatch
事实上是由 Reflector 一手操办的。
这一次,API Server
的压力大大减轻了,因为 Reflector 大部分时间都在 WATCH
,并没有通过 LIST 获取所有状态,这使 API Server
的压力大大减少。我想这次掌门人应该不会再批评我了吧。
然而没过几天,掌门人又找我谈话了:“你的手下每次来 WATCH 我,都要 WATCH 所有兄弟的状态,依然很消耗我的资源啊!我就纳闷了,你一次搞这么多兄弟,你虎啊?”
我一想有道理啊,没必要每次都 WATCH 所有兄弟的状态,于是告诉 Informer:“以后再去 API Server 那里 WATCH 状态的时候,只查 WATCH 特定资源的状态,不要一股脑儿全 WATCH。“
Informer 再把这个决策告诉 Reflector,事情就这么愉快地决定了。
本以为这次我会得到掌门人的夸奖,可没过几天安稳日子,它又来找我诉苦了:“兄弟,虽然你减轻了我的精神压力,但我的财力有限啊,如果每个控制器都招一个小弟,那我得多发多少人的工资啊,你想想办法。”
3. SharedInformer#
经过和其他控制器的讨论,我们决定这么做:所有控制器联合起来作为一个整体来分配 Informer
,针对每个(受多个控制器管理的)资源招一个 Informer 小弟,我们称之为 SharedInformer
。你们可以理解为共享 Informer,因为有很多资源是受多个控制器管理的,比如 Pod 同时受 Deployment
和 StatefulSet
管理。这样当多个控制器同时想查 Pod 的状态时,只需要访问一个 Informer 就行了。
但这又引来了新的问题,SharedInformer
无法同时给多个控制器提供信息,这就需要每个控制器自己排队和重试。
为了配合控制器更好地实现排队和重试,SharedInformer
搞了一个 Delta FIFO Queue
(增量先进先出队列),每当资源被修改时,它的助手 Reflector
就会收到事件通知,并将对应的事件放入 Delta FIFO Queue
中。与此同时,SharedInformer
会不断从 Delta FIFO Queue
中读取事件,然后更新本地缓存的状态。
这还不行,SharedInformer
除了更新本地缓存之外,还要想办法将数据同步给各个控制器,为了解决这个问题,它又搞了个工作队列(Workqueue),一旦有资源被添加、修改或删除,就会将相应的事件加入到工作队列中。所有的控制器排队进行读取,一旦某个控制器发现这个事件与自己相关,就执行相应的操作。如果操作失败,就将该事件放回队列,等下次排到自己再试一次。如果操作成功,就将该事件从队列中删除。
现在这个工作模式得到了大家的一致好评。虽然单个 SharedInformer
的工作量增加了,但 Informer 的数量大大减少了,老大可以把省下来的资金拿出一小部分给 SharedInformer
涨工资啊,这样大家都很开心。
4. CRD#
全民 Kubernetes 时代到了。
随着容器及其编排技术的普及,使用 Kubernetes 的用户大量增长,用户已经不满足 Kubernetes 自带的那些资源(Pod,Node,Service)了,大家都希望能根据具体的业务创建特定的资源,并且对这些资源的状态维护还要遵循上面所说的那一套控制循环机制。
幸好最近掌门人做了一次升级,新增了一个插件叫 CRD(Custom Resource Definition)
,创建一个全新的资源实例,只需要经过以下两步:
- 创建一个 CRD 资源(没错,CRD 也是一种资源类型),其中定义”自定义资源“的 API 组、API 版本和资源类型。这样就会向 API Server 注册该资源类型的 API。
- 指定上面定义的 API 组 和 API 版本,创建自定义资源。
当然,中间还要加入一些代码让 Kubernetes 认识自定义资源的各种参数。
到这一步就基本上完成了自定义资源的创建,但 Kubernetes 并不知道该资源所对应的业务逻辑,比如你的自定义资源是宿主机,那么对应的业务逻辑就是创建一台真正的宿主机出来。那么怎样实现它的业务逻辑呢?
5. 自定义控制器#
Controller Manager
见多识广,说:”这里的每个控制器都是我的一部分,当初创造你们是因为你们都属于通用的控制器,大家都能用得上。而自定义资源需要根据具体的业务来实现,我们不可能知道每个用户的具体业务是啥,自己一拍脑袋想出来的自定义资源,用户也不一定用得上。我们可以让用户自己编写自定义控制器,你们把之前使用的控制循环和 Informer 这些编码模式总结一下,然后提供给用户,让他们按照同样的方法编写自己的控制器。“
Deployment 控制器一惊,要把自己的秘密告诉别人?那别人把自己取代了咋办?赶忙问道:”那将来我岂不是很危险,没有存在的余地了?“
Controller Manager
赶忙解释道:”不用担心,虽然用户可以编写自定义控制器,但无论他们玩出什么花样,只要他们的业务跑在 Kubernetes 平台上,就免不了要跑容器,最后还是会来求你们帮忙的,你要知道,控制器是可以层层递进的,他们只不过是在你外面套了一层,最后还是要回到你这里,请求你帮忙控制 Pod。“
这下大家都不慌了,决定就把自定义控制器这件事情交给用户自己去处理,将选择权留给用户。
6. Operator#
用户自从获得了编写自定义控制器的权力之后,非常开心,有的用户(CoreOS)为了方便大家控制有状态应用,开发出了一种特定的控制器模型叫 Operator
,并开始在社区内推广,得到了大家的一致好评。不可否认,Operator
这种模式是很聪明的,它把需要特定领域知识的应用单独写一个 Operator 控制器,将这种应用特定的操作知识编写到软件中,使其可以利用 Kubernetes 强大的抽象能力,达到正确运行和管理应用的目的。
以 ETCD Operator
为例,假如你想手动扩展一个 ETCD 集群,一般的做法是:
- 使用 ETCD 管理工具添加一个新成员。
- 为这个成员所在的节点生成对应的启动参数,并启动它。
而 ETCD Operator 将这些特定于 etcd 的操作手法编写到了它的控制循环中,你只需要通过修改自定义资源声明集群期望的成员数量,剩下的事情交给 Operator 就好了。
本以为这是一个皆大欢喜的方案,但没过多久,就有开发 Operator 的小哥来抱怨了:“我们有很多开发的小伙伴都是不懂运维那一套的,什么高可用、容灾根本不懂啊,现在让我们将运维的操作知识编写到软件中,臣妾做不到啊。。”
这确实是个问题,这样一来就把开发和运维的工作都塞到了开发手里,既懂开发又懂运维的可不多啊,为了照顾大家,还得继续想办法把开发和运维的工作拆分开来。
7. OAM#
这时候阿里和微软发力了,他们联合发布了一个开放应用模型,叫 Open Application Model (OAM)。这个模型就是为了解决上面提到的问题,将开发和运维的职责解耦,不同的角色履行不同的职责,并形成一个统一的规范,如下图所示:
这个规范告诉我们:
- 开发人员负责描述组件的功能,如何配置组件,以及运行需要多少资源
- 运维人员负责将相关组件组合成一个应用,并配置运行时参数和运维支撑能力,比如是否需要监控,是否需要弹性伸缩。
- 基础设施工程师负责建立和维护应用的运行时环境(如底层系统)。
其中每一个团队负责的事情都用对应的 CRD 来配置。
这样一来,开发和运维人员的职责就被区分开来了,简化了应用的组合和运维。它将应用的配置和运维特征(如自动伸缩、流量监控)进行解耦,然后通过建模构成一个整体,避免了 Operator 这种模型带来的大量冗余。
自从用上了这个模型之后,运维和开发小哥表示现在他们的关系很融洽,没事还能一起出去喝两杯。