自定义资源支持:K8s Device Plugin 从原理到实现

如何实现一个K8S DevicePlugin? k8s允许限制容器对资源的使用,比如CPU和内存,并以此作为调度的依据。当其他非官方支持的设备类型需要参与到k8s的工作流程中时,就需要实现一个。Kubernetes提供了一个设备插件框架,你可以用它来将系统硬件资源发布到Kubelet。供应商可以实现设备插件,由你手动部署或作为 DaemonSet 来部署,而不必定制 Kubernetes 本身的代码。目标设备包括 GPU、高性能 NIC、FPGA、 InfiniBand 适配器以及其他类似的、可能需要特定于供应商的初始化和设置的计算资源。 阅读详情
AI摘要

本文分析了K8s中的Device Plugin机制工作原理,该机制可让K8s感知节点上的GPU等资源。其原理分为插件注册和Kubelet调用插件两部分。还实现了一个简单的i - device - plugin,包含启动注册、实现gRPC Server等,并进行了部署、初始化、创建测试Pod等测试。

kubernetes-horizontal-color.png

本文主要分析 k8s 中的 device-plugin 机制工作原理,并通过实现一个简单的 device-plugin 来加深理解。


1. 背景

默认情况下,k8s 中的 Pod 只能申请 CPU 和 Memory 这两种资源,就像下面这样:

resources:
  requests:
    memory: "1024Mi"
    cpu: "100m"
  limits:
    memory: "2048Mi"
    cpu: "200m"

随着 AI 热度越来越高,更多的业务 Pod 需要申请 GPU 资源,GPU 环境搭建指南:如何在裸机、Docker、K8s 等环境中使用 GPU 中我们分析了如何在 k8s 环境中使用 GPU,就是靠 Device Plugin 机制,通过该机制使得 k8s 能感知到节点上的 GPU 资源,就像原生的 CPU 和 Memory 资源一样使用。

实际上在早期,K8s 也提供了一种名为 alpha.kubernetes.io/nvidia-gpu 的资源来支持 NVIDIA GPU,不过后面也发现了很多问题,每增加一种资源都要修改 k8s 核心代码,k8s 社区压力山大。于是在 1.8 版本引入了 device plugin 机制,通过插件形式来接入其他资源,设备厂家只需要开发对应的 xxx-device-plugin 就可以将资源接入到 k8s 了。

ps:类似的还有引入 CSI 让存储插件从 Kubernetes 内部(in-tree)代码库中分离出来,改为独立的、可插拔的外部组件(out-of-tree),还有 CRI、CNI 等等,这里的 Device Plugin 也能算作其中的一种。

Device Plugin 有两层含义,下文中根据语义自行区分:

  • 首先它可以代表 k8s 中的 Device Plugin framework
  • 其次也可以代表厂家的具体实现,比如 NVIDIA/k8s-device-plugin,就是用于接入 NVIDIA GPU 资源的 Device Plugin 实现

2. 原理

Device Plugin 的工作原理其实不复杂,可以分为 插件注册 和 kubelet 调用插件两部分。

  • 插件注册:DevicePlugin 启动时会想节点上的 Kubelet 发起注册,这样 Kubelet就可以感知到该插件的存在了
  • kubelet 调用插件:注册完成后,当有 Pod 申请对于资源时,kubelet 就会调用该插件 API 实现具体功能

如 k8s 官网上的图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Kubelet 部分

为了提供该功能,Kubelet 新增了一个 Registration gRPC service:

service Registration {
	rpc Register(RegisterRequest) returns (Empty) {}
}

device plugin 可以调用该接口向 Kubelet 进行注册,注册接口需要提供三个参数:

  • device plugin 对应的 unix socket 名字:后续 kubelet 根据名称找到对应的 unix socket,并向插件发起调用

  • device plugin 调 API version:用于区分不同版本的插件

  • device plugin 提供的 ResourceName:遇到不能处理的资源申请时(CPU和Memory之外的资源),Kubelet 就会根据申请的资源名称来匹配对应的插件

    • ResourceName 需要按照vendor-domain/resourcetype 格式,例如nvidia.com/gpu。

device plugin 部分

要进行设备管理,device plugin 插件需要实现以下接口:

  • GetDevicePluginOptions:这个接口用于获取设备插件的信息,可以在其返回的响应中指定一些设备插件的配置选项,可以看做是插件的元数据

  • ListAndWatch:该接口用于列出可用的设备并持续监视这些设备的状态变化。

  • GetPreferredAllocation:将分配偏好信息提供给 device plugin,以便 device plugin 在分配时可以做出更好的选择

  • Allocate:该接口用于向设备插件请求分配指定数量的设备资源。
  • PreStartContainer: 该接口在容器启动之前调用,用于配置容器使用的设备资源。

只有 ListAndWatch 和 Allocate 两个接口是必须的,其他都是可以选的。

工作流程

一般所有的 Device Plugin 实现最终都会以 Pod 形式运行在 k8s 集群中,又因为需要管理所有节点,因此都会以 DaemonSet 方式部署。

device plugin 启动之后第一步就是向 Kubelet 注册,让 Kubelet 知道有一个新的设备接入了。

为了能够调用 Kubelet 的 Register 接口,Device Plugin Pod 会将宿主机上的 kubelet.sock 文件(unix socket)挂载到容器中,通过 kubelet.sock 文件发起调用以实现注册。

