一文详解Kubernetes的架构设计

架构哲学

Kubernetes核心建立在一个优雅而精妙的设计之上: 基于事件驱动以及贯穿始终的控制循环.

具体实现

期望状态/实际状态

先介绍K8s集群中最重要最核心的部分: ETCD

ETCD 是一个开源的、分布式、高可用的键值存储系统。它主要用于存储分布式系统或计算机集群的关键共享配置、服务发现和调度协调数据.

我们可以先看一个简单的nginx-deployment.yaml配置文件

# 这个配置代表着用户 希望 集群最终到达的期望状态
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  replicas: 3 # 期望运行 3 个 副本数量
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:latest
        ports:
        - containerPort: 80

当我们执行kubectl apply -f nginx-deployment.yaml的时候,kubectl客户端解析该文件,并将其转换成JSON格式并作为HTTPS的请求体发送给ApiServer.ApiServer接收到HTTPS请求之后经过一系列检查确认,就会把这个JSON写入到ETCD中.

这时查看数据库可以发现插入的新条目大概就是这样

idnamevalue
100/registry/deployments/default/nginx-deploymentDeployment资源对象的JSON数据
  • id是自增主键
  • name是资源的唯一路径,一定以/registry开头
  • value则是存经过apiserver处理过后的JSON数据

其实在这里我们可以先丢出结论

  1. ETCD就是一个只存键值对的数据库,这个数据库可以由MySQL/Postgresql/SQLite担任.
  2. ApiServer就是一个接受HTTP RESTful请求的后端程序,要做的工作就是正确接受请求然后写入数据库(ETCD),并推送事件给下级的Controller.
  3. 剩下的所有组件都是直接或者形式上的Controller,都会向ApiServer订阅自己感兴趣的资源!

存储在数据库中的资源定义代表用户希望集群达到的状态,这是所有工作的起点!
存储在数据库中的资源定义代表用户希望集群达到的状态,这是所有工作的起点!
存储在数据库中的资源定义代表用户希望集群达到的状态,这是所有工作的起点!

内置组件控制循环

  1. ApiServer启动之后,它会向ETCD发送一个watch请求,这个watch请求会告诉ETCD任何数据变化都要通知ApiServer,并告知变化的数据以及变化的类型.

  2. kubectl apply写入ETCD后,ETCDApiServer推送了一个数据发生变化(CRUD)的事件,并告诉了变化的类型和具体数值.

  3. 一旦ApiServer收到etcd推送的数据变化事件,会先将这个新的状态更新到其内存的缓存中.

  4. 这个时候就轮到Controller Manager登场了,CM运行着大量的Controllers,这些控制器会通过一个HTTP GET请求向APIServer告知它感兴趣(订阅)的资源.

    • Deployment Controller: 启动时订阅DeploymentsReplicaSets资源
    • ReplicaSet Controller: 启动时订阅ReplicaSetsPods资源.
    • StatefulSet Controller: 启动时订阅StatefulSetsPodsPersistentVolumeClaims资源.
    • Node Controller: 启动时订阅NodesPods资源.
    • Service Controller: 启动时订阅ServicesEndpoints资源.
    • Endpoints Controller: 启动时订阅ServicesPods资源.
    • Job Controller: 启动时订阅JobsPods资源.
    • CronJob Controller: 启动时订阅CronJobsJobs资源.
    • Namespace Controller: 启动时订阅Namespaces资源.
    • ServiceAccount Controller: 启动时订阅ServiceAccountsSecrets资源.
    • and so on.
  5. 因为第4步集群启动时这些控制器已经向ApiServer订阅了自己想要的资源,所以当第 3 步发生时,ApiServer知道是Deployment Controller订阅了Deployments资源对象,所以ApiServer会推送一个事件给Deployment Controller.

  6. Deployment Controller通过这种Informer/Watch机制接收到了Deployment资源对象创建的事件,它查看了推送过来的这个Deployment资源对象(期望状态为集群有3个标签为app:nginx且监听80端口的nginx pod),但是它发现集群当前还没有对应的Pod对象,这个时候Deployment Controller会创建 1 个ReplicaSet资源对象,并通过ApiServer最终写入到ETCD中去,条目如下所示.

