K8S下一代设备管理机制:DRA

本文涉及的产品
容器服务 Serverless 版 ACK Serverless,317元额度 多规格
容器服务 Serverless 版 ACK Serverless,952元额度 多规格
简介: 背景Kubernetes从1.8开始引入了Device Plugin机制,用于第三方设备厂商以插件化的方式将设备资源(GPU、RDMA、FPGA、InfiniBand等)接入Kubernetes集群中。用户无需修改Kubernetes代码,只需在集群中以DaemonSet方式部署设备厂商提供的插件,然后在Pod中申明使用该资源的使用量,容器在启动成功后,便可在容器中发现该设备。然而,随着Kuber

背景

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状态,其架构还在演进当中,需要持续关注。

相关实践学习
通过Ingress进行灰度发布
本场景您将运行一个简单的应用,部署一个新的应用用于新的发布,并通过Ingress能力实现灰度发布。
容器应用与集群管理
欢迎来到《容器应用与集群管理》课程,本课程是“云原生容器Clouder认证“系列中的第二阶段。课程将向您介绍与容器集群相关的概念和技术,这些概念和技术可以帮助您了解阿里云容器服务ACK/ACK Serverless的使用。同时,本课程也会向您介绍可以采取的工具、方法和可操作步骤,以帮助您了解如何基于容器服务ACK Serverless构建和管理企业级应用。 学习完本课程后,您将能够: 掌握容器集群、容器编排的基本概念 掌握Kubernetes的基础概念及核心思想 掌握阿里云容器服务ACK/ACK Serverless概念及使用方法 基于容器服务ACK Serverless搭建和管理企业级网站应用
目录
相关文章
|
17天前
|
消息中间件 Java Kafka
Kafka ACK机制详解!
本文深入剖析了Kafka的ACK机制,涵盖其原理、源码分析及应用场景,并探讨了acks=0、acks=1和acks=all三种级别的优缺点。文中还介绍了ISR(同步副本)的工作原理及其维护机制,帮助读者理解如何在性能与可靠性之间找到最佳平衡。适合希望深入了解Kafka消息传递机制的开发者阅读。
58 0
|
2月前
|
Kubernetes 监控 Perl
在k8S中,自动扩容机制是什么?
在k8S中,自动扩容机制是什么?
|
2月前
|
存储 网络安全 API
【Azure Service Bus】 Service Bus如何确保消息发送成功,发送端是否有Ack机制 
【Azure Service Bus】 Service Bus如何确保消息发送成功,发送端是否有Ack机制 
|
2月前
|
Kubernetes Java 调度
在K8S中,Pod突然挂掉,K8S有什么机制或功能自动清除Pod?
在K8S中,Pod突然挂掉,K8S有什么机制或功能自动清除Pod?
|
2月前
|
Kubernetes 安全 Linux
在k8S中,PodSecurityPolicy 机制能实现哪些安全策略?
在k8S中,PodSecurityPolicy 机制能实现哪些安全策略?
|
2月前
|
Kubernetes 安全 调度
在k8S中, PodSecurityPolicy机制是什么?
在k8S中, PodSecurityPolicy机制是什么?
|
2月前
|
Kubernetes 监控 Perl
在K8S中,RC的机制是什么?
在K8S中,RC的机制是什么?
|
5月前
|
Prometheus 监控 Kubernetes
Kubernetes 集群的监控与日志管理实践深入理解PHP的命名空间与自动加载机制
【5月更文挑战第30天】 在容器化和微服务架构日益普及的背景下,Kubernetes 已成为众多企业的首选容器编排工具。然而,随之而来的挑战是集群的监控与日志管理。本文将深入探讨 Kubernetes 集群监控的最佳实践,包括节点资源使用情况、Pods 健康状态以及网络流量分析等关键指标的监控方法。同时,我们也将讨论日志聚合、存储和查询策略,以确保快速定位问题并优化系统性能。文中将介绍常用的开源工具如 Prometheus 和 Fluentd,并分享如何结合这些工具构建高效、可靠的监控和日志管理系统。
|
5月前
|
消息中间件 Java Spring
五、消息确认机制(ACK)
五、消息确认机制(ACK)
195 1
|
10月前
|
存储 Kubernetes Unix
k8s教程(Volume篇)-CSI存储机制详解
k8s教程(Volume篇)-CSI存储机制详解
1315 0
k8s教程(Volume篇)-CSI存储机制详解