集群部署后,Kubelet 就会启动,

  • 1)Kubelet 启动 Registration gRPC 服务(kubelet.sock),提供 Register 接口

  • 2)device-plugin 启动后,通过 kubelet.sock 调用 Register 接口,向 Kubelet 进行注册,注册信息包括 device plugin 的 unix socket,API Version,ResourceName

  • 3)注册成功后,Kubelet 通过 device-plugin 的 unix socket 向 device plugin 调用 ListAndWatch, 获取当前节点上的资源

  • 4)Kubelet 向 api-server 更新节点状态来记录上一步中发现的资源

    • 此时 kubelet get node -oyaml 就能查看到 Node 对象的 Capacity 中多了对应的资源

    5)用户创建 Pod 并申请该资源,调度完成后,对应节点上的 kubelet 调用 device plugin 的 Allocate 接口进行资源分配

大致如下:

k8s-device-plugin-timeline


**【Kubernetes 系列】**持续更新中,搜索公众号【探索云原生】订阅,阅读更多文章。


3. 实现

源码:https://github.com/lixd/i-device-plugin

device plugin 实现大致分为三部分:

  • 1)启动时向 Kubelet 发起注册
    • 注意监控 kubelet 的重启,一般是使用 fsnotify 类似的库监控 kubelet.sock 的重新创建事件。如果 kubelet.sock 重新创建了,则认为 kubelet 是重启了,那么需要重新注册
  • 2)gRPC Server:主要是实现 ListAndWatch 和 Allocate两个方法

实现 gRPC Server

简单起见,这里只实现了ListAndWatch 和 Allocate 这两个必须的方法。

对 gRPC 不熟悉的童鞋可以看下这个 --> gRPC 系列教程

ListAndWatch

这是一个 gRPC 的 Stream 方法,建立长连接,可以持续向 Kubelet 发送设备的信息。

// ListAndWatch returns a stream of List of Devices
// Whenever a Device state change or a Device disappears, ListAndWatch
// returns the new list
func (c *GopherDevicePlugin) ListAndWatch(_ *pluginapi.Empty, srv pluginapi.DevicePlugin_ListAndWatchServer) error {
	devs := c.dm.Devices()
	klog.Infof("find devices:%s", String(devs))

	err := srv.Send(&pluginapi.ListAndWatchResponse{Devices: devs})
	if err != nil {
		return errors.WithMessage(err, "send device failed")
	}

	klog.Infoln("waiting for device update")
	for range c.dm.notify {
		devs = c.dm.Devices()
		klog.Infof("device update,new device list:%s", String(devs))
		_ = srv.Send(&pluginapi.ListAndWatchResponse{Devices: devs})
	}
	return nil
}

发现设备的部分代码如下:

func (d *DeviceMonitor) List() error {
	err := filepath.Walk(d.path, func(path string, info fs.FileInfo, err error) error {
		if info.IsDir() {
			klog.Infof("%s is dir,skip", path)
			return nil
		}

		d.devices[info.Name()] = &pluginapi.Device{
			ID:     info.Name(),
			Health: pluginapi.Healthy,
		}
		return nil
	})

	return errors.WithMessagef(err, "walk [%s] failed", d.path)
}

很简单,就是遍历查看 /etc/gophers 目录下的所有文件,每个文件都会当做一个设备。

然后再启动一个 Goroutine 监控设备的变化,即/etc/gophers 目录下文件有变化时通过 chan 发送通知,将最新的设备信息发送给 Kubelet。

func (d *DeviceMonitor) Watch() error {
	klog.Infoln("watching devices")

	w, err := fsnotify.NewWatcher()
	if err != nil {
		return errors.WithMessage(err, "new watcher failed")
	}
	defer w.Close()

	errChan := make(chan error)
	go func() {
		defer func() {
			if r := recover(); r != nil {
				errChan <- fmt.Errorf("device watcher panic:%v", r)
			}
		}()
		for {
			select {
			case event, ok := <-w.Events:
				if !ok {
					continue
				}
				klog.Infof("fsnotify device event: %s %s", event.Name, event.Op.String())

				if event.Op == fsnotify.Create {
					dev := path.Base(event.Name)
					d.devices[dev] = &pluginapi.Device{
						ID:     dev,
						Health: pluginapi.Healthy,
					}
					d.notify <- struct{}{}
					klog.Infof("find new device [%s]", dev)
				} else if event.Op&fsnotify.Remove == fsnotify.Remove {
					dev := path.Base(event.Name)
					delete(d.devices, dev)
					d.notify <- struct{}{}
					klog.Infof("device [%s] removed", dev)
				}

			case err, ok := <-w.Errors:
				if !ok {
					continue
				}
				klog.Errorf("fsnotify watch device failed:%v", err)
			}
		}
	}()

	err = w.Add(d.path)
	if err != nil {
		return fmt.Errorf("watch device error:%v", err)
	}

	return <-errChan
}
Allocate

Allocate 则是需要告知 kubelet 怎么将设备分配给容器,这里实现比较简单,就是在对应容器中增加一个环境变量,Gopher=$deviceId

