如何实现一个K8S DevicePlugin?

什么是device plugin

k8s允许限制容器对资源的使用,比如CPU内存,并以此作为调度的依据。

当其他非官方支持的设备类型需要参与到k8s的工作流程中时,就需要实现一个device plugin

Kubernetes提供了一个设备插件框架,你可以用它来将系统硬件资源发布到Kubelet

供应商可以实现设备插件,由你手动部署或作为 DaemonSet 来部署,而不必定制 Kubernetes 本身的代码。

目标设备包括 GPU、高性能 NIC、FPGA、 InfiniBand 适配器以及其他类似的、可能需要特定于供应商的初始化和设置的计算资源。

更多云原生、K8S相关文章请点击【专栏】查看!

发现插件

一个新的device plugin是如何被kubelet发现的?

device plugin通过gRPC的方式与kubelet通信,kubelet实现了Register接口,用于注册插件。

go 复制代码
service Registration {
	rpc Register(RegisterRequest) returns (Empty) {}
}

通过这个接口, 向kubelet提交当前插件的信息,包括插件的名称、版本、socket路径等。

已注册的插件信息并不会被持久化下来, 也就是说当kubelet重启后,插件需要重新调用Register方法。

kuelet重启时会删除插件的socket文件, 插件通过监听socket文件的方式来感知kubelet的重启并重新注册。

成功注册后,设备插件就向 kubelet 发送它所管理的设备列表,然后 kubelet 负责将这些资源发布到 API 服务器,作为 kubelet 节点状态更新的一部分。

当插件注册成功后, 根据插件中的配置与定义, 可能会有类似下面的pod配置以使用插件中的资源。

yaml 复制代码
apiVersion: v1
kind: Pod
metadata:
  name: demo-pod
spec:
  containers:
    - name: demo-container-1
      image: registry.k8s.io/pause:2.0
      resources:
        limits:
          hardware-vendor.example/foo: 2
#
# 这个 pod 需要两个 hardware-vendor.example/foo 设备
# 而且只能够调度到满足需求的节点上
#
# 如果该节点中有 2 个以上的设备可用,其余的可供其他 Pod 使用

AMD GPU插件源码解析

插件的实现并不复杂, 只需要实现几个接口函数即可。

go 复制代码
service DevicePlugin {
   // GetDevicePluginOptions 返回与设备管理器沟通的选项。
   // kuelet 在每次方法调用前都会调用这个方法,来获取可用的设备插件选项。
   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) {}
}

以下源码解析以AMD GPU插件为例。

代码版本 0.12.0

仓库地址 https://github.com/ROCm/k8s-device-plugin

源码解析

插件启动流程

AMD GPU插件的框架,是使用的"github.com/kubevirt/device-plugin-manager/pkg/dpm"这个包。

AMD的插件确实实现的很粗糙, 这里我们只用它分析实现一个插件需要做什么。

程序启动时实例化Manager对象, 并调用Run方法。

go 复制代码
func main() {
    // ...
    // Lister用于传递心跳与资源更新
	l := Lister{
		ResUpdateChan: make(chan dpm.PluginNameList),
		Heartbeat:     make(chan bool),
	}
	manager := dpm.NewManager(&l)
    // ...
    // 启动管理器
	manager.Run()
}

Run方法中启动了gRPC服务, 并注册了AMD GPU插件。

go 复制代码
func (dpm *Manager) Run() {
    // ...
    // 监听socket文件变化(kubelet会在重启时删除)
	fsWatcher, _ := fsnotify.NewWatcher()
	defer fsWatcher.Close()
	// DevicePluginPath = "/var/lib/kubelet/device-plugins/"
	fsWatcher.Add(pluginapi.DevicePluginPath)
    
    // 启动插件监听方法, 
    // 实际是将上面传入Liste.ResUpdateChan的数据转发到这个chan中
	pluginsCh := make(chan PluginNameList)
	defer close(pluginsCh)
	go dpm.lister.Discover(pluginsCh)
HandleSignals:
	for {
		select {
		case newPluginsList := <-pluginsCh:
			// 创建新的插件服务, 并启动服务
			dpm.handleNewPlugins(pluginMap, newPluginsList)
		case event := <-fsWatcher.Events:
			if event.Name == pluginapi.KubeletSocket {
				// kubelet重启时, 重新注册插件
				if event.Op&fsnotify.Create == fsnotify.Create {
					dpm.startPluginServers(pluginMap)
				}
				if event.Op&fsnotify.Remove == fsnotify.Remove {
					dpm.stopPluginServers(pluginMap)
				}
			}
		case s := <-signalCh:
			switch s {
			case syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGINT:
                // 优雅退出
				dpm.stopPlugins(pluginMap)
				break HandleSignals
			}
		}
	}
}

创建插件服务会返回一个devicePlugin对象:

go 复制代码
// dpm.handleNewPlugins(pluginMap, newPluginsList) 最终会调用这个方法
func newDevicePlugin(resourceNamespace string, pluginName string, devicePluginImpl PluginInterface) devicePlugin {
	return devicePlugin{
		DevicePluginImpl: devicePluginImpl,
		// DevicePluginPath = "/var/lib/kubelet/device-plugins/"
		// resourceNamespace = "amd.com"
		Socket:           pluginapi.DevicePluginPath + resourceNamespace + "_" + pluginName,
		ResourceName:     resourceNamespace + "/" + pluginName,
		Name:             pluginName,
		Starting:         &sync.Mutex{},
	}
}
type devicePlugin struct {
	// 实现的deviceplugin server
	DevicePluginImpl PluginInterface
	ResourceName     string
	Name             string
	// socket文件路径
	Socket           string
	Server           *grpc.Server
	Running          bool
	Starting         *sync.Mutex
}

