Kubernetes — 设计理念

目录

云基础设施的两个关键

基础设施是可编程的

与早期的基础设施相比,云提供商需要从头开始构建完全由 API 调用驱动的计算、存储、网络资源。借助标准且稳定的 API,提供商和用户都可以在其上构建更高级别的资源,例如:无服务器计算。还可以创建基本原语,例如:调度程序和控制平面,通过可编程的代码来管理这些资源。

云计算的服务自始自终就被设计为极具弹性、自助服务和 API 驱动。在这样的模型中,用户不需要手动干预即可创建虚拟机和数据库。云计算将 IT 资产的虚拟化复杂性封装为服务,最终用户只需要进行函数调用即可。

像所有程序一样,一旦核心服务公开为 API,就可以通过重用功能来利用它们来创建更高级别的抽象。例如:AWS Fargate、Lambda 之类的产品是更高抽象级别的计算服务,但实际上它们最终都会在 EC2 虚拟机上运行。云平台的用户还可以利用相同的 API 来构建自己的抽象。例如:Netflix 建立了 Titus,以 EC2 实例作为基础来调度和运行容器的工作负载。APIs 经济支持为不同供应商提供不同的专业产品。

也就是说,将基础设施原语作为 API 提供的创新使云提供商可以构建更高级别的服务。这层抽象快速将云提供商与传统的商品托管提供商分开,因为传统托管在没有没有足够投资的情况下不能满足足够的灵活性。

基础设施具有声明性

为了规模化交付基础设施,云提供商需要保护用户免受内部工作流故障的影响。而声明式 API 正式一种面向结果的编程方式,可以将底层基础设施配置的复杂性淹没。

在规模化的云交付场景中,不能依靠过去传统 IT 面向过程(命令式)的运维思路。从规模上讲,对于工作流中遇到的每个故障,手动恢复都是不切实际且不经济的。所以,业界需要一种让用户定义软件基础结构的方法,而又不让他们暴露于内部系统可能会发生的大量故障中。最终的解决方案是采用声明式 API。使用声明式 API,用户告知云计算平台他们想看到什么结果,而不是如何实现的过程。对于用户来说,他们找到了一种更加直观地与云提供商合作的方式:描述部署的外观,而不是精确地部署它。

对于云提供商而言,必须要将管理软件和底层硬件的复杂性淹没在 API 背后。用户不必受限于完成工作流中的特定步骤,例如:基于命令式的 API 构建应用服务。如此的,云提供商就可以根据技术发展快速迭代实现工作流的各个部分,用户的声明式 API 又可以始终保持不变。

可见,声明式 API 之所以对云提供商在可用性方面具有吸引力的决定性因素就是:以这种方式设计基础架构可以减少与用户的摩擦。随着摩擦的减少,云提供商在就可以提供以更高级别的服务和诸如计算调度程序之类的原语形式出现的新抽象。

声明式 API

声明式(Declarative)的编程方式一直都会被工程师们拿来与命令式(Imperative)进行对比,前者是 “面向结果的”,后者是 “面向过程的”。

通常,我们接触的都是命令式编程,它要求我们描述为了达到某一个效果或者目标所需要完成的指令,常见的编程语言 Go、Ruby、C++ 其实都为开发者了命令式的编程方法。

而 SQL 则是种常见的声明式编程语言,它能够让开发者自己去指定想要的数据是什么。声明式的编程方式能够大量地减少开发者的工作量,极大地提升了开发及运维的效率。当然了声明式编程,往往需要基于一个强大的 Resource Management System(资源管理系统)来作为支撑,例如:SQL 对应的 RDBMS。