idnamevalue
101/registry/replicasets/default/nginx-deployment-xxxxReplicaSet资源对象的JSON数据
  1. 这时就又循环到第2步,因为有数据变化所以ETCDApiServer推送了一个数据发生变化的事件,因为ReplicaSet Controller预先向ApiServer订阅了ReplicaSetsPods资源,所以ApiServer将这个新的ReplicaSet创建事件推送给ReplicaSet Controller.

  2. ReplicaSet Controller接收到了ReplicaSet对象被创建的事件,它查看了推送过来的这个ReplicaSet对象(期望状态是始终维持3个具有标签app:nginx且监听80端口的nginx pod),但是它发现集群没有对应的Pod资源,所以ReplicaSet Controller利用ReplicaSet对象中携带的Pod模板,构造出3个新的Pod对象,并通过ApiServerETCD提交这 3 个新的Pod资源对象的定义,条目如下所示.

idnamevalue
102/registry/pods/default/nginx-pod-xxxx-aPod资源对象的JSON数据(Pending状态)
103/registry/pods/default/nginx-pod-xxxx-bPod资源对象的JSON数据(Pending状态)
104/registry/pods/default/nginx-pod-xxxx-cPod资源对象的JSON数据(Pending状态)
  1. 因为ETCD数据发生了变化,循环再次启动,ETCD推送给ApiServer,ApiServer再把事件推送给所有订阅了Pod资源的Controller,ReplicaSet Controller也会收到自己创建的Pod事件,它发现实际Pod数量为3,期望数量和实际数量已经相等,ReplicaSet Controller认为自己的扩容任务已经完成,所以它不会再创建任何新的Pod,它会静静等待让K8s中的其他组件来完成将PodPending状态驱动到Running状态的工作.

  2. 这个时候就轮到Kube-Scheduler登场了,集群启动时调度器也向ApiServer订阅(Watch)了Pods资源(调度器也可以算是一种Controller,它的期望状态条件是:所有处于Pending状态且尚未设置nodeName字段的Pod,都必须被成功分配到满足所有约束条件的Node上).

  • 当它接收到了3个新的、处于Pending状态且尚未设置nodeName字段的Pod对象,调度器接收到这3个Pod对象后,开始执行调度算法,并最终确定这三个Pod对象应该运行在哪个node上,调度器更新了Pod对象nodeName字段,并通过ApiServerETCD更新了102/103/104这三行数据.
  1. 因为ETCD数据发生了变化所以循环又开始了,ETCDApiServer推送数据发生变化的事件,然后ApiServer再把事件推送给所有订阅了Pod资源的Controller.

  2. 这时候轮到Node节点上的Kubelet登场了,每一台Node节点在加入集群时会安装kubelet,并向ApiServer订阅(Watch)了Pods资源,所以ApiServer也会给每一台Node的Kubelet推送事件,但它只关心那些nodeName字段匹配自己节点名称的Pods对象.

  3. 当匹配中之后kubelet会检查pod定义,然后调用底层容器运行时接口(CRI),例如containerd,在本地创建Pod容器,当容器启动成功之后,Kubelet会将Pod的实际状态(例如: Running状态、IP地址等)通过ApiServer再次更新ETCD中102/103/104这三行数据.

  • 当事件再次被推送回kubelet时,Pod资源对象期望的状态是nodeName=本节点status=running,kubelet检查过后发现实际状态和期望状态一致,所以什么都不做!
  1. 所以又发生了循环,Pod对象的状态更新的事件再次推送到ReplicaSet Controller,查询集群后发现有3 个Pods处于 Running 状态,与期望状态一致,所以就将它所拥有的ReplicaSet资源对象的状态 (.status.readyReplicas 和 .status.availableReplicas)更新为3,即id为101的那一行数据,并通过ApiServer更新到ETCD中.

  2. 因为ReplicaSet对象状态的更新再次触发事件,推送到ApiServer,最终推送给了Deployment Controller,检查其子资源 ReplicaSet 的状态,发现其 .status.availableReplicas 已达到 3,Deployment 的期望状态已通过其子资源完全实现,Deployment Controller执行最终收尾工作,更新其自身 Deployment对象.status字段,即更新id为100的那一行数据,并通过ApiServer将更新写入到ETCD中.

  3. 执行最后一次控制循环和调谐,所以对象的期望状态都已实现,集群进入稳定状态,等待下一次数据更新!