// 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 (c *GopherDevicePlugin) Allocate(_ context.Context, reqs *pluginapi.AllocateRequest) (*pluginapi.AllocateResponse, error) {
	ret := &pluginapi.AllocateResponse{}
	for _, req := range reqs.ContainerRequests {
		klog.Infof("[Allocate] received request: %v", strings.Join(req.DevicesIDs, ","))
		resp := pluginapi.ContainerAllocateResponse{
			Envs: map[string]string{
				"Gopher": strings.Join(req.DevicesIDs, ","),
			},
		}
		ret.ContainerResponses = append(ret.ContainerResponses, &resp)
	}
	return ret, nil
}

简单看一下 NVIDIA 的 device plugin 是怎么实现 Allocate 的。

// Allocate which return list of devices.
func (plugin *NvidiaDevicePlugin) Allocate(ctx context.Context, reqs *pluginapi.AllocateRequest) (*pluginapi.AllocateResponse, error) {
	responses := pluginapi.AllocateResponse{}
	for _, req := range reqs.ContainerRequests {
		if err := plugin.rm.ValidateRequest(req.DevicesIDs); err != nil {
			return nil, fmt.Errorf("invalid allocation request for %q: %w", plugin.rm.Resource(), err)
		}
		response, err := plugin.getAllocateResponse(req.DevicesIDs)
		if err != nil {
			return nil, fmt.Errorf("failed to get allocate response: %v", err)
		}
		responses.ContainerResponses = append(responses.ContainerResponses, response)
	}

	return &responses, nil
}

核心其实是这个方法:

// updateResponseForDeviceListEnvvar sets the environment variable for the requested devices.
func (plugin *NvidiaDevicePlugin) updateResponseForDeviceListEnvvar(response *pluginapi.ContainerAllocateResponse, deviceIDs ...string) {
	response.Envs[plugin.deviceListEnvvar] = strings.Join(deviceIDs, ",")
}

给容器添加了一个环境变量,value 为设备 id,具体 deviceID 提供了两种测量,可能是编号或者 uuid

const (
	DeviceIDStrategyUUID  = "uuid"
	DeviceIDStrategyIndex = "index"
)

key 是一个变量 plugin.deviceListEnvvar,初始化如下:

	plugin := NvidiaDevicePlugin{
		deviceListEnvvar:     "NVIDIA_VISIBLE_DEVICES",
		socket:               pluginPath + ".sock",
	  // ...
	}

也就是说 NVIDIA 这个 device plugin 实现 Allocate 主要就是给容器增加了环境变量,例如:

NVIDIA_VISIBLE_DEVICES="0,1"

在文章 GPU 环境搭建指南:使用 GPU Operator 加速 Kubernetes GPU 环境搭建 中提到 GPU Operator 会使用 NVIDIA Container Toolit Installer 安装 NVIDIA Container Toolit。

这个 NVIDIA Container Toolit 的作用就是添加对 GPU 的支持,也包括了识别 NVIDIA_VISIBLE_DEVICES 这个环境变量,然后将对应设备挂载到容器里。

除此之外还会把设备挂载到容器里:

func (plugin *NvidiaDevicePlugin) apiDeviceSpecs(devRoot string, ids []string) []*pluginapi.DeviceSpec {
	optional := map[string]bool{
		"/dev/nvidiactl":        true,
		"/dev/nvidia-uvm":       true,
		"/dev/nvidia-uvm-tools": true,
		"/dev/nvidia-modeset":   true,
	}

	paths := plugin.rm.GetDevicePaths(ids)

	var specs []*pluginapi.DeviceSpec
	for _, p := range paths {
		if optional[p] {
			if _, err := os.Stat(p); err != nil {
				continue
			}
		}
		spec := &pluginapi.DeviceSpec{
			ContainerPath: p,
			HostPath:      filepath.Join(devRoot, p),
			Permissions:   "rw",
		}
		specs = append(specs, spec)
	}

	return specs
}

核心为:

		spec := &pluginapi.DeviceSpec{
			ContainerPath: p,
			HostPath:      filepath.Join(devRoot, p),
			Permissions:   "rw",
		}

这里指定了设备在宿主机上的 Path 和挂载到容器之后的 Path,后续就可以根据这些信息进行设备挂载了。

其他方法

另外几个方法非强制的,因此只做一个空实现。

// GetDevicePluginOptions returns options to be communicated with Device
// Manager
func (c *GopherDevicePlugin) GetDevicePluginOptions(_ context.Context, _ *pluginapi.Empty) (*pluginapi.DevicePluginOptions, error) {
	return &pluginapi.DevicePluginOptions{PreStartRequired: true}, nil
}

// GetPreferredAllocation returns a preferred set of devices to allocate
// from a list of available ones. The resulting preferred allocation is not
// guaranteed to be the allocation ultimately performed by the
// devicemanager. It is only designed to help the devicemanager make a more
// informed allocation decision when possible.
func (c *GopherDevicePlugin) GetPreferredAllocation(_ context.Context, _ *pluginapi.PreferredAllocationRequest) (*pluginapi.PreferredAllocationResponse, error) {
	return &pluginapi.PreferredAllocationResponse{}, nil
}

