11.1 了解架构

作者: 众神开挂 | 来源:发表于2021-08-01 06:35 被阅读0次

    11.1 了解架构

    在研究Kubernetes如何实现其功能之前,先具体了解下Kubernetes集群有哪些组件。在第一章中,可以看到,Kubernetes集群分为两部分:

    • Kubernetes控制平面
    • (工作)节点

    让我们具体看下这两个部分做了什么,以及内部运行的内容。

    控制平面的组件

    控制平面负责控制并使得整个集群正常运转。回顾一下,控制平面包含如下组件:

    • etcd分布式持久化存储
    • API服务器
    • 调度器
    • 控制器管理器

    这些组件用来存储、管理集群状态,但它们不是运行应用的容器。

    工作节点上运行的组件

    运行容器的任务依赖于每个工作节点上运行的组件:

    • Kubelet
    • Kubelet服务代理(kube-proxy)
    • 容器运行时(Docker、rkt或者其他)

    附加组件

    除了控制平面(和运行在节点上的组件,还要有几个附加组件,这样才能提供所有之前讨论的功能。包含:

    • Kubernetes DNS服务器
    • 仪表板
    • Ingress控制器
    • Heapster(容器集群监控),将在第14 章讨论
    • 容器网络接口插件(本章后面会做讨论)

    11.1.1 Kubernetes组件的分布式特性

    之前提到的组件都是作为单独进程运行的。图11.1描述了各个组件及它们之间的依赖关系。

    若要启用Kubernetes提供的所有特性,需要运行所有的这些组件。但是有几个组件无须其他组件,单独运行也能提供非常有用的工作。接下来会详细查看每一个组件。

    [图片上传失败...(image-d7df9c-1627770873428)]image

    图11.1 Kubernetes控制平面以及工作节点的组件

    检查控制平面组件的状态

    API服务器对外暴露了一个名为ComponentStatus的API资源,用来显示每个控制平面组件的健康状态。可以通过kubectl列出各个组件以及它们的状态:

    $ kubectl get componentstatuses
    

    组件间如何通信

    Kubernetes系统组件间只能通过API服务器通信,它们之间不会直接通信。API服务器是和etcd通信的唯一组件。其他组件不会直接和etcd通信,而是通过API服务器来修改集群状态。

    API服务器和其他组件的连接基本都是由组件发起的,如图 11.1 所示。但是,当你使用kubectl获取日志、使用kubectl attach连接到一个运行中的容器或运行kubectl port-forward命令时,API服务器会向Kubelet发起连接。

    注意 kubectl attach命令和kubectl exec命令类似,区别是:前者会附属到容器中运行着的主进程上,而后者是重新运行一个进程。

    单组件运行多实例

    尽管工作节点上的组件都需要运行在同一个节点上,控制平面的组件可以被简单地分割在多台服务器上。为了保证高可用性,控制平面的每个组件可以有多个实例。etcd和API服务器的多个实例可以同时并行工作,但是,调度器和控制器管理器在给定时间内只能有一个实例起作用,其他实例处于待命模式。

    组件是如何运行的

    控制平面的组件以及kube-proxy可以直接部署在系统上或者作为pod来运行(如图 11.1 所示)。听到这个你可能比较惊讶,不过后面我们讨论Kubelet时就都说得通了。

    Kubelet是唯一一直作为常规系统组件来运行的组件,它把其他组件作为pod来运行。为了将控制平面作为pod来运行,Kubelet被部署在master上。下面的代码清单展示了通过kubeadm(在附录B中阐述)创建的集群里的kube-system命名空间里的pod。

    代码清单11.1 作为pod运行的Kubernetes组件

    $ kubectl get po -o custom-columns=POD:metadata.name,NODE:spec.nodeName --sort-by spec.nodeName -n kube-system
    

    如代码清单所示,所有的控制平面组件在主节点上作为pod运行。这里有三个工作节点,每一个节点运行kube-proxy和一个Flannel pod,用来为pod提供重叠网络(后面我们会再讨论Flannel)。

    提示 如代码清单所示,可以通过-o custom-columns选项自定义展示的列以及--sort-by对资源列表进行排序。

    现在,让我们对每一个组件进行研究,从控制平面的底层组件——持久化存储组件开始。

    11.1.2 Kubernetes如何使用etcd

    本书让你创建的所有对象——pod、ReplicationController、服务和私密凭据等,需要以持久化方式存储到某个地方,这样它们的manifest在API服务器重启和失败的时候才不会丢失。为此,Kubernetes使用了etcd。etcd是一个响应快、分布式、一致的key-value存储。因为它是分布式的,故可以运行多个etcd实例来获取高可用性和更好的性能。

    唯一能直接和etcd通信的是Kubernetes的API服务器。所有其他组件通过API服务器间接地读取、写入数据到etcd。这带来一些好处,其中之一就是增强乐观锁系统、验证系统的健壮性;并且,通过把实际存储机制从其他组件抽离,未来替换起来也更容易。值得强调的是,etcd是Kubernetes存储集群状态和元数据的唯一的地方。

    关于乐观并发控制

    乐观并发控制(有时候指乐观锁)是指一段数据包含一个版本数字,而不是锁住该段数据并阻止读写操作。每当更新数据,版本数就会增加。当更新数据时,就会检查版本值是否在客户端读取数据时间和提交时间之间被增加过。如果增加过,那么更新会被拒绝,客户端必须重新读取新数据,重新尝试更新。

    两个客户端尝试更新同一个数据条目,只有第一个会成功。 所有的Kubernetes包含一个 metadata.resourceVersion 字段,当更新对象时,客户端需要返回该值到API服务器。如果版本值与etcd中存储的不匹配,API服务器会拒绝该更新。

    资源如何存储在etcd中

    当笔者撰写此书时,Kubernetes既可以用etcd版本2也可以用版本3,但目前更推荐版本3,它的性能更好。etcd v2 把key存储在一个层级键空间中,这使得键值对类似文件系统的文件。etcd中每个key要么是一个目录,包含其他key,要么是一个常规key,对应一个值。etcd v3 不支持目录,但是由于key格式保持不变(键可以包含斜杠),仍然可以认为它们可以被组织为目录。Kubernetes存储所有数据到etcd的 /registry下。下面的代码清单显示 /registry下存储的一系列key。

    代码清单11.2 etcd中存储的Kubernetes的顶层条目

    $ etcdctl  get /registry --prefix --keys-only
    
    $ kubectl -n kube-system exec -it etcd-minikube -- sh -c "ETCDCTL_API=3 ETCDCTL_CACERT=/var/lib/minikube/certs/etcd/ca.crt ETCDCTL_CERT=/var/lib/minikube/certs/etcd/server.crt ETCDCTL_KEY=/var/lib/minikube/certs/etcd/server.key etcdctl endpoint health"
    
    $ kubectl -n kube-system exec -it etcd-minikube -- sh -c "ETCDCTL_API=3 ETCDCTL_CACERT=/var/lib/minikube/certs/etcd/ca.crt ETCDCTL_CERT=/var/lib/minikube/certs/etcd/server.crt ETCDCTL_KEY=/var/lib/minikube/certs/etcd/server.key etcdctl  get /registry --prefix --keys-only"
    

    你可能会发现,这些key和之前几章中学习到的资源类型对应。

    注意 如果使用etcd v3 的API,就无法使用ls命令来查看目录的内容。但是,可以通过 etcdctl get /registry--prefix=true 列出所有以给定前缀开始的key。

    下面的代码清单显示了 /registry/pods 目录的内容。

    代码清单11.3 /registry/pods目录下的key

    $ etcdctl ls /registry/pods
    

    从名称可以看出,这两个条目对应default和kube-system命名空间,意味着pod按命名空间存储。下面的代码清单显示 /registry/pods/default 目录下的条目。

    代码清单11.4 default命名空间中pod的etcd条目

    $ etcdctl get /registry/pods/default --prefix
    

    每个条目对应一个单独的pod。这些不是目录,而是键值对。下面的代码清单展示了其中一条存储的内容。

    代码清单11.5 一个etcd条目代表一个pod

    $ etcdctl get /registry/pods/default/kubia-159041347-wt6ga
    

    你可能发现了,这就是一个JSON格式的pod定义。API服务器将资源的完整JSON形式存储到etcd中。由于etcd的层级键空间,可以想象成把资源以JSON文件格式存储到文件系统中。简单易懂,对吧?

    警告 Kubernetes 1.7 之前的版本,密钥凭据的JSON内容也像上面一样存储(没有加密)。如果有人有权限直接访问etcd,那么可以获取所有的密钥凭据。从 1.7 版本开始,密钥凭据会被加密,这样存储起来更加安全。

    确保存储对象的一致性和可验证性

    还记得第1章中提到的Kubernetes所依赖的谷歌的Borg和Omega系统吗?和Kubernetes类似,Omega使用一个集中存储模块保存集群状态。不同之处是,多个控制平面组件可以直接访问存储模块。所有这些组件需要确保它们都遵循同一个乐观锁机制,来保证能正确处理冲突。只要有一个组件没有完全遵循该机制就可能导致数据不一致。

    Kubernetes对此做了改进,要求所有控制平面组件只能通过API服务器操作存储模块。使用这种方式更新集群状态总是一致的,因为API服务器实现了乐观锁机制,如果有错误的话,也会更少。API服务器同时确保写入存储的数据总是有效的,只有授权的客户端才能更改数据。

    确保etcd集群一致性

    为保证高可用性,常常会运行多个etcd实例。多个etcd实例需要保持一致。这种分布式系统需要对系统的实际状态达成一致。etcd使用RAFT一致性算法来保证这一点,确保在任何时间点,每个节点的状态要么是大部分节点的当前状态,要么是之前确认过的状态。

    连接到etcd集群不同节点的客户端,得到的要么是当前的实际状态,要么是之前的状态(在Kubernetes中,etcd的唯一客户端是API服务器,但有可能有多个实例)。

    一致性算法要求集群大部分(法定数量)节点参与才能进行到下一个状态。结果就是,如果集群分裂为两个不互联的节点组,两个组的状态不可能不一致,因为要从之前状态变化到新状态,需要有过半的节点参与状态变更。如果一个组包含了大部分节点,那么另外一组只有少量节点成员。第一个组就可以更改集群状态,后者则不可以。当两个组重新恢复连接,第二个组的节点会更新为第一个组的节点的状态。

    img

    图11.2 在脑裂场景中,只有拥有大部分(法定数量)节点的组会接受状态变更

    为什么etcd实例数量应该是奇数

    etcd通常部署奇数个实例。你一定想知道为什么。让我们比较有一个实例和有两个实例的情况时。有两个实例时,要求另一个实例必须在线,这样才能符合超过半数的数量要求。如果有一个宕机,那么etcd集群就不能转换到新状态,因为没有超过半数。两个实例的情况比一个实例的情况更糟。对比单节点宕机,在有两个实例的情况下,整个集群挂掉的概率增加了 100%。

    比较 3 节点和 4 节点也是同样的情况。3 节点情况下,一个实例宕机,但超过半数(2个)的节点仍然运行着。对于 4 节点情况,需要 3个节点才能超过半数(2个不够)。对于 3 节点和 4 节点,假设只有一个实例会宕机。当以 4 节点运行时,一个节点失败后,剩余节点宕机的可能性会更大(对比 3 节点集群,一个节点宕机还剩两个节点的情况)。

    通常,对于大集群,etcd集群有5个或7个节点就足够了。可以允许 2 ~ 3个节点宕机,这对于大多数场景来说足够了。

    11.1.3 API服务器做了什么

    Kubernetes API服务器作为中心组件,其他组件或者客户端(如kubectl)都会去调用它。以RESTful API的形式提供了可以查询、修改集群状态的CRUD(Create、Read、Update、Delete)接口。它将状态存储到etcd中。

    API服务器除了提供一种一致的方式将对象存储到etcd,也对这些对象做校验,这样客户端就无法存入非法的对象了(直接写入存储的话是有可能的)。除了校验,还会处理乐观锁,这样对于并发更新的情况,对对象做更改就不会被其他客户端覆盖。

    API服务器的客户端之一就是本书一开始就介绍使用的命令行工具kubectl。举个例子,当以JSON文件创建一个资源,kubectl通过一个HTTP POST请求将文件内容发布到API服务器。图 11.3 显示了接收到请求后API服务器内部发生了什么,后面会做更详细的介绍。

    img

    image

    图11.3 API服务器的操作

    通过认证插件认证客户端

    首先,API服务器需要认证发送请求的客户端。这是通过配置在API服务器上的一个或多个认证插件来实现的。API服务器会轮流调用这些插件,直到有一个能确认是谁发送了该请求。这是通过检查HTTP请求实现的。

    根据认证方式,用户信息可以从客户端证书或者第8章使用的HTTP标头(例如Authorization)获取。插件抽取客户端的用户名、用户ID和归属组。这些数据在下一阶段,认证的时候会用到。

    通过授权插件授权客户端

    除了认证插件,API服务器还可以配置使用一个或多个授权插件。它们的作用是决定认证的用户是否可以对请求资源执行请求操作。例如,当创建pod时,API服务器会轮询所有的授权插件,来确认该用户是否可以在请求命名空间创建pod。一旦插件确认了用户可以执行该操作,API服务器会继续下一步操作。

    通过准入控制插件验证AND/OR修改资源请求

    如果请求尝试创建、修改或者删除一个资源,请求需要经过准入控制插件的验证。同理,服务器会配置多个准入控制插件。这些插件会因为各种原因修改资源,可能会初始化资源定义中漏配的字段为默认值甚至重写它们。插件甚至会去修改并不在请求中的相关资源,同时也会因为某些原因拒绝一个请求。资源需要经过所有准入控制插件的验证。

    注意 如果请求只是尝试读取数据,则不会做准入控制的验证。

    准入控制插件包括:

    • AlwaysPullImages —— 重写pod的imagePullPolicy为Always,强制每次部署pod时拉取镜像。
    • ServiceAccount —— 未明确定义服务账户的使用默认账户。
    • NamespaceLifecycle —— 防止在命名空间中创建正在被删除的pod,或在不存在的命名空间中创建pod。
    • ResourceQuota —— 保证特定命名空间中的pod只能使用该命名空间分配数量的资源,如CPU和内存。我们将会在第14章做深入了解。

    更多的准入控制插件可以在https://kubernetes.io/docs/admin/admission-controllers/中查看Kubernetes文档。

    验证资源以及持久化存储

    请求通过了所有的准入控制插件后,API服务器会验证存储到etcd的对象,然后返回一个响应给客户端。

    11.1.4 API服务器如何通知客户端资源变更

    除了前面讨论的,API服务器没有做其他额外的工作。例如,当你创建一个ReplicaSet资源时,它不会去创建pod,同时它不会去管理服务的端点。那是控制器管理器的工作。

    API服务器甚至也没有告诉这些控制器去做什么。它做的就是,启动这些控制器,以及其他一些组件来监控已部署资源的变更。控制平面可以请求订阅资源被创建、修改或删除的通知。这使得组件可以在集群元数据变化时候执行任何需要做的任务。

    客户端通过创建到API服务器的HTTP连接来监听变更。通过此连接,客户端会接收到监听对象的一系列变更通知。每当更新对象,服务器把新版本对象发送至所有监听该对象的客户端。图 11.4 显示客户端如何监听pod的变更,以及如何将pod的变更存储到etcd,然后通知所有监听该pod的客户端。

    [图片上传失败...(image-7c6e54-1627770873428)]image

    图11.4 更新对象时,API服务器给所有监听者发送更新过的对象

    kubectl工具作为API服务器的客户端之一,也支持监听资源。例如,当部署pod时,不需要重复执行kubectl get pods来定期查询pod列表。可以使用-watch标志,每当创建、修改、删除pod时就会通知你,如下面的代码清单所示。

    代码清单11.6 监听创建删除pod事件

    $ kubectl get pods --watch
    

    甚至可以让kubectl打印出整个监听事件的YAML文件,如下:

    $ kubectl get pods -o yaml --watch
    

    监听机制同样也可以用于调度器。调度器是下一个要着重讲解的控制平面组件。

    11.1.5 了解调度器

    前面已经学习过,我们通常不会去指定pod应该运行在哪个集群节点上,这项工作交给调度器。宏观来看,调度器的操作比较简单。就是利用API服务器的监听机制等待新创建的pod,然后给每个新的、没有节点集的pod分配节点。

    调度器不会命令选中的节点(或者节点上运行的Kubelet)去运行pod。调度器做的就是通过API服务器更新pod的定义。然后API服务器再去通知Kubelet(同样,通过之前描述的监听机制)该pod已经被调度过。当目标节点上的Kubelet发现该pod被调度到本节点,它就会创建并且运行pod的容器。

    尽管宏观上调度的过程看起来比较简单,但实际上为pod选择最佳节点的任务并不简单。当然,最简单的调度方式是不关心节点上已经运行的pod,随机选择一个节点。另一方面,调度器可以利用高级技术,例如机器学习,来预测接下来几分钟或几小时哪种类型的pod将会被调度,然后以最大的硬件利用率、无须重新调度已运行pod的方式来调度。Kubernetes的默认调度器实现方式处于最简单和最复杂程度之间。

    默认的调度算法

    选择节点操作可以分解为两部分,如图11.5 所示:

    过滤所有节点,找出能分配给pod的可用节点列表。

    对可用节点按优先级排序,找出最优节点。如果多个节点都有最高的优先级分数,那么则循环分配,确保平均分配给pod。

    [图片上传失败...(image-d51c27-1627770873428)]image

    图11.5 调度器为pod找到可用节点,然后选择最优节点

    查找可用节点

    为了决定哪些节点对pod可用,调度器会给每个节点下发一组配置好的预测函数。这些函数检查:

    • 节点是否能满足pod对硬件资源的请求。第14 章会学习如何定义它们。
    • 节点是否耗尽资源(是否报告过内存/硬盘压力参数)?
    • pod是否要求被调度到指定节点(通过名字),是否是当前节点?
    • 节点是否有和pod规格定义里的节点选择器一致的标签(如果定义了的话)?
    • 如果pod要求绑定指定的主机端口(第13 章中讨论)那么这个节点上的这个端口是否已经被占用?
    • 如果pod要求有特定类型的卷,该节点是否能为此pod加载此卷,或者说该节点上是否已经有pod在使用该卷了?
    • pod是否能够容忍节点的污点。污点以及容忍度在第16 章讲解。
    • pod是否定义了节点、pod的亲缘性以及非亲缘性规则?如果是,那么调度节点给该pod是否会违反规则?这个也会在第16 章介绍。

    所有这些测试都必须通过,节点才有资格调度给pod。在对每个节点做过这些检查后,调度器得到节点集的一个子集。任何这些节点都可以运行pod,因为它们都有足够的可用资源,也确认过满足pod定义的所有要求。

    为pod选择最佳节点

    尽管所有这些节点都能运行pod,其中的一些可能还是优于另外一些。假设有一个 2 节点集群,两个节点都可用,但是其中一个运行 10个pod,而另一个,不知道什么原因,当前没有运行任何pod。本例中,明显调度器应该选第二个节点。

    或者说,如果两个节点是由云平台提供的服务,那么更好的方式是,pod调度给第一个节点,将第二个节点释放回云服务商以节省资金。

    pod高级调度

    考虑另外一个例子。假设一个pod有多个副本。理想情况下,你会期望副本能够分散在尽可能多的节点上,而不是全部分配到单独一个节点上。该节点的宕机会导致pod支持的服务不可用。但是如果pod分散在不同的节点上,单个节点宕机,并不会对服务造成什么影响。

    默认情况下,归属同一服务和ReplicaSet的pod会分散在多个节点上。但不保证每次都是这样。不过可以通过定义pod的亲缘性、非亲缘规则强制pod分散在集群内或者集中在一起,相关内容会在第16 章中介绍。

    仅通过这两个简单的例子就说明了调度有多复杂,因为它依赖于大量的因子。因此,调度器既可以配置成满足特定的需要或者基础设施特性,也可以整体替换为一个定制的实现。可以抛开调度器运行一个Kubernetes,不过那样的话,就需要手动实现调度了。

    使用多个调度器

    可以在集群中运行多个调度器而非单个。然后,对每一个pod,可以通过在pod特性中设置schedulerName属性指定调度器来调度特定的pod。

    未设置该属性的pod由默认调度器调度,因此其schedulerName被设置为default-scheduler。其他设置了该属性的pod会被默认调度器忽略掉,它们要么是手动调用,要么被监听这类pod的调度器调用。

    可以实现自己的调度器,部署到集群,或者可以部署有不同配置项的额外Kubernetes调度器实例。

    11.1.6 介绍控制器管理器中运行的控制器

    如前面提到的,API服务器只做了存储资源到etcd和通知客户端有变更的工作。调度器则只是给pod分配节点,所以需要有活跃的组件确保系统真实状态朝API服务器定义的期望的状态收敛。这个工作由控制器管理器里的控制器来实现。

    单个控制器、管理器进程当前组合了多个执行不同非冲突任务的控制器。这些控制器最终会被分解到不同的进程,如果需要的话,我们能够用自定义实现替换它们每一个。控制器包括:

    • Replication管理器(ReplicationController资源的管理器)
    • ReplicaSet、DaemonSet以及Job控制器
    • Deployment 控制器
    • StatefulSet 控制器
    • Node 控制器
    • Service控制器
    • Endpoints控制器
    • Namespace 控制器
    • PersistentVolume 控制器
    • 其他

    每个控制器做什么通过名字显而易见。通过上述列表,几乎可以知道创建每个资源对应的控制器是什么。资源描述了集群中应该运行什么,而控制器就是活跃的Kubernetes组件,去做具体工作部署资源。

    了解控制器做了什么以及如何做的

    控制器做了许多不同的事情,但是它们都通过API服务器监听资源(部署、服务等)变更,并且不论是创建新对象还是更新、删除已有对象,都对变更执行相应操作。大多数情况下,这些操作涵盖了新建其他资源或者更新监听的资源本身(例如,更新对象的status)。

    总的来说,控制器执行一个“调和”循环,将实际状态调整为期望状态(在资源spec部分定义),然后将新的实际状态写入资源的status部分。控制器利用监听机制来订阅变更,但是由于使用监听机制并不保证控制器不会漏掉时间,所以仍然需要定期执行重列举操作来确保不会丢掉什么。

    控制器之间不会直接通信,它们甚至不知道其他控制器的存在。每个控制器都连接到API服务器,通过 11.1.3 节描述的监听机制,请求订阅该控制器负责的一系列资源的变更。

    我们概括地了解了每个控制器做了什么,但是如果你想深入了解它们做了什么,建议直接看源代码。边栏阐述了如何上手看源代码。

    浏览控制器源代码的几个要点

    如果你对控制器如何运作感兴趣,强烈推荐看一遍源代码。为了更容易上手,下面有几个小建议:

    控制器的源代码可以从https://github.com/kubernetes/kubernetes/blob/master/pkg/controller获取。

    每个控制器一般有一个构造器,内部会创建一个Informer,其实是个监听器,每次API对象有更新就会被调用。通常,Informer会监听特定类型的资源变更事件。查看构造器可以了解控制器监听的是哪个资源。

    接下来,去看worker()方法。其中定义了每次控制器需要工作的时候都会调用worker()方法。实际的函数通常保存在一个叫syncHandler或类似的字段里。该字段也在构造器里初始化,可以在那里找到被调用函数名。该函数是所有魔法发生的地方。

    Replication管理器

    启动ReplicationController资源的控制器叫作 Replication管理器。第4 章我们介绍过ReplicationController是如何工作的,其实不是ReplicationController做了实际的工作,而是Replication管理器。让我们快速回顾下该控制器做了什么,这有助于你理解其他控制器。

    在第4 章中,我们说过,ReplicationController的操作可以理解为一个无限循环,每次循环,控制器都会查找符合其pod选择器定义的pod 的数量,并且将该数值和期望的复制集(replica)数量做比较。

    既然你知道了API服务器可以通过监听机制通知客户端,那么明显地,控制器不会每次循环去轮询pod,而是通过监听机制订阅可能影响期望的复制集(replica)数量或者符合条件pod数量的变更事件(见图11.6)。任何该类型的变化,将触发控制器重新检查期望的以及实际的复制集数量,然后做出相应操作。

    你已经知道,当运行的pod实例太少时,ReplicationController会运行额外的实例,但它自己实际上不会去运行pod。它会创建新的pod清单,发布到API服务器,让调度器以及Kubelet来做调度工作并运行pod。

    [图片上传失败...(image-fb67f2-1627770873428)]image

    图11.6 Replication管理器监听API对象变更

    Replication管理器通过API服务器操纵pod API对象来完成其工作。所有控制器就是这样运作的。

    RerlicaSet、DaemonSet以及Job控制器

    ReplicaSet控制器基本上做了和前面描述的Replication管理器一样的事情,所以这里不再赘述。DaemonSet以及Job控制器比较相似,从它们各自资源集中定义的pod模板创建pod资源。与Replication管理器类似,这些控制器不会运行pod,而是将pod定义到发布API服务器,让Kubelet创建容器并运行。

    Deployment控制器

    Deployment控制器负责使deployment的实际状态与对应Deployment API对象的期望状态同步。

    每次Deployment对象修改后(如果修改会影响到部署的pod),Deployment控制器都会滚动升级到新的版本。通过创建一个ReplicaSet,然后按照Deployment中定义的策略同时伸缩新、旧RelicaSet,直到旧pod被新的代替。并不会直接创建任何pod。

    StatefulSet控制器

    StatefulSet控制器,类似于ReplicaSet控制器以及其他相关控制器,根据StatefulSet资源定义创建、管理、删除pod。其他的控制器只管理pod,而StatefulSet控制器会初始化并管理每个pod实例的持久卷声明字段。

    Node控制器

    Node控制器管理Node资源,描述了集群工作节点。其中,Node控制器使节点对象列表与集群中实际运行的机器列表保持同步。同时监控每个节点的健康状态,删除不可达节点的pod。

    Node控制器不是唯一对Node对象做更改的组件。Kubelet也可以做更改,那么显然可以由用户通过REST API调用做更改。

    Service控制器

    在第5章,当我们讨论服务时,你已经了解了存在不同服务类型。其中一个是LoadBalancer服务,从基础设施服务请求一个负载均衡器使得服务外部可以用。Service控制器就是用来在 LoadBalancer类型服务被创建或删除时,从基础设施服务请求、释放负载均衡器的。

    Endpoint控制器

    你会想起来,Service不会直接连接到pod,而是包含一个端点列表(IP和端口),列表要么是手动,要么是根据Service定义的pod选择器自动创建、更新。Endpoint控制器作为活动的组件,定期根据匹配标签选择器的pod的IP、端口更新端点列表。

    如图11.7所示,控制器同时监听了Service和pod。当Service被添加、修改,或者pod被添加、修改或删除时,控制器会选中匹配Service的pod选择器的pod,将其IP和端口添加到Endpoint资源中。请记住,Endpoint对象是个独立的对象,所以当需要的时候控制器会创建它。同样地,当删除Service时,Endpoint对象也会被删除。

    [图片上传失败...(image-57db6a-1627770873428)]image

    图11.7 Endpoint控制器监听Service和pod资源并管理Endpoint

    Namespace控制器

    想起命名空间了吗(第3章里讨论过)?大部分资源归属于某个特定命名空间。当删除一个Namespace资源时,该命名空间里的所有资源都会被删除。这就是Namespace控制器做的事情。当收到删除Namespace对象的通知时,控制器通过API服务器删除所有归属该命名空间的资源。

    PersistentVolume控制器

    第6章学习过持久卷以及持久卷声明。一旦用户创建了一个持久卷声明,Kubernetes必须找到一个合适的持久卷同时将其和声明绑定。这些由持久卷控制器实现。

    对于一个持久卷声明,控制器为声明查找最佳匹配项,通过选择匹配声明中的访问模式,并且声明的容量大于需求的容量的最小持久卷。实现方式是保存一份有序的持久卷列表,对于每种访问模式按照容量升序排列,返回列表的第一个卷。

    当用户删除持久卷声明时,会解绑卷,然后根据卷的回收策略进行回收(原样保留、删除或清空)。

    唤醒控制器

    现在,总体来说你应该对每个控制器做了什么,以及是如何工作的有个比较好的感觉了。再一次强调,所有这些控制器是通过API服务器来操作API对象的。它们不会直接和Kubelet通信或者发送任何类型的指令。实际上,它们不知道Kubelet的存在。控制器更新API服务器的一个资源后,Kubelet和Kubernetes Service Proxy(也不知道控制器的存在)会做它们的工作,例如启动pod容器、加载网络存储,或者就服务而言,创建跨pod的负载均衡。

    控制平面处理了整个系统的一部分操作,为了完全理解Kubernetes集群的内部运作方式,还需要理解Kubelet和Kubernetes Service Proxy做了什么。下面将学习这些内容。

    11.1.7 Kubelet做了什么

    所有Kubernetes控制平面的控制器都运行在主节点上,而Kubelet以及Service Proxy都运行在工作节点(实际pod容器运行的地方)上。Kubelet究竟做了什么事情?

    了解Kubelet的工作内容

    简单地说,Kubelet就是负责所有运行在工作节点上内容的组件。它第一个任务就是在API服务器中创建一个Node资源来注册该节点。然后需要持续监控API服务器是否把该节点分配给pod,然后启动pod容器。具体实现方式是告知配置好的容器运行时(Docker、CoreOS的Rkt,或者其他一些东西)来从特定容器镜像运行容器。Kubelet随后持续监控运行的容器,向API服务器报告它们的状态、事件和资源消耗。

    Kubelet也是运行容器存活探针的组件,当探针报错时它会重启容器。最后一点,当pod从API服务器删除时,Kubelet终止容器,并通知服务器pod已经被终止了。

    抛开API服务器运行静态pod

    尽管Kubelet一般会和API服务器通信并从中获取pod清单,它也可以基于本地指定目录下的pod清单来运行pod,如图 11.8 所示。如本章开头所示,该特性用于将容器化版本的控制平面组件以pod形式运行。

    不但可以按照原有的方式运行Kubernetes系统组件,也可以将pod清单放到Kubelet的清单目录中,让Kubelet运行和管理它们。

    img

    image

    图11.8 Kubelet基于API服务器/本地文件目录中的pod定义运行pod

    也可以同样的方式运行自定义的系统容器,不过推荐用DaemonSet来做这项工作。

    11.1.8 Kubernetes Service Proxy的作用

    除了Kubelet,每个工作节点还会运行kube-proxy,用于确保客户端可以通过Kubernetes API连接到你定义的服务。kube-proxy确保对服务IP和端口的连接最终能到达支持服务(或者其他,非pod服务终端)的某个pod处。如果有多个pod支撑一个服务,那么代理会发挥对pod的负载均衡作用。

    为什么被叫作代理

    kube-proxy最初实现为userspace代理。利用实际的服务器集成接收连接,同时代理给pod。为了拦截发往服务IP的连接,代理配置了iptables规则(iptables是一个管理Linux内核数据包过滤功能的工具),重定向连接到代理服务器。userspace代理模式大致如图11.9所示。

    [图片上传失败...(image-dd5697-1627770873428)]image

    图11.9 userspace代理模式

    kube-proxy之所以叫这个名字是因为它确实就是一个代理器,不过当前性能更好的实现方式仅仅通过iptables规则重定向数据包到一个随机选择的后端pod,而不会传递到一个实际的代理服务器。这个模式称为iptables代理模式,如图11.10 所示。

    [图片上传失败...(image-5db389-1627770873428)]image

    图11.10 iptables代理模式

    两种模式的主要区别是:数据包是否会传递给kube-proxy,是否必须在用户空间处理,或者数据包只会在内核处理(内核空间)。这对性能有巨大的影响。

    另外一个小的区别是:userspace代理模式以轮询模式对连接做负载均衡,而iptables代理模式不会,它随机选择pod。当只有少数客户端使用一个服务时,可能不会平均分布在pod中。例如,如果一个服务有两个pod支持,但有 5个左右的客户端,如果你看到 4个连接到pod A,而只有一个连接到pod B,不必惊讶。对于客户端数量更多的pod,这个问题就不会特别明显。

    在11.5节你会学习iptables代理模式具体是如何工作的。

    11.1.9 介绍Kubernetes插件

    我们已经讨论了Kubernetes集群正常工作所需要的一些核心组件。但在开头的几章中,我们也罗列了一些插件,它们不是必需的;这些插件用于启用Kubernetes服务的DNS查询,通过单个外部IP地址暴露多个HTTP服务、Kubernetes web仪表板等特性。

    如何部署插件通过提交YAML清单文件到API服务器(本书的通用做法),这些组件会成为插件并作为pod部署。有些组件是通过Deployment资源或者ReplicationController资源部署的,有些是通过DaemonSet。

    例如,写作本书时,在Minikube中,Ingress控制器和仪表板插件按照ReplicationController部署,如下面的代码清单所示。

    代码清单11.7 插件在Minikube中 作为ReplicationController部署

    $ kubectl get rc -n kube-system
    

    DNS插件作为Deployment部署,如下面的代码清单所示。

    代码清单11.8 kube-dns Deployment

    $ kubectl get deploy -n kube-system
    

    让我们看看DNS和Ingress控制器是如何工作的。

    DNS服务器如何工作

    集群中的所有pod默认配置使用集群内部DNS服务器。这使得pod能够轻松地通过名称查询到服务,甚至是无头服务pod的IP地址。

    DNS服务pod通过kube-dns服务对外暴露,使得该pod能够像其他pod一样在集群中移动。服务的IP地址在集群每个容器的 /etc/reslv.conf 文件的nameserver中定义。kube-dns pod利用API服务器的监控机制来订阅Service和Endpoint的变动,以及DNS记录的变更,使得其客户端(相对地)总是能够获取到最新的DNS信息。客观地说,在Service和Endpoint资源发生变化到DNS pod收到订阅通知时间点之间,DNS记录可能会无效。

    Ingress控制器如何工作

    和DNS插件相比,Ingress控制器的实现有点不同,但它们大部分的工作方式相同。Ingress控制器运行一个反向代理服务器(例如,类似Nginx),根据集群中定义的Ingress、Service以及Endpoint资源来配置该控制器。所以需要订阅这些资源(通过监听机制),然后每次其中一个发生变化则更新代理服务器的配置。

    尽管Ingress资源的定义指向一个Service,Ingress控制器会直接将流量转到服务的pod而不经过服务IP。当外部客户端通过Ingress控制器连接时,会对客户端IP进行保存,这使得在某些用例中,控制器比Service更受欢迎。

    使用其他插件

    你已经了解了DNS服务器和Ingress控制器插件同控制器管理器中运行的控制器比较相似,除了它们不会仅通过API服务器监听、修改资源,也会接收客户端的连接。

    其他插件也类似。它们都需要监听集群状态,当有变更时执行相应动作。我们会在剩余的章节中介绍一些其他的插件。

    11.1.10 总结概览

    你已经了解了整个Kubernetes系统由相对小的、完善功能划分的松耦合组件构成。API服务器、调度器、控制器管理器中运行的控制器、Kubelet以及kube-proxy一起合作来保证实际的状态和你定义的期望状态一致。

    例如,向API服务器提交一个pod配置会触发Kubernetes组件间的协作,这会导致pod的容器运行。这里的细节将会在接下来的部分详细说明。

    相关文章

      网友评论

        本文标题:11.1 了解架构

        本文链接:https://www.haomeiwen.com/subject/lrvnmltx.html