如何实现一个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接口,用于注册插件。

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

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

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

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

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

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

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插件源码解析

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

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方法。

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

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

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对象:

// 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这个方法来完成。

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方法返回的是一个空结构体

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文件变化, 重新注册插件。

代码实现

待补充…

### 回答1: 搭建一个Kubernetes集群需要以下步骤: 1. 准备节点:至少需要两台机器作为Master和Node。 2. 安装依赖:在所有节点上安装必要的依赖,如Docker,etcd,kubeadm等。 3. 初始化Master:使用kubeadm在Master节点上执行初始化。 4. 加入Node:在Node节点上执行kubeadm join命令,加入集群。 5. 安装网络组件:安装Kubernetes的网络组件,如Calico,Flannel等。 6. 部署应用:使用kubectl在集群上部署应用。 这些步骤中的每一步都需要仔细按照说明来执行,以确保集群的正确搭建。因为Kubernetes集群的搭建是一个复杂的过程,所以建议从官方文档或其他权威教程入手。 ### 回答2: 要搭建一个Kubernetes(简称K8s)集群,通常需要按照以下步骤进行: 1. 配置一组服务器:首先需要选择一组服务器来构建集群。这些服务器可以是物理机、虚拟机或云服务器。确保服务器之间可以相互通信,并具备足够的计算和存储资源来支持集群中的各种工作负载。 2. 安装Docker和Kubernetes组件:在每个服务器上安装Docker引擎,用于容器化应用程序。然后,根据Kubernetes的官方文档,安装其他Kubernetes组件,如kubelet、kube-proxy、kube-controller-manager和kube-scheduler。 3. 初始化集群:选择一个服务器作为主节点(Master),使用kubeadm工具初始化集群。这将安装和配置所需的所有Kubernetes组件,并为集群生成一个唯一的证书和密钥。 4. 部署网络插件:选择和安装适当的网络插件,以便集群中的Pod可以相互通信。常见的网络插件包括Calico、Flannel和Weave等。配置网络插件以与Kubernetes集群一起工作。 5. 添加工作节点(Node):使用kubeadm命令将其他服务器添加为工作节点,这样它们就可以参与集群中的工作负载。根据需要,可以将多个工作节点添加到集群中。 6. 运行应用程序:通过创建Deployment等资源对象,将应用程序部署到Kubernetes集群中。这些资源定义了容器的副本数量、访问策略和其他相关配置。 7. 管理和监控集群:使用命令行工具(如kubectl)或Kubernetes的Web界面(如Kubernetes Dashboard)来管理和监控集群。可以查看节点状态、应用程序日志、扩展集群等。 总结起来,搭建一个Kubernetes集群需要配置服务器、安装Docker和Kubernetes组件、初始化集群、部署网络插件、添加工作节点和运行应用程序。随着Kubernetes的不断演进和发展,有关搭建集群的最佳实践也可能会有所变化。因此,在开始搭建之前,建议仔细阅读官方文档和相关文档,并参考社区中的经验和建议。 ### 回答3: 搭建一个k8s集群需要以下步骤: 1. 准备环境:首先,需要一台或多台运行Linux操作系统的主机作为k8s节点。确保这些主机具有稳定的网络连接,并且它们的操作系统版本符合k8s的要求。 2. 安装Docker:k8s使用Docker来创建和管理容器。因此,在开始搭建k8s集群之前,需要在每个节点上安装Docker。可以从Docker官网下载适应你的操作系统版本的Docker安装程序,并按照官方文档进行安装。 3. 设置主机名:为了方便管理和识别节点,可以设置每个节点的主机名。可以使用命令行或修改主机的/etc/hostname文件来设置主机名。 4. 安装kubeadm、kubelet和kubectl:kubeadm是k8s的命令行工具,用于初始化和管理集群。kubelet是k8s的组件之一,负责管理主机上的容器。kubectl是k8s的命令行工具,用于与集群进行交互。在每个节点上安装kubeadm、kubelet和kubectl,可以使用官方提供的安装脚本或按照官方文档的步骤进行安装。 5. 初始化主节点:在搭建k8s集群的过程中,需要选择一个节点作为主节点,其他节点将作为工作节点加入到主节点中。在主节点上使用kubeadm init命令初始化集群,该命令会生成一个加入命令,用于将工作节点加入到集群中。 6. 加入工作节点:使用上一步骤生成的加入命令,在工作节点上执行命令,将其加入到集群中。 7. 配置网络插件:k8s需要一个网络插件来为容器提供网络功能。根据实际需求选择合适的网络插件,例如Calico、Flannel等,并按照插件提供的文档进行配置和安装。 8. 验证集群:最后,使用kubectl命令验证集群的状态和可用性。可以运行一些简单的测试应用程序来确保集群正常工作。 以上是搭建一个k8s集群的基本步骤,可以根据实际需求进行适当的调整和配置。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值