// PreStartContainer is called, if indicated by Device Plugin during registeration phase,
// before each container start. Device plugin can run device specific operations
// such as reseting the device before making devices available to the container
func (c *GopherDevicePlugin) PreStartContainer(_ context.Context, _ *pluginapi.PreStartContainerRequest) (*pluginapi.PreStartContainerResponse, error) {
	return &pluginapi.PreStartContainerResponse{}, nil
}

向 Kubelet 进行注册

注册也是很简单,调用 deviceplugin 提供的 RegisterRequest 方法即可。

// Register registers the device plugin for the given resourceName with Kubelet.
func (c *GopherDevicePlugin) Register() error {
	conn, err := connect(pluginapi.KubeletSocket, common.ConnectTimeout)
	if err != nil {
		return errors.WithMessagef(err, "connect to %s failed", pluginapi.KubeletSocket)
	}
	defer conn.Close()

	client := pluginapi.NewRegistrationClient(conn)
	reqt := &pluginapi.RegisterRequest{
		Version:      pluginapi.Version,
		Endpoint:     path.Base(common.DeviceSocket),
		ResourceName: common.ResourceName,
	}

	_, err = client.Register(context.Background(), reqt)
	if err != nil {
		return errors.WithMessage(err, "register to kubelet failed")
	}
	return nil
}

监控 kubelet.sock 状态

使用 fsnotify 库监控 kubelet.sock 文件状态,通过 kubelet.sock 文件的变化来判断 kubelet 是否重启,当 kubelet 重启后 device plugin 也需要重启,然后注册到新的 kubelet.sock。

// WatchKubelet restart device plugin when kubelet restarted
func WatchKubelet(stop chan<- struct{}) error {
	watcher, err := fsnotify.NewWatcher()
	if err != nil {
		return errors.WithMessage(err, "Unable to create fsnotify watcher")
	}
	defer watcher.Close()

	go func() {
		// Start listening for events.
		for {
			select {
			case event, ok := <-watcher.Events:
				if !ok {
					continue
				}
				klog.Infof("fsnotify events: %s %v", event.Name, event.Op.String())
				if event.Name == pluginapi.KubeletSocket && event.Op == fsnotify.Create {
					klog.Warning("inotify: kubelet.sock created, restarting.")
					stop <- struct{}{}
				}
			case err, ok := <-watcher.Errors:
				if !ok {
					continue
				}
				klog.Errorf("fsnotify failed restarting,detail:%v", err)
			}
		}
	}()

	// watch kubelet.sock
	err = watcher.Add(pluginapi.KubeletSocket)
	if err != nil {
		return errors.WithMessagef(err, "Unable to add path %s to watcher", pluginapi.KubeletSocket)
	}
	return nil
}
为什么需要重新注册

因为Kubelet 中使用一个 map 来存储注册的插件,因此每次 Kubelet 重启都会丢失,所以我们在实现 device plugin 时就要监控 Kubelet 重启状态并重新注册。

Kubelet Register 方法 实现如下:

// /pkg/kubelet/cm/devicemanager/plugin/v1beta1/server.go#L143-L165
func (s *server) Register(ctx context.Context, r *api.RegisterRequest) (*api.Empty, error) {
	klog.InfoS("Got registration request from device plugin with resource", "resourceName", r.ResourceName)
	metrics.DevicePluginRegistrationCount.WithLabelValues(r.ResourceName).Inc()

	if !s.isVersionCompatibleWithPlugin(r.Version) {
		err := fmt.Errorf(errUnsupportedVersion, r.Version, api.SupportedVersions)
		klog.InfoS("Bad registration request from device plugin with resource", "resourceName", r.ResourceName, "err", err)
		return &api.Empty{}, err
	}

	if !v1helper.IsExtendedResourceName(core.ResourceName(r.ResourceName)) {
		err := fmt.Errorf(errInvalidResourceName, r.ResourceName)
		klog.InfoS("Bad registration request from device plugin", "err", err)
		return &api.Empty{}, err
	}

	if err := s.connectClient(r.ResourceName, filepath.Join(s.socketDir, r.Endpoint)); err != nil {
		klog.InfoS("Error connecting to device plugin client", "err", err)
		return &api.Empty{}, err
	}

	return &api.Empty{}, nil
}

核心在 connectClient 方法:

func (s *server) connectClient(name string, socketPath string) error {
	c := NewPluginClient(name, socketPath, s.chandler)

	s.registerClient(name, c)
	if err := c.Connect(); err != nil {
		s.deregisterClient(name)
		klog.ErrorS(err, "Failed to connect to new client", "resource", name)
		return err
	}

	go func() {
		s.runClient(name, c)
	}()

	return nil
}

怎么保存这个 client 的呢?

func (s *server) registerClient(name string, c Client) {
	s.mutex.Lock()
	defer s.mutex.Unlock()

	s.clients[name] = c
	klog.V(2).InfoS("Registered client", "name", name)
}

定义如下:

type server struct {
	socketName string
	socketDir  string
	mutex      sync.Mutex
	wg         sync.WaitGroup
	grpc       *grpc.Server
	rhandler   RegistrationHandler
	chandler   ClientHandler
	clients    map[string]Client // 使用 map 存储,并为持久化
}

main.go

main 方法分为三个部分:

  • 1)启动 gRPC 服务
  • 2)向 Kubelet 进行注册
  • 3)监控 kubelet.sock 状态
