NRI 位于 containerd 架构中的 CRI 插件,提供一个在容器运行时级别来管理节点资源的插件框架。NRI可以用来解决批量计算,延迟敏感性服务的性能问题,以及满足服务SLA/SLO、优先级等用户需求,性能需求,比如通过将容器的 CPU 分配同一个 numa node,来保证 numa 内的内存调用。当然除了 numa,还有 CPU、L3 cache 等资源拓扑亲和性。
编辑|阎锡叁
技术深度|简单
简介
Containerd提供容器进程的管理,镜像的管理,文件系统快照以及元数据和依赖管理,关于Containerd的介绍,可以参看前文,Containerd深度剖析-runtime篇,下图为Containerd架构总览图,其基于微服务实现,内部通过rpc(ttrpc)调用:
在 CRI 这一层,包含了 CRI、CNI、NRI 等类型的插件接口:
NRI允许将第三方自定义逻辑注入到支持OCI标准的运行时中,即可以接管容器,也可以,在容器生命周期的某些时间节点上执行OCI以外的操作。例如,可用于优化设备及其他容器资源的分配和管理。
NRI定义了节点资源接口,实现了公用的基础库,以支持这种可插拔的运行时扩展,即NRI插件。
当前社区希望在常用的OCI运行时(containerd和CRI-O)中实现对于NRI插件的支持。
设计实现
对于NRI来说,API的重构改变了NRI的作用范围以及与运行时集成方式。NRI v0.1.0使用的是类似OCI钩子的一次性插件调用机制,在这种机制下,每个NRI事件都会产生一个单独的插件实例。这个实例使用其标准输入和输出来接收请求和提供响应。
NRI中的插件类似daemon的守护进程。现在,一个插件的实例负责处理NRI的全部事件和请求。通信的传输使用unix的套接字。NRI被定义为基于protobuf的"NRI插件协议",而不是基于JSON请求和响应,主要是为了提高通信效率,降低消息的开销,并能直接实现有状态的NRI插件。
设计组件
NRI由许多组件组成,其中的核心部分对于运行时实现端到端的NRI支持至关重要。核心组件主要是NRI协议的实现和NRI运行时的适配。
它们建立了运行时与NRI交互的模型,以及插件如何通过NRI与运行时中的容器互动,此外还定义了在哪些条件下,插件可以对容器进行改变,以及改变的程度等。
其余的组件是NRI插件标准库和一些NRI插件的示例。一些插件在现实的场景中实现了有用的功能,还有一些是用于调试的。
协议与API
底层插件API的protobuf协议是NRI的核心。该API定义了两个服务,Runtime和Plugin。
Runtime服务是暴露给NRI插件的公共接口,所有请求都是由插件发起的。该接口提供以下功能:
a.插件注册
b.容器更新
插件服务是NRI用于与插件互动的接口。这个接口上的所有请求都是由NRI/运行时发起的。该接口提供以下功能
a. 配置插件
b. 获得已经存在的pod和容器的初始列表
c. 将插件hook到pod/container的生命周期事件中
d.关闭插件
插件注册
在一个插件可以开始接收和处理容器事件之前,它需要在NRI上注册。在注册过程中,插件和NRI需要先进行握手,其中包括以下几个步骤:
1. 插件向运行时表明身份
2.NRI向该插件提供特定的配置信息
3.插件订阅感兴趣的pod和容器的生命周期事件
4.NRI向插件发送现有pod和container的列表
5.插件请求对现有的容器进行必要的更新
该插件通过插件名称和插件索引向NRI标识自己。NRI用插件索引来与其他插件对比,以明确被hook进pod和容器生命周期事件的顺序。
插件名称被用于挑选插件特定的数据,作为配置信息发送给插件。这个数据只有在该插件被NRI启动时才会出现。如果该插件是由外部启动的,那么它只能通过外部手段获得其配置。该插件在对配置的响应中订阅其感兴趣的pod和容器生命周期事件。
作为注册和握手过程的最后一步,NRI会向运行时发送已知的全部pod和容器。插件可以请求更新它认为必要的任何已知的容器作为回应。
一旦握手序列结束,并且插件已经在NRI注册,它将开始根据其订阅接收pod和容器的生命周期事件
Pod数据与事件
NRI插件可以订阅以下容器生命周期事件。
创建 (*)
创建后
开始
启动后
更新 (*)
更新后
停止 (*)
移除
插件可以请求调整或更新容器以响应这些事件。
在NRI中,下列容器元数据对插件是可用的。
ID
pod ID
name
state
labels
annotations
command line arguments
environment variables
mounts
OCI hooks
linux
namespace IDs
devices
resources
memory
limit
reservation
swap limit
kernel limit
kernel TCP limit
swappiness
OOM disabled flag
hierarchical accounting flag
hugepage limits
CPU
shares
quota
period
realtime runtime
realtime period
cpuset CPUs
cpuset memory
Block I/O class
RDT class
除了识别容器的数据外,这些信息还代表了容器的OCI规格中的相应数据。
容器调整
在容器创建过程中,插件可以请求对以下容器参数进行更改。
annotations
mounts
environment variables
OCI hooks
linux
devices
resources
memory
limit
reservation
swap limit
kernel limit
kernel TCP limit
swappiness
OOM disabled flag
hierarchical accounting flag
hugepage limits
CPU
shares
quota
period
realtime runtime
realtime period
cpuset CPUs
cpuset memory
Block I/O class
RDT class
容器更新
一旦一个容器被创建,插件可以请求对其进行更新。这些更新可以在响应另一个容器创建请求时请求,在响应任何容器更新请求时请求,在响应任何容器停止请求时请求,或者可以作为单独的非请求的容器更新请求的一部分请求。以下容器参数可以通过这种方式被更新。
resources
memory
limit
reservation
swap limit
kernel limit
kernel TCP limit
swappiness
OOM disabled flag
hierarchical accounting flag
hugepage limits
CPU
shares
quota
period
realtime runtime
realtime period
cpuset CPUs
cpuset memory
Block I/O class
RDT class
Runtime改造
NRI运行时适配库是运行时用来集成到NRI并与NRI插件交互的接口。它实现了基本的插件发现、启动和配置。它还提供了必要的功能,将NRI插件与运行时的Pod和容器的生命周期事件挂钩。
多个NRI插件可能正在处理任何一个pod或容器的生命周期事件,它负责以正确的顺序调用插件,并将多个插件的响应合并为一个。在合并响应时,当检测到多个插件对单个容器所做的任何的冲突性改变,并将此类事件作为一个错误标记给运行时。
封装OCI Spec生成器
OCISpec生成器封装了相应的库,增加了将NRI容器调整和更新应用到OCI Specs的功能。这个库可以被运行时的NRI集成代码用来将NRI响应应用于容器。
小结
当前 kubelet 的实现是通过 cpuManager 的处理对象只能是 guaranteed 类的 pod, topologyManager 通过 cpuManager 提供的 hints 实现资源分配。
kubelet 当前也不适合处理多种需求的扩展,因为在 kubelet 增加细粒度的资源分配会导致 kubelet 和 CRI 的界限越来越模糊。而上述 CRI 内的插件,则是在 CRI 容器生命周期期间调用,适合做 resoruce pinning 和节点的拓扑的感知。并且在 CRI 内部做插件定义和迭代,可以做到上层 kubernetes 以最小代价来适配变化。
在容器生命周期中,CNI/NRI 插件能够注入到容器初始化进程的 Create 和 Start 之间:
Create->NRI->Start
以官方clearcfs示例:在启动容器前,依据 qos 类型调用 cgroup 命令,cpu.cfs_quota_us 为-1 表示不设上限。
可以分析出 NRI 直接控制 cgroup,所以能有更底层的资源分配方式。不过越接近底层,处理逻辑的复杂度也越高
由于笔者时间、视野、认知有限,本文难免出现错误、疏漏等问题,期待各位读者朋友、业界专家指正交流。
参考文献
1.https://edwardesire.com/posts/the-trending-of-cpu-management-in-k8s/
2.https://insujang.github.io/2019-10-31/container-runtime/
3.https://github.com/containerd/nri