@dujun
2015-02-28T16:36:26.000000Z
字数 16734
阅读 3515
Kubernetes写书
什么是Admission Control
Kubernetes授权机制根据访问控制规则,致力于回答某个用户是否有权限执行某个操作,而Admission Control可以解读为对资源的管理控制,它是站在系统资源员的角度,致力于回答是否允许该操作真正应用到Kubernetes集群的资源对象上。换句话说,Kubernetes可能会根据任意的Admission Control策略选择驳回某个API请求,即使该用户和他的API请求已经成功通过认证和授权。譬如一个用户要在一个namespace中创建一个pod,结果该namespace预先分配的资源不足以创建这个pod,那么这个API请求就会被Admission Control驳回。与Kubernetes用户认证与授权模块一样,对Admission Control的调用也是由APIServer来完成的--APIServer启动过程中就进行了三个初始化操作,创建了三个对象:认证、授权和资源管理控制(Admission Control)模块。需要说明的是,Admission Control只负责管理API对资源的请求量,一旦pod或容器实际在某台机器上运行后,并不控制他们的行为。因此Admission Control实际上是一个静态的、运行前的概念,而不是运行时的概念。
设计Admission Control的目标有:
Kubernetes APIServer接受以下可选参数来启用Admission Control。
AlwaysAdmit
,即永远允许。下文将重点介绍怎样向Kubernetes集群添加任意数量的Admission Control插件来完成对集群资源的管理控制。
Admission Control各插件详解
Admission Control插件是以下接口的一个实现。
package admission
type Attributes interface {
GetNamespace() string
GetKind() string
GetOperation() string
GetObject() runtime.Object
}
Attributes
接口被Admission Control用来获取一个API请求中能够用于帮助进行决策的信息,这些信息包括:API请求所处上下文的namespae、API操作的Kubernetes对象类型、API操作类型和实际运行时的Kubernetes对象。
package admission
type Interface interface {
Admit(a Attributes) (err error)
}
Interface
接口是一个抽象的、可插拔的、用于进行资源管理控制决策的Admission Control接口。Interface
接口的Admit
则根据一个API请求的实际信息进行实际的管理决策,负责主要逻辑部分。
任意一个Admission Control插件都是admission.Interface
接口的一个实现,
必须被编译成二进制文件并通过提供一个名字向Admission Control进行注册。在我们撰写这本书的时候,总共有5个Admission Control插件,分别是:AlwaysDeny
,AlwaysAdmit
,ResourceDefaults
,LimitRanger
, ResourceQuota
。下面从代码实现的角度,分别对这几个插件做简单介绍。
AlwaysDeny
是admission.Interface
的一个实现,它永远对API请求说no,一般用在单元测试中。该插件向Admission Control注册的代码如下所示。
package deny
func init() {
admission.RegisterPlugin("AlwaysDeny", func(client client.Interface, config io.Reader) (admission.Interface, error) {
return NewAlwaysDeny(), nil
})
}
然后实现Admit
方法,该实现拒绝所有的API请求并返回请求出错原因,代码如下所示。
func (alwaysDeny) Admit(a admission.Attributes) (err error) {
return apierrors.NewForbidden(a.GetKind(), "", errors.New("Admission control is denying all modifications"))
}
与AlwaysDeny
类似,AlwaysAdmit
也是admission.Interface
的一个实现,它永远对API请求说yes,一般用在单元测试或者一个开放Kubernetes集群中。该插件向Admission Control注册的代码如下所示。
package admit
func init() {
admission.RegisterPlugin("AlwaysAdmit", func(client client.Interface, config io.Reader) (admission.Interface, error) {
return NewAlwaysAdmit(), nil
})
}
然后实现Admit
方法,该实现不做任何操作,直接返回一个空的error
对象,代码如下所示。
func (alwaysAdmit) Admit(a admission.Attributes) (err error) {
return nil
}
Kubernetes对资源的配额与限制是通过 Admission Control的ResourceDefaults
、LimitRanger
和ResourceQuota
等插件实现的。下面将逐一介绍之。
ResourceDefaults插件
首先来看一个描述pod的资源文件。
{
"kind": "Pod",
...
"desiredState": {
"manifest": {
...
"containers": [{
...
"cpu": 1000,
"memory": 1048576,
}]
}
},
}
请注意下面这两行。
"cpu": 1000,
"memory": 1048576,
以上字段在我们之前定义pod的资源文件中从未出现过,分别代表pod中某个容器需要的CPU计算计算资源(1000)以及内存限制(最多1048576 bytes)。这些预分配值会被传递给Docker,Docker再传递给内核的cgroup模块,其中cpu字段用于设置容器可以获得的时间片,memory字段用于设置容器能够使用的最大内存,一旦容器实际使用的内存超过这个数值,该容器就会被杀死。
这里,需要简单说明下Kubernetes中CPU、内存等资源数值单位与计算方法。
Kubernetes所有的资源类型都是可计算的,并且每一种资源数值都有对应的单位(例如:内存的单位是bytes,网络带宽的单位是bytes/second等)。这些单位都采用相应资源类型的原始基础单位,例如内存的单位是bytes而不是MB。另外,1M和1Mi的含义是不同的,前者代表二进制,即1024*1024,1MB=1024*1024bytes;后者代表十进制,即1000*1000,1MiB=1000*1000bytes。而一些小数值可以直接用十进制或千分制来表示,例如:0.3,用十进制表示就是0.3,而用千分制表示则是300m(300/1000)。
注意:M和m的含义是不同的,前者代表million(百万),后者代表milli(千分之一)。
Kubernetes对CPU资源的计数方式有些特别,现详细说明如下。
对一个多核的机器,其CPU资源可以表示成:ncores*1000,其中ncores代表CPU的核心数,Kubernetes粗略地将每个CPU核心的计算资源记为1000,如果该机器CPU有2个核心,则该机器的总CPU计算资源为2*1000=2000。因此上述pod的 "cpu": 1000
字段可以粗鲁地认为该pod(该pod内只有一个容器)使用一个cpu核心,至于pod具体使用哪个cpu核心无法确定,因为docker目前并不支持将容器锁定到特定处理器上运行。
如果描述pod的资源文件中未出现上述容器对资源的请求值,则需要设置一个默认值。Kubernetes通过Admission Control的ResourceDefaults
插件来完成上述默认值的设置。接下来通过代码来详细解释如何实现这个插件。
首先,向Admission Control注册一个名为ResourceDefaults
的插件,如下代码所示。
package resourcedefaults
func init() {
admission.RegisterPlugin("ResourceDefaults", func(client client.Interface, config io.Reader) (admission.Interface, error) {
return NewResourceDefaults(), nil
})
}
其次,定义默认资源需求量,例如:512MiB内存,cpu计算资源1*1000(可以粗略认为是一个cpu核心)。
const (
defaultMemory string = "512Mi"
defaultCPU string = "1"
)
然后,实现Admit
方法,该实现在对pod进行PUT
和UPDATE
操作时,为pod内的每个容器设置默认Memory和CPU的需求量。
func (resourceDefaults) Admit(a admission.Attributes) (err error) {
// ignore deletes, only process create and update
if a.GetOperation() == "DELETE" {
return nil
}
// we only care about pods
if a.GetKind() != "pods" {
return nil
}
// get the pod, so we can validate each of the containers within have default mem / cpu constraints
obj := a.GetObject()
pod := obj.(*api.Pod)
for index := range pod.Spec.Containers {
if pod.Spec.Containers[index].Memory.Value() == 0 {
pod.Spec.Containers[index].Memory = resource.MustParse(defaultMemory)
}
if pod.Spec.Containers[index].CPU.Value() == 0 {
pod.Spec.Containers[index].CPU = resource.MustParse(defaultCPU)
}
}
return nil
}
以上代码说明了只有当发生对pod(因为pod是Kubernetes管理和调度的最小单位)的创建和更新操作(因为只有创建和更新操作才涉及到对资源的请求)时,而且当描述该pod的资源文件未定义该pod中容器对cpu、memory的需求量时,上述默认值才会被应用到该pod内的每个容器。需要注意的是,ResourceDefaults
插件只负责对pod中所有容器的默认资源需求量进行统一设置,相当于为资源字段(cpu,memory等)填充一个开发人员认为合理的值,但并不保证这个默认值是"放之四海皆合理"的,尤其当Admission Control结合使用下面介绍的LimitRanger
插件时。
LimitRanger插件
LimitRanger
插件的作用是判断客户端API请求中的资源需求是否符合系统管理员预设的上/下限,从而为Admission Control提供决策支持(拒绝或接受该API请求)。为了配合LimitRanger
插件的使用,Kubernetes新引入LimitRange
对象,用于对特定namespace中的Kubernetes对象(pod、container等)设置资源使用的上/下限。
我们先来看一下LimitRange
的数据结构定义。
type LimitRange struct {
TypeMeta `json:",inline"`
Spec LimitRangeSpec `json:"spec,omitempty"`
}
注:Kubernetes所有对象类型的数据结构定义均能在
pkg/api/{apiversion}/types.go
中找到。
其中,TypeMeta
表示LimitRange
对象的元数据,而LimitRangeSpec
的数据结构定义如下所示。
type LimitRangeSpec struct {
Limits []LimitRangeItem `json:"limits"`
}
由上可知,LimitRangeSpec
本质上就是一个包含LimitRangeItem
类型数组的结构体。而LimitRangeItem
数据结构的定义如下所示。
type LimitType string
type ResourceList map[ResourceName]util.IntOrString
type LimitRangeItem struct {
Type LimitType `json:"type,omitempty"`
Max ResourceList `json:"max,omitempty"`
Min ResourceList `json:"min,omitempty"`
}
const (
LimitTypePod LimitType = "Pod"
LimitTypeContainer LimitType = "Container"
)
由上可知,LimitRangeItem
是对具体Kubernetes对象类型(LimitType
)应用其能够使用的资源列表的上限与下限,其中资源列表(ResourceList
)的类型是键为资源名(ResourceName
)值为整型或字符串类型的map。在我们撰写这本书的时候,Kubernetes支持的LimitType
包括Pod和Container。
最后,看一个具体的例子——limit-range.json
,该Kubernetes资源文件描述了LimitRange
对象(见kind
字段)。
{
"id": "mylimit",
"kind": "LimitRange",
"apiVersion": "v1beta1",
"spec": {
"limits": [
{
"type": "Pod",
"max": {
"memory": "1073741824",
"cpu": "2",
},
"min": {
"memory": "1048576",
"cpu": "0.25"
}
},
{
"type": "Container",
"max": {
"memory": "1073741824",
"cpu": "2",
},
"min": {
"memory": "1048576",
"cpu": "0.25"
}
},
],
}
}
与定义Kubernetes其他对象(pod等)的资源文件一样,该json文件的kind字段表明该资源文件定义的是LimitRange
对象。spec:limits:type
字段指定应用限制的对象,这里分别是pod和container。spec:limits:max
和spec:limits:min
字段均为非负整数,分别表示资源列表上限与下限,该文件定义的资源列表包含两个资源类型:memory和cpu。该文件表明,应用到namespace中每个pod的资源限制均为:最大内存 1073741824 bytes,最小内存 1048576 bytes,最大cpu计算资源 2*1000(约为2个CPU核心),最小cpu计算资源 0.25*1000(约为0.25个CPU核心);应用到namespace中每个容器的资源限制与pod的相同。
根据以上资源文件创建一个名为mylimit
的LimitRange
对象。
#限定执行资源限制操作的namespace为myspace
$ kubectl namespace myspace
#根据`limit-range.json`资源文件创建`limitrange`对象
$ kubectl create -f limit-range.json
#获取系统的`limitrange`对象列表
$ kubectl get limits
NAME
mylimit
#kubectl describe limits命令输出较详细的limitrange对象信息
$ kubectl describe limits mylimit
Name: limits
Type Resource Min Max
---- -------- --- ---
Pod memory 1Mi 1Gi
Pod cpu 250m 2
Container cpu 250m 2
Container memory 1Mi 1Gi
注意:在我们撰写这本书的时候,
LimitRange
对象不支持针对某个特定pod或容器的资源限制,只支持对namespace中所有的pod或容器做一个统一的限制。
介绍完LimitRange
对象后,我们通过几组测试用例来演示一下LimitRanger
插件的工作过程。
首先创建一个LimitRange
实例,该实例定义了对pod和container的资源限制,如下所示。
Pod | CPU | Memory |
---|---|---|
max | 200m | 4Gi |
min | 50m | 2Mi |
表x Pod的资源限制值
Container | CPU | Memory |
---|---|---|
max | 100m | 2Gi |
min | 25m | 1Mi |
表x Container的资源限制值
然后,定义几个pod资源请求列表,如下所示。
Container | CPU | Memory |
---|---|---|
c1 | 100m | 2Gi |
c2 | 100m | 2Gi |
sum | 200m | 4Gi |
表x Pod-1各容器的资源需求量
Pod-1的资源请求是合法的,检验的一般过程如下所示。
(1) 将Pod内所有容器的CPU,Memory需求量逐一与Container表的{min,max}
值进行比较,看是否落在[min,max]
区间(包括边界值)内。只要有一个容器不符合条件,则检验过程结束,拒绝该API请求并返回请求者错误信息。
(2) 计算pod内所有容器的CPU,Memory需求量的总和sum作为该pod的资源需求量,并与Pod表的{min,max}
值进行比较,看是否落在[min,max]
区间(包括边界值)内。如果不符合要求,则拒绝该API请求并返回请求者错误信息;反之通过该API的资源请求。
Container | CPU | Memory |
---|---|---|
c1 | 60m | 1Mi |
c2 | 60m | 1Mi |
c1 | 60m | 1Mi |
c2 | 60m | 1Mi |
sum | 240m | 4Mi |
表x Pod-2各容器的资源需求量
Pod-2的资源请求是非法的,原因是该Pod的CPU资源需求量(240m)超过了LimitRange
Pod的CPU上限值(200m)。
通过以上分析可知,pod的资源请求量(Requirement)是个静态而非运行时的概念,LimitRanger
插件只是将LimitRange
对象的{min,max}
值与Requirement做比较,看Requirement是否落在[min,max]
区间上,然后选择接受或拒绝该API请求。
可能有读者会感到疑惑,LimitRange
的max
字段值的作用好理解,即出于集群资源的安全性考虑,防止某个容器或Pod请求过多的集群资源,那LimitRange
的min
字段值的作用又是什么呢?主要原因有以下几点。
通过对上述实例的分析,我们知道LimitRange
对象对API请求资源的限制是无状态的,每次都只针对单个API请求,没有累加的概念。Admission Control也提供有状态的,累加的资源请求限制,见下文介绍的ResourceQuota
插件。接下来我们将从代码的角度,简要分析下LimitRanger
插件的实现原理。
首先,向Admission Control注册一个名为LimitRanger
的插件,如下代码所示。
package limitranger
func init() {
admission.RegisterPlugin("LimitRanger", func(client client.Interface, config io.Reader) (admission.Interface, error) {
return NewLimitRanger(client, PodLimitFunc), nil
})
}
实际负责代码逻辑部分的是PodLimitFunc
函数,对该函数的详细解释见下文。
然后,实现Admit
方法,该方法对API请求的资源需求量(Requirement
)与LimitRange
对象设定的资源上/下限值({min,max}
)进行比较。如果Requirement
不在{min,max}
范围内,则返回错误信息。
func (l *limitRanger) Admit(a admission.Attributes) (err error) {
// ignore deletes
if a.GetOperation() == "DELETE" {
return nil
}
// look for a limit range in current namespace that requires enforcement
items, err := l.client.LimitRanges(a.GetNamespace()).List(labels.Everything())
if err != nil {
return err
}
// ensure it meets each prescribed min/max
for i := range items.Items {
limitRange := &items.Items[i]
err = l.limitFunc(limitRange, a.GetKind(), a.GetObject())
if err != nil {
return err
}
}
return nil
}
与resourcedefaults
插件类似,LimitRanger
插件只适用于Kubernetes对象创建和更新操作,并不受理删除操作。需要注意的是,一个namespace中可能存在不止一个LimitRange
对象,因此,任何一个针对Kubernetes对象的创建和更新操作都要接受该namespace中所有LimitRange
对象的限制。另外,limitFunc
函数负责逻辑处理部分,该函数声明如下所示。
type LimitFunc func(limitRange *api.LimitRange, kind string, obj runtime.Object) error
由上可知,limitFunc
函数其实是个接口函数,它接受三个参数,分别是:LimitRange
对象、Kubetrnetes对象类型和实际运行时的Kubetrnetes对象。上文提到的实际逻辑处理函数PodLimitFunc
则是limitFunc
函数接口的一个实现。具体的实现代码有兴趣的读者可以参见plugin/pkg/admission/limitranger/admission.go#PodLimitFunc
,这里并不赘述。
ResourceQuota插件
ResourceQuota插件的作用是为特定namespace应用资源使用的配额。与LimitRanger
插件类似,为了配合ResourceQuota
插件的使用,Kubernetes新引入了ResourceQuota
对象,用于对特定namespace中的Kubernetes对象设置资源配额。此外,还引入ResourceQuotaUsage
对象用于支持ResourceQuota
对象状态的自动更新。
这里,我们必须先梳理一下Kubernetes资源(Resource)的概念。直观地,cpu、memory等通用计算机资源也属于Kubernetes资源类型,在pkg/api/{apiversion}/types.go
文件中可以找到以下定义。
type ResourceName string
const (
ResourceCPU ResourceName = "cpu"
ResourceMemory ResourceName = "memory"
)
而Kubernetes系统定义的对象类型,譬如:Pod,Service,Replication Controller,ResourceQuota等,也属于Kubernetes资源类型,在pkg/api/{apiversion}/types.go
文件中的定义如下所示。
const (
ResourcePods ResourceName = "pods"
ResourceServices ResourceName = "services"
ResourceReplicationControllers ResourceName = "replicationcontrollers"
ResourceQuotas ResourceName = "resourcequotas"
)
每种Kubernetes资源对象都对应一组元数据(metadata),包括:Name
、Namespace
和ResourceVersion
,分别表示该资源对象的名字,所在Namespace和资源版本号,其中资源版本号用于标识该资源的新鲜程度,方便进行版本控制。
ResourceQuota
的数据结构定义如下所示。
type ResourceQuota struct {
TypeMeta `json:",inline"`
Spec ResourceQuotaSpec `json:"spec,omitempty"`
Status ResourceQuotaStatus `json:"status,omitempty"`
}
其中,TypeMeta
表示ResourceQuota
对象的元数据。ResourceQuota
包含两个数据结构:ResourceQuotaSpec
和ResourceQuotaStatus
,分别表示预设的资源配额和资源配额的实际状态。
ResourceQuotaSpec
数据结构定义如下所示。
type ResourceQuotaSpec struct {
Hard ResourceList `json:"hard,omitempty"`
}
ResourceList
的数据结构与上文提到的完全一样,是一个key为资源名,value为int或string值的map类型。于是,Hard
对象表示特定namespace预设的资源配额列表。
ResourceQuotaStatus
数据结构定义如下所示。
type ResourceQuotaStatus struct {
Hard ResourceList `json:"hard,omitempty"`
Used ResourceList `json:"used,omitempty"`
}
ResourceQuotaStatus
包含两个ResourceList
类型的数据对象:Hard
和Used
,其中Hard
的含义与ResourceQuotaSpec.Hard
的含义一样,Used
则表示特定namespace观测到的实际资源使用总量。
而ResourceQuotaUsage
的数据结构如下所示。
type ResourceQuotaUsage struct {
TypeMeta `json:",inline"`
Status ResourceQuotaStatus `json:"status,omitempty"`
}
ResourceQuotaUsage
的数据部分即ResourceQuotaStatus
,记录特定namespace的资源配额及资源使用情况。这里有必要说明下ResourceQuota
和ResourceQuotaUsage
之间的关系,即:
ResourceQuotaUsage.Status = ResourceQuota.Status
ResourceQuotaStatus
用于实时更新存储在etcd中的ResourceQuota.Status
字段的数据。
结束了对ResourceQuota
和ResourceQuotaUsage
对象的讨论后,看一个具体的例子——resource-quota.json
,该资源文件描述了一个ResourceQuota
对象(见kind
字段)。
{
"id": "quota",
"kind": "ResourceQuota",
"apiVersion": "v1beta1",
"spec": {
"hard": {
"memory": "1073741824",
"cpu": "20",
"pods": "10",
"services": "5",
"replicationcontrollers":"20",
"resourcequotas":"1",
},
}
}
如上所示,spec:hard
即资源配额列表,其各字段的含义和单位如下所示。
资源列表字段 | 含义 | 单位 |
---|---|---|
cpu | cpu配额 | 核心数 |
memory | 内存配额 | 字节数 |
pods | pod配额 | 个数 |
services | service配额 | 个数 |
replicationcontrollers | replication controller配额 | 个数 |
resourcequotas | resourcequota配额 | 个数 |
表x ResourceQuota资源列表各字段含义和单位
需要特别说明的是resourcequotas
字段的含义。我们知道,一个namespace可以存在多个ResourceQuota
对象,就像可以存在多个Pod
对象那样。如果一个namespace存在多个ResourceQuota
对象,则意味着任意一个API请求都要依次受到这些ResourceQuota
对象的限制。出于系统性能的考虑,我们建议一个namespace最多创建一个ResourceQuota
对象,而实现这一限制条件最简单的方法便是设置第一个ResourceQuota
对象的spec:hard:resourcequotas = 1
,这样就阻止了在同一个namespace中继续创建ResourceQuota
对象。
根据以上资源文件创建一个名为myquota
的ResourceQuotas
对象。
$ kubectl namespace myspace
$ kubectl create -f resource-quota.json
$ kubectl get quota
NAME
myquota
$ kubectl describe quota myquota
Name: myquota
Resource Used Hard
-------- ---- ----
cpu 100m 20
memory 0 1.5Gb
pods 1 10
replicationControllers 1 10
services 2 3
kubectl describe quota
命令输出ResourceQuotas
对象的状态信息即ResourceQuotaStatus
的数据,输出分为三列,分别是:资源名、已使用量和配额。ResourceQuota
插件的职责就是保证Admission Control接受API请求之后资源使用量仍不超过预设的配额。
ResourceQuota
插件检验API请求的合法性与API操作类型密切相关,其一般过程如下所示。
1,如果API请求的操作类型是DELETE
(删除),直接允许该API请求,因为DELETE
操作只会释放资源而不会消耗资源。
2,如果API请求的操作类型不是DELETE
,则根据API请求的资源需求量,更新namespace的资源使用量(ResourceQuotaUsage.Used
)。更新的方法与API请求的操作类型密切相关。
CREATE
(创建),则将请求的资源需求量与当前ResourceQuotaUsage.Used
中对应的资源使用量求和,并将ResourceQuotaUsage.Used
中对应的API操作对象数量+1。UPDATE
(更新),则将请求的资源更新值(请求值减去原先值)与当前ResourceQuotaUsage.Used
中对应的资源的使用量求和。3,一旦ResourceQuotaUsage.Used
的某个资源使用量超过ResourceQuotaUsage.Hard
中对应资源的配额值,Admission Control则驳回该API请求,反之则在Admission Control接受该API请求之前,在etcd中持久化ResourceQuotaUsage
实例。
接下来,我们通过几组测试用例来演示一下ResourceQuota
插件的工作过程。首先创建一个ResourceQuota
实例,预设特定namespace的资源配额,如下所示。
Resource | cpu | memory | pods | ReplicationControllers | services |
---|---|---|---|---|---|
Hard | 200m | 4Gi | 2 | 2 | 1 |
表x ResourceQuota
实例预设的资源配额
而表示ResourceQuota
状态的ResourceQuotaUsage
实例的初始状态如下所示。
Resource | cpu | memory | pods | ReplicationControllers | services |
---|---|---|---|---|---|
Hard | 200m | 4Gi | 2 | 2 | 0 |
Used | 0m | 0Gi | 0 | 0 | 0 |
表x ResourceQuotaUsage
实例的初始状态
然后,定义几个API请求,请求顺序依次如下。
假设Request-1是系统的第1个API请求,它执行的操作是创建一个名为Pod1
的pod,该pod内有两个容器,分别是c1和c2,其资源需求量如下表所示。
Container | CPU | Memory |
---|---|---|
c1 | 50m | 1Gi |
c2 | 50m | 1Gi |
sum | 100m | 2Gi |
表x Request-1对Pod1资源需求量
如果接受Request-1
,则ResourceQuotaUsage
实例的状态信息将变成这样。
Resource | cpu | memory | pods | ReplicationControllers | services |
---|---|---|---|---|---|
Hard | 200m | 4Gi | 2 | 2 | 0 |
Used | 100m | 2Gi | 1 | 0 | 0 |
表x ResourceQuotaUsage
实例的状态信息
Used
各字段的值未超过Hard
各字段的值,故Request-1的资源请求是合法的。在Admission Control接受该API请求之前,持久化ResourceQuotaUsage
实例。
Request-2是系统的第2个API请求,它执行的操作是更新Pod1
,其资源需求量如下表所示。
Container | CPU | Memory |
---|---|---|
c1 | 50m | 1Gi |
c2 | 150m | 1Gi |
sum | 200m | 2Gi |
表x Request-2对Pod1资源需求量
如果接受Request-2
,则ResourceQuotaUsage
实例的状态信息将变成这样。
Resource | cpu | memory | pods | ReplicationControllers | services |
---|---|---|---|---|---|
Hard | 200m | 4Gi | 2 | 2 | 0 |
Used | 200m | 2Gi | 1 | 0 | 0 |
表x ResourceQuotaUsage
实例的状态信息
Used
各字段的值仍未超过Hard
各字段的值,故Request-2的资源请求是合法的。同样,在Admission Control接受该API请求之前,持久化ResourceQuotaUsage
实例。
Request-3是系统的第3个API请求,它执行的操作创建一个service。
如果接受Request-3
,则ResourceQuotaUsage
实例的状态信息将变成这样。
Resource | cpu | memory | pods | ReplicationControllers | services |
---|---|---|---|---|---|
Hard | 200m | 4Gi | 2 | 2 | 0 |
Used | 200m | 2Gi | 1 | 0 | 1 |
因为Used.services
> Hard.services
值,故Request-3的资源请求是非法的,Admission Control驳回该API请求。
通过对上述实例的分析,我们知道ResourceQuota
插件对API请求的资源限制是有状态的,不同的API请求会更新namespace中资源的总使用量。接下来我们将从代码的角度,简要分析下ResourceQuota
插件的实现原理。
首先,向Admission Control注册一个名为ResourceQuota
的插件,如下代码所示。
func init() {
admission.RegisterPlugin("ResourceQuota", func(client client.Interface, config io.Reader) (admission.Interface, error) {
return NewResourceQuota(client), nil
})
}
然后,实现Admit
方法。
func (q *quota) Admit(a admission.Attributes) (err error) {
//`DELETE`操作无条件通过
if a.GetOperation() == "DELETE" {
return nil
}
...
//获取namespace中的`ResourceQuotas`对象列表
list, err := q.client.ResourceQuotas(a.GetNamespace()).List(labels.Everything())
...
//对API请求,依次应用`ResourceQuotas`对象列表的资源限制
for i := range list.Items {
quota := list.Items[i]
//根据API请求的类型更新namespace的资源使用量
dirty, err := IncrementUsage(a, "a.Status, q.client)
//如果资源使用量超过资源配额,则拒绝该API请求并返回错误信息
if err != nil {
return err
}
//dirty变量表示资源使用量是否被更新过,true表示被更新过
if dirty {
//资源使用量被更新过后需要重新构建一个新的ResourceQuotaUsage对象
usage := api.ResourceQuotaUsage{
ObjectMeta: api.ObjectMeta{
Name: quota.Name,
Namespace: quota.Namespace,
ResourceVersion: quota.ResourceVersion},
}
usage.Status = quota.Status
//Create()函数就是调用kubernetes API(POST方法),在etcd中对ResourceQuotaUsage对象进行持久化
err = q.client.ResourceQuotaUsages(usage.Namespace).Create(&usage)
if err != nil {
return apierrors.NewForbidden(a.GetResource(), name, fmt.Errorf("Unable to %s %s at this time because there was an error enforcing quota", a.GetOperation(), a.GetResource()))
}
}
}
return nil
}
Admission Control与资源配额相关的主要代码逻辑如上所示。但以上代码并没有回答下面这几个问题。
DELETE
操作后对namespace中资源使用量的更新在何处进行持久化?答案是ResourceQuotaManager
负责以上工作,它由kube-controllermanager
运行,用于跟踪并同步系统的资源配额及使用量,默认的同步频率为10秒钟一次,如果需要自定义同步频率,只要在kube-controllermanager
启动时传入resource_quota_sync_period
参数即可。ResourceQuotaManager
的具体代码逻辑见package resourcequota
的syncResourceQuota
函数,它通过调用kubeClient
来获取ResourceQuotaUsage
实例定义的各资源对象的当前使用量并与上一轮的同步结果进行比较,如果发现有更新,则同步到etcd。