本节介绍Kubernetes中的Device Plugin概念,整理了社区在此方向的架构设计,并简要归纳了Device Plugin的生命周期。

Background

Kubernetes能在集群上做容器的编排,限制容器、Pod对CPU、内存资源的使用,并根据worker nodes的可用资源情况,进行及时和精准的容器调度。自然而然地,诸如GPU、NIC等其异构资源也被考虑到容器编排调度的资源池中,以加速异构资源的容器部署,并扩展容器隔离性的范畴。

不过社区也考虑了这些硬件的多样性,很难说把各种管理不同硬件资源的代码都合入K8s项目中,所以就试图提供统一的插件化方案来让用户(主要是设备提供商)在K8s上自定义这些资源的管控。
这个方案叫Device Manager,实际上是通过在K8s中内置Extended ResourceDevice Plugin两个模块,来允许用户自己编写对应硬件资源的Device Plugin,最终串起硬件资源在集群级别的调度、以及在nodes上的实际绑定。为此,K8s社区也下足了决心,移除了nvidia-gpu在K8s项目主干的代码。

Overview

Objective

首先明确一下Device Plugin以及Device Manager的工作范畴:

  • Device Plugin会利用Kubelet内置的扩展机制,来管控特定设备,包括设备的发现和health check,以及协助runtime利用和清理设备资源。
  • Device Manager方案负责为Device Plugin提供部署和版本控制的API。
  • Device Manager不负责异构节点及拓扑相关的问题。
  • Device Manager只解决硬件资源的health check,不负责收集metrics。
  • Device Plugin目前版本的资源分配未涉及QoS,默认为limits=requests。

TL; DR

Extended Resource:

这个模块提供了自定义资源的扩展,用户可以把资源的名称和数量上报给API Server。Scheduler调度pod时,会根据该类资源的数量条件进行判断。
目前这类资源的delta必须为整数,未来可能允许浮点数。

Device Plugin:

Device Plugins其实是一些以Pod中容器或者bare-metal-mode运行的简单的gRPC servers。

device-plugin-overview

这些servers会实现Device Manager定义的gRPC interface,在Kubelet中注册,让Kubelet通过gRPC去调用Device Plugin的ListAndWatch()Allocate()方法。

  • ListAndWatch()方法帮助Kubelet discover和watch设备资源的变化;
  • Allocate()方法允许Kubelet在创建使用该设备资源的容器时,告诉Device Plugin进行设备绑定容器的具体操作,并告诉Device Plugin使用的device、volume以及env配置。

Design

API Design

前面说了,Device Plugins实际上是一些gRPC servers。

service DevicePlugin {
    // returns a stream of []Device
    rpc ListAndWatch(Empty) returns (stream ListAndWatchResponse) {}
    rpc Allocate(AllocateRequest) returns (AllocateResponse) {}
}

更详细的API规范请参考Device Plugin API Spec

Lifecycle

一开始,Device Plugin需要主动去提醒Kubelet的gRPC server,以告知它的存在。
Kubelet和Device Plugin的gRPC server通过host上的Unix Socket进行通信,比如/var/lib/kubelet/device-plugins/nvidiaGPU.sock

具体的Device Plugin生命周期如下图所示,分为注册、发现、分配、停止四个阶段。

device-plugin-lifecycle

Registration

  1. 社区推荐的是以daemonset的方式去部署Device Plugin,i.e. kubectl create -f nvidia.io/device-plugin.yml。Daemonset的容器会在所有nodes上run起来,看当前node上是否有可用的设备资源,如果没有就terminate掉(假设restartPolicy是OnFailure)。
  2. Device Plugin启动时,通过gRPC往/var/lib/kubelet/device-plugins/kubelet.sock(Kubelet gRPC server的socket)发RegisterRequest,在Kubelet中注册,并提供Device Plugin的Unix Socket、API version、device name(ResourceName)。Kubelet会回复RegisterResponse,包含了可能触发的error,比如API version不支持或者已有同名的Device Plugin注册过。如果Kubelet没有回复error,Device Plugin才会开始运行自己的gRPC server。
  3. 同时,Kubelet也会将设备资源信息暴露到Node status中,设备资源会以Extended Resources的形式(vendor-domain/vendor-device的格式,如nivida.com/gpu)在Apiserver上公布,后续Scheduler会利用该信息进行调度。

Discovery

  1. Device Plugin启动后,Kubelet会和它建立一个ListAndWatch的长连接。Device Plugin向Kubelet公告一个devices list,如果设备情况发生变化则会重发。
  2. 当某个设备的health check失败,就会通知Kubelet。如果该设备空闲,Kubelet会将其挪出可分配列表;如果该设备被某个Pod使用,Kubelet会将该kill掉该Pod。
  3. Device Plugin可以利用Kubelet的socket持续检查Kubelet的状态,在Kubelet重启时让插件也重启,向Kubelet重新注册。

Allocation

  1. Scheduler从cache中选择资源满足的node;Node收到Add Pod,对Pod信息进行admit()方法来判断是否可运行。判断中就包括了UpdatePluginResource()这个handler。该方案会调用devicePluginManagerAllocate()方法,对Kubelet缓存中记录的资源可用量进行判断和计算,并选定要使用的设备。
  2. 确定创建容器时,Kubelet会调用Device Plugin的Allocate()函数。
  3. Device Plugin被call后,根据request的device id,检查设备是否可用;可用的话会执行设备特定的命令,比如GPU cleanup、QRNG初始化等;并向Kubelet返回在容器创建时对设备的配置,包括EnvMountsDevices;该配置会被记录在podDevices这个map中。
  4. Kubelet创建Pod的容器时,产生如下调用链,从podDevices在获取容器使用的设备,组织成容器运行时的参数opts,传递到container runtime中,最终run container时会用opts到。比如GPU容器,会在opts中增加--devices的参数指定,让容器挂上需要的设备。
GenerateRunContainerOptions() -> containerManager.GetResources()
-> devicePluginManager.GetDeviceRunContainerOptions() -> deviceRunContainerOptions()

Stop

  1. 这里的Stop指Device Plugin自身停止时想做的清理工作,比如清理Socket、unload driver之类的事情,即对应Device Plugin生命周期的Stop,而非Kubelet控制的Pod生命周期。
  2. 至于设备失效,Device Plugin会通过ListAndWatch gRPC stream来提示Kubelet,Kubelet会据此让Pod失效,比如Kill掉Pod。

References

  1. Device Plugins - Kubernetes
  2. Device Manager Proposal - kubernetes/community
  3. Kubernetes的Device Plugin设计解读 - 阿里云开发者社区
  4. k8s的扩展资源设计和device-plugin - 阿里云云栖社区