Kubernetes 就是这么一个面向应用开发与部署的 Resource Management System,它围绕 etcd 构建出来的一套 “面向终态” 编排体系。当用户向 Kubernetes 提交了一个 API 对象(Kubernetes Object)的描述(Spec)之后,Kubernetes 会负责为你保证整个集群里各项资源的状态(Status),都与你的 API 对象描述的需求相一致。更重要的是,这个保证是一项 “无条件的”、“没有期限” 的承诺:对于每个保存在 etcd 里的 API 对象,Kubernetes 都通过启动一种叫做 “控制器模式”(Controller Pattern)的无限循环,不断检查,然后调谐,最后确保整个集群的状态与这个 API 对象的描述一致。

可见,Kubernetes 项目实现 “容器编排” 的核心,在于一个叫做 “控制器模式” 的机制,即:通过对 etcd 里的 API 对象的变化进行监视(Watch),Kubernetes 项目就可以在一个叫做 Controller 的组件里对这些变化进行响应。而无论是 Pod 等应用对象,还是 iptables、存储设备等服务对象,任何一个 API 对象发生变化,那么 Kubernetes 接下来需要执行的响应逻辑,就是对应的 Controller 里定义的编排动作。

for {
    
    
    实际状态 := 获取集群中对象X的实际状态(Actual State)
    期望状态 := 获取集群中对象X的期望状态(Desired State)

    if 实际状态 == 期望状态{
    
    
      什么都不做
    } else {
    
    
      执行编排动作,将实际状态调整为期望状态。
    }
}

例如:当我们通过 YAML 来描述(定义)预期服务的拓扑结构和状态后,Kubernetes 就会帮助我们从现有的状态中进行迁移至预期的终态。

apiVersion: v1
kind: Pod
metadata:
  name: rss-site
  labels:
    app: web
spec:
  containers:
    - name: front-end
      image: nginx
      ports:
        - containerPort: 80
    - name: rss-reader
      image: nickchase/rss-php-nginx:v1
      ports:
        - containerPort: 88

当然,这里 Controller 正是依靠 etcd 的 Watch API 来实现对 API 对象变化的感知的。

简而言之,Kubernetes 中声明式的 API 就是指定集群所期望的运行状态,在出现任何与预期不一致的时候,它本身都可以通过指定的 YAML 文件对线上集群进行状态的迁移,最终会根据当前的状态自动做出做合适的操作。

一切皆在 API 中

Kelsey Hightower 经常提到这个目标,Daniel Smith 也提出过未来目标的设想:Kubernetes API 可以管理一切,从虚拟化(如:虚拟机)到物理层面(如:路由器设备)的所有元素。

Kubernetes API 首先对资源进行建模,且对资源要求非常严格。资源的数据结构必须全部遵循标准规范(apiVersion、type,metadata 和 spec),并且资源(发布,放置,获取,删除等)动作都必须与 API 动词(HTTP Method)一致。

基于各类资源的广泛标准,Kubernetes 的工具和库可以在所有场景中保持一致地工作,而无需为每个资源进行定制化。Kubernetes 社区中的许多 SIG 组织都在不断的扩展着这一 API 系统,而不是将功能转移到其他组件上。当然了,这也是 Kubernetes 复杂度讨论的高峰。

因为 Kubernetes API 标准化,可以将所有基础设施进行资源建模。因为资源数据结构标准化,控制器也可以忽略资源之间的差异。例如:检查资源标签上的策略是否符合要求的控制器不需要知道每个资源的作用,只要结构是一致的,控制器可以去相同的位置为每个对象检查。

定义控制平面

数据面跟控制面分离的思想来源于 SDN 领域,Marc Brooker 进一步概括了这一概念:

  1. 数据平面组件直接位于请求路径上。组件需要成功运行并可以随着系统的请求数量线性扩展。
  2. 控制平面组件帮助数据平面完成其工作。包括:资源管理(添加、删除资源),容错(监控和纠正硬件/软件故障)和部署(随时间推移更改系统)。由于不需要控制平面来满足系统请求,因此控制平面组件可以中断一段时间而不影响数据平面。