启动服务最终会由StartServer这个方法来完成。

go 复制代码
func (dpi *devicePlugin) StartServer() error {
	// ...
	if dpi.Running {
		return nil
	}
    // 启动grpc服务
	err := dpi.serve()
	if err != nil {
		return err
	}
    // 调用Register方法向kubelet注册插件
	err = dpi.register()
	if err != nil {
		dpi.StopServer()
		return err
	}
	dpi.Running = true

	return nil
}
func (dpi *devicePlugin) serve() error {
	// ...
	// 可以看见是以socket文件启动的grpc服务
	sock, err := net.Listen("unix", dpi.Socket)
	if err != nil {
		glog.Errorf("%s: Failed to setup a DPI gRPC server: %s", dpi.Name, err)
		return err
	}

	dpi.Server = grpc.NewServer([]grpc.ServerOption{}...)
	pluginapi.RegisterDevicePluginServer(dpi.Server, dpi.DevicePluginImpl)
	go dpi.Server.Serve(sock)
	// ...
	return nil
}
func (dpi *devicePlugin) register() error {
	// KubeletSocket = DevicePluginPath + "kubelet.sock"
	// "/var/lib/kubelet/device-plugins/kubelet.sock"
	// 与kubelet通信
	conn, err := grpc.Dial(pluginapi.KubeletSocket, grpc.WithInsecure(),
		grpc.WithDialer(func(addr string, timeout time.Duration) (net.Conn, error) {
			return net.DialTimeout("unix", addr, timeout)
		}))
	defer conn.Close()
	client := pluginapi.NewRegistrationClient(conn)
	// 向kubelet注册插件
	reqt := &pluginapi.RegisterRequest{
		Version:      pluginapi.Version,
		Endpoint:     path.Base(dpi.Socket),
		ResourceName: dpi.ResourceName,
		Options:      options,
	}
	_, err = client.Register(context.Background(), reqt)
	// ...
	return nil
}

socket文件默认会放在/var/lib/kubelet/device-plugins目录下, 所以当以daemonset的方式部署插件时,需要将这个目录挂载到容器中。

服务实现

AMD GPU插件只实现了两个关键方法(因为不同设备插件的实现都不一样,所以这里不展开):

  • ListAndWatch
  • Allocate

所以它的GetDevicePluginOptions方法返回的是一个空结构体

go 复制代码
func (p *Plugin) GetDevicePluginOptions(ctx context.Context, e *pluginapi.Empty) (*pluginapi.DevicePluginOptions, error) {
	return &pluginapi.DevicePluginOptions{}, nil
}
type DevicePluginOptions struct {
	// 是否需要调用 PreStartContainer 方法
	PreStartRequired bool `protobuf:"varint,1,opt,name=pre_start_required,json=preStartRequired,proto3" json:"pre_start_required,omitempty"`
	// 是否需要调用 GetPreferredAllocation 方法
	GetPreferredAllocationAvailable bool     `protobuf:"varint,2,opt,name=get_preferred_allocation_available,json=getPreferredAllocationAvailable,proto3" json:"get_preferred_allocation_available,omitempty"`
}

服务部署

设备插件可以作为节点操作系统的软件包来部署、作为 DaemonSet 来部署或者手动部署。

如果你将设备插件部署为 DaemonSet, /var/lib/kubelet/device-plugins 目录必须要在插件的 PodSpec 中声明作为 卷(Volume)被挂载到插件中。

实现一个设备插件

  1. 实现一个虚假设备, 用于测试插件。(可选)
  2. 实现DevicePlugin接口。 我们可以仅实现ListAndWatchAllocate两个关键方法。
  3. 注册gRPC服务, 并向kubelet注册插件。
  4. 监听kubelet的socket文件变化, 重新注册插件。

代码实现

待补充...

相关推荐
AKAMAI6 小时前
AI需要防火墙,云计算需要重新构想
人工智能·云原生·云计算
Agome997 小时前
Docker之自定义jkd镜像上传阿里云
阿里云·docker·容器
小猿姐9 小时前
KubeBlocks for Milvus 揭秘
数据库·云原生
wdxylb12 小时前
云原生俱乐部-RH134知识点总结(2)
linux·云原生
deeper_wind12 小时前
k8s-单主机Master集群部署+单个pod部署lnmp论坛服务(小白的“升级打怪”成长之路)
云原生·容器·kubernetes
zhenshanrenhao15 小时前
#买硬盘欲安装k8s记
云原生·容器·kubernetes
bing.shao16 小时前
微服务容错与监控体系设计
微服务·云原生·架构
tb_first16 小时前
k8sday09
linux·云原生·容器·kubernetes
稚辉君.MCA_P8_Java18 小时前
豆包 Java的23种设计模式
java·linux·jvm·设计模式·kubernetes
天上掉下来个程小白19 小时前
Docker-14.项目部署-DockerCompose
运维·docker·微服务·容器