k8s技术预研9--Kubernetes核心组件运行原理分析

1、Kubernetes API Server原理分析

Kubernetes API Server的核心功能是提供了Kubernetes各类资源对象(如Pod、RC、Service等)的增、删、改、查及Watch等HTTP Rest接口,成为集群内各个功能模块之间数据交互和通信的中心枢纽,是整个系统的数据总线和数据中心。除此之外,它还有以下一些功能特性:
  • 是集群管理的API入口。
  • 是资源配额控制的入口。
  • 提供了完备的集群安全机制。

1.1 k8s API Server概述

k8s API Server通过一个名为kube-apiserver的进程提供服务,该进程运行在Master节点上。在默认情况下,kube-apiserver进程在本机的8080端口(对应参数--insecure-port)提供REST服务。我们可以同时启动HTTPS安全端口(--secure-prt=6443)来启动安全机制,加强REST API访问的安全性。
(1)方式一:通常我们可以通过命令行工具kubectl 来与k8s API Server交互,它们之间的接口是REST调用。
下面使用curl进行一些测试验证。
登录Master节点,查看k8s API的版本信息:
[[email protected] ~]# curl localhost:8080/api
{
  "kind": "APIVersions",
  "versions": [
    "v1"
  ],
  "serverAddressByClientCIDRs": [
    {
      "clientCIDR": "0.0.0.0/0",
      "serverAddress": "10.0.2.5:6443"
    }
  ]

运行下面的curl命令,分别返回集群中的Pod列表、Service列表、RC列表:
curl localhost:8080/api/v1/pods
curl localhost:8080/api/v1/services
curl localhost:8080/api/v1/replicationcontrollers

(2)方式二:如果我们只想对外暴露部分REST服务,则可以在Master或其他任何节点上通过运行kubectl proxy进程启动一个内部代理来实现
运行下面的命令,在8001端口启动代理,并且拒绝客户端访问RC的API:
[[email protected] ~]# kubectl proxy --reject-paths="^/api/v1/replicationcontrollers" --port=8001 --v=2
Starting to serve on 127.0.0.1:8001
已经禁止访问:
[[email protected] ~]# curl localhost:8001/api/v1/replicationcontrollers
<h3>Unauthorized</h3>

kubectl proxy具有很多特性,最实用的一个是提供了简单有效的安全机制,比如采用白名单方式限制非法客户的访问时,只要增加下面的参数即可:
--accept-hosts="^localhost,^127\\.0\\.0\\.1$,^\\[::1\\]$"

(3)方式三:通过编程的方式调用k8s API Server
通常用来实现分布式集群搭建,或者开发基于k8s的管理平台。

1.2 独特的k8s Proxy API接口

k8s API Server最主要的REST接口是资源对象的增删改查,另外还有一类特殊的REST接口—k8s Proxy API接口,这类接口的作用是代理REST请求,即kubernetes API Server把收到的REST请求转发到某个Node上的kubelet守护进程的REST端口上,由该kubelet进程负责响应。
(1)k8s Proxy API关于Node的相关接口
关于Node相关的接口的REST路径为:/api/v1/proxy/nodes/{name},其中{name}为节点的名称或IP地址。
包括以下几个具体的接口:
/api/v1/proxy/nodes/{name}/pods/    #列出指定节点内所有Pod的信息
/api/v1/proxy/nodes/{name}/stats/   #列出指定节点内物理资源的统计信息
/api/v1/prxoy/nodes/{name}/spec/    #列出指定节点的概要信息

需要说明的是:这里获取的Pod信息来自Node而非etcd数据库,两者时间点可能存在偏差。

此外,如果在kubelet进程启动时加--enable-debugging-handles=true参数,那么kubernetes Proxy API还会增加以下接口:
/api/v1/proxy/nodes/{name}/run      #在节点上运行某个容器
/api/v1/proxy/nodes/{name}/exec     #在节点上的某个容器中运行某条命令
/api/v1/proxy/nodes/{name}/attach   #在节点上attach某个容器
/api/v1/proxy/nodes/{name}/portForward   #实现节点上的Pod端口转发
/api/v1/proxy/nodes/{name}/logs     #列出节点的各类日志信息
/api/v1/proxy/nodes/{name}/metrics  #列出和该节点相关的Metrics信息
/api/v1/proxy/nodes/{name}/runningpods  #列出节点内运行中的Pod信息
/api/v1/proxy/nodes/{name}/debug/pprof  #列出节点内当前web服务的状态,包括CPU和内存的使用情况

(2)k8s Proxy API关于Pod的相关接口
通过这些接口,我们可以访问Pod里某个容器提供的服务。
/api/v1/proxy/namespaces/{namespace}/pods/{name}/{path:*}      #访问pod的某个服务接口
/api/v1/proxy/namespaces/{namespace}/pods/{name}                    #访问Pod
/api/v1/proxy/namespaces/{namespace}/services/{name}/{path:*}      #访问service的某个服务接口
/api/v1/proxy/namespaces/{namespace}/services/{name}                   #访问service

Pod的proxy接口的作用是在kubernetes集群之外访问某个pod容器的服务(HTTP服务),可以用Proxy API实现,这种场景多用于管理目的,比如逐一排查Service的Pod副本,检查哪些Pod的服务存在异常问题。
注:k8s Proxy API关于Pod的相关接口在v1.8.8版本的一个实验环境中验证测试失败,问题待解。

1.3 集群功能模块之间的通信

kubernetes API Server作为集群的核心,负责集群各功能模块之间的通信,集群内各个功能模块通过API Server将信息存入etcd,当需要获取和操作这些数据时,通过API Server提供的REST接口(GET\LIST\WATCH方法)来实现,从而实现各模块之间的信息交互。
k8s技术预研9--Kubernetes核心组件运行原理分析

主要有以下三类通信交互的场景:
1)kubelet与API Server交互
每个Node节点上的kubelet定期就会调用API Server的REST接口报告自身状态,API Server接收这些信息后,将节点状态信息更新到etcd中。kubelet也通过API Server的Watch接口监听Pod信息,如果监听到新的Pod副本被调度绑定到本节点,则执行Pod对应的容器的创建和启动逻辑;如果监听到Pod对象被删除,则删除本节点上的相应的Pod容器;如果监听到修改Pod信息,则kubelet监听到变化后,会相应的修改本节点的Pod容器。