func main() {
	klog.Infof("device plugin starting")
	dp := device_plugin.NewGopherDevicePlugin()
	go dp.Run()

	// register when device plugin start
	if err := dp.Register(); err != nil {
		klog.Fatalf("register to kubelet failed: %v", err)
	}

	// watch kubelet.sock,when kubelet restart,exit device plugin,then will restart by DaemonSet
	stop := make(chan struct{})
	err := utils.WatchKubelet(stop)
	if err != nil {
		klog.Fatalf("start to kubelet failed: %v", err)
	}

	<-stop
	klog.Infof("kubelet restart,exiting")
}

4. 测试

部署

首先是部署 i-device-plugin,一般使用 DaemonSet 方式部署,完整 yaml 如下:

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: i-device-plugin
  namespace: kube-system
  labels:
    app: i-device-plugin
spec:
  selector:
    matchLabels:
      app: i-device-plugin
  template:
    metadata:
      labels:
        app: i-device-plugin
    spec:
      containers:
        - name: i-device-plugin
          image: docker.io/lixd96/i-device-plugin:latest
          imagePullPolicy: IfNotPresent
          resources:
            limits:
              cpu: "1"
              memory: "512Mi"
            requests:
              cpu: "0.1"
              memory: "128Mi"
          volumeMounts:
            - name: device-plugin
              mountPath: /var/lib/kubelet/device-plugins
            - name: gophers
              mountPath: /etc/gophers
      volumes:
        - name: device-plugin
          hostPath:
            path: /var/lib/kubelet/device-plugins
        - name: gophers
          hostPath:
            path: /etc/gophers

以 hostPath 方式将用到的两个目录挂载到 Pod 里:

  • /var/lib/kubelet/device-plugins:请求 kubelet.sock 发起调用,同时将 device-plugin gRPC 服务的 sock 文件写入该目录供 kubelet 调用
  • /etc/gophers:在该 Demo 中,把 /etc/gophers 目录下的文件作为设备,因此需要将其挂载到 Pod 里。

确保 i-device-plugin 已经启动。

[root@test ~]# kubectl -n kube-system get po
i-device-plugin-vnw6z            1/1     Running   0          17s

初始化

在该 Demo 中,把 /etc/gophers 目录下的文件作为设备,因此我们只需要到 /etc/gophers 目录下创建文件,模拟有新的设备接入即可。

mkdir /etc/gophers

touch /etc/gophers/g1

查看 device plugin 日志

[root@test ~]# kubectl -n kube-system logs -f i-device-plugin-vnw6z
I0719 13:52:24.674737       1 main.go:10] device plugin starting
I0719 13:52:24.675440       1 device_monitor.go:33] /etc/gophers is dir,skip
I0719 13:52:24.675679       1 device_monitor.go:49] watching devices
I0719 13:52:24.682141       1 api.go:22] find devices []
I0719 13:52:24.682315       1 api.go:29] waiting for device update
I0719 13:53:09.369381       1 device_monitor.go:70] fsnotify device event: /etc/gophers/g1 CREATE
I0719 13:53:09.370394       1 device_monitor.go:79] find new device [g1]
I0719 13:53:09.370445       1 device_monitor.go:70] fsnotify device event: /etc/gophers/g1 CHMOD
I0719 13:53:09.370659       1 api.go:32] device update,new device list [g1]

可以看到,已经感知到新增的设备了。

不出意外的话可以在 node 上看到新资源了

[root@test gophers]# k get node n1 -oyaml|grep  capacity -A 7
  capacity:
    cpu: "4"
    ephemeral-storage: 20960236Ki
    hugepages-1Gi: "0"
    hugepages-2Mi: "0"
    lixueduan.com/gopher: "1"
    memory: 8154984Ki
    pods: "110"

果然,node capacity 中新增了lixueduan.com/gopher: "1"。

创建测试 Pod

接下来创建一个 Pod 申请该资源试试

apiVersion: v1
kind: Pod
metadata:
  name: gopher-pod
spec:
  containers:
  - name: gopher-container
    image: busybox
    command: ["sh", "-c", "echo Hello, Kubernetes! && sleep 3600"]
    resources:
      requests:
        lixueduan.com/gopher: "1"
      limits:
        lixueduan.com/gopher: "1"

Pod 启动成功

[root@test ~]# kubectl get po
NAME         READY   STATUS    RESTARTS   AGE
gopher-pod   1/1     Running   0          27s

之前分配设备是添加 Gopher=xxx 这个环境变量,现在看下是否正常分配

[root@test ~]# kubectl exec -it gopher-pod -- env|grep Gopher
Gopher=g1

ok,环境变量存在,可以看到分配给该 Pod 的设备是 g1。

新增设备

使用同样的 yaml 改下名称再创建一个 Pod

[root@test ~]# k get po
NAME          READY   STATUS    RESTARTS   AGE
gopher-pod    1/1     Running   0          3m9s
gopher-pod2   0/1     Pending   0          2s

因为只有一个 gopher 资源,因此第二个 Pod pending 了。

Events:
  Type     Reason            Age   From               Message
  ----     ------            ----  ----               -------
  Warning  FailedScheduling  7s    default-scheduler  0/1 nodes are available: 1 Insufficient lixueduan.com/gopher. preemption: 0/1 nodes are available: 1 No preemption victims found for incoming pod..