通过控制平面来管理基础设施的理论依据来源于 “控制论” 这一个跨越多个行业的广阔领域:

  1. 声明所需状态:系统不是静态的,对外部和内部情况做出响应并反馈变化。稳定的系统需要向预定的状态迁移。
  2. 控制循环:循环持续观察数据平面组件,连续的控制回路和反馈机制可以不断纠正与期望状态的偏差。

那么,Kubernetes 如何应用控制平面原理呢?Kubernetes 在 Informer 和 Workqueues 之类的控制器负责控制循环的构建。

贯穿 Kubernetes 架构最常见主题是标准化的思想:Kubernetes 资源模型很大程度上依赖于资源结构的一致性,并严格限制运行访问数据的入口。可以在许多不同的抽象中使用相同的原语,这些原语还实现了云计算的核心原理:资源分层以创建更高级别的抽象(例如:Deployments 抽象 Pods)。

因此,通用控制平面更多地取决于 Kubernetes API 的设计,而不是容器编排。

无侵入性

为了尽可能满足用户(工程师)的需求,减少工程师的工作量与任务并增强灵活性,Kubernetes 为工程师提供了无侵入式的接入方式,每一个应用或者服务一旦被打包成了容器镜像就可以直接在 Kubernetes 中无缝使用,不需要修改应用程序中的任何代码。这一点得益于容器技术的特征。

Container 和 Kubernetes 就像包裹在应用程序上的两层,它们两个为应用程序提供了容器化以及编排的能力,在应用程序内部却不需要任何的修改就能够在 Container 和 Kubernetes 集群中运行,这是 Kubernetes 在设计时选择无侵入带来最大的好处,同时无侵入的接入方式也是目前几乎所有应用程序或者服务都必须考虑的一点。

可移植性

在微服务架构中,我们往往都会让所有处理业务的服务变成无状态的服务,以前在内存中存储的数据、Session 等缓存,现在都会放到 Redis、etcd 等数据库中存储,微服务架构要求我们对业务进行拆分并划清服务之间的边界,所以有状态的服务往往会对架构的水平迁移带来障碍。

然而有状态的服务其实是无可避免的,我们将每一个基础服务或者业务服务都变成了一个个只负责计算的进程,但是仍然需要有其他的进程负责存储易失的缓存和持久的数据,Kubernetes 对这种有状态的服务也提供了比较好的支持。

PersistentVolume(持久化存储)和 PersistentVolumeClaim 的概念用来屏蔽底层存储的差异性,目前的 Kubernetes 支持下列类型的 PersistentVolume。这些不同的 PersistentVolume 会被开发者声明的 PersistentVolumeClaim 分配到不同的服务中,对于上层来讲,所有的服务都不需要接触 PersistentVolume,只需要直接使用 PersistentVolumeClaim 得到的卷就可以了。

在这里插入图片描述

Operator

有状态应用(e.g. Kafka、TiDB)跟无状态应用(e.g. Jave Web 网站)的不同之处,在于前者对某些外部资源有着绑定性的依赖,比如:远程存储,或者网络设备。以及,有状态应用的多个实例之间往往有着拓扑关系。

所以,在容器的世界里,有状态应用就成了一个 “异类”。我们知道容器的本质,其实就是一个被隔离和限制的 “沙箱”,对外部世界(宿主机)的不进行感知,所以容器的 “状态” 一定是 “易失” 的。

而分布式应用就是有状态应用的一个典型,一旦这些容器实例像分布式应用这样具有了拓扑关系,以及,这些实例本身不完全等价的时候,容器化的解决方案就再次变得 “丑陋” 起来:这种情况下,应用开发者们不仅又要为这些容器实例编写一套难以维护的管理脚本,还必须要想办法应对容器重启后状态丢失的难题。而这些容器状态的维护,实际上往往需要打破容器的隔离性、让容器对外部世界有所感知才能做到,这就使得容器化与有状态,成为了两种完全相悖的需求。