2)kube-controller-manager与API Server交互
kube-controller-manager中的Node Controller模块通过API Server提供的Watch接口,实时监控Node的信息,并做相应处理。

3)kube-scheduler与API Server交互
Scheduler通过API Server的Watch接口监听到新建Pod副本的信息后,它会检索所有符合该Pod要求的Node列表,开始执行Pod调度逻辑。调度成功后将Pod绑定到目标节点上。

为了缓解各模块对API Server的访问压力,各功能模块都采用缓存机制来缓存数据,各功能模块定时从API Server获取指定的资源对象信息(LIST/WATCH方法),然后将信息保存到本地缓存,功能模块在某些情况下不直接访问API Server,而是通过访问缓存数据来间接访问API Server。

2、Controller Manager原理分析

Controller Manager作为集群内部的管理控制中心,负责集群内的Node、Pod副本、服务端点(Endpoint)、命名空间(Namespace)、服务账号(ServiceAccount)、资源定额(ResourceQuota)的管理,当某个Node意外宕机时,Controller Manager会及时发现此故障并执行自动化修复流程,确保集群始终处于预期的工作状态。
k8s技术预研9--Kubernetes核心组件运行原理分析k8s技术预研9--Kubernetes核心组件运行原理分析
Controller Manager内部包含多种Controller,每种Controller都负责一种具体的控制流程,而Controller Manager正是这些Controller的核心
  • Replication Controller
  • Node Controller
  • ResourceQuota Controller
  • Namespace Controller
  • ServiceAccount Controller
  • Token Controller
  • Service Controller
  • Endpoint Controller
每个Controller通过API Server提供的接口实时监控整个集群的每个资源对象的当前状态,当发生各种故障导致系统状态发生变化时,会尝试着将系统状态从"现有状态"修正到"期望状态"。

2.1 Replication Controller

这里的Replication Controller所指的是副本控制器,并不是RC资源对象,注意不要混淆。
Replication Controller 副本控制器的核心作用是确保在任何时候集群中一个RC所关联的Pod副本数始终保持预设值。需要注意的一点是:只有当Pod的重启策略是Always的时候(RestartPolicy=Always),副本控制器才会管理该Pod的操作(例如创建、销毁、重启等)。
Replication Controller 副本控制器在管理Pod时的一些特征:
  • 在通常情况下,Pod对象被成功创建后不会消失,唯一的例外是当Pod处于succeeded或failed状态的时间过长(超时参数由系统设定)时,该Pod会被系统自动回收。管理该Pod的副本控制器将在其他工作节点上重新创建、运行该Pod副本。
  • RC中的Pod模板就像一个模具,模具制造出来的东西一旦离开模具,它们之间就再没关系了。一旦Pod被创建,无论模板如何变化,也不会影响到已经创建的Pod。
  • 此外,Pod可以通过修改标签来脱离RC的管控,该方法可以用于将Pod从集群中迁移,数据修复等调试。
  • 删除一个RC不会影响它所创建的Pod,如果要删除Pod需要将RC的副本数属性设置为0。
最好不要越过RC而直接创建Pod,因为Replication Controller会通过RC管理Pod副本,实现自动创建、补足、替换、删除Pod副本,这样能提高系统的容灾能力,减少由于节点崩溃等意外状况造成的损失。即使你的应用程序只用到一个Pod副本,我们也强烈建议使用RC来定义Pod。

Replication Controller的职责:
1)确保当前集群中有且仅有N个Pod实例,N是RC中定义的Pod副本数量。
2)通过调整RC中的spec.replicas属性值来实现系统扩容或缩容。
3)通过改变RC中的Pod模板(主要是镜像版本)来实现系统的滚动升级。

与上述三类职责所对应的是Replication Controller的三类典型使用场景:
1)重新调度,当发生节点故障或Pod被意外终止运行时,可以重新调度保证集群中仍然运行指定的副本数。
2)弹性伸缩,通过手动或自动扩容代理修复副本控制器的spec.replicas属性,可以实现弹性伸缩。
3)滚动更新,推荐的方式是创建一个新的只有一个副本的RC,若新的RC副本数量加1,则旧的RC的副本数量减1,直到这个旧的RC的副本数量为零,然后删除该旧的RC。kubectl rolling-update命令就是按该推荐方式所实现的。

2.2 Node Controller

kubelet进程在启动时会通过API Server注册自身的节点信息,并定时向API Server汇报状态信息,API Server接收到信息后将信息更新到etcd中。etcd中存储的节点信息包括节点健康状况、节点资源、节点名称、节点地址信息、操作系统版本、Docker版本、kubelet版本等。节点健康状况包含"就绪"(True)、"未就绪"(False)和"未知"(Unknown)三种。
Node Controller通过API Server实时获取Node的相关信息,实现管理和监控集群中的各个Node节点的相关控制功能。

Node Controller的核心工作流程如下图所示:
k8s技术预研9--Kubernetes核心组件运行原理分析
对流程中关键点的解释如下:
(1)Controller Manager在启动时如果设置了--cluster-cidr参数,那么为每个没有设置Spec.PodCIDR的Node节点生成一个CIDR地址,并用该CIDR地址设置节点的Spec.PodCIDR属性,防止不同的节点的CIDR地址发生冲突。
(2)Controller Manager在处理节点的状态监控管理时的具体流程见以上流程图。
(3)逐个读取节点信息,如果节点状态变成非“就绪”状态,则将节点加入待删除队列,否则将节点从该队列删除。如果节点状态为非"就绪"状态,则删除etcd中的节点信息,并删除和该节点相关的Pod等资源信息。

2.3 ResourceQuota Controller

