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

article/2025/7/20 18:23:04

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),还有 CRICNI 等等,这里的 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: 该接口在容器启动之前调用,用于配置容器使用的设备资源。

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

工作流程

一般所有的 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:主要是实现 ListAndWatchAllocate两个方法

实现 gRPC Server

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

对 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.DeviceSpecfor _, 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.sockerr = 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] = cklog.V(2).InfoS("Registered client", "name", name)
}

定义如下:

type server struct {socketName stringsocketDir  stringmutex      sync.Mutexwg         sync.WaitGroupgrpc       *grpc.Serverrhandler   RegistrationHandlerchandler   ClientHandlerclients    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 startif 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 DaemonSetstop := make(chan struct{})err := utils.WatchKubelet(stop)if err != nil {klog.Fatalf("start to kubelet failed: %v", err)}<-stopklog.Infof("kubelet restart,exiting")
}

4. 测试

部署

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

apiVersion: apps/v1
kind: DaemonSet
metadata:name: i-device-pluginnamespace: kube-systemlabels:app: i-device-plugin
spec:selector:matchLabels:app: i-device-plugintemplate:metadata:labels:app: i-device-pluginspec:containers:- name: i-device-pluginimage: docker.io/lixd96/i-device-plugin:latestimagePullPolicy: IfNotPresentresources:limits:cpu: "1"memory: "512Mi"requests:cpu: "0.1"memory: "128Mi"volumeMounts:- name: device-pluginmountPath: /var/lib/kubelet/device-plugins- name: gophersmountPath: /etc/gophersvolumes:- name: device-pluginhostPath:path: /var/lib/kubelet/device-plugins- name: gophershostPath: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/gopherstouch /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 7capacity:cpu: "4"ephemeral-storage: 20960236Kihugepages-1Gi: "0"hugepages-2Mi: "0"lixueduan.com/gopher: "1"memory: 8154984Kipods: "110"

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

创建测试 Pod

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