Operator 的设计,其实就是把控制器模式(Controller Pattern)的思想,贯彻的更加彻底。在 Operator 里,用户提交的 API 对象不再是一个单体应用的描述,而是一个完整的分布式应用集群的描述。这里的区别在于,整个分布式应用集群的状态和定义,都成了 Kubernetes 控制器需要保证的 “终态”。比如:这个应用有几个实例,实例间的关系如何处理,实例需要把数据存储在哪里,如何对实例数据进行备份和恢复,都是这个控制器需要根据 API 对象的变化进行处理的逻辑。

Operator 其实就是一段代码,这段代码 Watch 了 etcd 里一个描述分布式应用集群的 API 对象,然后这段代码通过实现 Kubernetes 的控制器模式,来保证这个集群始终跟用户的定义完全相同。而在这个过程中,Operator 也有能力利用 Kubernetes 的存储、网络插件等外部资源,协同的为应用状态的保持提供帮助。

所以说,Operator 本身在实现上,其实是在 Kubernetes 声明式 API 基础上的一种 “微创新”。它合理的利用了 Kubernetes API 可以添加自定义 API 类型的能力,然后又巧妙的通过 Kubernetes 原生的 “控制器模式”,完成了一个面向分布式应用终态的调谐过程。

而 Operator 本身在用法上,则是一个需要用户大量编写代码的的开发者工具。不过,这个编写代码的过程,并没有像很多人当初料想的那样导致 Operator 项目走向小众,反而在短短三年的时间里, Operator 就迅速成为了容器化分布式应用管理的事实标准。时至今日,Operator 项目的生态地位已经毋庸置疑。

Operator 的实现原理

顾名思义,Operator 这个项目最开始的初衷,是用来帮助开发者实现运维(Operate)能力的。但 Operator 的核心思想,却并不是 “替开发者做运维工作”,而是 “让开发者自己编写运维工具”。更有意思的是,这个运维工具的编写标准,或者说,编写 Operator 代码可以参考的模板,正是 Kubernetes 的 “控制器模式(Controller Pattern)”。

前面已经说过, Kubernetes 的 “控制器模式”,是围绕着比如 Pod 这样的 API 对象,在 Controller 通过响应它的增删改查来定义对 Pod 的编排动作。

而 Operator 的设计思路,就是允许开发者在 Kubernetes 里添加一个新的 API 对象,用来描述一个分布式应用的集群。然后,在这个 API 对象的 Controller 里,开发者就可以定义对这个分布式应用集群的运维动作了。

举个例子, 假设下面这个 YAML 文件定义的,是一个 3 节点 etcd 集群的描述:

"etcd.database.coreos.com/v1beta2"
kind: "EtcdCluster"
metadata:
    name: "example-etcd-cluster"
spec:
    size: 3
    version: "3.2.13"

有了这样一个 EtcdCluster 对象,那么开发者接下来要做的事情,就是编写一个 EtcdCluster Controller,使得当任何用户提交这样一个 YAML 文件给 Kubernetes 之后,我们自己编写的 Controller 就会响应 EtcdCluster “增加” 事件,为用户创建出 3 个节点的 etcd 集群出来。然后,它还会按照我们在 Controller 编写的事件响应逻辑,自动的对这个集群的节点更新、删除等事件做出处理,执行我定义的其他运维功能。像这样一个 EtcdCluster Controller,就是 etcd Operator 的核心组成部分了。

可是,要完成这个 Operator 真正困难在于:Kubernetes 只认识 Pod、Node、Service 等这些 Kubernetes 自己原生的 API 对象,它怎么可能认识开发者自己定义的这个 EtcdCluster 对象呢?

在当时, Kubernetes 项目允许用户自己添加 API 对象的插件能力,叫做 TPR(Third Party Resource,第三方资源),现在可以称之为 CRD(Custom Resource Definition,自定义资源)。