资源配额管理确保指定的资源对象在任何时候都不会超量占用系统物理资源。
目前k8s支持如下三个层次的资源配额管理:
    1)容器级别:对CPU和Memory进行限制
    2)Pod级别:对一个Pod内所有容器的可用资源进行限制
    3)Namespace级别:是Namespace级别(多租户)的限制,包括:
  • Pod数量
  • Replication Controller数量
  • Service数量
  • ResourceQuota数量
  • Secret数量
  • 可持有的PV(Persistent Volume)数量

k8s配额管理是通过Admission Control(准入控制)来控制的。 Admission Control当前提供两种配额约束的方式,分别是LimitRanger和ResourceQuota。
其中LimitRanger作用于Pod和Container上,而ResourceQuota则作用于Namespace上,限定一个Namespace里的各类资源的使用总额。

ResourceQuota Controller流程图:
k8s技术预研9--Kubernetes核心组件运行原理分析

2.4 Namespace Controller

用户通过API Server可以创建新的Namespace并保存在etcd中,Namespace Controller定时通过API Server读取这些Namespace信息。
如果Namespace被API标记为优雅删除(即设置删除期限,DeletionTimestamp),则将该Namespace状态设置为“Terminating”并保存到etcd中。同时Namespace Controller删除该Namespace下的ServiceAccount、RC、Pod、Secret、PersistentVolume、ListRange、ResourceQuota和Event等资源对象。
当Namespace状态设置为“Terminating”后,由Admission Controller的NamespaceLifecycle插件来阻止为该Namespace创建新的资源。同时,在Namespace Controller删除完该Namespace中的所有资源对象后,Namespace Controller对该Namespace执行finalize操作,删除Namespace的spec.finalizers域中的信息。

2.5 Service Controller与Endpoint Controller

如下图所示,Endpoints表示一个Service对应的所有Pod副本的访问地址,而Endpoints Controller就是负责生成和维护所有Endpoints对象的控制器。
k8s技术预研9--Kubernetes核心组件运行原理分析
Endpoints表示了一个Service对应的所有Pod副本的访问地址,而Endpoints Controller负责生成和维护所有Endpoints对象的控制器。它负责监听Service和对应的Pod副本的变化。
  • 如果监测到Service被删除,则删除和该Service同名的Endpoints对象;
  • 如果监测到新的Service被创建或修改,则根据该Service信息获得相关的Pod列表,然后创建或更新Service对应的Endpoints对象。
  • 如果监测到Pod的事件,则更新它对应的Service的Endpoints对象。
kube-proxy进程获取每个Service的Endpoints,实现Service的负载均衡功能。

Service Controller
Service Controller是属于kubernetes集群与外部的云平台之间的一个接口控制器。Service Controller监听Service变化,如果是一个LoadBalancer类型的Service,则确保外部的云平台上对该Service对应的LoadBalancer实例被相应地创建、删除及更新路由转发表。

3、Scheduler原理分析

k8s Scheduler在整个系统中承担了"承上启下"的重要功能。承上,是指它负责接收Controller Manager创建的新Pod,为其安排一个落脚的“家”, 即目标Node。启下,是指安置工作完成后,目标Node上的kubelet服务进程接管后继工作,负责Pod生命周期中的“下半生”。
具体来说,k8s Scheduler的作用是将待调度的Pod(API新创建的Pod、Controller Manager为补足副本而创建的Pod等)按照特定的调度算法和调度策略绑定到集群中的某个合适的Node上,并将绑定信息写入etcd中。
在整个调度过程中涉及三个对象,分别是:待调度Pod列表、可用Node列表,以及调度算法和策略。

随后,目标节点上的kubelet通过API Server监听到k8s Scheduler产生的Pod绑定事件,然后获取对应的Pod清单,下载Image镜像,并启动容器。
完整的流程如下所示:
k8s技术预研9--Kubernetes核心组件运行原理分析
k8s Scheduler当前提供的默认调度流程分为以下两步:
1)预选调度过程,即遍历所有目标Node,筛选出符合要求的候选节点,kubernetes内置了多种预选策略(xxx Predicates)供用户选择。
2)确定最优节点,在第一步的基础上采用优选策略(xxx Priority)计算出每个候选节点的积分,最高积分者胜出。
k8s Scheduler的调度流程是通过插件方式加载的“调度算法提供者”(AlgorithmProvider)具体实现的。一个AlgorithmProvider其实就是包括了一组预选策略与一组优选策略的结构体。
注册AlgorithmProvider的函数如下:
func RegisterAlgorithmProvider(name string, predicateKeys, priorityKeys util.StringSet)
它包含三个参数:
  • name.string,算法名;
  • predicateKeys,为算法用到的预选策略集合;
  • priorityKeys,为算法用到的优选策略的集合;

Scheduler中可用的预选策略包含:NoDiskConflict, PodFitsResources, PodSelectorMatches, PodFirstHost, CheckNodeLabelPresence, CheckServiceAffinity和PodFitsPorts策略等。
其默认的AlgorithmProvider加载的预选策略Predicates包括:PodFitsPorts, PodFitsResources, NoDiskConflict, MatchNodeSelector(PodSelectorMatches) 和 HostName(PodFitsHost),即每个节点只有通过前面提及的5个默认预选策略后,才能初步被选中,进入下一个流程。

3.1 下面列出的是对所有预选策略的详细说明。

1)NoDiskConflict
判断备选Pod的gcePersistentDisk或AWSElasticBlockStore和备选的节点中已存在的Pod是否存在冲突。检测过程如下:
  • 首先,读取备选Pod的所有Volume的信息(即pod.Spec.Volumes),对每个Volume执行以下步骤进行冲突检测。
  • 如果该Volume是gcePersistentDisk,则将Volume和备选节点上的所有Pod的每个Volume进行比较,如果发现相同的gcePersistentDisk,则返回false,表明存在磁盘冲突,检查结束,反馈给调度器该备选节点不适合作为备选Pod;如果该Volume是AWSElasticBlockStore,则将Volume和备选节点上的所有Pod的每个Volume进行比较,如果发现相同的AWSElasticBlockStore,则返回false,表明存在磁盘冲突,检查结束,反馈给调度器该备选节点不适合备选Pod。
  • 如果检查完备选Pod的所有Volume均未发现冲突,则返回true,表明不存在磁盘冲突,反馈给调度器该备选节点适合备选Pod。
