设备插件

使用 Kubernetes 设备插件框架来实现适用于 GPU、NIC、FPGA、InfiniBand 以及类似的需要特定于供应商设置的资源的插件。

FEATURE STATE: Kubernetes v1.10 [beta]

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

供应商可以实现设备插件,由你手动部署或作为 DaemonSet 来部署,而不必定制 Kubernetes 本身的代码。目标设备包括 GPU、高性能 NIC、FPGA、 InfiniBand 适配器以及其他类似的、可能需要特定于供应商的初始化和设置的计算资源。

注册设备插件

kubelet 提供了一个 Registration 的 gRPC 服务:

  1. service Registration {
  2. rpc Register(RegisterRequest) returns (Empty) {}
  3. }

设备插件可以通过此 gRPC 服务在 kubelet 进行注册。在注册期间,设备插件需要发送下面几样内容:

  • 设备插件的 Unix 套接字。
  • 设备插件的 API 版本。
  • ResourceName 是需要公布的。这里 ResourceName 需要遵循 扩展资源命名方案, 类似于 vendor-domain/resourcetype。(比如 NVIDIA GPU 就被公布为 nvidia.com/gpu。)

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

比如,设备插件在 kubelet 中注册了 hardware-vendor.example/foo 并报告了 节点上的两个运行状况良好的设备后,节点状态将更新以通告该节点已安装 2 个 “Foo” 设备并且是可用的。

然后用户需要请求其他类型的资源的时候,就可以在 Container 规范请求这类设备,但是有以下的限制:

  • 扩展资源仅可作为整数资源使用,并且不能被过量使用
  • 设备不能在容器之间共享

假设 Kubernetes 集群正在运行一个设备插件,该插件在一些节点上公布的资源为 hardware-vendor.example/foo。 下面就是一个 Pod 示例,请求此资源以运行某演示负载:

  1. ---
  2. apiVersion: v1
  3. kind: Pod
  4. metadata:
  5. name: demo-pod
  6. spec:
  7. containers:
  8. - name: demo-container-1
  9. image: k8s.gcr.io/pause:2.0
  10. resources:
  11. limits:
  12. hardware-vendor.example/foo: 2
  13. #
  14. # 这个 pod 需要两个 hardware-vendor.example/foo 设备
  15. # 而且只能够调度到满足需求的节点上
  16. #
  17. # 如果该节点中有 2 个以上的设备可用,其余的可供其他 Pod 使用

设备插件的实现

设备插件的常规工作流程包括以下几个步骤:

  • 初始化。在这个阶段,设备插件将执行供应商特定的初始化和设置, 以确保设备处于就绪状态。

  • 插件使用主机路径 /var/lib/kubelet/device-plugins/ 下的 Unix 套接字启动 一个 gRPC 服务,该服务实现以下接口:

    1. service DevicePlugin {
    2. // ListAndWatch 返回 Device 列表构成的数据流。
    3. // 当 Device 状态发生变化或者 Device 消失时,ListAndWatch
    4. // 会返回新的列表。
    5. rpc ListAndWatch(Empty) returns (stream ListAndWatchResponse) {}
    6. // Allocate 在容器创建期间调用,这样设备插件可以运行一些特定于设备的操作,
    7. // 并告诉 kubelet 如何令 Device 可在容器中访问的所需执行的具体步骤
    8. rpc Allocate(AllocateRequest) returns (AllocateResponse) {}
    9. // GetPreferredAllocation 从一组可用的设备中返回一些优选的设备用来分配,
    10. // 所返回的优选分配结果不一定会是设备管理器的最终分配方案。
    11. // 此接口的设计仅是为了让设备管理器能够在可能的情况下做出更有意义的决定。
    12. rpc GetPreferredAllocation(PreferredAllocationRequest) returns (PreferredAllocationResponse) {}
    13. // PreStartContainer 在设备插件注册阶段根据需要被调用,调用发生在容器启动之前。
    14. // 在将设备提供给容器使用之前,设备插件可以运行一些诸如重置设备之类的特定于
    15. // 具体设备的操作,
    16. rpc PreStartContainer(PreStartContainerRequest) returns (PreStartContainerResponse) {}
    17. }

    说明:

    插件并非必须为 GetPreferredAllocation()PreStartContainer() 提供有用 的实现逻辑,调用 GetDevicePluginOptions() 时所返回的 DevicePluginOptions 消息中应该设置这些调用是否可用。kubelet 在真正调用这些函数之前,总会调用 GetDevicePluginOptions() 来查看是否存在这些可选的函数。

  • 插件通过 Unix socket 在主机路径 /var/lib/kubelet/device-plugins/kubelet.sock 处向 kubelet 注册自身。

  • 成功注册自身后,设备插件将以服务模式运行,在此期间,它将持续监控设备运行状况, 并在设备状态发生任何变化时向 kubelet 报告。它还负责响应 Allocate gRPC 请求。 在 Allocate 期间,设备插件可能还会做一些设备特定的准备;例如 GPU 清理或 QRNG 初始化。 如果操作成功,则设备插件将返回 AllocateResponse,其中包含用于访问被分配的设备容器运行时的配置。 kubelet 将此信息传递到容器运行时。