CRD 允许你提交一个 YAML 文件,来定义你想要的的新 API 对象的名字,比如:EtcdCluster;也允许你定义这个对象允许的合法的属性,比如:int 格式的 size 字段, string 格式的 version 字段。然后,你就可以提交一个具体的 EtcdCluster 对象的描述文件给 Kubernetes,等待该对应的 Controller 进行处理。而这个 Controller,就是 Operator 的主干代码了。

etcd Operator 就是借助 Kubernetes 的核心原理来自动化的管理 etcd 集群,更重要的是,不需要使用 Kubernetes 里自带的 StatefulSet。

可见,Operator 依赖的 Kubernetes 能力,只有最核心的声明式 API 与控制器模式;Operator 具体的实现逻辑,则编写在自定义 Controller 的代码中。这种设计给开发者赋予了极高的自由度。

StatefulSet 与 Operator 的区别

然而,Kubernetes 项目一直以来,其实都内置着一个管理有状态应用的能力叫作 StatefulSet。而如果你稍微了解 Kubernetes 项目的话就不难发现,Operator 和 StatefulSet,虽然在对应用状态的抽象上有所不同,但它们的设计原理,几乎是完全一致的,即:这两种机制的本质,都是围绕 Kubernetes API 对象的“终态”进行调谐的一个控制器(Controller)而已。

可是,为什么在一个开源社区里,会同时存在这样的两个核心原理完全一致、设计目标也几乎相同的有状态应用管理方案呢?

StatefulSet 的核心原理,其实是对分布式应用的两种状态进行了保持:

  • 分布式应用的拓扑状态,或者说,节点之间的启动顺序;
  • 分布式应用的存储状态,或者说,每个节点依赖的持久化数据。

可是,为了能够实现上述两种状态的保持机制,StatefulSet 的设计就给应用开发者带来了额外的束缚。

比如,etcd 集群各节点之间的拓扑关系,并不依赖于节点名字或者角色(比如 Master 或者 Slave)来确定,而是记录在每个 etcd 节点的启动参数当中。这使得 StatefulSet 通过 “为节点分配有序的 DNS 名字” 的拓扑保持方式,实际上没有了用武之地,反而还得要求开发者在节点的启动命令里添加大量的逻辑来生成正确的启动命令,非常不优雅。类似的,对于存储状态来说,etcd 集群对数据的备份和恢复方法,也跟 StatefulSet 依赖的的远程持久化数据卷方案并没有太大关系。

不难看到, StatefulSet 其实比较适用于应用本身节点管理能力不完善的项目,比如:MySQL。而对于 etcd 这种已经借助 Raft 实现了自管理的分布式应用来说, StatefulSet 的使用方法和带来的各种限制,其实是非常别扭的。

而借助 Kubernetes 原生的扩展机制实现的 Operator,正是一个比 StatefulSet 更加灵活、能够把控制权重新交还给开发者的分布式应用管理工具。

Kubernetes API Server 将成为软件的通用控制平面

Kubernetes API 生态风起云涌,几乎在每个星期,甚至每一天,都有太多围绕着 Kubernetes 开发者生态的角逐,在这个无比繁荣的社区背后,以不为人知的方式开始或者谢幕。

而这一切纷争的根本原因却无比直白。Kubernetes 项目,已经被广泛认可为云计算时代应用开发者们的终端入口。这正是为何,无论是 Google、微软,还是 CoreOS 以及 Heptio,所有这个生态里的大小玩家,都在不遗余力的在 Kubernetes API 层上捍卫着自己的话语权,以期在这个未来云时代的开发者入口上,争取到自己的一席之地。

在过去几年激烈的竞争中,Kubernetes 的战略定于为容器编排平台的标准。但从长远来看,Kubernetes 的愿景不仅是围绕容器,而是围绕其 API,它试图成为一个在更加基本和更广泛层面上的软件管理平台。这个目标会使 Kubernetes API 成为软件的通用控制平面,API Server 会成为管理软件的权威接口。

猜你喜欢

转载自blog.csdn.net/Jmilk/article/details/109909993