什么是 device plugin
Kubernetes 作为一个自动化容器编排系统,在调度 pod 的时候会根据容器需要的资源进行节点的选择,节点的选择会分为预选和优选阶段。预选阶段会根据所有节点上剩余的资源量与 pod 需要的资源量进行对比,选出能够满足需求的节点。通常情况下,这里的资源都会包括 CPU 和 Memory,就像下面这样:
resources:
requests:
memory: "64Mi"
cpu: "250m"
limits:
memory: "128Mi"
cpu: "500m"
但是现在我们有了新的需求,我们有一个 tensorflow 的模型需要借助 tensorflow serving 来部署,同时我们希望采用 GPU 部署的方案以加快模型的在线推算速度。这样我们就需要一个 GPU 的资源并借助 Kubernetes 的调度器将容器调度到有空余 GPU 资源的方案。
在 1.11 版本之前的 Kubernetes 中,提供了 alpha.kubernetes.io/nvidia-gpu
的资源名称来帮助我们根据 GPU 资源调度。但是这也带来了一些问题:
- Kubernetes 需要维护 NVIDIA GPU 相关的代码,增加了维护成本。
- NVIDIA GPU 方面的专家不一定熟悉 Kubernetes,这不符合让最擅长的人做最擅长的事的原则。
- 除了 NVIDIA GPU,还会有其他的计算资源需要支持。
因此,Kubernetes 在 1.8 版本引入了 device plugin 机制,将第三方的计算资源通过插件的方式引入 Kubernetes,并且由第三方厂商自行维护。Kubernetes 社区的活瞬间就轻松了,第三方厂商也开心了。
通过以上的说明,可以总结出 device plugin 主要用来解耦第三方计算资源和 kubernetes 系统,将第三方的计算资源通过插件的方式引入 Kubernetes。当然 cpu 和 memory 除外,毕竟谁还能少了 CPU 和 Memory 呢。
device plugin 能做什么
目前一些常用的 device plugin 有:
- Nvidia 提供的 GPU 插件:NVIDIA device plugin for Kubernetes
- AMD 提供的 GPU 插件:RadeonOpenCompute/k8s-device-plugin
- 高性能低延迟 RDMA 卡插件:RDMA device plugin for Kubernetes
- 低延迟 Solarflare 万兆网卡驱动:Solarflare Device Plugin
我了解的还有腾讯的 Gaia Scheduler,通过 device plugin 实现的 GPU 虚拟化方案。如果 NVIDIA 的 GPU 方案还不够适合你,可以看看腾讯的这个方案:tkestack/gpu-manager
我觉得腾讯的 GPU 虚拟化方案是最能说明 device plugin 使用场景的例子,通过定义 tencent.com/vcuda-core
和 tencent.com/vcuda-memory
这两个计算资源,来将一个物理 GPU 划分成多个虚拟 GPU 进行调度,这样可以实现一个 GPU 上部署多个 tensorflow serving。你不需要购买特殊的硬件或者修改任何 Kubernetes 的代码,就有了 GPU 虚拟化的能力。
下面我们开个脑洞,现在我们有了一种叫做 cola
的计算资源,可以提高程序的 IO 能力。但是 cola 的资源有限,只分配给特定的容器使用。这时候我们就可以通过实现自己的 device plugin 来满足这个需求。我们的计算资源名就叫做: myway5.com/cola
,在下面一小节做具体的实现。
device plugin 的实现方案
device plugin 的工作原理其实不复杂。主要有以下步骤:
- 首先 device plugin 可以通过手动或 daemonset 部署到需要的节点上。
- 为了让 Kubernetes 发现 device plugin,需要向 kubelet 的 unix socket。 进行注册,注册的信息包括 device plugin 的 unix socket,API Version,ResourceName。
- kubelet 通过 grpc 向 device plugin 调用 ListAndWatch, 获取当前节点上的资源。
- kubelet 向 api server 更新节点状态来通知资源变更。
- 用户创建 pod,请求资源并调度到节点上后,kubelet 调用 device plugin 的 Allocate 进行资源分配。
时序图如下:
在 device plugin 的实现中,最关键的两个要实现的方法是 ListAndWatch
和 Allocate
。除此之外,还要注意监控 kubelet 的重启,一般是使用 fsnotify
类似的库监控 kubelet.sock 的重新创建事件。如果重新创建了,则认为 kubelet 是重启了,我们需要重新向 kubelet 注册 device plugin。
ListAndWatch
我们上面定义的 myway5.com/cola
资源用 /etc/colas
下的文件代表。每一个文件代表一个可用的资源。因此实现 ListAndWatch
就是查找该文件夹下的文件,然后添加到设备列表发送给 kubelet,之后调用 fsnotify
去监控文件的 CREATE
和 REMOVE
事件。每次设备列表发生变更都重新向 kubelet 发送更新过的设备列表。
// ListAndWatch returns a stream of List of Devices
// Whenever a Device state change or a Device disappears, ListAndWatch
// returns the new list
func (s *ColaServer) ListAndWatch(e *pluginapi.Empty, srv pluginapi.DevicePlugin_ListAndWatchServer) error {
log.Infoln("ListAndWatch called")
devs := make([]*pluginapi.Device, len(s.devices))
i := 0
for _, dev := range s.devices {
devs[i] = dev
i++
}
err := srv.Send(&pluginapi.ListAndWatchResponse{Devices: devs})
if err != nil {
log.Errorf("ListAndWatch send device error: %v", err)
return err
}
// 更新 device list
for {
log.Infoln("waiting for device change")
select {
case <-s.notify:
log.Infoln("开始更新device list, 设备数:", len(s.devices))
devs := make([]*pluginapi.Device, len(s.devices))
i := 0
for _, dev := range s.devices {
devs[i] = dev
i++
}
srv.Send(&pluginapi.ListAndWatchResponse{Devices: devs})
}
}
}
Allocate
在用户创建的 Pod 请求资源时,Kubernetes 的调度器会进行调度,并通过 kubelet 向 device plugin 发出 Allocate 调用,这一步的调用主要是为了让 device plugin 为容器调度资源。 在调度成功后向 kubelet 返回调度结果即可。
// Allocate is called during container creation so that the Device
// Plugin can run device specific operations and instruct Kubelet
// of the steps to make the Device available in the container
func (s *ColaServer) Allocate(ctx context.Context, reqs *pluginapi.AllocateRequest) (*pluginapi.AllocateResponse, error) {
log.Infoln("Allocate called")
resps := &pluginapi.AllocateResponse{}
for _, req := range reqs.ContainerRequests {
log.Infof("received request: %v", strings.Join(req.DevicesIDs, ","))
resp := pluginapi.ContainerAllocateResponse{
Envs: map[string]string{
"COLA_DEVICES": strings.Join(req.DevicesIDs, ","),
},
}
resps.ContainerResponses = append(resps.ContainerResponses, &resp)
}
return resps, nil
}
部署
device plugin 可以手动部署到机器上,也可以通过 Daemonset 进行部署。这里当然是 Daemonset 进行部署了。部署的时候有几个注意事项:
- 需要挂载 hostPath,其中
/var/lib/kubelet/device-plugins
是必须的。这个文件夹下有kubelet.sock
,以及我们也需要将 device plugin 的 unix socket 文件存在这里。使得 kubelet 可以和我们的应用通信。 - 为 device plugin 的 Pod 设置调度优先级别,通常设置成
priorityClassName: "system-node-critical"
。这样可以保证不会因为节点利用率过高被逐出。 - 如果资源设备不是每台机器都有,建议使用
nodeSelector
将 device plugin 调度到指定的机器上。
device plugin 的开发源代码可以参考上面的 cola
例子:cola device plugin
部署结束之后,可以查看一下节点的资源情况:
$ kubectl describe nodes test
Capacity:
cpu: 2
ephemeral-storage: 17784752Ki
hugepages-2Mi: 0
memory: 1986740Ki
myway5.com/cola: 2
pods: 110
Allocatable:
cpu: 2
ephemeral-storage: 17784752Ki
hugepages-2Mi: 0
memory: 1986740Ki
myway5.com/cola: 2
pods: 110
创建一个 pod,请求 myway5.com/cola
资源:
$ kubectl apply -f e2e/pod-with-cola.yaml
然后查看一下 cola pod 的日志来了解设备发现和调度情况:
$ kubectl -n kube-system logs cola-thtm9
time="2020-03-24T08:16:53Z" level=info msg="cola device plugin starting"
time="2020-03-24T08:16:53Z" level=info msg="find device 'cocacola'"
time="2020-03-24T08:16:53Z" level=info msg="find device 'peisicola'"
time="2020-03-24T08:16:53Z" level=info msg="watching devices"
time="2020-03-24T08:16:53Z" level=info msg="start GPPC server for 'myway5.com/cola'"
time="2020-03-24T08:16:53Z" level=info msg="Register to kubelet with endpoint cola.sock"
time="2020-03-24T08:16:53Z" level=info msg="register to kubelet successfully"
time="2020-03-24T08:16:53Z" level=info msg="ListAndWatch called"
time="2020-03-24T08:16:53Z" level=info msg="waiting for device change"
time="2020-03-24T08:17:10Z" level=info msg="Allocate called"
博主你好,遇到一个关于device plugin的问题,想请教下。删除一个第三方device plugin之后,describe node输出的Allocatable字段中仍然还会有device plugin定义的扩展设备,该怎么删除呢?重启过kubelet发现还是会有?
我觉得这应该属于 device plugin 的问题,应该在退出时向 kubelet 清理节点上注册的设备资源。
博主你好,看了下你写的device plugin的例子,但是对你提到的监控kubelet.sock从而使得device plugin重新注册的部分不太明白,监控kubelet.sock的代码应该是这一部分吧,https://github.com/joyme123/cola-device-plugin/blob/master/cmd/server/app.go#L40-L50,但是又如何能出发device plugin的重新注册呢?不是很明白,拜托解答下
我看了一下,应该是 watch 到 kubelet.sock 文件的创建事件,程序就会退出。然后因为是通过 daemonset 等方式部署的,可以被重新拉起,然后就重新走注册的逻辑
明白了,是log.Fatalf() https://github.com/joyme123/cola-device-plugin/blob/master/cmd/server/app.go#L46 结束的主进程,之前不知道log模块还可以退出进程。。。