处理 kubelet 重启

设备插件应能监测到 kubelet 重启,并且向新的 kubelet 实例来重新注册自己。 在当前实现中,当 kubelet 重启的时候,新的 kubelet 实例会删除 /var/lib/kubelet/device-plugins 下所有已经存在的 Unix 套接字。 设备插件需要能够监控到它的 Unix 套接字被删除,并且当发生此类事件时重新注册自己。

设备插件部署

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

规范目录 /var/lib/kubelet/device-plugins 是需要特权访问的,所以设备插件 必须要在被授权的安全的上下文中运行。 如果你将设备插件部署为 DaemonSet,/var/lib/kubelet/device-plugins 目录必须要在插件的 PodSpec 中声明作为 卷(Volume) 被挂载到插件中。

如果你选择 DaemonSet 方法,你可以通过 Kubernetes 进行以下操作: 将设备插件的 Pod 放置在节点上,在出现故障后重新启动守护进程 Pod,来进行自动升级。

API 兼容性

Kubernetes 设备插件支持还处于 beta 版本。所以在稳定版本出来之前 API 会以不兼容的方式进行更改。 作为一个项目,Kubernetes 建议设备插件开发者:

  • 注意未来版本的更改
  • 支持多个版本的设备插件 API,以实现向后/向前兼容性。

如果你启用 DevicePlugins 功能,并在需要升级到 Kubernetes 版本来获得较新的设备插件 API 版本的节点上运行设备插件,请在升级这些节点之前先升级设备插件以支持这两个版本。 采用该方法将确保升级期间设备分配的连续运行。

监控设备插件资源

FEATURE STATE: Kubernetes v1.15 [beta]

为了监控设备插件提供的资源,监控代理程序需要能够发现节点上正在使用的设备, 并获取元数据来描述哪个指标与容器相关联。 设备监控代理暴露给 Prometheus 的指标应该遵循 Kubernetes Instrumentation Guidelines, 使用 podnamespacecontainer 标签来标识容器。

kubelet 提供了 gRPC 服务来使得正在使用中的设备被发现,并且还未这些设备提供了元数据:

  1. // PodResourcesLister 是一个由 kubelet 提供的服务,用来提供供节点上
  2. // Pods 和容器使用的节点资源的信息
  3. service PodResourcesLister {
  4. rpc List(ListPodResourcesRequest) returns (ListPodResourcesResponse) {}
  5. rpc GetAllocatableResources(AllocatableResourcesRequest) returns (AllocatableResourcesResponse) {}
  6. }