外置组件控制循环

有软件工程思想的读者会发现,K8s的这套架构有着很多优点,诸如:

  • 拓展性极高,易于定制和组件替换
  • 耦合度很低,且能够自我修复
  • 统一且规范的集群通信接口
  • 声明式的API

整套架构的核心就是ETCD中的每一行条目,也可以说是每一个对象.当我们的需求不能够被K8s集群的内置组件满足时,K8s也为我们提供了自定义资源(Custom Resource)的接口,让我们的自定义资源对象也可以加入整个事件驱动和控制循环的流程.

而这个接口就是CustomResourceDefinition资源对象,CRD也是一种内置的Kubernetes资源类型,而这个资源对象写入ETCD后就是其value值表示的是想要添加到集群中的新的自定义资源类型(Custom Resource,CR)的模式、作用域和版本等信息.

可以先看一个database-crd.yaml配置文件

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: databases.stable.bitrate.top 
spec:
  # API 组名称
  group: stable.bitrate.top
  # 作用域:Namespaced 表示该资源在命名空间内可见
  scope: Namespaced
  names:
    # 复数形式 (用于 API 路径: /apis/stable.example.com/v1/databases)
    plural: databases
    # 单数形式
    singular: database
    # Kind 名称 (用于 YAML 文件: kind: Database)
    kind: Database
    # 简称
    shortNames:
    - db
  versions:
    - name: v1
      served: true
      storage: true
      schema:
        # 定义自定义资源 (CR) 的结构
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              properties:
                # 数据库类型 (例如:MySQL, PostgreSQL)
                engine:
                  type: string
                # 数据库容量 (例如:small, large)
                size:
                  type: string
              required:
                - engine
                - size
  1. 定义资源对象蓝图
  • 当执行kubectl apply -f database-crd.yaml时,kubectl解析文件并通过ApiServer写入ETCD数据库,而在ETCD中条目大概如下所示
idnamevalue
105/registry/apiextensions.k8s.io/crds/databases.stable.bitrate.topCRD资源对象的JSON数据
  • ApiServer会根据CRD资源对象的定义,在ETCD中为新的Database资源类型确定了存储路径,而这个存储路径遵循/registry/<group>/<version>/<plural-name>/规则,所以Database资源的存储路径根据CRD资源对象就可以知道是/registry/stable.bitrate.top/v1/databases/,当未来第一个Database资源写入ETCD时,其name就是这个路径.

  • CRD资源对象写入ETCD时同样会被推送一个CRD资源对象添加的事件,然后ApiServer根据订阅信息推送事件给CRD Controller,同样进行了控制循环.

  • 上面代表着我们定义的资源蓝图对象其实就相当于面向对象中的类/结构体/模板,而根据类/结构体创建的资源对象(CR)才是我们真正想要的.

  1. 创建自定义资源(CR)实例
# 我们自己写的my-db.yaml(CR)
apiVersion: stable.bitrate.top/v1
kind: Database
metadata:
  name: my-db-01
  namespace: default
spec:
  engine: mysql
  version: "8.0"
  size: 50Gi
  • 当您使用kubectl apply -f my-db.yaml时,ApiServer会验证这个对象是否符合 databases.stable.bitrate.top CRD中定义的模式(schema),然后将其存储到ETCD中确定的路径下.
idnamevalue
106/registry/stable.example.com/v1/databases/default/my-db-01Database资源对象的JSON数据
  1. 事件驱动与Operator监听
  • ETCD推送数据变化事件给ApiServer之后,ApiServer会查询订阅了Databases资源的Controller,而这个Controller是我们自己编写的,在启动时向ApiServer订阅了Databases资源.

  • 对于这种非集群默认内置的外置控制器,K8s给了他一个高大上的名字Operator,其本质上就是一个Controller.

之后的一切也都和上面的内置组件控制循环一样,通过事件驱动和控制循环推动整个集群的运行.