2)PodFitsResources
判断备选节点的资源是否满足备选Pod的需求,检测过程如下:
  • 计算备选Pod和节点中已存在Pod的所有容器的需求资源(内存和CPU)的总和。
  • 获得备选节点的状态信息,其中包含节点的资源信息。
  • 如果备选Pod和节点中已存在Pod的所有容器的需求资源(内存和CPU)的总和,超出了备选节点拥有的资源,则返回false,表明备选节点不适合备选Pod,否则返回true,表明备选节点适合备选Pod。
3)PodSelectorMatches
判断备选节点是否包含备选Pod的标签选择器指定的标签。
  • 如果Pod没有指定spec.nodeSelector标签选择器,则返回true。
  • 否则,获得备选节点的标签信息,判断节点是否包含备选Pod的标签选择器(spec.nodeSelector)所指定的标签,如果包含,则返回true,否则返回false。
4)PodFitsHost
判断备选Pod的spec.nodeName域所指定的节点名称和备选节点的名称是否一致,如果一致,则返回true,否则返回false。
5)CheckNodeLabelPresence
如果用户在配置文件中指定了该策略,则Scheduler会通过RegisterCustomFitPredicate方法注册该策略。该策略用于判断策略列出的标签在备选节点中存在时,是否选择该备选节点。
  • 读取备选节点的标签列表信息。
  • 如果策略配置的标签列表存在于备选节点的标签列表中,且策略配置的presence值为false,则返回false,否则返回true;如果策略配置的标签列表不存在于备选节点的标签列表中,且策略配置的presence为true,则返回false,否则返回true。
6)CheckServiceAffinity
如果用户在配置文件中指定了该策略,则Scheduler会通过RegisterCustomFitPredicate方法注册该策略。该策略用于判断备选节点是否包含策略指定的标签,或包含和备选Pod在相同Service和Namespace下的Pod所在节点的标签列表。如果存在,则返回true,否则返回false。
7)PodFitsPorts
判断备选Pod所用的端口列表中的端口是否在备选节点中已被占用,如果被占用,则返回false,否则返回true。

3.2 Scheduler中的优选策略

Scheduler中的优选策略包含:LeastRequestedPriority、CalculateNodeLabelPriority和BalancedResourceAllocation等。
每个节点通过优选策略时都会算出一个得分,计算各项得分,最终选出得分值最大的节点作为优选的结果(也是调度算法的结果)。
下面是对优选策略的详细说明:
1) LeastRequestedPriority
优先从备选节点列表中选择资源消耗最小的节点(CPU+内存)。

2) CalculateNodeLabelPriority
如果用户在配置文件中指定了该策略,则scheduler会通过RegisterCustomPriorityFunction方法注册该策略。该策略用于判断策略列出的标签在备选节点中存在时,是否选择该备选节点。
如果备选节点的标签在优先策略的标签列表中且优选策略的presence为true,或者备选节点的标签不在优选策略的标签列表中且优选策略的presence值为false,则备选节点score=10,否则备选节点score=0。

3) BalancedResourceAllocation
优先从备选节点列表中选择各项资源使用率最均衡的节点。

4、kubelet运行机制分析

在kubernetes集群中,每个Node节点(又称Minion)上都会启动一个kubelet服务进程。该进程用于处理Master节点下发到本节点的任务,管理Pod和其中的容器。每个kubelet进程会在API Server上注册节点自身信息,定期向Master节点汇报节点资源的使用情况,并通过cAdvisor监控容器和节点资源。

4.1 节点管理

节点通过设置kubelet的启动参数“--register-node”,来决定是否自动主动地向API Server注册自己,默认为true。
在自注册时,kubelet启动时还包含以下参数:
  • --api-servers: API Server的位置
  • --kubeconfig:kubeconfig文件,用于访问API Server的配置文件
  • --cloud-provider:云服务商地址,仅用于公有云环境
当前每个kubelet被授予创建和修改任务节点的权限,但实践中,它仅仅创建和修改自己。未来将会限制这一权限,仅允许它修改和创建其所在节点的权限。
在集群运行过程中,如果遇到资源不足的情况,则用户很容易通过添加机器及利用kubelet的自注册模式来实现扩容。
如果系统管理员希望手动创建节点信息,则可以通过设置--register-node=false即可。

通过--node-status-update-frequency参数,可以配置kubelet向API Server报告节点状态的时间频率,默认为间隔10s。

4.2 Pod管理

kubelet通过以下几种方式获取自身Node上所需要运行的Pod清单。
  • 文件:kubelet启动参数"--config"指定的配置文件目录下的文件,通过--file-check-frequency设置检查该文件目录的时间间隔,默认为20s。
  • HTTP端点(URL):通过"--manifest-url"参数设置,通过--http-check-frequency设置检查时间间隔,默认为20s。
  • API Server:kubelet通过API Server监听etcd目录,同步Pod列表。
所有以非API Server方式创建的Pod都叫做Static Pod。kubelet将Static Pod状态汇报给API Server,API Server为该Static Pod创建一个Mirror Pod和其相匹配。Mirror Pod的状态将真实反映Static Pod的状态。当Static Pod被删除时,与之相对应的Mirror Pod也会被删除。
在本文只讨论通过API Server获得Pod清单的方式。kubelet通过API Server Client使用Watch加List的方式监听etcd中/registry/nodes/${当前节点名称}和/registry/pods的目录,将获取的信息同步到本地缓存中。

kubelet监听etcd,所以针对Pod的操作将会被kubelet监听到。如果发现有新的绑定到本节点的Pod,则按照Pod清单的要求创建该Pod。
如果发现本地Pod被修改,则kubelet会做出相应的修改,比如删除Pod中的某个容器时,则通过Docker Client删除该容器。
如果发现删除本节点的Pod,则删除相应的Pod,并通过Docker Client删除Pod中的容器。

