Kubernetes开发知识–device-plugin的实现

什么是 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 有:

我了解的还有腾讯的 Gaia Scheduler,通过 device plugin 实现的 GPU 虚拟化方案。如果 NVIDIA 的 GPU 方案还不够适合你,可以看看腾讯的这个方案:tkestack/gpu-manager

我觉得腾讯的 GPU 虚拟化方案是最能说明 device plugin 使用场景的例子,通过定义 tencent.com/vcuda-coretencent.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 plugins

在 device plugin 的实现中,最关键的两个要实现的方法是 ListAndWatchAllocate。除此之外,还要注意监控 kubelet 的重启,一般是使用 fsnotify 类似的库监控 kubelet.sock 的重新创建事件。如果重新创建了,则认为 kubelet 是重启了,我们需要重新向 kubelet 注册 device plugin。

ListAndWatch

我们上面定义的 myway5.com/cola 资源用 /etc/colas 下的文件代表。每一个文件代表一个可用的资源。因此实现 ListAndWatch 就是查找该文件夹下的文件,然后添加到设备列表发送给 kubelet,之后调用 fsnotify 去监控文件的 CREATEREMOVE 事件。每次设备列表发生变更都重新向 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"

《Kubernetes开发知识–device-plugin的实现》上有6条评论

  1. 博主你好,遇到一个关于device plugin的问题,想请教下。删除一个第三方device plugin之后,describe node输出的Allocatable字段中仍然还会有device plugin定义的扩展设备,该怎么删除呢?重启过kubelet发现还是会有?

    1. 我觉得这应该属于 device plugin 的问题,应该在退出时向 kubelet 清理节点上注册的设备资源。

  2. 博主你好,看了下你写的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的重新注册呢?不是很明白,拜托解答下

    1. 我看了一下,应该是 watch 到 kubelet.sock 文件的创建事件,程序就会退出。然后因为是通过 daemonset 等方式部署的,可以被重新拉起,然后就重新走注册的逻辑

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据