在创建一个设备

touch /etc/gophers/g2

device plugin 立马感知到了设备变化,相关日志如下:

I0719 14:01:00.308599       1 device_monitor.go:70] fsnotify device event: /etc/gophers/g2 CREATE
I0719 14:01:00.308986       1 device_monitor.go:79] find new device [g2]
I0719 14:01:00.309017       1 device_monitor.go:70] fsnotify device event: /etc/gophers/g2 CHMOD
I0719 14:01:00.309141       1 api.go:32] device update,new device list [g2,g1]

node 上的资源数量也更新为 2

[root@argo-1 ~]# k get node argo-1 -oyaml|grep  capacity -A 7
  capacity:
    cpu: "4"
    ephemeral-storage: 20960236Ki
    hugepages-1Gi: "0"
    hugepages-2Mi: "0"
    lixueduan.com/gopher: "2"
    memory: 8154984Ki
    pods: "110"

然后 pod2 也可以正常启动了

[root@test ~]# kubectl get po
NAME          READY   STATUS    RESTARTS   AGE
gopher-pod    1/1     Running   0          4m31s
gopher-pod2   1/1     Running   0          84s

删除设备

然后删除 g2 设备

rm -rf /etc/gophers/g2

device plugin 也是能正常感知到,相关日志

I0719 14:03:55.904983       1 device_monitor.go:70] fsnotify device event: /etc/gophers/g2 REMOVE
I0719 14:03:55.905203       1 device_monitor.go:84] device [g2] removed
I0719 14:03:55.905267       1 api.go:32] device update,new device list [g1]

查看 Node 上的资源数量更新没有

[root@test ~]# k get node argo-1 -oyaml|grep  capacity -A 7
  capacity:
    cpu: "4"
    ephemeral-storage: 20960236Ki
    hugepages-1Gi: "0"
    hugepages-2Mi: "0"
    lixueduan.com/gopher: "1"
    memory: 8154984Ki
    pods: "110"

对应资源也变成 1 个了,一切正常。

5. 小结

本文主要分析了 k8s 中的 Device Plugin 机制的工作原理,并实现了一个简单的 **i-device-plugin**来进一步加深理解。

Device Plugin 的工作原理其实不复杂,可以分为 插件注册 和 kubelet 调用插件两部分:

  • 插件注册:DevicePlugin 启动时会想节点上的 Kubelet 发起注册,这样 Kubelet就可以感知到该插件的存在了
  • kubelet 调用插件:注册完成后,当有 Pod 申请对于资源时,kubelet 就会调用该插件 API 实现具体功能

**【Kubernetes 系列】**持续更新中,搜索公众号【探索云原生】订阅,阅读更多文章。


6. 参考

k8s 文档:device-plugins

https://github.com/NVIDIA/k8s-device-plugin

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

Kubelet Register 源码

k8s英伟达GPU插件(nvidia-device-plugin) 阅读详情

相关推荐

【云原生】深入浅出 K8s 设备插件技术(Device Plugin)

Kubernetes 提供了 Device Plugin 机制,用于向 kubelet 上报硬件信息并配置容器资源。本文以 NVIDIA GPU Plugin 为例,通俗易懂 并 深入浅出 地剖析注册、ListAndWatch、Allocate 及 kubelet 管理流程,介绍常见问题和配置要点。

qq871325148的博客 2089

【k8s调度】梳理调度相关知识与device plugin

比如,PodFitsHost 算法(负责的是,检查 Pod 的 nodeSelector 与 Node 的名字是否匹配),这种情况下,除非 Node 的名字发生变化,否则你即使删除再多的 Pod,抢占者也不可能调度成功。具体来说,在为某一对 Pod 和 Node 执行 Predicates 算法的时候,如果待检查的 Node 是一个即将被抢占的节点,即:调度队列里有 nominatedNodeName 字段值是该 Node 名字的 Pod 存在(可以称之为:“潜在的抢占者”)。

叮当猫的喵i 1818

打通任督二脉 - Device Plugin 让 k8s “看见” GPU

你可以把k8s的 Device Plugin 框架想象成一个开放的“硬件接入标准”,有点像电脑上的 USB 接口标准。类比:有了 USB 标准,无论是鼠标、键盘、U 盘还是摄像头,只要硬件厂商按照这个标准来设计接口,你的电脑就能识别并使用它们,而不需要电脑厂商为市面上每一款新硬件都单独修改操作系统。k8s标准接口:它定义了一套标准的通信协议(基于 gRPC),让外部程序(就是所谓的 Device Plugin)可以和kubelet(运行在每个k8s节点上的“管家进程”)对话。硬件厂商负责。

weixin_42587823的博客 1682

【K8S device-plugin】以 vgpu 项目分析 device-plugin、Scheduler Extender Plugin、KubeSchedulerConfiguration 关系

注意此处,若没有 KubeSchedulerConfiguration 的声明配置,k8s 会认为这些(nvidia.com/gpumem、nvidia.com/gpucores)是资源设备,k8s 调度时候会进行寻找,发现所有节点上都没此资源设备配额(kubectl describe node nodeName 可以看到每个 node 的设备资源情况),导致该 pod 调度失败。首先创建个 device-plugin,注册 gpu 资源(nvidia.com/gpu)