kubelet读取监听到的信息,如果是创建和修改Pod任务,则做如下处理:
1)为该Pod创建一个数据目录。
2)从API Server读取该Pod清单。
3)为该Pod挂载外部卷(External Volume)
4)下载Pod用到的Secret。
5)检查已运行在节点中的Pod,如果该Pod没有容器或Pause容器没有启动,则先停止Pod里所有容器的进程。如果在Pod中有需要删除的容器,则删除这些容器。
6)用"kubernetes/pause"镜像为每个Pod创建一个容器。该Pause容器用于接管Pod中所有其它容器的网络。
7)为Pod中的每个容器做如下处理:
  • 为容器计算一个hash值,然后用容器的名字去查询对应Docker容器的hash值。这里的hash值可以理解为是依据容器的定义文件所生成的。对比两个hash值,发现有不同则会停止Docker中容器的进程,并停止与之关联的Pause容器的进程。若二者相同,则不做任何处理。
  • 如果容器被终止了,且没有指定restartPolicy重启策略,则不做任何处理。
  • 调用Docker Client下载容器镜像,调用Docker Client运行容器。

4.3 容器健康检查

Pod的健康状态由两类探针来检查:LivenessProbe和ReadinessProbe。
LivenessProbe
  • 用于判断容器是否存活(running状态)。
  • 如果LivenessProbe探针探测到容器非健康,则kubelet将杀掉该容器,并根据容器的重启策略做相应处理。
  • 如果容器不包含LivenessProbe探针,则kubelet认为该探针的返回值永远为“success”。

kubelet定期执行LivenessProbe探针来判断容器的健康状态。
LivenessProbe参数:
  • initialDelaySeconds:启动容器后首次进行健康检查的等待时间,单位为秒。
  • timeoutSeconds:健康检查发送请求后等待响应的时间,如果超时响应kubelet则认为容器非健康,重启该容器,单位为秒。
LivenessProbe三种实现方式
1)ExecAction
在一个容器内部执行一个命令,如果该命令状态返回值为0,则表明容器健康。
apiVersion: v1
kind: Pod
metadata:
  name: liveness-exec
spec:
  containers:
  - name: liveness
    image: busybox
    args:
    - /bin/sh
    - -c
    - echo ok > /tmp/health;sleep 10;rm -fr /tmp/health;sleep 600
    livenessProbe:
      exec:
        command:
        - cat
        - /tmp/health
      initialDelaySeconds: 15
      timeoutSeconds: 1

2)TCPSocketAction
通过容器IP地址和端口号执行TCP检查,如果能够建立TCP连接,则表明容器健康。
apiVersion: v1
kind: Pod
metadata:
  name: pod-with-healthcheck
spec:
  containers:
  - name: nginx
    image: nginx
    ports:
    - containnerPort: 80
    livenessProbe:
      tcpSocket:
        port: 80
      initialDelaySeconds: 15
      timeoutSeconds: 1

3)HTTPGetAction
通过容器的IP地址、端口号及路径调用HTTP Get方法,如果响应的状态码大于等于200且小于等于400,则认为容器健康。
apiVersion: v1
kind: Pod
metadata:
  name: pod-with-healthcheck
spec:
  containers:
  - name: nginx
    image: nginx
    ports:
    - containnerPort: 80
    livenessProbe:
      httpGet:
        path: /_status/healthz
        port: 80
      initialDelaySeconds: 15
      timeoutSeconds: 1

ReadinessProbe
  • 用于判断容器是否启动完成(read状态),可以接受请求。
  • 如果ReadnessProbe探针检测失败,则Pod的状态将被修改。Endpoint Controller将从Service的Endpoint中删除包含该容器所在Pod的Endpoint。

4.4 cAdvisor资源监控

在kubernetes集群中,应用程序的执行情况可以在不同的级别上监测到。这些级别包括:容器、Pod、Service和整个集群。
Heapster项目为k8s提供了一个基本的监控平台,它是集群级别的监控和事件数据集成器(Aggregator)。Heapster作为Pod运行在k8s集群中,通过kubelet发现所有运行在集群中的节点,并查看来自这些节点的资源使用状况信息。
kubelet通过cAdvisor获取其所在节点及容器的数据,Heapster通过带着关联标签的Pod分组这些信息,这些数据被推动一个可配置的后端,用于存储和展示。当前支持的后端包括InfluxDB(with Grafana for Visualization)和Google Cloud Monitoring。

cAdvisor是一个开源的分析容器资源使用率和性能特性的代理工具。在k8s项目中,cAdvisor被集成到k8s代码中,cAdvisor自动查找所有在其所在节点上的容器,自动采集CPU、内存、文件系统和网络使用的统计信息。
cAdvisor通过它所在节点机的Root容器,采集并分析该节点机的全面使用情况。
在大部分k8s集群中,cAdvisor通过它所在节点机的4194端口暴露一个简单的UI。
k8s技术预研9--Kubernetes核心组件运行原理分析

小结:
kubelet作为连接k8s Master和各节点机之间的桥梁,管理运行在节点机上的Pod和容器。kubelet将每个Pod转换成它的成员容器,同时从cAdvisor获取单独的容器使用统计信息,然后通过该REST API暴露这些聚合后的Pod资源使用的统计信息。

5、kube-proxy运行机制分析


5.1 基本原理

为了支持集群的水平扩展、高可用性,k8s抽象出来Service的概念。Service是一组Pod的抽象,它会根据访问策略(如负载均衡策略)来访问这组Pod。
k8s在创建服务时会为服务分配一个虚拟的IP地址,客户端通过访问这个虚拟的IP地址来访问服务,而服务则负责将请求转发到后端的Pod上。这就相当于是一个反向代理。但是它和普通的反射代理有一些不同之处:首先,它的IP地址是虚拟的;其次是,它的部署和启停是k8s统一自动管理的。

Service在很多情况下只是一个概念,而真正将Service的作用落实的是背后的kube-proxy服务进程。

在k8s集群的每个Node上都会运行一个kube-proxy服务进程,这个进程可以看作Service的透明代理兼负载均衡器,其核心功能是将到某个Service的访问请求转发到后端的多个Pod实例上。对每一个TCP类型的k8s Service,kube-proxy都会在本地Node上建立一个SocketServer来负责接收请求,然后均匀发送到后端某个Pod的端口上,这个过程默认采用Round Robin负载均衡算法。另外,k8s也提供通过修改Service的service.spec.sessionAffinity参数的值来实现会话保持特性的定向转发。如果设置的值为"ClientIP",则将来自同一个ClientIP的请求都转发到同一个后端Pod上。

