Brian Grant 是 Borg 和 Kubernetes 的 core developer,最近他在 Twitter 上分享了 Kubernetes 和 Borg 的渊源,以及为什么 Kubernetes 会设计成如今的架构。
1. 为什么会有 Pod?
最早的时候 Borg 允许提交 Job 到集群中,最终形成 Alloc Instance 运行实体,后来在实际中发现用户总是会把多个 Job 绑在一起运行,常见的模式是日志、cache 的 sidecar。因此 Kubernetes 设计之初就考虑了多 container 的实体 Pod 作为调度和管理的基本单位。这个设计在后续的使用中被证明是非常正确的决定
2. 无处不在的 Key-Value
Borg 最早只有 Machine(也就是 Kubernetes 中的 Node)上面会有 key-value 属性的概念,Borgmon(类似于)暴露的 metrics 会有运行服务的位置、环境、集群等信息,Job 上并没有 key-value。Borg 使用者会把属性放到 Job name 中,导致 name 非常长,而且需要自定义各种字符处理来获取 key-value。因此 Kubernetes 在设计之初,就让所有的资源对象带有可扩展的 key-value,通过 label selector 对所有的资源实体可以进行精细化,而且无重复的管理。
3. Annotations?
Borg Job 有一个 note 字段,可以用来保存一些应用专用的信息,比如 DNS TXT 记录。但是单个字段根本不够应用保存更多的信息,因此 Kubernetes 提出了 Annotation 的概念,开发者和使用者可以在 annotation 中保存非常复杂的数据,用来扩展自己的业务逻辑。尽管 Label 和 Annotation 非常相似,但是两者还是有使用场景上的区别,把他们独立开来也是有意义的
4. workload Controllers
和当时其他容器编排系统非常不同的一点是 Kubernetes 声明式的 API 设计和自动化的 controllers。在 Kubernetes 最初开源的时候,只实现了 ReplicationController 一个 controller, Borg 其实只支持一种 controller:Job(可以由多个 task 组成)。Job 可以实现各种各样的 workloads:Deployment、DaemonSet、StatefulSet 等等,因此导致它配置非常复杂,控制逻辑也很复杂,因此在 Kubernetes 中拆分出了Deployment、DamonSet、StatefulSet 等 workloads。把 controller 独立出来可以非常容易扩展,加上社区在推的 operator,用户可以非常简单地自定义 controller 实现自己的业务逻辑。
> The key is to explicitly model state such that it can be externally manipulated.
5. asynchronous controllers
BorgMaster 使用的是同步的、事务性的、边缘触发的状态机,扩展、演化和scale 变得很困难。比如:有些处理逻辑对资源对象的修改超过了单个事务处理的范围,新加 state 会让客户端不可用,添加新的状态比较难。
因此 Kubernetes 采用了异步的 controller,并且用同一个资源对象 同时保存用户期望的状态以及当前状态的信息(也就是我们看到的 spec 和 status)。这个决定让 apiserver 成为交互中心,所有对象都能通过访问 apiserver API 查看和修改,apiserver 成为了 source of truth。
> The entire system can now be described as an unbounded number of independent asynchronous control loops reading and writing from/to a schematized resource store as the source of truth. This model has proven to be very resilient, evolvable, and extensible.
6. scheduling 调度
调度从原来单一的任务放置,变成了现在非常复杂的资源管理领域:队列、多调度器、资源限额和资源共享、自动扩缩容、超卖、任务放置、重调度、preemption 等。
因为容器的灵活性,大多数使用 Kubernetes 的用户更关心如何提供应用的部署和发布效率和稳定性,而不关心集群的资源使用率,所以 QoS、超卖、autoscaling 才会这么重要。
7. watch 机制
Google 内部的系统(比如Chubby、Colossus、BigTable)都有实现 watch 机制,后来还抽象出来一层更标准化的 watch 定义和接口,来防止不同系统独立造轮子。
Kubernetes 最初选择 etcd 作为后端也是因为它和 Chubby 比较像,而且提供了 watch 接口。
之前,业界的系统一般都喜欢使用消息队列来做通知,为什么 Kubernetes 会选择 watch 机制呢?因为 Kubernetes 的 controller 重启后会从零开始,需要知道初始状态(也就是集群当前状态),而且还要正确处理丢失的事件和过期的事件;此外,引入消息队列还会增加整个系统的架构和运维的复杂性。
对于发布式系统来说,它们通常会依赖很多的外部基础组件:使用分布式 KV 来存储配置和选主,使用数据库作为存储,使用分布式缓存作为 cache 来加速,消息队列处理事件(还要消息持久化存储)。这么多的组件安装和运维会非常复杂,而 Kubernetes 独辟蹊径,只使用了 etcd 这样一个外部依赖,把它作为数据库和分布式 KV,用 watch 机制替代了消息队列,用 apiserver 内部的 cache 取代了分布式缓存,让整个系统复杂度降低了不少。
Watch 机制可以说是 Kubernetes 核心的技术之一,不仅让 controller 变得可能,从而大大增加了整个系统的可扩展性;并且减少了外部消息队列的依赖,基于 operator 的编程模式和框架也非常容易上手。
8.总结
一个复杂系统虽然在最初的设计阶段就会比较复杂,但是随着时间的发展还会继续演化,不断有新功能添加导致它会逐渐庞大,从而和最初的设计不太一样,并且因为很多设计上的缺陷导致再往后扩展会越来越复杂。这个时候会对系统做一些大的重构,也有可能会进行重写,根据之前的经验来重新设计整个架构。
乐生活