本节介绍Kubernetes中的Device Plugin概念,整理了社区在此方向的架构设计,并简要归纳了Device Plugin的生命周期。
Background
Kubernetes能在集群上做容器的编排,限制容器、Pod对CPU、内存资源的使用,并根据worker nodes的可用资源情况,进行及时和精准的容器调度。自然而然地,诸如GPU、NIC等其异构资源也被考虑到容器编排调度的资源池中,以加速异构资源的容器部署,并扩展容器隔离性的范畴。
不过社区也考虑了这些硬件的多样性,很难说把各种管理不同硬件资源的代码都合入K8s项目中,所以就试图提供统一的插件化方案来让用户(主要是设备提供商)在K8s上自定义这些资源的管控。
这个方案叫Device Manager
,实际上是通过在K8s中内置Extended Resource
和Device 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。
这些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生命周期如下图所示,分为注册、发现、分配、停止四个阶段。
Registration
- 社区推荐的是以daemonset的方式去部署Device Plugin,i.e.
kubectl create -f nvidia.io/device-plugin.yml
。Daemonset的容器会在所有nodes上run起来,看当前node上是否有可用的设备资源,如果没有就terminate掉(假设restartPolicy是OnFailure)。 - 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。 - 同时,Kubelet也会将设备资源信息暴露到Node status中,设备资源会以Extended Resources的形式(
vendor-domain/vendor-device
的格式,如nivida.com/gpu
)在Apiserver上公布,后续Scheduler会利用该信息进行调度。
Discovery
- Device Plugin启动后,Kubelet会和它建立一个ListAndWatch的长连接。Device Plugin向Kubelet公告一个devices list,如果设备情况发生变化则会重发。
- 当某个设备的health check失败,就会通知Kubelet。如果该设备空闲,Kubelet会将其挪出可分配列表;如果该设备被某个Pod使用,Kubelet会将该kill掉该Pod。
- Device Plugin可以利用Kubelet的socket持续检查Kubelet的状态,在Kubelet重启时让插件也重启,向Kubelet重新注册。
Allocation
- Scheduler从cache中选择资源满足的node;Node收到Add Pod,对Pod信息进行
admit()
方法来判断是否可运行。判断中就包括了UpdatePluginResource()
这个handler。该方案会调用devicePluginManager
的Allocate()
方法,对Kubelet缓存中记录的资源可用量进行判断和计算,并选定要使用的设备。 - 确定创建容器时,Kubelet会调用Device Plugin的
Allocate()
函数。 - Device Plugin被call后,根据request的device id,检查设备是否可用;可用的话会执行设备特定的命令,比如GPU cleanup、QRNG初始化等;并向Kubelet返回在容器创建时对设备的配置,包括
Env
、Mounts
、Devices
;该配置会被记录在podDevices
这个map中。 - Kubelet创建Pod的容器时,产生如下调用链,从
podDevices
在获取容器使用的设备,组织成容器运行时的参数opts
,传递到container runtime中,最终run container时会用opts
到。比如GPU容器,会在opts
中增加--devices
的参数指定,让容器挂上需要的设备。
GenerateRunContainerOptions() -> containerManager.GetResources() -> devicePluginManager.GetDeviceRunContainerOptions() -> deviceRunContainerOptions()
Stop
- 这里的Stop指Device Plugin自身停止时想做的清理工作,比如清理Socket、unload driver之类的事情,即对应Device Plugin生命周期的Stop,而非Kubelet控制的Pod生命周期。
- 至于设备失效,Device Plugin会通过
ListAndWatch
gRPC stream来提示Kubelet,Kubelet会据此让Pod失效,比如Kill掉Pod。