背景
Kubernetes从1.8开始引入了Device Plugin机制,用于第三方设备厂商以插件化的方式将设备资源(GPU、RDMA、FPGA、InfiniBand等)接入Kubernetes集群中。用户无需修改Kubernetes代码,只需在集群中以DaemonSet方式部署设备厂商提供的插件,然后在Pod中申明使用该资源的使用量,容器在启动成功后,便可在容器中发现该设备。
然而,随着Kubernetes的应用越来越广泛,已有的Device Plugin机制已不能完全覆盖一些场景。下面是一些例子:
-
设备初始化:当启动一些使用FPGA的应用时,在启动应用之前,需要重新配置FPGA或者重新编程FPGA。
-
设备清理:当应用运行完成时,需要清理这个应用在设备上的配置,数据等信息。例如,FPGA需要重置。目前的Device plugin机制并没有包含清理操作的接口。
-
部分分配:某些场景下,允许一个pod中某个容器使用某个设备的部分资源,并且该pod的其他容器可以使用这个设备的剩余资源。例如:新一代GPU支持MIG模式,允许将一张GPU卡划分为更小的GPU(称为GPU实例,MIG)。在一张GPU卡上划分MIG设备是一个动态过程,当有某个pod使用MIG时,应该先为其划分出一个MIG实例,当该pod运行完成后,需要销毁这个MIG实例。
-
可选分配:部署工作负载时,用户希望指定软(可选)设备要求,如果设备存在并且可分配,那么执行分配操作。如果设备不存在,那么将回退到没有设备的节点上运行。GPU和加密引擎就是此类例子。如果一个节点有GPU设备,那么使用GPU设备运行;如果GPU没有,那么回退到使用CPU来运行同一任务。
-
带有拓扑性质的设备分配:某些设备分配时,需要考虑拓扑性质,比如RDMA和GPU。
针对Device Plugin机制的不足,K8s社区提出Dynamic Resource Allocation机制。
DRA介绍
Kubernetes从v1.26开始引入DRA机制,DRA(Dynamic Resource Allocation,动态资源分配)是对现有Device Plugin机制的补充,并不是要替代Device Plugin机制,相比于Device Plugin,它有如下优点:
-
更加的灵活:
-
Pod申请资源时,支持填写任意参数,目前device plugin机制中申请资源只能通过在resource.limits填写资源请求量,其他参数不支持。
-
自定义资源的设置和清理操作。
-
用于描述资源请求的友好的API。
-
运行管理资源的组件自主开发和部署,而无需编译Kubernetes核心组件。
-
足够丰富的语义,使得当前所有的设备插件都可以基于DRA来实现。
当然,讲了优点也得提一提缺点,相比于Device Plugin机制,DRA有如下的缺点:
-
DRA调度效率要比Device Plugin低得多,后面将详细介绍这一块。
-
对于用户而言,DRA资源申请的方式比Device Plugin复杂得多。
DRA组件
如果某个设备厂商需要利用DRA机制将其设备接入Kubernetes集群中使用,那么如下两个组件必须开发完成:
-
Resource Controller:一个中心化的组件,通过监听ResourceClaims并在分配完成后更新ResourceClaim状态来处理资源的分配,简单理解就是维护整个Kubernetes集群中设备资源的账本以及进行资源分配。
-
Resource Kubelet Plugin:与Kubelet配合使用,与Device Plugin插件类似,每个节点部署一个,为Pod准备节点资源的工作,例如:挂载宿主机设备到容器中,设置某些环境变量。
而把这两个组件合起来统称为DRA Resource Driver。
关键概念
Resource Class
资源类,类似于存储中的StorageClass,某种硬件对应一个ResourceClass,ResourceClass主要职能是用来影响资源的分配方式。其定义如下:
// +genclient
// +genclient:nonNamespaced
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// +k8s:prerelease-lifecycle-gen:introduced=1.26
// ResourceClass is used by administrators to influence how resources
// are allocated.
//
// This is an alpha type and requires enabling the DynamicResourceAllocation
// feature gate.
type ResourceClass struct {
metav1.TypeMeta `json:",inline"`
// Standard object metadata
// +optional
metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
// DriverName defines the name of the dynamic resource driver that is
// used for allocation of a ResourceClaim that uses this class.
//
// Resource drivers have a unique name in forward domain order
// (acme.example.com).
DriverName string `json:"driverName" protobuf:"bytes,2,name=driverName"`
// ParametersRef references an arbitrary separate object that may hold
// parameters that will be used by the driver when allocating a
// resource that uses this class. A dynamic resource driver can
// distinguish between parameters stored here and and those stored in
// ResourceClaimSpec.
// +optional
ParametersRef *ResourceClassParametersReference `json:"parametersRef,omitempty" protobuf:"bytes,3,opt,name=parametersRef"`
// Only nodes matching the selector will be considered by the scheduler
// when trying to find a Node that fits a Pod when that Pod uses
// a ResourceClaim that has not been allocated yet.
//
// Setting this field is optional. If null, all nodes are candidates.
// +optional
SuitableNodes *v1.NodeSelector `json:"suitableNodes,omitempty" protobuf:"bytes,4,opt,name=suitableNodes"`
}
// ResourceClassParametersReference contains enough information to let you
// locate the parameters for a ResourceClass.
type ResourceClassParametersReference struct {
// APIGroup is the group for the resource being referenced. It is
// empty for the core API. This matches the group in the APIVersion
// that is used when creating the resources.
// +optional
APIGroup string `json:"apiGroup,omitempty" protobuf:"bytes,1,opt,name=apiGroup"`
// Kind is the type of resource being referenced. This is the same
// value as in the parameter object's metadata.
Kind string `json:"kind" protobuf:"bytes,2,name=kind"`
// Name is the name of resource being referenced.
Name string `json:"name" protobuf:"bytes,3,name=name"`
// Namespace that contains the referenced resource. Must be empty
// for cluster-scoped resources and non-empty for namespaced
// resources.
// +optional
Namespace string `json:"namespace,omitempty" protobuf:"bytes,4,opt,name=namespace"`
}
对于这个定义,有几个地方需要说明:
-
每种Resource Class都有一个Resource Driver,每个Resource Driver都有一个唯一的DriverName,所以每种Resource Class都有唯一的DriverName与之对应。
-
Resource Class的SuitableNodes是一个NodeSelector,其作用是方便调度器在调度Pod为该Pod选择节点时,如果Resource Class存在NodeSelector,那么调度器可以根据NodeSelector过滤一部分节点。例如:某个Resource Class的SuitableNodes记录的是只有带有标签gpu.nvidia.com/name的节点才有GPU资源。
-
Resource Class本身不支持用户自定义某些参数,因为Resource Class这几个字段(DriverName、ParametersRef、SuitableNodes)都是明确定义的。
-
虽然Resource Class本身不支持设备厂商自定义某些参数(比如GPU设备中的GPU卡型、显存总大小等),但是它有ParametersRef这个字段,这个字段中记录了一个CR( Custom Objects,与之配套的是CRD)信息,设备厂商自定义参数的就存放在这个CR中。Resource Class通过这个ParametersRef字段告诉使用者:我虽然不知道设备厂商自定义参数是什么,但是你可以通过ParametersRef记录的信息去获取设备厂商自定义参数。
最后,给一个KEP中关于Resource Class示例:
apiVersion: gpu.example.com/v1
kind: GPUInit
metadata:
name: acme-gpu-init
# DANGER! This option must not be accepted for
# user-supplied parameters. A real driver might
# not even allow it for admins. This is just
# an example to show the conceptual difference
# between ResourceClass and ResourceClaim
# parameters.
initCommand:
- /usr/local/bin/acme-gpu-init
- --cluster
- my-cluster
---
apiVersion: core.k8s.io/v1alpha2
kind: ResourceClass
metadata:
name: acme-gpu
driverName: gpu.example.com
parametersRef:
apiGroup: gpu.example.com
kind: GPUInit
name: acme-gpu-init
Resource Claim
资源申请,类似于存储中的PersistentVolumeClaim,其定义如下:
// +genclient
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// +k8s:prerelease-lifecycle-gen:introduced=1.26
// ResourceClaim describes which resources are needed by a resource consumer.
// Its status tracks whether the resource has been allocated and what the
// resulting attributes are.
//
// This is an alpha type and requires enabling the DynamicResourceAllocation
// feature gate.
type ResourceClaim struct {
metav1.TypeMeta `json:",inline"`
// Standard object metadata
// +optional
metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
// Spec describes the desired attributes of a resource that then needs
// to be allocated. It can only be set once when creating the
// ResourceClaim.
Spec ResourceClaimSpec `json:"spec" protobuf:"bytes,2,name=spec"`
// Status describes whether the resource is available and with which
// attributes.
// +optional
Status ResourceClaimStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"`
}
ResourceClaim主要也分为ResourceClaimSpec和ResourceClaimStatus两部分。而ResourceClaimSpec定义如下:
// ResourceClaimSpec defines how a resource is to be allocated.
type ResourceClaimSpec struct {
// ResourceClassName references the driver and additional parameters
// via the name of a ResourceClass that was created as part of the
// driver deployment.
ResourceClassName string `json:"resourceClassName" protobuf:"bytes,1,name=resourceClassName"`
// ParametersRef references a separate object with arbitrary parameters
// that will be used by the driver when allocating a resource for the
// claim.
//
// The object must be in the same namespace as the ResourceClaim.
// +optional
ParametersRef *ResourceClaimParametersReference `json:"parametersRef,omitempty" protobuf:"bytes,2,opt,name=parametersRef"`
// Allocation can start immediately or when a Pod wants to use the
// resource. "WaitForFirstConsumer" is the default.
// +optional
AllocationMode AllocationMode `json:"allocationMode,omitempty" protobuf:"bytes,3,opt,name=allocationMode"`
}
对于ResourceClaimSpec说明如下:
-
ResourceClassName记录Resource Class名称,表示向哪一个资源类申请资源。
-
ParametersRef与Resource Class中的ParametersRef功能类似,表示申请资源时,提出了一些参数请求。比如:在GPU调度中,如果要申请2张卡,那么卡数就是一个参数。
-
AllocationMode定义了该Resource Claim使用哪种分配模式,关于分配模式,后面将会介绍。
ResourceClaimStatus记录ResourceClaim相关状态信息,一般由Resource Controller和调度器维护,结构如下:
// ResourceClaimStatus tracks whether the resource has been allocated and what
// the resulting attributes are.
type ResourceClaimStatus struct {
// DriverName is a copy of the driver name from the ResourceClass at
// the time when allocation started.
// +optional
DriverName string `json:"driverName,omitempty" protobuf:"bytes,1,opt,name=driverName"`
// Allocation is set by the resource driver once a resource or set of
// resources has been allocated successfully. If this is not specified, the
// resources have not been allocated yet.
// +optional
Allocation *AllocationResult `json:"allocation,omitempty" protobuf:"bytes,2,opt,name=allocation"`
// ReservedFor indicates which entities are currently allowed to use
// the claim. A Pod which references a ResourceClaim which is not
// reserved for that Pod will not be started.
//
// There can be at most 32 such reservations. This may get increased in
// the future, but not reduced.
//
// +listType=map
// +listMapKey=uid
// +optional
ReservedFor []ResourceClaimConsumerReference `json:"reservedFor,omitempty" protobuf:"bytes,3,opt,name=reservedFor"`
// DeallocationRequested indicates that a ResourceClaim is to be
// deallocated.
//
// The driver then must deallocate this claim and reset the field
// together with clearing the Allocation field.
//
// While DeallocationRequested is set, no new consumers may be added to
// ReservedFor.
// +optional
DeallocationRequested bool `json:"deallocationRequested,omitempty" protobuf:"varint,4,opt,name=deallocationRequested"`
}
ResourceClaimStatus关键字段解释如下:
-
Allocation:由Resource Controller维护,Resource Controller将资源分配信息记录到该字段内,含义为该Resource Claim申请的资源由Allocation中记录的节点提供。
-
ReservedFor:一般由调度器维护,表示该Resource Claim的Owner是哪个对象,一般情况下为pod。含义为该Resource Claim属于某一个或某几个Pod。
-
DeallocationRequested:提出解除分配的请求,由调度器和Resource Controller维护。申请Resource Claim的Pod在调度过程中,可能需要尝试多次调度才能成功,某些情况下,为Resource Claim分配的节点已经无法运行正在被调度的pod,此时需要解除分配,重新选择节点,调度器会将该字段值设置为true,Resource Controller监听到以后会重新选择节点分配给Resource Claim。
下面是一个分配完成的Resource Claim例子:
apiVersion: resource.k8s.io/v1alpha2
kind: ResourceClaim
metadata:
creationTimestamp: "2023-06-27T08:26:29Z"
finalizers:
- gpu.resource.nvidia.com/deletion-protection
name: pod1-gpu
namespace: gpu-test1
ownerReferences:
- apiVersion: v1
blockOwnerDeletion: true
controller: true
kind: Pod
name: pod1
uid: 92c3bd8a-7fa1-4c43-b93f-f9efd54f6e80
resourceVersion: "483231"
uid: ad51e716-0d79-42f9-bac6-2ede24ba41a8
spec:
allocationMode: WaitForFirstConsumer
resourceClassName: gpu.nvidia.com
status:
allocation:
availableOnNodes:
nodeSelectorTerms:
- matchFields:
- key: metadata.name
operator: In
values:
- izj6ce3r1zhvbsrk3ww4cpz
shareable: true
driverName: gpu.resource.nvidia.com
reservedFor:
- name: pod1
resource: pods
uid: 92c3bd8a-7fa1-4c43-b93f-f9efd54f6e80
Allocation Mode
Resource Claim中的分配模式。对于一个Resource Claim,目前有两种分配模式可供选择:
-
立即分配(Immediate):Resource Claim创建时,Resource Controller立即寻找资源分配给该Claim,该模式适用于分配资源成本高昂(例如,对 FPGA 进行编程)并且该资源将由多个不同的Pod使用时。缺点是在选择分配到哪个节点时无法考虑Pod资源需求。例如,假设创建Resource Claim 1时,立即选择节点Node1分配资源,但是申请使用Resource Claim1的pod在被调度时,发现Node1无法运行该Pod(比如缺少CPU、Memory等资源)。
-
延迟分配(WaitForFirstConsumer,默认):创建Resource Claim时不分配资源,当Pod调度时才分配资源,此种模式将Pod调度与Resource Claim资源分配结合,避免立即分配存在的问题。
PodSchedulingContext
在DRA机制中,调度器与Resource Controller不直接通信,而是通过一个名称为PodSchedulingContext的Kubernetes资源对象交换信息。
PodSchedulingContext定义如下:
// PodSchedulingContext objects hold information that is needed to schedule
// a Pod with ResourceClaims that use "WaitForFirstConsumer" allocation
// mode.
//
// This is an alpha type and requires enabling the DynamicResourceAllocation
// feature gate.
type PodSchedulingContext struct {
metav1.TypeMeta `json:",inline"`
// Standard object metadata
// +optional
metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
// Spec describes where resources for the Pod are needed.
Spec PodSchedulingContextSpec `json:"spec" protobuf:"bytes,2,name=spec"`
// Status describes where resources for the Pod can be allocated.
// +optional
Status PodSchedulingContextStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"`
}
调度器和Resource Controller交互的数据存放在Spec和Status两个字段中。
Spec字段是一个PodSchedulingContextSpec对象,主要是由调度器维护。其定义如下:
// PodSchedulingContextSpec describes where resources for the Pod are needed.
type PodSchedulingContextSpec struct {
// SelectedNode is the node for which allocation of ResourceClaims that
// are referenced by the Pod and that use "WaitForFirstConsumer"
// allocation is to be attempted.
// +optional
SelectedNode string `json:"selectedNode,omitempty" protobuf:"bytes,1,opt,name=selectedNode"`
// PotentialNodes lists nodes where the Pod might be able to run.
//
// The size of this field is limited to 128. This is large enough for
// many clusters. Larger clusters may need more attempts to find a node
// that suits all pending resources. This may get increased in the
// future, but not reduced.
//
// +listType=set
// +optional
PotentialNodes []string `json:"potentialNodes,omitempty" protobuf:"bytes,2,opt,name=potentialNodes"`
}
相关字段说明如下:
-
SelectedNode:在调度器的dynamicresource插件在Reserve阶段会将所有调度插件挑选出的最优节点写入该字段,用于指示Resource Controller将该节点上的资源分配给Resource Claim。
-
PotentialNodes:在调度器的dynamicresource插件在Reserve阶段会将其PreScore阶段获得的节点列表写入该字段,用于指示Resource Controller从这一组节点中,给出哪些节点不适合运行当前调度的pod,后面将会详细介绍。
Status字段是一个PodSchedulingContextStatus对象,主要由Resource Controller维护,其定义如下:
// PodSchedulingContextStatus describes where resources for the Pod can be allocated.
type PodSchedulingContextStatus struct {
// ResourceClaims describes resource availability for each
// pod.spec.resourceClaim entry where the corresponding ResourceClaim
// uses "WaitForFirstConsumer" allocation mode.
//
// +listType=map
// +listMapKey=name
// +optional
ResourceClaims []ResourceClaimSchedulingStatus `json:"resourceClaims,omitempty" protobuf:"bytes,1,opt,name=resourceClaims"`
// If there ever is a need to support other kinds of resources
// than ResourceClaim, then new fields could get added here
// for those other resources.
}
// ResourceClaimSchedulingStatus contains information about one particular
// ResourceClaim with "WaitForFirstConsumer" allocation mode.
type ResourceClaimSchedulingStatus struct {
// Name matches the pod.spec.resourceClaims[*].Name field.
// +optional
Name string `json:"name,omitempty" protobuf:"bytes,1,opt,name=name"`
// UnsuitableNodes lists nodes that the ResourceClaim cannot be
// allocated for.
//
// The size of this field is limited to 128, the same as for
// PodSchedulingSpec.PotentialNodes. This may get increased in the
// future, but not reduced.
//
// +listType=set
// +optional
UnsuitableNodes []string `json:"unsuitableNodes,omitempty" protobuf:"bytes,2,opt,name=unsuitableNodes"`
}
相关字段说明如下:
-
UnsuitableNodes:Resource Controller根据PodSchedulingContext.Spec.PotentialNodes节点列表,过滤出不适合运行正在调度的pod的节点,并更新到UnsuitableNodes字段中,如果该pod调度失败,下次调度该pod时,在调度器的dynamicresource插件的Filter扩展点会根据Resource Controller这个提升过滤掉一些节点。
使用DRA
在KEP中有一个Pod申请Resource Claim的例子,首先创建一个类型为GPURequirements的CR,该CR中定义了申请GPU资源的参数,该参数比较简单,只是包含使用GPU内存的数量:
apiVersion: gpu.example.com/v1
kind: GPURequirements
metadata:
name: device-consumer-gpu-parameters
memory: "2Gi" # 申请GPU资源的参数,这里为GPU内存大小
然后创建一个类型为ResourceClaimTemplate的资源对象,这个Resource Claim的参数由上面定义的CR提供。
apiVersion: resource.k8s.io/v1alpha2
kind: ResourceClaimTemplate
metadata:
name: device-consumer-gpu-template
spec:
metadata:
# Additional annotations or labels for the
# ResourceClaim could be specified here.
spec:
resourceClassName: "acme-gpu" # 声明使用的资源类
parametersRef: # 声明该resource claim的参数
apiGroup: gpu.example.com
kind: GPURequirements
name: device-consumer-gpu-parameters # 与前面定义的CR名称一致
此时这个Resource Claim所代表的含义比较清晰:“需要2G显存GPU资源”。
最后,在Pod.Spec中声明需要使用该ResourceClaimTemplate,最终pod完成了申请显存大小为2Gi的GPU资源声明。
apiVersion: v1
kind: Pod
metadata:
name: device-consumer
spec:
resourceClaims: # 定义resource claim来源于模板device-consumer-gpu-template
- name: "gpu"
template:
resourceClaimTemplateName: device-consumer-gpu-template
containers:
- name: workload
image: my-app
command: ["/bin/program"]
resources:
requests:
memory: "64Mi"
cpu: "250m"
limits:
memory: "128Mi"
cpu: "500m"
claims:
- "gpu" # 申请使用一个名称为gpu的resource claim
- name: monitor
image: my-app
command: ["/bin/other-program"]
resources:
requests:
memory: "32Mi"
cpu: "25m"
limits:
memory: "64Mi"
cpu: "50m"
claims:
- "gpu" # 申请使用一个名称为gpu的resource claim
调度
当某个Pod申请使用Resource Claim,调度器在调度该Pod时,逻辑将会有些变化。调度器中新增一个调度插件dynamicresources(路径:pkg/scheduler/framework/plugins/dynamicresources/dynamicresources.go),该插件用于处理使用DRA的Pod,本小节将会介绍dynamicresources插件的工作逻辑,调度器与Resource Driver的交互逻辑将在下一节介绍。
说明:本小节涉及的代码为Kubernetes v1.27.3分支提供,后续逻辑可能会发生变化。
假设当前被调度的Pod名称为Pod1,接下来介绍dynamicresources插件各扩展点逻辑时,如果没有特别说明,都是指调度Pod1。
PreFilter
dynamicresources插件在PreFilter扩展点主要逻辑如下:
-
从Pod1的Spec字段中获取所有的Resource Claim。
-
遍历每个Resource Claim,对于每一个Resource Claim,操作如下:
-
如果Resource Claim的分配模式为“立即分配”,并且Resource Driver还没有为该Resource Claim分配资源,那么该Resource Claim还不能提供给Pod1使用,Pod1仍需处于Pending,Pod1这一轮调度提前结束。
-
如果Resource Claim的Status.DeallocationRequested=true,表示该Resource Claim需要解除当前分配,然后Resource Driver重新选择节点分配给该Resource Claim。此时,Resource Claim是一种不可用状态,那么Pod1仍需处于Pending,Pod1这一轮调度提前结束。
-
如果Resource Claim请求的资源已经分配给其他Pod,并且该Resource Claim声明只能分配给一个Pod,那么Pod1不能使用这个Resource Claim,Pod1仍需处于Pending,Pod1这一轮调度提前结束。
-
如果Resource Claim的Status.Allocation.AvailableOnNodes不为空,那么该字段指示了哪些节点为Resource Claim提供资源,后续扩展点可以借助该字段过滤节点,需要将AvailableOnNodes内容保存在CycleState中,供后续扩展点使用。
-
Pod1经过所有检查,允许进入下一个调度环节(即Filter环节)。
Filter
dynamicresources插件在Filter扩展点主要逻辑如下(假设正在进行Filter的节点为Node1):
-
从Pod1的Spec字段中获取所有的Resource Claim。
-
遍历所有的Resource Claim,对每个Resource Claim操作如下:
-
如果Resource Driver已经为Resource Claim分配资源(Status.Allocation不为空),那么检查Node1是否与Resource Claim的Status.Allocation.AvailableOnNodes这个NodeSelector相匹配。
-
如果匹配,那么直接返回Success,表示Node1可以运行Pod1。
-
如果不匹配,那么返回Unschedulable,表示Node1不能运行Pod1。
-
-
如果Resource Claim的Status.DeallocationRequested=true,表示该Resource Claim需要解除当前分配,然后Resource Driver重新选择节点分配给该Resource Claim。此时,Resource Claim是一种不可用状态,那么Pod1仍需处于Pending,那么直接返回Unschedulable,表示Node1不能运行Pod1。
-
如果Resource Claim的分配模式是“延迟分配”,那么:
-
从Resource Claim对应的Resource Class中获取拥有该资源的节点的NodeSelector,如果当前节点与Resource Class的NodeSelector不匹配,那么节点被过滤掉,直接返回Unschedulable,表示Node1不能运行Pod1。
-
从PodSchedulingContext.Status中获取Resource Driver标记的不适合运行该Pod的节点列表。如果当前节点在列表中,那么节点被过滤掉,直接返回Unschedulable,表示Node1不能运行Pod1。
-
-
节点通过检查,返回Success,表示节点Node1能够运行Pod1。
PostFilter
当运行完成所有调度插件的Filter扩展点以后,如果没有一个节点能够运行Pod,dynamicresources插件的PostFilter将被触发,其主要操作逻辑如下:
-
将某些已经分配资源的Resource Claim随机挑选一个
-
将随机挑选的Resource Claim的Status.DeallocationRequested设置为true,同时Status.ReservedFor设置为nil。
-
返回Unschedulable,Pod被扔回调度队列,等待再次被调度。
Resource Driver监听到Resource Claim的Status.DeallocationRequested设置为true以后会执行解除分配操作。
PreScore
dynamicresources插件在PreScore扩展点主要逻辑如下:
-
获取Pod1所对应的PodSchedulingContext。
-
获取Pod1所有Resource Claim,检查所有Resource Claim是否已经完成分配。
-
如果还存在Resource Claim没有分配资源,并且PodSchedulingContext.Spec.PotentialNodes记录的节点列表中没有全部包含本轮调度中适合运行Pod1的节点,那么需要将新节点列表更新到PodSchedulingContext.Spec.PotentialNodes字段中,需要注意此步骤只是更新了本地的PodSchedulingContext.Spec.PotentialNodes,并没有同步到API Server,同步到API Server的操作在Reserve阶段完成。
Reserve
dynamicresources插件在Reserve扩展点主要逻辑如下:
-
变量numDelayedAllocationPending记录未分配资源的Resource Claim数,变量numClaimsWithStatusInfo记录已经给出UnsuitableNodes列表的Resource Claim数。
-
获取Pod1所有Resource Claim,遍历每个Resource claim,对每个Resource Claim做如下操作:
-
如果Resource Claim已经分配资源:
-
将Resource Claim的owner设置为当前pod
-
如果Resource Claim还没有分配资源:
-
未分配资源的Resource Claim数加1(numDelayedAllocationPending++)
-
如果Resource Claim提供了UnsuitableNodes列表,那么numClaimsWithStatusInfo++
-
-
如果numDelayedAllocationPending为0,那么直接返回Success,该pod所有资源分配成功,Pod允许进入Bind阶段。
-
如果numDelayedAllocationPending == 1 或者numClaimsWithStatusInfo == numDelayedAllocationPending (表示所有还未分配资源的Resource Claim都提供了UnsuitableNodes列表),那么将最终进入Reserve阶段的Node名称更新到PodScheduleingContext.Spec.SelectedNode中。
-
更新PodSchedulingContext.Spec.PotentialNodes到API Server中。
-
返回Unschedulable,Pod被扔回调度队列,等待再次被调度。
UnReserve
dynamicresources插件在UnReserve扩展点主要逻辑如下:遍历每一个Resource Claim,如果其Owner是当前的pod,那么将其Owner设置为空。
PostBind
dynamicresources插件在PostBind扩展点主要逻辑是删除pod的PodSchedulingContext。
Scheduler与Resource Driver协同工作
本小节主要介绍调度器如何与Resource Driver协同工作,完成对pod的调度。
最优情况
最优、非最优情况指的是:调度器调度该pod尝试多少次才成功。对于没有使用DRA机制的pod,最优情况就是调度器1次调度成功。而使用DRA机制的pod,最优情况是调度器要尝试2次才能成功调度该Pod。
调度器第一次尝试
调度器中dynamicresource插件各个扩展点的逻辑已经在前面详细介绍了。调度器在第一次调度使用DRA机制的Pod时,省去其他细枝末节,关键的几个操作如下:
-
PreScore:在dynamicresource插件的PreScore阶段,dynamicresource插件将能够运行Pod的节点列表存入CycleState中,例如图中的节点列表[Node1,Node2,Node3]。
-
Reserve:在dynamicresource插件的Reserve扩展点,dynamicresource插件从CycleState读取节点列表[Node1,Node2,Node3],然后将该节点列表和这一轮调度最终胜出的节点(假设为Node1)一起写入该Pod对应的PodSchedulingContext。
-
Reserve扩展点返回的状态为Unschedulable,该Pod将被扔回调度队列,等待下一次调度。这对于调度来讲,就是在等待Resource Driver为Resource Claim分配资源。
Resource Controller分配资源
当调度器更新完PodSchedulingContext后,Resource Controller会立即监听到PodSchedulingContext有变化,接着Resource Controller会做两件事:
-
根据PodSchedulingContext.Spec.SelectedNode记录的Node名称,确定Resource Claim中申请的资源在该Node上是否还存在且可用:
-
如果可用,那么将该Node Name更新到ResourceClaim.Status.Allocation.AvailableOnNodes(一个NodeSelector),表示的含义是:该ResourceClaim申请的资源可以到ResourceClaim.Status.Allocation中所记录的节点去寻找。
-
如果不可用,那么ResourceClaim.Status.Allocation不会记录任何东西。
-
Resource Controller根据PodSchedulingContext.Spec.PotentialNodes记录的节点列表,从自身管理的资源角度,给出哪些节点不适合运行当前调度的pod,然后更新到PodSchedulingContext.Status.UnsuitableNodes中,这一步操作的主要意义在于:如果当前调度的pod需要重新调度,那么调度器能够根据Resource Controller给的“提示”过滤一部分节点。
Resource Controller做上述两件事的主要原因在于:
-
调度器在第一次调度某一个Pod时,在没有Resource Controller参与的情况,调度器把其他插件过滤后的最优节点传递给Resource Controller并指示Resource Controller在这个节点上为Resource Claim分配资源,这个过程就是调度器的“盲猜”,因为没有Resource Controller参与,调度器也不知道哪些节点拥有资源,只能盲猜一个节点。Resource Controller对于调度器的指示,有两种反应:
-
如果这个节点确实有可用资源,那么Resource Controller将节点写入给ResourceClaim(运气好,盲猜猜中了)。
-
如果这个节点没有可用资源,那么Resource Controller不会做任何分配操作(运气不好,只有等待调度器再指示新的节点)。
-
为了避免调度器在“盲猜”失败的情况下,调度器下一次调度该pod时仍然猜错。Resource Controller给了调度器一些提示,该提示记录在PodSchedulingContext.Status.UnsuitableNodes中,意思是告诉调度器:下一次调度该pod时,这些节点不适合运行该Pod,可以过滤掉。
调度器第二次尝试
假设调度器第二次调度该pod之前,Resource Controller已经完成了ResourceClaim和PodSchedulingContext更新(最优情况,否则第二次调度将仍然调度失败)。调度器第二次调度该pod时,主要做如下几件事:
-
Filter:在dynamicresource插件Filter扩展点,重要的两个逻辑是:
-
借助Resource Controller在Resource Claim的Status.Allocation.AvailableOnNodes(该字段是一个NodeSelector)给的提示,过滤不满足条件节点。
-
借助Resource Controller在PodSchedulingContext.Status.UnsuitableNodes给的提升,过滤不满足条件的节点。
-
Reserve:在dynamicresource插件Reserve扩展点,通过判断ResourceClaim.Status.Allocation != nil检查Resource Claim是否已经分配资源,如果已经分配资源,那么该Pod在Reserve阶段成功,进入Bind环节,否则调度失败,pod被扔回调度队列,等待下一次调度。
非最优情况
在最优情况下,调度器对于某个使用DRA功能的Pod尝试调度两次,即可完成调度。但是大多数情况都是非最优情况,例如下面给的一些情形:
-
情形1:调度器对于某个pod第一次调度后,等待Resource Controller操作,但是直到调度器第二次调度该pod时,Resource Controller还没完成操作,导致调度器第二次调度该Pod仍然以失败而告知。Pod被扔回调度队列,等待下一次被调度。
-
情形2:调度器第一次盲猜的节点上没有Resource claim需要的资源,调度器不得不再次尝试。
-
情形3:假设某个Pod(名称为Pod1)同时申请CPU资源和某个Resource Claim,调度器第一次调度Pod1时,盲猜的节点为Node1,Node1上确实存在Resource Claim申请的资源,Resource Controller完成Resource Claim的分配。但是在第二次调度Pod1之前,调度器在Node1上又调度了一些Pod,这些Pod申请了CPU资源,导致第二次调度Pod1时,Node1已经不满足运行Pod1了,此时调度器需要指示Resource Controller完成resource claim解分配操作,调度器不得不多尝试几次才能成功调度这个Pod。
从上面几个例子中可以发现,DRA机制的调度效率是比较低的。
适用范围
从上面的分析中可以发现,DRA并不是适合所有场景。虽然DRA相比Device Plugin更加灵活、更加自主。但是目前版本的DRA性能相比Device Plugin有很大的劣势,而且在用户使用方式上DRA相比Device Plugin并无优势。所以:
-
如果设备厂商需要支持的设备在Kubernetes集群中使用场景比较简单,那么使用Device Plugin比较合适。
-
如果设备厂商需要支持的设备在Kubernetes集群中使用场景比较复杂,那么使用DRA比较合适。
开发
Resource Controller
前面分析过,Resource Controller主要的任务是:为Resource Claim分配资源并且给调度器一些过滤节点的提示。Resource Controller借助K8S Client-go event事件监听Resource Claim和PodSchedulingContext的变化,然后根据这些变化做出一些处理,最后更新Resource Claim和PodSchedulingContext。
为了简化Resource Controller的开发,K8S社区提供了一个Resource Controller Framework,帮助开发者实现诸如ResourceClaim和PodSchedulingContext事件监听等与设备资源分配无关的逻辑。仅需开发者实现几个特定函数即可完成Resource Controller开发:
// Driver provides the actual allocation and deallocation operations.
type Driver interface {
// GetClassParameters gets called to retrieve the parameter object
// referenced by a class. The content should be validated now if
// possible. class.Parameters may be nil.
//
// The caller will wrap the error to include the parameter reference.
GetClassParameters(ctx context.Context, class *resourcev1alpha2.ResourceClass) (interface{}, error)
// GetClaimParameters gets called to retrieve the parameter object
// referenced by a claim. The content should be validated now if
// possible. claim.Spec.Parameters may be nil.
//
// The caller will wrap the error to include the parameter reference.
GetClaimParameters(ctx context.Context, claim *resourcev1alpha2.ResourceClaim, class *resourcev1alpha2.ResourceClass, classParameters interface{}) (interface{}, error)
// Allocate gets called when a ResourceClaim is ready to be allocated.
// The selectedNode is empty for ResourceClaims with immediate
// allocation, in which case the resource driver decides itself where
// to allocate. If there is already an on-going allocation, the driver
// may finish it and ignore the new parameters or abort the on-going
// allocation and try again with the new parameters.
//
// Parameters have been retrieved earlier.
//
// If selectedNode is set, the driver must attempt to allocate for that
// node. If that is not possible, it must return an error. The
// controller will call UnsuitableNodes and pass the new information to
// the scheduler, which then will lead to selecting a diffent node
// if the current one is not suitable.
//
// The objects are read-only and must not be modified. This call
// must be idempotent.
Allocate(ctx context.Context, claim *resourcev1alpha2.ResourceClaim, claimParameters interface{}, class *resourcev1alpha2.ResourceClass, classParameters interface{}, selectedNode string) (*resourcev1alpha2.AllocationResult, error)
// Deallocate gets called when a ResourceClaim is ready to be
// freed.
//
// The claim is read-only and must not be modified. This call must be
// idempotent. In particular it must not return an error when the claim
// is currently not allocated.
//
// Deallocate may get called when a previous allocation got
// interrupted. Deallocate must then stop any on-going allocation
// activity and free resources before returning without an error.
Deallocate(ctx context.Context, claim *resourcev1alpha2.ResourceClaim) error
// UnsuitableNodes checks all pending claims with delayed allocation
// for a pod. All claims are ready for allocation by the driver
// and parameters have been retrieved.
//
// The driver may consider each claim in isolation, but it's better
// to mark nodes as unsuitable for all claims if it not all claims
// can be allocated for it (for example, two GPUs requested but
// the node only has one).
//
// The result of the check is in ClaimAllocation.UnsuitableNodes.
// An error indicates that the entire check must be repeated.
UnsuitableNodes(ctx context.Context, pod *v1.Pod, claims []*ClaimAllocation, potentialNodes []string) error
}
关于这几个函数,做如下说明:
-
GetClassParameters:用于获取Resource Class提供的参数,这些参数在具体分配资源时需要用到。
-
GetClaimParameters:用于获取Resource Claim提供的参数,这些参数在具体分配资源时需要用到。
-
Allocate:执行分配资源的操作。
-
Deallocate:解除分配,比如某个Resource Claim所需的资源由某个Node提供,现在不需要这个Node提供,换其他Node提供,那么先得执行解除分配操作。
-
UnsuitableNodes:调度器给了一些候选节点(PotentialNodes Nodes),从这些节点中确认哪些节点不适合运行当前调度的pod,便于下次调度该Pod时,提示调度器这些节点不能运行该pod。
Kubelet Plugin
前面提到过,一个Resource Driver包含一个中心化的Resource Controller以及每个节点部署一个类似于Device Plugin的Kubelet Plugin,开发一个Kubelet Plugin需要实现两个函数:
// NodeServer is the server API for Node service.
type NodeServer interface {
NodePrepareResource(context.Context, *NodePrepareResourceRequest) (*NodePrepareResourceResponse, error)
NodeUnprepareResource(context.Context, *NodeUnprepareResourceRequest) (*NodeUnprepareResourceResponse, error)
}
对这两个函数说明如下:
-
NodePrepareResource:Pod被调度到节点以后,kubelet在创建该Pod前,会通过GRPC调用Kubelet Plugin的这个函数,Plugin会在节点上准备相关资源,同时也提供Device Plugin类似的能力,将需要挂载到容器的设备路径,设置环境变量等信息返回给Kublelet。
-
NodeUnprepareResource:Pod被删除时,该函数被触发,用于执行某些与pod相关的资源清理。
弹性
当遇到使用Resource Claim的 pod 时,Autoscaler需要Resource Driver的帮助才完成弹性伸缩的目标,具体细节这里就不展开了。
总结
本文介绍了DRA的相关概念、架构组成、调度过程、调度器与Resource Driver协同工作、如何开发DRA等内容。目前DRA在K8S还是alpha状态,其架构还在演进当中,需要持续关注。