叮当猫的喵i 1744

【k8s-device plugin】如何编写 k8s device plugin

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 资源为例。

叮当猫的喵i 2514

Kubernetes 中的 GPU 管理与 Device Plugin 机制深度解析

Device Plugin 是 Kubernetes 中的一种扩展机制,用于将特殊的硬件设备暴露给 Kubernetes 集群。对于 GPU 来说,Device Plugin 可以将节点上的 GPU 设备注册到 Kubernetes 中,使其成为可分配的资源。Kubernetes 中的 GPU 管理和 Device Plugin 机制为使用 GPU 进行高性能计算提供了强大的支持。

yonggeit的博客 3655

深入解析 Kubernetes Device Plugin:从原理到实现

Kubernetes 本身通过节点资源(如 CPU、内存、存储)来进行资源管理。然而,标准的资源管理机制无法满足特定硬件资源的需求,尤其是当需要为特殊硬件资源(如 GPU、FPGA 或其他专用硬件加速器)提供支持时。机制是 Kubernetes 提供的一种扩展方式,允许用户为集群添加对非标准硬件资源的管理支持。动态发现节点上可用的设备。将设备资源分配给 Pod。在 Pod 生命周期内管理设备资源。支持设备健康监控。

这里只有干货:从 Modbus 通信到 MES 对接,从 YOLO 训练到工控机部署,带你搞定工业软件开发全流程。 1245

LinuxMint 22.1(Ubuntu24.04)下通过Wine完美运行同花顺远航版的实战指南

本文详细介绍了在LinuxMint 22.1(Ubuntu24.04)系统下通过Wine完美运行同花顺远航版的实战指南。从基础环境准备到深度优化,包括Wine环境配置、同花顺远航版安装、字体显示调优、任务栏兼容性修复等关键步骤,帮助Linux用户高效使用同花顺远航版进行股票交易和行情分析。

weixin_30919429的博客 571

kubernetes Device Plugin原理与源码分析

深入理解kubernetes device plugin原理

NUCEMLS的博客 2451

FSMC驱动LCD显示屏学习2

液晶显示器,简称 LCD(Liquid Crystal Display)相对于上一代 CRT 显示器 (阴极射线管显示器),LCD显示器具有功耗低、体积小、承载的信息量大及不伤眼的优点,因而它成为了现在的主流电子显示设备液晶是一种介于固体和液体之间的特殊物质,它是一种有机化合物,常态下呈液态,但是它的分 子排列却和固体晶体一样非常规则,因此取名液晶。

usjjjsj的博客 1696

AnolisOS网卡驱动源码编译与安装实战指南

本文提供了一份详尽的AnolisOS网卡驱动源码编译与安装实战指南。针对系统自带通用驱动不兼容导致网络无法使用的问题,文章从环境准备、源码获取、编译安装到新旧驱动替换与持久化配置,一步步解析了如何通过源码编译安装官方驱动,解决硬件兼容性问题,并确保驱动在系统重启后自动生效。

weixin_28358083的博客 304

在 K8S 中创建 Pod 是如何使用到 GPU 的: nvidia device plugin 源码分析

在 两篇文章中分别分享了在不同环境如何使用 GPU,以及在 k8s 中使用 GPU Operator 来加速部署。在 中则是分析了 Device Plugin 的实现原理。宿主机上的 GPU 是怎么能够被 K8s 中的 Pod 使用的。1)k8s 是如何感知到 GPU 的2)GPU 是如何分配给 Pod 的。

探索云原生 2050

NetApp 7-mode初始化详解(原AGGR重建)及注意事项

没想到有生之年还能再写个7-mode的文章,上次接触可能已将近十年,本次恰巧碰到这个机会,边学习边整理,整整一天时间,碰到了不少坑,作为记录。

RaySun的技术Blog 2752

【K8s运维实战】Kubernetes GPU 调度实战:解决 Device Plugin 部署中 5 类高频故障

本文为SRE工程师提供了一份30分钟内快速部署和排查Kubernetes GPU设备插件的实战指南。主要内容包括:1. 核心原理:DevicePlugin通过gRPC服务实现GPU资源注册、上报和分配流程 2. 部署步骤: 必须前置安装NVIDIA驱动和Container Toolkit 提供Helm和静态YAML两种部署方式 强调containerd环境下RuntimeClass的必要配置 五大常见故障排查: 容器无法访问GPU 节点资源显示异常 Pod调度失败 插件Pod崩溃 资源申请语法错误 生产建议

kguogao的博客 1060

从0开始,部署micro_ros+stm32f407+stm32cubeIDE

(5)安装依赖项rosdep时,必须处于创建的microros_ws中,因为里面有clone的src文件夹。(3)教程a中的安装docker不是必须的,取决于运行agent时是否想使用docker。(4)记得安装rosdep,并在以后的命令中将rosdep替换为rosdepc。4、运行agent,选择udp、tcp、can、串口,这里没有使用docker。(1)首先参考教程a安装虚拟机和ubuntu,注意版本选择22.04;(2)参考教程b部署micro_ros构建系统。二、安装和运行agent。

m0_37963053的博客 2234

SerDes学习笔记:均衡技术详解 01