这一 List 端点提供运行中 Pods 的资源信息,包括类似独占式分配的 CPU ID、设备插件所报告的设备 ID 以及这些设备分配所处的 NUMA 节点 ID。

  1. // ListPodResourcesResponse 是 List 函数的响应
  2. message ListPodResourcesResponse {
  3. repeated PodResources pod_resources = 1;
  4. }
  5. // PodResources 包含关于分配给 Pod 的节点资源的信息
  6. message PodResources {
  7. string name = 1;
  8. string namespace = 2;
  9. repeated ContainerResources containers = 3;
  10. }
  11. // ContainerResources 包含分配给容器的资源的信息
  12. message ContainerResources {
  13. string name = 1;
  14. repeated ContainerDevices devices = 2;
  15. repeated int64 cpu_ids = 3;
  16. }
  17. // Topology 描述资源的硬件拓扑结构
  18. message TopologyInfo {
  19. repeated NUMANode nodes = 1;
  20. }
  21. // NUMA 代表的是 NUMA 节点
  22. message NUMANode {
  23. int64 ID = 1;
  24. }
  25. // ContainerDevices 包含分配给容器的设备信息
  26. message ContainerDevices {
  27. string resource_name = 1;
  28. repeated string device_ids = 2;
  29. TopologyInfo topology = 3;
  30. }

端点 GetAllocatableResources 提供最初在工作节点上可用的资源的信息。 此端点所提供的信息比导出给 API 服务器的信息更丰富。

  1. // AllocatableResourcesResponses 包含 kubelet 所了解到的所有设备的信息
  2. message AllocatableResourcesResponse {
  3. repeated ContainerDevices devices = 1;
  4. repeated int64 cpu_ids = 2;
  5. }

ContainerDevices 会向外提供各个设备所隶属的 NUMA 单元这类拓扑信息。 NUMA 单元通过一个整数 ID 来标识,其取值与设备插件所报告的一致。 设备插件注册到 kubelet 时 会报告这类信息。

gRPC 服务通过 /var/lib/kubelet/pod-resources/kubelet.sock 的 UNIX 套接字来提供服务。 设备插件资源的监控代理程序可以部署为守护进程或者 DaemonSet。 规范的路径 /var/lib/kubelet/pod-resources 需要特权来进入, 所以监控代理程序必须要在获得授权的安全的上下文中运行。 如果设备监控代理以 DaemonSet 形式运行,必须要在插件的 PodSpec 中声明将 /var/lib/kubelet/pod-resources 目录以 的形式被挂载到设备监控代理中。

对“PodResourcesLister 服务”的支持要求启用 KubeletPodResources 特性门控。 从 Kubernetes 1.15 开始默认启用,自从 Kubernetes 1.20 开始为 v1。

设备插件与拓扑管理器的集成

FEATURE STATE: Kubernetes v1.18 [beta]

拓扑管理器是 Kubelet 的一个组件,它允许以拓扑对齐方式来调度资源。 为了做到这一点,设备插件 API 进行了扩展来包括一个 TopologyInfo 结构体。

  1. message TopologyInfo {
  2. repeated NUMANode nodes = 1;
  3. }
  4. message NUMANode {
  5. int64 ID = 1;
  6. }

设备插件希望拓扑管理器可以将填充的 TopologyInfo 结构体作为设备注册的一部分以及设备 ID 和设备的运行状况发送回去。然后设备管理器将使用此信息来咨询拓扑管理器并做出资源分配决策。

TopologyInfo 支持定义 nodes 字段,允许为 nil(默认)或者是一个 NUMA 节点的列表。 这样就可以使设备插件可以跨越 NUMA 节点去发布。

下面是一个由设备插件为设备填充 TopologyInfo 结构体的示例:

  1. pluginapi.Device{ID: "25102017", Health: pluginapi.Healthy, Topology:&pluginapi.TopologyInfo{Nodes: []*pluginapi.NUMANode{&pluginapi.NUMANode{ID: 0,},}}}

设备插件示例

下面是一些设备插件实现的示例:

接下来