参考
-
Device Plugin 入门笔记(一)
-
Device Plugin 入门笔记(二)
-
从零开始入门 K8s:GPU 管理和 Device Plugin 工作机制
-
Kubernetes开发知识–device-plugin的实现
-
https://github.com/oceanweave/cola-device-plugin/blob/master/pkg/server/server.go#L117:22
-
https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/device-plugins/ device plugin api spec
-
https://github.com/kubernetes/design-proposals-archive/blob/main/resource-management/device-plugin.md
明确需求
我们想让 k8s 纳入一个硬件设备,假如是 GPU,那接下来怎么管理呢 —— 答:k8s device plugin 机制
由下图其实可以简单看出几个步骤
- 将硬件设备信息注册到 k8s 中,也就是由 kubelet 管理,与 kubelet 通信
- k8s 如何知道该硬件资源的变化呢 —— ListWatch 机制,但 k8s 只知道此硬件,不明白其中原理 —— 所以需要该硬件自行开发一个 ListWatch 接口,来满足 k8s 的 ListWatch 机制
- k8s 如何分配此硬件资源给 Pod 等使用呢 —— 因为k8s 只知道此硬件,不明白其中原理 —— 所以同样有该硬件自行开发一个资源分配管理接口 Allocate,到时候进行资源的分配
注意上述其实都是,该硬件和 kebelet 打交道,因为 kubelet 负责容器的生命周期(创建、删除等)还有资源的监控,所以该硬件资源通过与 kubelet 交互,才能完成接入到 k8s 中
而 device plugin 就是 中间商,帮忙将硬件设备注册到 kubelet 中
概述总流程
-
Kubernetes开发知识–device-plugin的实现
-
[Gaia Scheduler] gpu-manager 启动流程分析
-
[Gaia Scheduler] gpu-manager 的虚拟化 gpu 分配流程
以 gpu-manager 项目为例,关注了两种资源 vcore 和 vmemory(我为其命名为 dfy.com/vgpu-core 和 dfy.com/vgpu-memory,项目中不是这个名字哈)
所以考虑一下几个方面,以 vcore 资源为例
- 首先为我们资源命名,符合 k8s 格式,取名为 dfy.com/vgpu-core
- 如何与 kubelet 通信呢?
- 需要为该资源创建个 vcore.sock,将其名字 dfy.com/vgpu-core 和 vcore.sock 注册到 kubelet(就是收到此资源请求,就发给 vcore.sock)
- vcore.sock 收到 kubelet 发来的资源请求后怎么处理呢?
- 需要为创建个 grpc server,监听 vcore.sock 传来的消息,之后 grpc.server 进行处理
- 那 vcore.sock 传来的消息是什么呢?(也就是 kubelet 会调用什么函数吗?)
- 这个已经是规定好的
- 就是是下面四个函数
- Allocate 分配资源
- ListAndWatch 监听资源变化
- PreStartContainer 每次启动容器前做的操作(在 Allocate 之后执行)
- GetDevicePluginOptions 可选功能,目前只能开始 PreStartContainer
- 之后就要考虑这些方法的实现,并注册到 vcore grpc server 上
- 这里就是 NvidiaTopoAllocator 结构体进行实现的
总结,我们就是要串起上面的流程
- 为资源定义名称,创建 socket,注册到 kubelet (作用:让 kubelet 知道这种资源,并且知道通信方式)
- 创建一个结构体,实现 device plugin 定义的接口(Allocate、ListAndWatch 重要,其他两个可选)
- 创建 grpc server,监听此 socket,并将上面结构体绑定到该 grpc server 上(作用:kubelet 对此资源发起请求,会通过 socket 到达 grpc server,调用此机构体的处理函数进行处理)
注册设备插件
kubelet
提供了一个 Registration
的 gRPC 服务:
service Registration {
rpc Register(RegisterRequest) returns (Empty) {}
}
device plugin(设备插件) 可以通过此 gRPC 服务在 kubelet 进行注册。在注册期间,设备插件需要发送下面几样内容:
- 设备插件 的 Unix socket(套接字)。
- 设备插件 的 API 版本。
ResourceName
是需要公布的。这里ResourceName
需要遵循扩展资源命名方案, 类似于vendor-domain/resourcetype
。(比如 NVIDIA GPU 就被公布为nvidia.com/gpu
。)
成功注册后,设备插件就向 kubelet 发送它所管理的设备列表,然后 kubelet 负责将这些资源发布到 API 服务器,作为 kubelet 节点状态更新的一部分。
// 举例子 来自 https://github.com/oceanweave/cola-device-plugin/blob/master/pkg/server/server.go#L117:22 项目
// pluginapi "k8s.io/kubelet/pkg/apis/deviceplugin/v1beta1"
// RegisterToKubelet 向kubelet注册device plugin
func (s *ColaServer) RegisterToKubelet() error {
socketFile := filepath.Join(DevicePluginPath + KubeletSocket) // kubelet 的 unix 套接字
conn, err := s.dial(socketFile, 5*time.Second) // 每 5s 访问一次 kubelet
if err != nil {
return err
}
defer conn.Close()
client := pluginapi.NewRegistrationClient(conn)
req := &pluginapi.RegisterRequest{
Version: pluginapi.Version, // 该硬件设备插件的 API 版本
Endpoint: path.Base(DevicePluginPath + colaSocket), // 该硬件设备插件的 unix socket
ResourceName: resourceName, // 该硬件设备插件定义的 资源名称
}
log.Infof("Register to kubelet with endpoint %s", req.Endpoint)
_, err = client.Register(context.Background(), req) // 将该设备插件 注册到 kubelet 中
if err != nil {
return err
}
return nil
}
设备插件的实现
主要就是实现两个函数
- ListAndWatch —— 监听该硬件设备的变化
- Allocate —— 分配该硬件设备资源
承接上面项目例子 https://github.com/oceanweave/cola-device-plugin/blob/master/pkg/server/server.go#L117:22
可以看到该项目实现了下面定义的大部分函数规定
设备插件的常规工作流程包括以下几个步骤:
-
初始化。在这个阶段,设备插件将执行供应商特定的初始化和设置, 以确保设备处于就绪状态。
-
插件使用主机路径
/var/lib/kubelet/device-plugins/
下的 Unix 套接字启动一个 gRPC 服务,该服务实现以下接口:// k8s.io/kubelet/pkg/apis/deviceplugin/v1beta1/api.proto service DevicePlugin { // GetDevicePluginOptions 返回与设备管理器沟通的选项。 rpc GetDevicePluginOptions(Empty) returns (DevicePluginOptions) {} // ListAndWatch 返回 Device 列表构成的数据流。 // 当 Device 状态发生变化或者 Device 消失时,ListAndWatch // 会返回新的列表。 rpc ListAndWatch(Empty) returns (stream ListAndWatchResponse) {} // Allocate 在容器创建期间调用,这样设备插件可以运行一些特定于设备的操作, // 并告诉 kubelet 如何令 Device 可在容器中访问的所需执行的具体步骤 rpc Allocate(AllocateRequest) returns (AllocateResponse) {} // GetPreferredAllocation 从一组可用的设备中返回一些优选的设备用来分配, // 所返回的优选分配结果不一定会是设备管理器的最终分配方案。 // 此接口的设计仅是为了让设备管理器能够在可能的情况下做出更有意义的决定。 rpc GetPreferredAllocation(PreferredAllocationRequest) returns (PreferredAllocationResponse) {} // PreStartContainer 在设备插件注册阶段根据需要被调用,调用发生在容器启动之前。 // 在将设备提供给容器使用之前,设备插件可以运行一些诸如重置设备之类的特定于 // 具体设备的操作, rpc PreStartContainer(PreStartContainerRequest) returns (PreStartContainerResponse) {} }
说明:
插件并非必须为
GetPreferredAllocation()
或PreStartContainer()
提供有用的实现逻辑, 调用GetDevicePluginOptions()
时所返回的DevicePluginOptions
消息中应该设置这些调用是否可用。kubelet
在真正调用这些函数之前,总会调用GetDevicePluginOptions()
来查看是否存在这些可选的函数。 -
插件通过 Unix socket 在主机路径
/var/lib/kubelet/device-plugins/kubelet.sock
处向 kubelet 注册自身。 -
成功注册自身后,设备插件将以服务模式运行,在此期间,它将持续监控设备运行状况, 并在设备状态发生任何变化时向 kubelet 报告。它还负责响应
Allocate
gRPC 请求。 在Allocate
期间,设备插件可能还会做一些设备特定的准备;例如 GPU 清理或 QRNG 初始化。 如果操作成功,则设备插件将返回AllocateResponse
,其中包含用于访问被分配的设备容器运行时的配置。 kubelet 将此信息传递到容器运行时。
异常处理(socket 被恶意删除怎么办?)
- 参考:KUBERNETES如何通过DEVICE PLUGINS来使用NVIDIA GPU
设备插件应能监测到 kubelet 重启,并且向新的 kubelet 实例来重新注册自己。 在当前实现中,当 kubelet 重启的时候,新的 kubelet 实例会删除 /var/lib/kubelet/device-plugins
下所有已经存在的 Unix 套接字。 设备插件需要能够监控到它的 Unix 套接字被删除,并且当发生此类事件时重新注册自己。
- 就是监控到 kubelet 重启后,重新将自身的 socket 通信文件,放入到
/var/lib/kubelet/device-plugins
目录下
kubelet socket 的异常处理
- 每次 kubelet 启动 (重启) 时,都会将 /var/lib/kubelet/device-plugins 下的所有 sockets 文件删除。
- Device Plugin 要负责监测自己的 socket 被删除,然后进行重新注册,重新生成自己的 socket。
- 当 plugin socket 被误删,Device Plugin 该怎么办?
我们看看 Nvidia Device Plugin 是怎么处理的,相关的代码如下:
github.com/NVIDIA/k8s-device-plugin/main.go:15
func main() {
...
log.Println("Starting FS watcher.")
watcher, err := newFSWatcher(pluginapi.DevicePluginPath)
...
restart := true
var devicePlugin *NvidiaDevicePlugin
L:
for {
if restart {
if devicePlugin != nil {
devicePlugin.Stop()
}
devicePlugin = NewNvidiaDevicePlugin()
if err := devicePlugin.Serve(); err != nil {
log.Println("Could not contact Kubelet, retrying. Did you enable the device plugin feature gate?")
log.Printf("You can check the prerequisites at: https://github.com/NVIDIA/k8s-device-plugin#prerequisites")
log.Printf("You can learn how to set the runtime at: https://github.com/NVIDIA/k8s-device-plugin#quick-start")
} else {
restart = false
}
}
select {
case event := <-watcher.Events:
if event.Name == pluginapi.KubeletSocket && event.Op&fsnotify.Create == fsnotify.Create {
log.Printf("inotify: %s created, restarting.", pluginapi.KubeletSocket)
restart = true
}
case err := <-watcher.Errors:
log.Printf("inotify: %s", err)
case s := <-sigs:
switch s {
case syscall.SIGHUP:
log.Println("Received SIGHUP, restarting.")
restart = true
default:
log.Printf("Received signal \"%v\", shutting down.", s)
devicePlugin.Stop()
break L
}
}
}
}
- 通过
fsnotify.Watcher
监控/var/lib/kubelet/device-plugins/
目录。 - 如果
fsnotify.Watcher
的 Events Channel 收到 Createkubelet.sock
事件(说明 kubelet 发生重启),则会触发 Nvidia Device Plugin 的重启。 - Nvidia Device Plugin 重启的逻辑是:先检查 devicePlugin 对象是否为空(说明完成了 Nvidia Device Plugin 的初始化):
- 如果不为空,则先停止 Nvidia Device Plugin 的 gRPC Server。
- 然后调用 NewNvidiaDevicePlugin () 重建一个新的 DevicePlugin 实例。
- 调用 Serve () 启动 gRPC Server,并先 kubelet 注册自己。
device plugin socket 的异常处理
因此,这其中只监控了 kubelet.sock
的 Create 事件,能很好处理 kubelet 重启的问题,但是并没有监控自己的 socket 是否被删除的事件。所以,如果 Nvidia Device Plugin 的 socket 被误删了,那么将会导致 kubelet 无法与该节点的 Nvidia Device Plugin 进行 socket 通信,则意味着 Device Plugin 的 gRPC 接口都无法调通:
- 无法 ListAndWatch 该节点上的 Device 列表、健康状态,Devices 信息无法同步。
- 无法 Allocate Device,导致容器创建失败。
因此,建议加上对自己 device plugin socket 的删除事件的监控,一旦监控到删除,则应该触发 restart。
select {
case event := <-watcher.Events:
if event.Name == pluginapi.KubeletSocket && event.Op&fsnotify.Create == fsnotify.Create {
log.Printf("inotify: %s created, restarting.", pluginapi.KubeletSocket)
restart = true
}
// 增加对nvidia.sock的删除事件监控
if event.Name == serverSocket && event.Op&fsnotify.Delete == fsnotify.Delete {
log.Printf("inotify: %s deleted, restarting.", serverSocket)
restart = true
}
...
}