本文系统阐述了高速SerDes链路中均衡技术的应用原理与实现方法。重点介绍了发射端预加重/FIR均衡和接收端CTLE/DFE/FFE等主流均衡方案,详细探讨了各类均衡器的频域特性、时域效果及硬件实现架构。

一只豌豆象的博客 1302

Kubernetes GPU 管理与 Device Plugin 机制

本文探讨了Kubernetes对GPU资源管理的实现机制。主要内容包括:1)GPU在AI计算中的核心作用,其并行计算特性与CPU形成互补;2)Kubernetes通过ExtendedResource机制支持GPU资源声明和调度,利用DevicePlugin插件自动发现和管理GPU设备;3)DevicePlugin通过gRPC与kubelet交互,完成设备分配和驱动挂载;4)当前设计存在局限性,如无法支持复杂设备属性和高级调度策略;5)该架构遵循最小化接口原则,权衡了功能扩展性与系统稳定性。整体展现了Kube

QI8811481的博客 725

从 SAP BTP trial 到第一个 CPI iFlow 的实操路线图

本文介绍了如何从零开始学习SAP CPI(现称SAP Integration Suite中的Cloud Integration能力)。主要内容包括:1)明确学习边界,建议使用SAP BTP trial账号进行个人练习,注意90天有效期限制;2)理解SAP BTP账号模型层级结构(Global Account→Subaccount→Space);3)详细说明申请trial账号流程,推荐选择Singapore区域以避免兼容问题;4)指导如何检查Entitlements和Quotas,强调应专注于SAP Inte

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 167

从 K8s Device Plugin 到 Volcano 多元算力管理:GPU 显存共享实战与深度解析

在云原生 AI 和大数据场景下,Kubernetes(K8s)的原生资源管理模型在面对 GPU 等异构算力时显得力不从心。K8s 的 Device Plugin 机制虽然提供了扩展能力,但默认只能以 “整卡” 为单位分配 GPU,导致资源利用率低下。Volcano 作为云原生批量计算引擎,通过扩展 Device Plugin 机制,实现了包括 GPU 显存共享在内的多元算力精细化管理。

qq_43394776的博客 731

OSG第三方库编译之二十一:minizip编译(Windows、Linux、Macos环境下编译)

OSG编译;第三方库;跨平台编译;minizip编译(Windows、Linux、Macos环境下编译)

LuckyHanMo的博客 2621

TMS_WebGMaps_1.9.8.0_Full_Source

TMS_WebGMaps_1.9.8.0_Full_Source 全源码 delphi cbuilder

Kubernetes GPU 调度和 Device Plugin、CDI、NFD、GPU Operator 概述

通过CDINFD和Operator机制,Kubernetes 实现了对 GPU 等特殊硬件资源的自动化管理与高效调度。然而,特定厂商的硬件仍然存在差异化的部署与配置,使用时需要额外关注。

凌虚Blog 1248

计算机控制技术及应用

《计算机控制技术及应用》通过总结作者的科研成果和吸收国内外的先进理论、方法和技术,以计算机控制系统的网络化、开放化、智能化和集成化发展趋势为主线索,主要介绍计算机控制系统的基本概念和计算机控制系统的典型形式,计算机控制系统的数学描述与基本分析方法,以及数字PID控制器、最少拍控制系统、达林算法、状态变量设计法等设计方法及其各种改进算法。重点突出计算机控制系统输入输出通道接口的设计方法和典型控制系统的设计案例与实现方法等。

Kubernetes接入NVIDIA GPU完整指南:从驱动到Device Plugin

GPU(图形处理器)已成为AI训练与推理场景中的核心算力资源,如何在容器化环境中高效调度GPU,成为云原生技术栈中备受关注的工程问题。Kubernetes作为业界标准的容器编排平台,通过扩展资源机制与设备插件(Device Plugin)框架,能够将底层GPU硬件抽象为可调度的集群资源。这一机制依赖于三层基础设施的协同工作:宿主机NVIDIA驱动提供设备访问能力,容器运行时负责将GPU设备与库文件注入容器,Device Plugin则向kubelet上报资源并为Pod分配显卡。理解这一分层架构,有助于快速定

weixin_34088583的博客 226

NTscan扫描工具

ntscan是一个扫描器,可以扫描没有密码和弱密码的共享,先到配置文件中填写猜测的用户名和密码,扫描过程中右侧出现的是扫描日志。

Camera2Demo

在Camera2 的基础上进行封装,使得Camera2一堆初始化逻辑与Activity分离,降低耦合度,完成预览拍照并保存的操作

NVIDIA k8s-device-plugin中的GPU特性发现功能详解

在Kubernetes集群中管理NVIDIA GPU资源时,准确识别和标记GPU节点的硬件特性至关重要。NVIDIA k8s-device-plugin项目中的GPU特性发现(GFD)组件正是为此而生。本文将深入解析GFD的工作原理、部署方式以及生成的标签含义。 ## 核心功能 GFD作为Kubernetes节点特性发现(NFD)的扩展组件,能够自动检测并标记节点上的GPU硬件特性。主要功能包

gitblog_01086的博客 586
上一篇: 大模型推理指南:使用 vLLM 实现高效推理
下一篇: 在 K8S 中创建 Pod 是如何使用到 GPU 的: nvidia device plugin 源码分析
探索云原生
博客等级 码龄9年 730粉丝 131原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值