Service的ClusterIP与NodePort等概念是kube-proxy服务通过iptables的NAT转换实现的,kube-proxy在运行过程中动态创建与Service相关的iptables规则,这些规则实现了Cluster IP及NodePort的请求流量重定向到kube-proxy进程上对应服务的代理端口的功能。
由于iptables机制针对的是本地的kube-proxy端口,所以每个Node上都要运行kube-proxy组件。

综上所述,由于kube-proxy的作用,在Service的调用过程中客户端无须关心后端有几个Pod,中间过程的通信、负载均衡及故障恢复都是透明的。
访问Service的请求,不论是用ClusterIP+TargetPort的方式,还是用节点机IPO+NodePort的方式,都被节点机的iptables规则重定向到kube-proxy监听Service服务代理端口。如下图所示。
k8s技术预研9--Kubernetes核心组件运行原理分析

5.2 深入分析kube-proxy的实现细节

kube-proxy通过查询和监听API Server中Service与Endpoints的变化,为每个Service都建立了一个"服务代理对象",并自动同步。服务代理对象是kube-proxy程序内部的一种数据结构,它包括一个用于监听此服务请求的SocketServer,SocketServer的端口是随机选择的一个本地空闲端口。
此外,kube-proxy内部也创建了一个负载均衡器——LoadBalancer,LoadBalancer上保存了Service到对应的后端Endpoint列表的动态路由转发路由表,而具体的路由选择则取决于RoundRobin负载均衡算法及Service的Session会话保持(SessionAffinity)这两个特性。

1)针对发生变化的Service列表,kube-proxy的具体处理流程如下:
(1)如果该Service没有设置集群IP(ClusterIP),则不做任何处理,否则,获取该Service的所有端口定义列表(spec.ports域)。
(2)逐个读取服务端口定义列表中的端口信息,根据端口名称、Service名称和Namespace判断本地是否已经存在对应的服务代理对象,如果不存在则新建;如果存在并且Service端口被修改过,则先删除iptables中和该Service端口相关的规则,关闭服务代理对象,然后走新建流程,即为该Service端口分配服务代理对象并为该Service创建相关的iptables规则。
(3)更新负载均衡器组件中对应Service的转发地址列表,对于新建的Service,确定转发时的会话保持策略。
(4)对于已删除的Service则进行清理。

2)针对Endpoint的变化,kube-proxy会自动更新负载均衡器中对应Service的转发地址列表。

3)以上两个步骤,kube-proxy针对iptables所做的一些具体操作如下:
kube-proxy在启动时和监听到Service或Endpoint的变化后,会在本机iptables的NAT表中添加4条规则链。
(1)KUBE-PORTALS-CONTAINER:从容器中通过Service Cluster IP和端口号访问Service的请求。
(2)KUBE-PORTALS-HOST:从主机中通过Service Cluster IP和端口号访问Service的请求。
(3)KUBE-NODEPORT-CONTAINER:从容器中通过Service的NodePort端口号访问Service的请求。
(4)KUBE-NODEPORT-HOST:从主机中通过Service的NodePort端口号访问Service的请求。


5.3 kube-proxy在iptables中为每个Service创建由ClusterIP+Service端口到kube-proxy所在主机IP+Service代理服务所监听的端口的转发规则

Kubernetes通过在目标node的iptables中的nat表的PREROUTING和POSTROUTING链中创建一系列的自定义链 (这些自定义链主要是“KUBE-SERVICES”链、“KUBE-POSTROUTING”链、每个服务所对应的“KUBE-SVC-XXXXXXXXXXXXXXXX”链和“KUBE-SEP-XXXXXXXXXXXXXXXX”链),然后通过这些自定义链对流经到该node的数据包做DNAT和SNAT操作以实现路由、负载均衡和地址转换。
k8s技术预研9--Kubernetes核心组件运行原理分析

基于iptables的kube-proxy的实现代码在pkg/proxy/iptables/proxier.go,其主要职责包括两大块,一块是侦听Service更新事件,并更新Service相关的iptables规则,一块是侦听Endpoint更新事件,更新Endpoint相关的iptables规则,将包请求转入Endpoint对应的Pod,如果某个Service尚没有Pod创建,那么针对此Service的请求将会被drop掉。

kube-proxy对iptables的链进行了扩充,自定义了KUBE-SERVICES,KUBE-NODEPORTS,KUBE-POSTROUTING,KUBE-MARK-MASQ和KUBE-MARK-DROP五个链,并主要通过为
KUBE-SERVICES chain增加rule来配制Routing Traffic规则。

在iptables表中,通过iptables-save可以看到在Nat表中创建好的这些链。
:KUBE-MARK-DROP - [0:0] /*对于未能匹配到跳转规则的traffic set mark 0x8000,有此标记的数据包会在filter表drop掉*/
:KUBE-MARK-MASQ - [0:0] /*对于符合条件的包 set mark 0x4000, 有此标记的数据包会在KUBE-POSTROUTING chain中统一做MASQUERADE*/
:KUBE-NODEPORTS - [0:0] /*针对通过nodeport访问的package做的操作*/
:KUBE-POSTROUTING - [0:0]
:KUBE-SERVICES - [0:0] /*操作跳转规则的主要chain*/

为默认的PREROUTING,Output和POSTROUTING chain增加规则,跳转至Kubernetes自定义的新chain。
-A PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
-A OUTPUT -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
-A POSTROUTING -m comment --comment "kubernetes postrouting rules" -j KUBE-POSTROUTING

对于KUBE-MARK-MASQ链中所有规则设置了Kubernetes独有MARK标记,在KUBE-POSTROUTING链中对NODE节点上匹配Kubernetes独有MARK标记的数据包,进行SNAT处理。
-A KUBE-MARK-MASQ -j MARK --set-xmark 0x4000/0x4000