apiVersion: v1
kind: Pod
metadata:name: gopher-pod
spec:containers:- name: gopher-containerimage: busyboxcommand: ["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 7capacity:cpu: "4"ephemeral-storage: 20960236Kihugepages-1Gi: "0"hugepages-2Mi: "0"lixueduan.com/gopher: "2"memory: 8154984Kipods: "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 7capacity:cpu: "4"ephemeral-storage: 20960236Kihugepages-1Gi: "0"hugepages-2Mi: "0"lixueduan.com/gopher: "1"memory: 8154984Kipods: "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 源码


http://www.hkcw.cn/article/RnYwYYUaAm.shtml

相关文章

在K8S上部署OceanBase的最佳实践

在K8S上部署OceanBase的最佳实践 目录 1. 背景与选型 1.1 为什么选择OB1.2 为什么选择ob-operator实现OB on K8S 2. 部署实操 2.1 环境准备2.2 安装 ob-operator2.3 配置 OB 集群2.4 配置 OBProxy 集群2.5 Headless Service 和 CoreDNS 配置2.6 监控与运维 2.6.1 Promethues部…

Centos 安装 Kubernets(k8s)

Centos安装部署Kubernetes (k8s) cat /etc/os-release #查看系统版本 uname -r #查看系统内核 1.设置主机名 准备三台服务器&#xff0c;至少2核2G hostnamectl set-hostname k8s-master #在第一台机器上执行hostnamectl set-hostname k8s-node01 #在第二台机器上执行hostna…

K8s - openeuler2203SP1安装 K8s + flannel

环境说明 [rootmaster-1 ~]# uname -a Linux master-1 5.10.0-136.12.0.86.oe2203sp1.x86_64 #1 SMP Tue Dec 27 17:50:15 CST 2022 x86_64 x86_64 x86_64 GNU/Linux安装过程 1、安装 containerd 下载 tar 包 # 确保没有使用官方仓库的containerd [rootlocalhost ~]# yum rem…

K8S从0完整搭建

介绍 以下搭建的环境使用的是VMware虚拟机。这里大家可以自行选择。K8S每个版本可能都会有差异&#xff0c;请保证相关组件的兼容性。 准备工具 部署版本介绍 名称 版本 描述 centos 7.9 docker 20.10.8 k8s v1.20.10 calico v3.20.0 K8S节点规划 3个m…

【Kubernetes】k8s的helm扩展之监控管理、日志管理、部署efk【elk的升级版】详细说明

部署prometheus【mon节点】 镜像查看【需要先配置好helm源,这里是配置的ali源】【集群必须通外网才能部署啊】helm search repo prometheus-operator 安装prometheushelm install mon ali/prometheus-operator【后面就是上面查询到的helm信息】 下载完毕以后呢就会有一个helm…

最全Linux centos9环境基于k8s(k3s)搭建HomeAssistant,并接入米家官方XiaoMiHome插件

背景 环境 1、linux centos 9&#xff08;该环境的ip可以被你的其他设备访问到&#xff0c;例如你可以用windows访问到家里的服务器&#xff09; 2、k3s集群已完成部署&#xff0c;参考:国内k3s环境搭建-CSDN博客 3、HA部署方式采用容器化部署 概念 1、k3s&#xff1a;k8…

Git 使用全指南

Git 使用全指南 第一部分&#xff1a;核心概念 什么是 Git&#xff1f; 一个分布式版本控制系统 (DVCS)。目标&#xff1a; 跟踪文件变化、协作开发、回溯历史、管理不同开发线&#xff08;分支&#xff09;。核心思想&#xff1a; 每个开发者都有完整的仓库副本&#xff08;包…

Leetcode 269. 火星词典

1.题目基本信息 1.1.题目描述 现有一种使用英语字母的外星文语言&#xff0c;这门语言的字母顺序与英语顺序不同。 给定一个字符串列表 words &#xff0c;作为这门语言的词典&#xff0c;words 中的字符串已经 按这门新语言的字母顺序进行了排序 。 请你根据该词典还原出此…

若依框架-Feign的应用

代码资料链接&#xff1a;https://download.csdn.net/download/ly1h1/90945836 1.背景 若依的微服务框架&#xff0c;少不了各微服务之间的接口调用&#xff0c;以下是采用feign来进行微服务之间的方法调用。 2.案例说明 在system模块下的某个接口&#xff0c;调用factory&am…

印度奥迪沙邦一巴士翻车 50余人被困 紧急救援展开

6月2日,一辆巴士在印度东部奥迪沙邦的山路上发生翻车事故,车内50多名乘客被困。事故发生在当天清晨,巴士在下坡过程中于弯道上失去平衡,导致车辆失控。事故发生后,当地村民和紧急救援人员迅速展开救援行动,当地政府也派出救援队赶往现场。目前,官方尚未公布具体的伤亡情…

从4小时到20分钟 青岛港科技升级货物“秒通关”

作为中国北方重要的国际航运枢纽,青岛港的航线通达全球700多个港口,一季度集装箱吞吐量同比增速达到了7.4%。在当下复杂多变的国际贸易形势下,这座港口如何找准方向为外贸企业“保驾护航”?港口“流量”剧增折射外贸新机遇在山东港口青岛港前湾港区,这艘前往美国东部的集装…

Flannel MAC地址冲突导致Pod 跨节点通信异常

问题背景 客户在扩容 Kubernetes 节点后&#xff0c;发现部分服务 Pod 跨节点通信异常&#xff0c;表现为&#xff1a; • Pod 间通信间歇性失败&#xff1b; • 某些业务服务异常或响应慢&#xff1b; • 怀疑是网络问题引起的。 问题排查 1️⃣ 初步排查网络路由信息 我们…

[前端计算机网络]资源加载过程的详细性能信息浏览器加载资源的全过程

资源加载过程的详细性能信息 基于 PerformanceResourceTiming 对象对页面中某个资源加载过程的详细性能信息进行采集与封装&#xff0c;并结合了计算机网络中的请求生命周期进行度量。 export function observeEvent():void{const type"resource";const entryHand…

德布劳内将接受那不勒斯体检 加盟在即

据报道,德布劳内将加盟那不勒斯。预计他将在比利时国家队比赛结束后接受那不勒斯的体检。上月公布的比利时世预赛名单中包括了德布劳内。比利时队将在世预赛中先后对阵北马其顿和威尔士,其中与威尔士的比赛将于本月10日北京时间2点45分开球。现年33岁的德布劳内在今夏合同到期…

《在人间》是2025最难看懂的剧吗 烧脑剧情挑战观众

《在人间》这部剧集让艺绽君感到难以评价。这种“难评”并非贬义,而是因为观剧过程中真实地感受到了大脑爆炸、脑细胞死了不少,只能用一个相对中立的词汇来形容这部“神剧”。《在人间》共8集,隶属于爱奇艺的微尘剧场,这一剧场以短小精悍的作品著称。主创团队中有知名导演兼…

警方辟谣北京有人高空撒一千万:不是故意的,系工人施工碰倒钱箱 干活不慎掉落

5月29日,北京昌平区住总万科天地一带发生了一起撒钱事件。有市民发帖称,有人在楼上撒了一千万元。视频画面显示,空中飘着几张纸币,一些市民在楼下接钱。次日,北京七里渠派出所工作人员表示,当事人是因为工作时不小心掉落了钱,并提醒市民不要听信网络谣言。至于掉落的金钱…

96岁老兵走失找回 曾为陈毅元帅送信 抗战英雄平安归家

6月2日,山东省济宁市一救援队成功找回了走失的96岁高龄抗战老兵吕企荣老人,老人身体无碍。吕企荣家住泗水县济河街道何家庄村。5月31日早晨7时许,老人从家中步行到村北侧的小公园遛弯,直到中午都没有回家。家属通过监控发现,老人沿着城区北侧万象城附近道路一直向北走,直…

地磁暴雷暴大风与暴雨交织登场 双预警齐发

6月2日6时,中央气象台发布暴雨蓝色预警和强对流天气蓝色预警,覆盖福建、广东、广西等十余省份。中国气象局国家空间天气监测预警中心指出,6月1日至3日可能连续发生地磁暴,6月2日左右,我国北部有机会出现较为明显的极光,部分地区甚至有出现红绿复合极光的可能。预计6月2日…

6月1日起 新疆伊尔克什坦口岸试行24小时货运通关

6月1日,新疆伊尔克什坦口岸货运通道正式启动为期6个月的24小时通关试行。随着首批出入境货车有序通关,该口岸成为中国首个面向吉尔吉斯斯坦实行24小时货运通关的公路口岸。伊尔克什坦口岸位于新疆克孜勒苏柯尔克孜自治州乌恰县,是我国最西端的陆路口岸。口岸主要出口日用百货…

美防长炒作中国威胁论难获东盟支持 东盟强调战略自主

在新加坡举行的第22届香格里拉对话会上,美国国防部长赫格塞思极力渲染所谓的“中国威胁”,以迫使盟国增加军费开支。然而,东盟国家的国防部长们强调了“战略自主”的概念。菲律宾国防部长吉尔伯特特奥多罗表示,菲律宾作为美国的条约盟友,并非没有战略自主的棋子。他虽然仍…