kubenetes的整体架构
Kubernetes由两种节点组成:master节点和工作节点,前者是管理节点,后者是容器运行的节点。其中master节点中主要有3个重要的组件,分别是APIServer,scheduler和controller manager。APIServer组件负责响应用户的管理请求、进行指挥协调等工作;scheduler的作用是将待调度的pod绑定到合适的工作节点上;controller manage提一组控制器的合集,负责控制管理对应的资源,如副本(replication)和工作节点(node)等。工作节点上运行了两个重要组件,分别为kubelet和kube-proxy。前者可以被看作一个管理维护pod运行的agent,后者则负责将service的流量转发到对应的endpoint。在实际生产环境中,不少用户都弃用了kube-proxy,而选择了其他的流量转发组件。
Kubernetes架构可以用下图简单描述。可以看到,位于master节点上的APIServer将负责与master节点、工作节点上的各个组件之间的交互,以及集群外用户(例如用户的kubectl命令)与集群的交互,在集群中处于消息收发的中心地位;其他各个组件各司其职,共同完成应用分发、部署与运行的工作。
Kubernetes的架构体现了很多分布式系统设计的最佳实践,比如组件之间松耦合,各个组件之间不直接存在依赖关系,而是都通过APIServer进行交互。又比如,作为一个不试图形成技术闭环的项目,Kubernetes只专注于编排调度等工作,而在存储网络等方面留下插件接口,保证了整体的可扩展性和自由度,例如可以注册用户自定义的调度器、资源管理控制插件、网络插件和存储插件等,这使得用户可以在不hack核心代码的前提下,极大地丰富Kubernetes的适用场景。
APIServer
Kubernetes APIServer负责对外提供Kubernetes API服务,它运行在Kubernetes的管理节点master节点中。作为系统管理指令的统一入口,APIServer担负着统揽全局的重任,任何对资源进行增删改查的操作都要交给APIServer处理后才能提交给etcd。
Kubernetes APIServer总体上由两个部分组成:HTTP/HTTPS服务和一些功能性插件。其中这些插件又可以分成两类:一部分与底层IaaS平台(Cloud Provider)相关,另一部分与资源的管理控制(admission control)相关。
APIServer的职能
APIserver作为Kubernetes集群的全局掌控者,主要负责以下5个方面的工作。
- 对外提供基于RESTfuI的管理接口,支持对Kubernetes的资源对象譬如:pod, service,replication controller、工作节点等进行增、删、改、查和监听操作。例如,GET<apiserver-ip>:<apiserver-port>/api/v1/pods表示查询默认namespace中所有pod的信息。GET<apiserver-ip>:<apiserver-port>/api/v1/watch/pods表示监听默认namespace中所有pod的状态变化信息,返回pod的创建、更新和删除事件。该功能在前面的设计讲解中经常提到,这样一个get请求可以保持TCP长连接,持续监听pod的变化事件。
- 配置Kubernetes的资源对象,并将这些资源对象的期望状态和当前实际存储在etcd中供Kubernetes其他组件读取和分析。(Kubernetes除了etcd之外没有任何持久化节点)
- 提供可定制的功能性插件(支持用户自定义),完善对集群的管理。例如,调用内部或外部的用户认证与授权机制保证集群安全性,调用admission control插件对集群资源的使用进行管理控制,调用底层IaaS接口创建和管理Kubernetes工作节点等。
- 系统日志收集功能,暴露在/logs API。
- 可视化的API(用Swagger实现)。
APIServer启动过程
APIServer的启动程序读者可以参考cmd/kube-apiserver/apiserver.go的main函数,其启动流程如下所示。
- 新建APIServer,定义一个APIServer所需的关键信息。
首先是组件自身所需信息及其所需的依赖和插件配置,可以到官方文档查询。
- 接受用户命令行输入,为上述各参数赋值。
- 解析并格式化用户传入的参数,最后填充APIServer结构体的各字段。
- 初始化log配置,包括log输出位置、log等级等。Kubernetes组件使用glog作为日志函数库,Kubernetes能保证即使APIServer异常崩溃也能够将内存中的log信息保存到磁盘文件中。
- 启动运行一个全新的APIServer。 APIServer作为master节点上的一个进程(也可以运行在容器中)通常会监听2个端口对外提供Kubernetes API服务,分别为一个安全端口(6443)和一个非安全端口(8080)。
APIServer对etcd的封装
Kubernetes使用etcd作为后台存储解决方案,而APIServer基于etcd实现了一套RESTfuI API,用于操作存储在etcd中的Kubernetes对象实例。所有针对Kubernetes资源对象的操作都是典型的RESTfuI风格操作,如下所示:
- GET /<resourceNamePlural> 返回类型为resourceName的资源对象列表,例如GET /pods返回一个pod列表。
- POST /<resourceNamePlural>根据客户端提供的描述资源对象的JSON文件创建一个新的资源对象。
- GET /<resourceNamePlural>/<name>根据一个指定的资源名返回单个资源对象信息,例如GET /pods/first返回一个名为first的pod信息。
- DELETE /<resourceNamePlural>/<name>根据一个指定的资源名删除一个资源对象。
- POST /<resourceNamePlural>/<name>根据客户端提供的描述资源对象的JSON文件创建或更新一个指定名字的资源对象。
- GET /watch/<resourceNamePlural>使用etcd的watch机制,返回指定类型资源对象实时的变化信息。
- GET /watch/<resourceNamePlural>/<name>使用etcd的watch机制,根据客户端提供的描述资源对象的JSON文件,返回一个名为name的资源对象实时的变化信息。
APIServer如何操作资源
APIServer将集群中的资源都存储在etcd中,默认情况下其路径都由/registry开始,用户可以通过传入etcd-prefix参数来修改该值。
当用户向APIServer发起请求之后,APIServer将会借助一个被称为registry的实体来完成对etcd的所有操作,这也是为什么在etcd中,资源的存储路径都是以registry开始的。
代码语言:txt复制Kubernetes目前支持的资源对象很多,详情可到官方文档查询。
一次创建pod请求的响应流程
- (1) APIServer在接收到用户的请求之后,会根据用户提交的参数值来创建一个运行时的pod对象。
- (2)根据API请求的上下文和该pod对象的元数据来验证两者的namespace是否匹配,如不匹配则创建pod失败。
- (3) namespace验证匹配后,APIServer会向pod对象注入一些系统元数据,包括创建时间和uid等。如果定义pod时未提供pod的名字,则APIServe侩将pod的uid作为pod的名字。
- (4) APIServer接下来会检查pod对象中的必需字段是否为空,只要有一个字段为空,就会抛出异常并终止创建过程。
- (5)在etcd中持久化该pod对象,将异步调用返回结果封装成restful.Response,完成操作结果反馈。
至此,APIServer在pod创建的流程中的任务已经完成,剩余步骤将由Kubernetes其他组件(kube-scheduler和kubelet)通过watch APIServer继续执行下去。
APIServer如何保证API操作的原子性
由于Kubernetes使用了资源的概念来对容器云进行抽象,就不得不面临APIServer响应多个请求时竞争和冲突的问题。所以,Kubernetes的资源对象都设置了一个resourceVersion作为其元数据(详见pkg/api/v1/types.go的ObjectMeta结构体)的一部分,APIServer以此保证资源对象操作的原子性。
resourceVersion是用于标识一个资源对象内部版本的字符串,客户端可以通过它判断该对象是否被更新过。每次Kubernetes资源对象的更新都会导致APIServer修改它的值,该版本仅对当前资源对象和namespace限定域内有效。
scheduler
资源调度器本身经历了长足的发展,一向受到广泛关注。Kubernetes scheduler是一个典型的单体调度器。它的作用是根据特定的调度算法将pod调度到指定的工作节点上,这一过程通常被称为绑定(bind)。
scheduler的输入是待调度pod和可用的工作节点列表,输出则是应用调度算法从列表中选择的一个最优的用于绑定待调度pod的节点。如果把这个scheduler看成一个黑盒,那么它的工作过程正如图所示。
scheduler的数据采集模型
不同于很多平台级开源项目(比如Cloud Foundry ), Kubernetes里并没有消息系统来帮助用户实现各组件间的高效通信,这使得scheduler需要定时地向APIServer获取各种各样它感兴趣的数据,比如已调度、待调度的pod信息,node状态列表、service对象信息等,这会给APIServe谴成很大的访问压力。
所以scheduler专门为那些感兴趣的资源和数据设置了本地缓存机制,以避免一刻不停的暴力轮询APIServer带来额外的性能开销。这里的缓存机制可以分为两类,一个是简单的cache对象(缓存无序数据,比如当前所有可用的工作节点),另一个是先进先出的队列(缓存有序数据,比如下一个到来的pod)。scheduler使用reflector来监测APIServer端的数据变化。
最后,我们总结一下scheduler调度器需要的各项数据、如何捕获这些数据,以及这些数据存储在本地缓存的什么数据结构中,如表所示。
scheduler调度算法
在Kubernetes的最早期版本中,scheduler为pod选取工作节点的算法是round robin ---即依次从可用的工作节点列表中选取一个工作节点,并将待调度的pod绑定到该工作节点上运行,而不考虑譬如工作节点的资源使用情况、负载均衡等因素。这种调度算法显然不能满足系统对资源利用率的需求,而且极容易引起竞争性资源的冲突,譬如端口,无法适应大规模分布式计算集群可能面临的各种复杂情况。当然,这之后scheduler对调度器的算法框架进行了较大的调整,已经能够支持一定程度的资源发现。目前默认采用的是系统自带的唯一调度算法default,当然,scheduler调度器提供了一个可插拔的算法框架,开发者能够很方便地往scheduler添加各种自定义的调度算法。接下来将以default法为例,详细解析scheduler调度算法的整体设计。
Kubernetes的调度算法都使用如下格式的方法模板来描述:
代码语言:txt复制func RegisterAlgorithmProvider(name string, predicateKeys, priorityKeys sets.String) string {
//TODO
}
其中,第1个参数即算法名(比如default),第2个和第3个参数组成了一个算法的调度策略。
Kubernetes中的调度策略分为两个阶段:Predicates和Priorities,其中Predicates回答“能不能”的问题,即能否将pod调度到某个工作节点上运行,而Priorities则在Predicates回答“能”的基础上,通过为候选节点设置优先级来描述“适合的程度有多高”。
具体到default算法,目前可用的Predicates包括:PodFitsHostPorts , PodFitsResources ,NoDiskConflict, NoVolumeZoneConflict, MatchNodeSelector, HostName, MaxEBSVoIumeCount和MaxGCEPDVoIumeCount。所以,工作节点能够被选中的前提是需要经历这几个Predicates条件的检验,并且每一条都是硬性标准。一旦通过这些筛选,候选的工作节点就可以进行打分(评优先级)了。
打分阶段的评分标准(Priorities)有7项:LeastRequestedPriority, BalancedResourceAllocation、SelectorSpreadPriority、NodeAffinityPriority、EqualPriority、ServiceSpreadingPriority和Image-LocalityPriority。每一项都对应一个范围是0~10的分数,0代表最低优先级,10代表最高优先级。除了单项分数,每一项还需要再分配一个权值(weight )。以default算法为例,它包含了LeastRequestedPriority, BalancedResourceAllocation, SelectorSpreadPriority和NodeAffmityPriority这三项,每一项的权值均为1。所以一个工作节点最终的优先级得分是每个Priorities计算得分的加权和,即Sum(score*weight)。最终,scheduler调度器会选择优先级得分最高的那个工作节点作为pod调度的目的地,如果存在多个优先级得分相同的工作节点,则随机选取一个工作节点。
Predicates
- PodFitsHostPorts
PodFitsHostPorts的评估依据就是宿主机上的端口是否冲突,即检查待调度的pod中所有容器需要用到的HostPort集与工作节点上已使用的端口是否冲突。需要注意容器内部打开的端口(ContainerPort)和HostPort的区别,在同一个工作节点上,ContainerPort可以随意重复,但HostPort不能冲突。具体检测过程如下所示。
(1)枚举待调度的pod要用到的所有HostPort,即查询pod中每个容器的ContainerPort所对应的HostPort。由于HostPort是一个1~65535的整数,这里使用了一个key为int型,value为bool型的map结构,value值为true用于标记某个HostPort需要被该pod使用。
(2)根据cache中存储的node相关信息,采用步骤((1)中的方法获得node上运行的所有pod中每个容器的ContainerPort所对应的HostPort。
(3)比较步骤((1)和步骤(2)得到的两个HostPort集合是否有交集。如果有交集则表明将pod调度到该工作节点上会产生端口冲突,返回一个false值表示不适合调度;否则表明不会产生端口冲突,返回一个true值表示适合调度。
- podFitsResources
podFitsResources的评估依据就是node上的资源是否够用,即检测每个node上已经在运行的所有pod对资源的需求总量与待调度pod对资源的需求量之和是否会超出工作节点的资源容量(node的capacity )。目前,这条规则检查node上允许部署的最大pod数目,以及CPU ( milliCPURequested )和Mem (memoryRequested)这两种资源的容量是否满足条件。需要注意的是,对于CPU和Mem,podFitsResources只计算资源的请求量而不是资源的实际使用量。
- NoDiskConflict
NoDiskConflict对应的实现函数是NoDiskConflict,它的评估依据就是容器挂载的卷(volume)是否有冲突。
具体的检测过程如下所示:
(1)使用两层嵌套循环,交叉对比待调度pod包含的所有volume信息(即pod.Spec.Volumes)和node上已调度的pod的volume信息,进行判断。
(2)如果待调度pod上的volume不是GCEPersistentDisk, AWSElasticBlockStore或者RBD类型,不进行任何操作,继续检查下一个volume;反之,则将该volume与工作节点上所有pod的每一个volume进行比较,如果发现相同,则表示有磁盘冲突,检查结束,反馈给调度器不适合调度。
(3)如果检查完待调度的pod的所有volume均未发现冲突,则反馈给调度器表示该工作节点适合调度。
- NoVolumeZoneConflict
NoVolumeZoneConflict用于检查pod的挂载卷的zone限制是否与node对应的zone-label相匹配,目前只支持PersistentVolumeClaims,确切地说,仅在它们bound到的PersistentVolume范围内检查。
在检查过程中,我们首先获取node的zone-label信息,查询以 ailure-domain.beta.kubernetes.io/zone和failure-domain.beta.kubernetes.io/region两个label为key的value值。顾名思义,这些label对应的是工作节点调度限制,所以如果没有发现任何限制条件,则检查结束,说明该工作节点可以调度。接下来我们查找pod manifest中PersistentVolumeClaims对应的PersistentVolume的failure-domain.beta.kubernetes.io/zone和failure-domain.beta.kubernetes.io/regionlabel下对应的值,并与node对应的label进行交叉匹配,一旦发现有不相符的一项,则返回zone冲突信息,说明该node不适合被调度。
特别地,我们允许工作节点有除了与pod所指定的label之外的其他zone限制,也就是说,工作节点的zone-label与pod的zone label必须是包含关系。这条规则其实也非常直观,即如果某个pod上的挂载volume可能在A zone调度失败,而它被调度到的工作节点一定不能位于A zone,即一定存在相应的zone-label。
- MatchNodeSelector
MatchNodeSelector对应的实现函数是podSelectorMatches,它的评估依据是node是否能被pod的NodeSelector选中以及该node是否符合pod对于NodeAffinity的要求。也就是说,调度器会首先检查工作节点的labels属性和pod的NodeSelector的要求(label selector的一种)是否一致;接下来再检查pod manifest中的scheduler.alpha.kubernetes.io/affinitylabel与node名字是否相吻合。
podSelectorMatchesl作流程如下所示:
(1)如果pod的NodeSelector属性(即pod.Spec.NodeSelector)不为空,则解析工作节点对象的元数据,提取labels属性,应用NodeSelecto树工作节点的labels进行匹配,如果匹配不成功,则表明该node不适合调度。
(2)获取pod的Spec中scheduler.alpha.kubernetes.io/affinitylabel对应的值NodeAffinity。NodeAffinity是一组亲和性调度规则,目前实现了其中两种,分别为RequiredDuringSchedulingIgnoredDuringExecution和PreferredDuringSchedulingIgnoredDuringExecution。其中,仅有前者在这里的检查中使用到了,意指在pod被调度时,选择的node必须符合这一规则的定义。这同样是通过label匹配与否进行判定的。
- HostName
HostName评估的依据被定义在PodFitsHost中,即如果待调度的pod指定了pod.Spec.Host的值为hostname,则将它调度到主机名为指定hostname的工作节点上运行,这个策略非常简单。
- MaxEBSVoIumeCount
MaxEBSVoIumeCount检查node上即将被挂载的AWS EBS Volume是否超过了默认限制39。
- MaxGCEPDVoIumeCount
MaxGCEPDVoIumeCount检查node上即将被挂载的GCE Persistent Disk是否超过了默认限制16。
Priorities
- LeastRequestedPriority
LeastRequestedPriority的计算原则是尽量将pod调度到资源占用比较小的工作节点上,这样能够尽可能地实现Kubernetes集群工作节点上pod资源均衡分配。
具体计算分数的方法可以用如下公式描述:
代码语言:txt复制 cpu((capacity-sum(requested))10 / capacity) memory((capacity-sum(requested))10/capacity)/2
其中,requested cpu和requested memory是被调度的pod所需申请的资源总量加上正在被检查的工作节点上所有运行的pod所申请的资源总量,而capacity则是正在检查的工作节点目前可用的容量。
- BalancedResourceAllocation
BalancedResourceAllocation,即在调度时偏好CPU和内存利用率相近的节点,具体的计算公式如下:
代码语言:txt复制 score=10一abs(cpuFraction-memoryFraction)*10
我们采用类似于LeastRequestedPriority中的计算方式,分别求出节点上CPU和内存的已分配量,以及待调度pod所需要的CPU和内存值,将对应的资源相加并分别除以节点上的资源总量,求出占用率,再参考公式求出分数。
- SelectorSpreadPriority
SelectorSpreadPriority的设计来源于对集群中rc和service的高可用以及流量分布均衡的要求,其基本理念在于要求对相同service/RC(包括replication controller和replicaSet)的pod在节点及zone上尽量分散,对应实现在CalculateSpreadPriority中,评分流程如下:
(1)对给定的待调度pod,查询该pod所在的namespace下对应的service。由于一个pod对应的service的数目是没有限制的(可能为0个,1个或多个),如果与该pod匹配的service数目不为0,则此处会返回所有匹配的service列表,否则返回错误标识没有找到匹配的service。
(2)对给定的待调度pod,查询该pod所在的namespace下对应的ReplicationController。同样地,当匹配的ReplicationController数目不为0时,返回匹配列表,否则返回错误。
(3)对给定的待调度pod,查询该pod所在的namespace下对应的ReplicaSet。当匹配的ReplicaSet数目不为0时,返回匹配列表,否则返回错误。
(4)对于上述返回的与pod有相同label selector的service, ReplicationController和ReplicaSet,将其整合在一起,并且计算与待调度pod处于namespace下各个node上具有同样label selector的pod数目,并将所有node中相同label的pod数量最多的值记为maxCountByNodeName。
(5)同样地,我们再针对zone进行类似的计算,将所有zone中相同label的pod数目最多的值即为maxCountByZone。当然,有一些集群中的工作节点并没有zone这一特征,在这种情况下,无需在后续步骤中考虑zone因素的影响。
(6)运用简单的打分策略对各个工作节点进行打分,将该节点上相同label的pod与maxCount-ByNodeName及CountByZone进行投射比对,得到一个0~10分间的分数,具体计算过程如下:
- ServiceSpreadingPriority
ServiceSpreadingPriority可以认为是上述介绍的SelectorSpreadPriority的前身,在早期的设计中,该函数仅考虑了使节点上属于同一个service的后端pod尽量少。我们不难发现,这个功能点已经被SelectorSpreadPriority所覆盖,出于兼容低版本v1.0的考虑,现在仍然将其保留在系统中,而在注册函数时,将传入参数的RC列表设定为空即可。
- NodeAffinityPriority
NodeAffinityPriority是一个新的特征,允许用户在pod manifest中指定pod的工作节点亲和性,对应的annotation为scheduler.alpha.kubernetes.io/affinity。
node亲和性本质上是一些调度规则,目前实现了其中两种,其一为强规则requiredDuringSchedulingIgnoredDuringExecution,若某个工作节点不满足该字段的要求,则待调度的pod一定不会被调度到该工作节点上;其二为弱规则preferredDuringSchedulingIgnoredDuringExecution,即说明pod偏好的工作节点,但是调度器仍然可能将该pod调度到不满足这一字段的工作节点上。注意,这两条规则都只考虑到调度发生时,而不考虑具体的运行过程。也就是说,一旦pod被成功调度到某个工作节点上,在它运行的生命周期内,即使工作节点不再满足该字段的要求,调度器也不会将pod从该工作节点上删除。在将来,还可能会有其他新的规则,如requiredDuringSchedulingRequiredDuringExecution等。目前在实际的调度算法中使用到的仅有弱规则preferredDuringSchedulingIgnoredDuringExecution。对于亲和性的具体表现形式,系统实现了多种支持方式,包括In, NotIn, Exists, DoesNotExist, Gt和Lt。
在实际的检查过程中,首先将pod的node亲和性信息抽取出来,并与各个工作节点的node selector逐一比较,得到结果存放在一个以NodeName为key值的map中。一旦出现匹配,则对应的value值加1。最后将各个工作节点的得分投影到0一10之间。
- EqualPriority
EqualPriority对应的实现函数是EqualPriority,它的计算原则是平等对待NodeLister中的每一个工作节点。与其他计算函数相比,EqualPriority函数的工作流程简单很多,即遍历NodeLister中所有备选的工作节点,将每个工作节点的优先级(score)均置为1。
- ImageLocalityPriority
ImageLocalityPriority根据主机上已经存在的且将会被待调度pod使用到的镜像(大小)进行打分。在检查过程中,遍历pod.Spec.Containers项,对各个node分别检查是否存在对应的镜像,并且将存在镜像的大小和累加作为评分依据。存在镜像和越大的工作节点对应的得分越高。
scheduler的启动与运行
scheduler组件的启动程序放在plugin/and/kube-scheduler目录下,负责进行调度工作的核心进程为scheduler server,它的结构相对来说比较简单,主要的属性如表所示:
在程序入口的main函数中,首先完成对SchedulerServer的初始化工作,这是一个涵盖了要运行调度器所需要的参数的结构体,并且调用Run函数来运行一个真正的调度器。Run函数完成的事情如下:
(1) 收集scheduler产生的事件信息并构建事件对象,然后向APIServer发送这些对象,最终由APIServer调用etcd客户端接口将这些事件进行持久化。event来源非常广泛,除了scheduler外,它的来源还包括kubelet, pod, Docker容器、Docker镜像、pod Volume和宿主机等。
(2) 创建一个http server,默认情况下绑定到IP地址Address(见表8-12)上并监听10251端口。在启用对scheduler的profiling功能时,该server上会被注册3条路由规则(/debug/pprof/,/debug/pprof/profile和/debug/pprof/symbol),可以通过Web端对scheduler的运行状态进行辅助性检测和debug。
(3) 根据配置信息创建调度器并启动SchedulerServer。在启动调度器之前,需要进行一些初始化操作,这些初始化操作的结果将作为调度器的配置信息传入,如下所示:
1、客户端对象client,用于与APIServer通信。
2、用于缓存待调度pod对象的队列podQueue。
3、存储所有已经调度完毕的Pod的链表ScheduledPodLister。
4、存储已调度的所有pod对象的链表podLister,其中包括已经调度完毕的以及完成了调度决策但可能还没有被运行起来的pod。
5、存储所有node对象的链表NodeLister。
6、存储所有PersistentVolumes的链表PVLister。
7、存储所有PersistentVolumeClaims的链表PVCLister。
8、存储所有service对象的链表ServiceLister。
9、存储所有控制器的链表ControllerLister。
10、存储所有ReplicaSet的链表ReplicaSetLister。
11、用于关闭所有reflectors的channel, StopEverything。
12、用于操作ScheduledPodLister池的控制器scheduledPodPopulator,负责在完成调度的pod被更新时进行相应的操作。
13、用于提前更新pod在系统中被调度的状态,使得调度器能够提前感知的Modeler。
14、调度器的名字SchedulerName。
controller manager
Kubernetes controller manager运行在集群的master节点上,是基于pod API上的一个独立服务,它管理着Kubernetes集群中的各种控制器,包括读者已经熟知的replication controller和node controller。相比之下,APIServer负责接收用户的请求,并完成集群内资源的“增删改”,而controller manager系统中扮演的角色是在一旁默默地管控这些资源,确保它们永远保持在用户所预期的状态。
Contorller Manager启动过程
Contorller Manager启动过程大致分为以下几个步骤:
- (1) 根据用户传入的参数以及默认参数创建kubeconfig和kubeClient。前者包含了controller manager工作中需要使用的配置信息,如同步endpoint, rc, node等资源的周期等;后者是用于与APIServer行交互的客户端。
- (2) 创建并运行一个http server,对外暴露/debug/pprof/、/debug/pprof/profile, /debug/pprof/symbol和/metrics,用作进行辅助debug和收集metric数据之用。
- (3) 按顺序创建以下几个控制管理器:服务端点控制器、副本管理控制器、垃圾回收控制器、节点控制器、服务控制器、路由控制器、资源配额控制器、namespace控制器,horizontal控制器、daemon sets制器、job控制器、deployment控制器、replicaSet控制器、persistent volume控制器(可细分为persistent volume claim binder , persistent volume recycler及persistent volume provision controller ), service account控制器,再根据预先设定的时间间隔运行。特别地,垃圾回收控制器、路由控制器仅在用户启用相关功能时才会被创建,而horizontal控制器、daemon set控制器、job控制器、deployment控制器、replicaSet控制器仅在extensions/vlbetal的API版本中会被创建。
controller manager控制pod、工作节点等资源正常运行的本质,就是靠这些controller定时对pod、工作节点等资源进行检查,然后判断这些资源的实际运行状态是否与用户对它们的期望一致,若不一致,则通知APIServer进行具体的“增删改”操作。理解controller工作的关键就在于理解每个检查周期内,每种资源对象的实际状态从哪里来,期望状态又从哪里来。接下来,我们以服务端点控制器、副本管理控制器、垃圾回收控制器、节点控制器和资源配额控制器为例,分析这些controller的具体工作方式。
服务端点控制器(endpoint controller)
要想了解endpoint controller的工作原理,首先要从它的数据结构开始说起。
当用户在Kubernetes中创建一个包含label selector的service对象时,系统会随之创建一个对应的endpoint对象,该对象即保存了所有匹配service的label selector端pod的IP地址和端口。可以预见,endpoint controller为endpoint对象的维护者,需要在service或者pod的期待状态或实际状态发生变化时向APIServe泼送请求,调整系统中endpoin时象的状态。
顺着这条思路,可以发现endpoint controller维护了两个缓存池,其中serviceStore用于存储service, podStore用于存储pod,并且使用controller的reflector机制实现两个缓存与etcd内数据的同步。具体而言,就是当controller监听到来自etcd的service或pod的增加、更新或者删除事件时,对serviceStore或podStore做出相应变更,并且将该service或者该 pod对应的service加入到queue中。也就是说,queue是一个存储了变更service的队列。endpoint controller通过多个goroutine来同时处理service队列中的状态更新,goroutine的数量由controller manager的ConcurrentEndpointSyncs参数指定,默认为5个,不同goroutine相互之间不会相互干扰。
每个goroutine的工作可以分为如下几个步骤:
(1) 从service队列中取出当前处理的service,在serviceStore中查找该service对象。若该对象已不存在,则删掉其对应的所有endpoint;否则进入步骤(2)。
(2) 构建与该service对应的endpoint的期望状态。根据service.Spec.Selector,从podStore获取该service对应的后端pod对象列表。对于每一个pod,将以下信息组织为一个新的EndpointSubset对象:pod.Status.PodIP, pod.Spec.Hostname, service.spec中定义的端口名、端口号、端口协议、和pod的资源版本号(ResourceVersion,同样作为endpoint对象的资源版本号),并且将所有EndpointSubset对象组成一个slice subset,这是期望的endpoint状态。
(3) 使用service名作为检索键值,调用APIServer的API获取当前系统中存在的endpoint对象列表currentEndpoints,即endpoint的实际状态。如果找不到对应的endpoint,则将一个新的Endpoint对象赋值给currentEndpoints,此时它的ResourceVersion为0。将步骤(2)中endpoint期望状态与实际endpoint对象列表进行比较,包括两者的pod.beta.kubernetes.io/hostname的annotation, subset(包含端口号、pod IP地址等信息),以及service的label与目前endpoint的label,如果发现不同,则调用APIServer的API进行endpoint的创建或者更新。如何判断需要进行的是创建还是更新呢?这就与ResourceVersion分不开了。如果ResourceVersion为0,说明需要创建一个新的endpoint,否则,则是对旧的endpoint的更新。
副本管理控制器(replication controller)
replication controller负责保证rc管理的pod的期望副本数与实际运行的pod数量匹配。可以预见,replication controller需要在rc或者pod的期待状态发生变化时向APIServer发送请求,调整系统中endpoint对象的状态。同样地,先来通过数据结构大致了解一下它的工作模式。
它在本地维护了两个缓存池rcStore和podStore,分别同于同步rc与pod在etcd中的数据,同样调用controller的reflector机制进行list和watch更新。一旦发现有rc的创建、更新或者删除事件,都将在本地rcStore中进行更新,并且将该rc对象加入到待更新队列queue中。
对于监听到pod的事件,则相对较为复杂。对于创建和更新pod,都要检查pod是否实际上已经处于被删除的状态(通过其DeletionTimestamp的标记),如果是则触发删除pod事件;对于创建与删除pod,还需要在expectations中写入相应的变更;expectations是replication controller用于记住每个rc期望看到的pod数的TTL cache,为每个rc维护了两个原子计数器(分别为add和del,用于追踪pod的创建或者删除)。对于pod的创建事件,add数目减少1,说明该rc需要期待被创建的pod数目减少了1个;类似地,对于删除事件,则是del的数目减少1。所以说,如果add的数目和del的数目都小于或等于0,我们就认为该rc的期望已经被满足了(即对应的Fulfilled方法返回为true值)。读者们也许会好奇,expectations中add和del的初始值为多少呢?事实上,在replication controller创建时,它们都被初始化为0,直到TTL超时或者期望满足时,该rc才会被加入到sync队列中,此时重新为该rc设置add和del值。最后,不管是哪种事件,都要将pod对应的rc加入queue队列。
replication controller的同步工作将处理rc队列queue,对系统中rc中副本数的期望状态及pod的实际状态进行对比,并启用了多个goroutine对其进行同步工作,每个goroutine的工作流程大致如下:
(1) 从rc队列中取出当前处理的rc名,通过rcStore获得该rc对象。如果该rc不存在,则从expectations中将该rc删除;如果查询时返回的是其他错误,则重新将该rc入队;这两种情况均不再进行后续步骤。
(2) 检查该rc的expectations是否被满足或者TTL超时,如果是,说明该rc需要被同步,在步骤(2)执行结束后将进入步骤((3),否则进入步骤(4)。调用APIServer的API获取该rc对应的pod列表,并且筛选出其中处于活跃状态的pod(即.status.phase不是Succeeded, Failed以及尚未进入被删除阶段)。
(3) 调整rc中的副本数。将(2)步骤中获得的活跃pod列表与rc的.spec.replicas字段相减得到diff,如果diff小于0,说明rc仍然需要更多的副本,设置expectations中的add值为diff,并且调用APIServer的API发起pod的创建请求,创建pod完毕后还需要将expectations的add相应减少1。如果diff大于0,说明rc的副本数过多,需要清除pod,将expectations中的del设为diff值,并且调用APIServer的API发起pod的删除请求,删除pod后还需要将expectations的del相应减少1。实际上因为工程的需要,引人了一个burstReplicas,默认为500,限制diff数目小于或等于该值。
(4) 最后,调用APIServer的API更新rc的status.replicas。
可以看到,Controller的运作过程依然遵循了旁路控制的原则,真正操作资源的工作是交给APIServer去做的。
垃圾回收控制器(gc controller)
在用户启动pod的垃圾回收功能时,该控制器会被创建。所谓回收pod,是指将系统中处于终止状态的pod删除。注意:kubelet也执行的垃圾回收,但是针对的是容器和镜像的回收,而此处针对的是pod。在Kubernetes的设计中两者并非紧密关联,因此它们的回收流程是分开执行的。
gc controller维护了一个缓存池podStore,用于存储终止状态(即podPhase不是Pending,Running, Unknown三者)的pod,并使用reflecto峡用list和watch机制监听APIServer对podStore进行更新。
要执行垃圾回收,首先会考察podStore中的pod数量是否已经到达触发垃圾回收的阈值。如果没有到达,不进行任何操作;否则,将所有pod按照创建时间进行排序,最先创建的pod将被优先回收。当然,删除pod的实际操作也是通过调用APIServer的API实现。
节点控制器(node controller)
node controller是主要用于检查Kubernetes的工作节点是否可用的控制器,它会定期检查所有在运行的工作节点上的kubelet进程来获取这些工作节点信息,如果kubelet在规定时间内没有推送该工作节点的状态,则将其NodeCondition为Ready的状态置成Unknown,并写入etcd中。
在介绍node controller的具体职责之前,先明确一下工作节点在Kubernetes的表示方式。
- 工作节点的描述方式
Kubernetes将工作节点也看作资源对象的一种,用户可以像pod那样,通过资源配置文件或kubectl命令行工具来创建一个node资源对象。当然,真正物理层面的工作节点(物理机或虚拟机)并不是由Kubernetes创建的,创建node资源对象只是为了抽象并维护工作节点的相关信息,并对工作节点是否可用进行持续的追踪。
Kubernetes主要维护工作节点对象的两个属性—spec和status,分别被用来描述一个工作节点的期望状态和当前状态。其中,期望状态由一个json资源配置文件构成,描述了一个工作节点的具体信息,而当前状态信息则包含如下一系列节点相关信息:
1、Node Addresses:工作节点的主机地址信息,通常以slice(数组)的形式存在。如果工作节点是由IaaS平台创建的虚拟机,那么它的主机地址通常可以通过调用IaaS API来获取。Addresses的种类可能是Hostname, ExternalIP或InternalIP中的一种。经常被使用到的是后两者,并且通过能否从集群外部访问到进行区分。
2、Node Phase:即工作节点的生命周期,它也由Kubernetes controller manager管理。工作节点的生命周期可以分为3个阶段:Pending, Running和Terminated。刚创建的工作节点处于Pending状态,直到它被Kubernetes发现并通过检查。检查通过后(譬如工作节点上的服务进程都在运行),它会被标记为Running状态。工作节点生命周期结束称为Terminated状态,处于Terminated状态的工作节点不会接收任何调度请求,且本来在其上运行的pod也都会被移除。一个工作节点处于Running状态是可调度pod的必要而非充分条件。如果一个工作节点要成为一个调度候选节点,它还需要满足被称为Node Condition的条件。
3、Node Condition:描述Running状态下工作节点的细分状况,也就是说,一个Running的工作节点,并不一定可以接收pod,还要观察它是不是满足一些列细分要求。可用的Condition值包括如:NodeReady和NodeOutOfDisk,前者意味着工作节点上的kubele进程处于健康状态,且已经准备好接收pod了;后者表示该工作节点上的可用磁盘空间不足,导致无法接收新的pod。
4、Node Capacity与Node Allocatable:分别标识工作节点上的资源总量及当前可供调度的资源余量,涉及的资源通常包括CPU、内存及Volume大小。
5、Node Info:一些工作节点相关的信息,如内核版本、runtime版本(如docker ), kubelet版本等,这些信息经由kubelet收集。
6、Images:工作节点上存在的容器镜像列表。
7、Daemon Endpoints:工作节点上运行的kubelet监听的端口。
- 工作节点管理机制
与pod和service不同的是,工作节点并不是真正由Kubernetes创建的,它是要么由IaaS平台(譬如GCE)创建,要么就是用户管理的物理机或者虚拟机。这意味着,当Kubernetes创建一个node时,它只是创建了一个工作节点的“描述”。因此在工作节点被创建之后,Kubernetes必须检查该工作节点是否合法。以下资源配置文件描述了一个工作节点的具体信息,可以通过该文件创建一个node对象。
一旦用户创建节点的请求被成功处理,Kubernetes会立即在内部创建一个node对象,再根据metadata.name去检查该工作节点的健康状况,这一字段是该节点在集群内全局唯一的标志。
工作节点的动态维护过程是依靠node controller(节点控制器)来完成的,它是Kubernetes controller manager下属的一个控制器。简单地说,controller manager中一直运行着一个循环,负责集群内各个工作节点的同步及健康检查。这个循环周期由传入参数node-monitor-period控制。
在每个循环周期内,node controller不断地检测当前Kubernetes已知的每台工作节点是否正常工作,而如果一个之前已经失败的工作节点在这个检测循环中变成了“可以工作”,那么node controller就把这个机器添加为工作节点中的一员;反之node controller则会把一个已有的工作点删除掉。需要注意的是,被删除的只是etcd中的minion对象,Kubernetes总是有办法知道当前整个物理环境下有哪些机器是可以作为工作节点的,并且不断地检查这个机器池。
- node controller检查工作节点的循环
node controller维护了3个缓存podStore, nodeStore, daemonSetStore,分别存储pod, node,daemonSet资源,同时有3个相应的controllerpodController,nodeController,daemonSetController来应用list/watch机制同步etcd中相应资源的状态。有趣的是,它们对于监控到的资源变化非常地不积极。
podControlle:只响应pod创建和更新事件,此时将检查该pod是否处于终止状态或者没有被成功调度到一个正常运行的工作节点上,如果是的话,则调用APIServer的API将其强行删除。而nodeController和daemonSetController则对这些变化不做任何操作。
node controller的主要职责是负责监控由kubelet发送过来的工作节点的运行状态,这个监控间隔是5秒钟。它将其维护的已知工作节点列表记录在knownNodeSet中,并由kubelet推送的信息判断其是否准备好接收pod的调度(即处于Ready状态),如果工作节点的不Ready状态超过了一定时限,还会调用APIServer的API将其上运行的pod删除。此外,工作节点是否处于OutOfDisk状态,也同样被关心。在这一工作流程中,也会处理新工作节点的注册和旧工作节点的删除。
node controller会每隔30秒进行一次孤儿pod的清除。所谓的孤儿进程,是指podStore中缓存的pod中被bind到一个不再处于nodeStore中的工作节点的pod。
daemonSetStore缓存用来做什么呢?实际上,它将在删除工作节点上的pod时用作判断,如果被删除的pod是被daemonSet管理的,那么将会跳过该pod,不进行删除工作。
node controller和其他controller最大的不同在于,事实上的工作节点资源并不由Kubernetes系统产生和销毁,而是依靠底层的物理机器资源或者云服务提供商的IaaS平台。etcd里存放的node资源只是一种说明它是否正常工作的描述性资源,而它是否能够提供服务的信息则由kubelet来提供。
资源配额控制器(resource quota controller)
集群资源配额一般以一个namespace为单位进行配置,它的期望值(即集群管理员指定的配额大小)由集群管理员静态设置,而它的实际使用值会在集群运行过程中随着资源的动态增删而不断变化,resource quota controller用于追踪集群资源配额的实际使用量,每隔一resource-quota-sync-period时间间隔就会执行一次检查,如果发现使用量发生了变化,它就会调用APIServer的API在etcd中进行使用量的动态更新。它支持的资源包括pod/service/replication controller/persistent volume/secret和configMap/cpu/memory,当然还有resource quota本身。
为了完成resource quota的同步工作,resource quota controller维护一个队列,所有需要同步的resource quota都将入队。
首先,对创建、删除以及有.spec.hard更新resource quota,将其加入队列中。注意,这些变更事件是采用了list/watch机制从APIServer监听获得的,并且将缓存在rqIndexer里。
其次,每隔一段时间(默认为5分钟),会进行一个full resync,此时所有的resource quota会全部被加入到队列中。
另外,对于其他支持的资源(pod, serivce, replication controller, persistent volume claim,secret, ConfigMap ),分别设置了对应的replenishmentController,同样使用了list/watch机制监听资源,并对这些资源的更新或删除做出响应,即将这些资源所在的namespace下对其进行了规定的resource quota入队。通俗地讲,即当某个resource quota对pod的数量进行了规定时,那么当同一个namespace下的pod发生了更新或删除时,将该resource quota入队。
需要被同步的resource quota资源将被加入队列中后,将采用先入先出的方式进行处理,与其他的controller一样,负责处理的worker不止一个,而且它们是并发工作的。
同步资源的处理函数可以归结为,使得resource quota的状态(status)与其期望值(spec)保持一致。如果出现了以下情况中的任意一种,都将调用APIServer的API对resource quota进行更新。
- resource quota的.spec.hard与.status.hard不同。
- resource quota的.status.hard或.status.used为空,意味着这是第一次进行同步工作。
- resource quota的.status.Used与controller实际观察到的资源使用量不同,实际观察到的资源使用量是通过读取etcd中其他支持资源(如pod)的累加值求和得到的。