Kube-proxy接着对每个服务创建“KUBE-SVC-”链,并在Nat表中将KUBE-SERVICES链中每个目标地址是Service的数据包导入这个“KUBE-SVC-”链,如果Endpoint尚未创建,KUBE-SVC-链中没有规则,任何Incoming Packets在规则匹配失败后会被KUBE-MARK-DROP。

如果一个Service对应的Pod有多个Replicas,在iptables中会有多条记录,并通过 -m statistic --mode random --probability来控制比率。

PREROUTING chain的最终跳转规则,抽象为下图:
k8s技术预研9--Kubernetes核心组件运行原理分析

转发规则的包匹配规则部分如下所示:
-m comment --comment $SERVICESTRING -p $PROTOCOL -m $PROTOCOL --dport $DESTPORT -d $DESTIP
其中:
  • “-m comment --comment”表示匹配规则使用iptables的显示扩展的注释功能,$SERVICESTRING为注释的内容;
  • “-p $PROTOCOL -m $PROTOCOL --dport $DESTPORT -d $DESTIP”表示协议为“$PROTOCOL”且目标地址和端口为“$DESTIP”和“$DESTPORT”的包,其中“$PROTOCOL”可以为TCP或UDP,“$DESTIP”和“$DESTPORT”为Service的ClusterIP和TargetPort。

转发规则的跳转部分(-j部分):
  • -j REDIRECT --to-ports $proxyPort 。如果请求来自本地容器且Service代理服务监听的是所有的接口,则跳转部分实为端口重定向。该规则的功能是实现数据包的端口重定向,重定向到$proxyPort端口(Service代理服务监听的端口);
  • -j DNAT --to-destination proxyIP:proxyPort。当请求不是来自本地容器时,需要走数据包转发的规则,数据包的目的地址变为“proxyIP:proxyPort”(即Service代理服务所在的IP地址和端口,这些地址和端口都会被替换成实际的地址和端口)。

如果Service类型为NodePort,则kube-proxy在iptables中除了添加上面介绍的规则外,还会为每个Service创建由NodePort端口到kube-proxy所在主机IP+Service代理服务所监听的端口的转发规则。
转发规则的包匹配规则部分如下所示:
-m comment --comment $SERVICESTRING -p $PROTOCOL -m $PROTOCOL --dport $NODEPORT
上面所列的内容用于匹配目的端口为"$NODEPORT"的包。

转发规则的跳转部分(-j部分)和前面提及的跳转规则一致。

5.4 kube-proxy创建出的iptables规则样例


[[email protected] ~]# iptables-save
# Generated by iptables-save v1.4.21 on Fri Apr  6 19:38:41 2018
*filter
:INPUT ACCEPT [137:45852]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [126:11590]
:DOCKER - [0:0]
:DOCKER-ISOLATION - [0:0]
:DOCKER-USER - [0:0]
:KUBE-FIREWALL - [0:0]
:KUBE-FORWARD - [0:0]
:KUBE-SERVICES - [0:0]
-A INPUT -j KUBE-FIREWALL
-A INPUT -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
-A FORWARD -j DOCKER-USER
-A FORWARD -j DOCKER-ISOLATION
-A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -o docker0 -j DOCKER
-A FORWARD -i docker0 ! -o docker0 -j ACCEPT
-A FORWARD -i docker0 -o docker0 -j ACCEPT
-A FORWARD -m comment --comment "kubernetes forward rules" -j KUBE-FORWARD
-A OUTPUT -j KUBE-FIREWALL
-A OUTPUT -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
-A DOCKER -d 172.17.0.5/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 443 -j ACCEPT
-A DOCKER -d 172.17.0.5/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 80 -j ACCEPT
-A DOCKER-ISOLATION -j RETURN
-A DOCKER-USER -j RETURN
-A KUBE-FIREWALL -m comment --comment "kubernetes firewall for dropping marked packets" -m mark --mark 0x8000/0x8000 -j DROP
-A KUBE-FORWARD -m comment --comment "kubernetes forwarding rules" -m mark --mark 0x4000/0x4000 -j ACCEPT
COMMIT
# Completed on Fri Apr  6 19:38:41 2018
# Generated by iptables-save v1.4.21 on Fri Apr  6 19:38:41 2018
*nat
:PREROUTING ACCEPT [14:966]
:INPUT ACCEPT [0:0]
:OUTPUT ACCEPT [9:540]
:POSTROUTING ACCEPT [9:540]
:DOCKER - [0:0]
:KUBE-MARK-DROP - [0:0]
:KUBE-MARK-MASQ - [0:0]
:KUBE-NODEPORTS - [0:0]
:KUBE-POSTROUTING - [0:0]
:KUBE-SEP-4RATAVQC2G7Y6Z6O - [0:0]
:KUBE-SEP-BHZYIGDTOBFGE25J - [0:0]
:KUBE-SEP-OGWO7LMK6DKAZC2Q - [0:0]
:KUBE-SEP-QVN6FUUI4LG2VPZV - [0:0]
:KUBE-SEP-RXSC7ZQ2D5HSX4JT - [0:0]
:KUBE-SEP-WJGEXG4NESKNJ3Q7 - [0:0]
:KUBE-SERVICES - [0:0]
:KUBE-SVC-2QFLXPI3464HMUTA - [0:0]
:KUBE-SVC-ERIFXISQEP7F7OF4 - [0:0]
:KUBE-SVC-NPX46M4PTMTKRN6Y - [0:0]
:KUBE-SVC-R36H7ZIERA5ZZDML - [0:0]
:KUBE-SVC-TCOU7JCQXEZGVUNU - [0:0]
-A PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
-A OUTPUT -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
-A POSTROUTING -m comment --comment "kubernetes postrouting rules" -j KUBE-POSTROUTING
-A POSTROUTING -s 172.17.0.5/32 -d 172.17.0.5/32 -p tcp -m tcp --dport 443 -j MASQUERADE
-A POSTROUTING -s 172.17.0.5/32 -d 172.17.0.5/32 -p tcp -m tcp --dport 80 -j MASQUERADE
-A DOCKER -i docker0 -j RETURN
-A DOCKER ! -i docker0 -p tcp -m tcp --dport 443 -j DNAT --to-destination 172.17.0.5:443
-A DOCKER ! -i docker0 -p tcp -m tcp --dport 80 -j DNAT --to-destination 172.17.0.5:80
-A KUBE-MARK-DROP -j MARK --set-xmark 0x8000/0x8000
-A KUBE-MARK-MASQ -j MARK --set-xmark 0x4000/0x4000
-A KUBE-POSTROUTING -m comment --comment "kubernetes service traffic requiring SNAT" -m mark --mark 0x4000/0x4000 -j MASQUERADE
-A KUBE-SEP-4RATAVQC2G7Y6Z6O -s 172.17.0.6/32 -m comment --comment "kube-system/kube-dns:dns" -j KUBE-MARK-MASQ
-A KUBE-SEP-4RATAVQC2G7Y6Z6O -p udp -m comment --comment "kube-system/kube-dns:dns" -m udp -j DNAT --to-destination 172.17.0.6:53
-A KUBE-SEP-BHZYIGDTOBFGE25J -s 172.17.0.3/32 -m comment --comment "kube-system/default-http-backend:" -j KUBE-MARK-MASQ
-A KUBE-SEP-BHZYIGDTOBFGE25J -p tcp -m comment --comment "kube-system/default-http-backend:" -m tcp -j DNAT --to-destination 172.17.0.3:8080
-A KUBE-SEP-OGWO7LMK6DKAZC2Q -s 172.17.0.6/32 -m comment --comment "kube-system/kube-dns:dns-tcp" -j KUBE-MARK-MASQ
-A KUBE-SEP-OGWO7LMK6DKAZC2Q -p tcp -m comment --comment "kube-system/kube-dns:dns-tcp" -m tcp -j DNAT --to-destination 172.17.0.6:53
-A KUBE-SEP-QVN6FUUI4LG2VPZV -s 10.0.2.5/32 -m comment --comment "default/kubernetes:https" -j KUBE-MARK-MASQ
-A KUBE-SEP-QVN6FUUI4LG2VPZV -p tcp -m comment --comment "default/kubernetes:https" -m recent --set --name KUBE-SEP-QVN6FUUI4LG2VPZV --mask 255.255.255.255 --rsource -m tcp -j DNAT --to-destination 10.0.2.5:6443
-A KUBE-SEP-RXSC7ZQ2D5HSX4JT -s 172.17.0.4/32 -m comment --comment "kube-system/webapp:" -j KUBE-MARK-MASQ
-A KUBE-SEP-RXSC7ZQ2D5HSX4JT -p tcp -m comment --comment "kube-system/webapp:" -m tcp -j DNAT --to-destination 172.17.0.4:8080
-A KUBE-SEP-WJGEXG4NESKNJ3Q7 -s 172.17.0.3/32 -m comment --comment "kube-system/webapp:" -j KUBE-MARK-MASQ
-A KUBE-SEP-WJGEXG4NESKNJ3Q7 -p tcp -m comment --comment "kube-system/webapp:" -m tcp -j DNAT --to-destination 172.17.0.3:8080
-A KUBE-SERVICES -d 10.10.10.2/32 -p udp -m comment --comment "kube-system/kube-dns:dns cluster IP" -m udp --dport 53 -j KUBE-SVC-TCOU7JCQXEZGVUNU
-A KUBE-SERVICES -d 10.10.10.2/32 -p tcp -m comment --comment "kube-system/kube-dns:dns-tcp cluster IP" -m tcp --dport 53 -j KUBE-SVC-ERIFXISQEP7F7OF4
-A KUBE-SERVICES -d 10.10.10.206/32 -p tcp -m comment --comment "kube-system/webapp: cluster IP" -m tcp --dport 8080 -j KUBE-SVC-R36H7ZIERA5ZZDML
-A KUBE-SERVICES -d 10.10.10.1/32 -p tcp -m comment --comment "default/kubernetes:https cluster IP" -m tcp --dport 443 -j KUBE-SVC-NPX46M4PTMTKRN6Y
-A KUBE-SERVICES -d 10.10.10.22/32 -p tcp -m comment --comment "kube-system/default-http-backend: cluster IP" -m tcp --dport 80 -j KUBE-SVC-2QFLXPI3464HMUTA
-A KUBE-SERVICES -m comment --comment "kubernetes service nodeports; NOTE: this must be the last rule in this chain" -m addrtype --dst-type LOCAL -j KUBE-NODEPORTS
-A KUBE-SVC-2QFLXPI3464HMUTA -m comment --comment "kube-system/default-http-backend:" -j KUBE-SEP-BHZYIGDTOBFGE25J
-A KUBE-SVC-ERIFXISQEP7F7OF4 -m comment --comment "kube-system/kube-dns:dns-tcp" -j KUBE-SEP-OGWO7LMK6DKAZC2Q
-A KUBE-SVC-NPX46M4PTMTKRN6Y -m comment --comment "default/kubernetes:https" -m recent --rcheck --seconds 10800 --reap --name KUBE-SEP-QVN6FUUI4LG2VPZV --mask 255.255.255.255 --rsource -j KUBE-SEP-QVN6FUUI4LG2VPZV
-A KUBE-SVC-NPX46M4PTMTKRN6Y -m comment --comment "default/kubernetes:https" -j KUBE-SEP-QVN6FUUI4LG2VPZV
-A KUBE-SVC-R36H7ZIERA5ZZDML -m comment --comment "kube-system/webapp:" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-WJGEXG4NESKNJ3Q7
-A KUBE-SVC-R36H7ZIERA5ZZDML -m comment --comment "kube-system/webapp:" -j KUBE-SEP-RXSC7ZQ2D5HSX4JT
-A KUBE-SVC-TCOU7JCQXEZGVUNU -m comment --comment "kube-system/kube-dns:dns" -j KUBE-SEP-4RATAVQC2G7Y6Z6O
COMMIT
# Completed on Fri Apr  6 19:38:41 2018


参考资料:
《Kubernetes权威指南——从Docker到Kubernetes实践全接触》第3章