Github:https://github.com/containerd/nri.git
Slide:https://static.sched.com/hosted_files/kccncna2022/cc/KubeCon-NA-2022-NRI-presentation.pdf
基本介绍
NRI(Node Resource Interface),即节点资源接口,对标 CNI(容器网络接口),是管理容器相关资源的接口框架,独立于具体的容器运行时。NRI 支持将特定逻辑插入兼容 OCI 的运行时,例如,在容器生命周期时间点执行 OCI 规定范围之外的操作,分配和管理容器的设备和其它资源。目前为止,NRI 已经演进到 2.0 版本,该版本在 1.0 版本上进行了重构,增强了接口的能力。
工作原理
“your cluster, your plugin, your rules”,NRI 提供不同生命周期事件的接口,用户在不修改容器运行时源代码的情况下添加自定义逻辑。
以下是 NRI 的工作流程:
NRI 和 CRI 一起工作,在 CRI runtime 源代码中增加了 NRI adaptation 的逻辑。NRI adaptation 的功能包括插件发现、启动和配置,将 NRI 插件与运行时 Pod 和容器的生命周期事件关联,可以理解为 NRI 插件的 client,将 Container 和 Pod 的信息(OCI Spec 的子集)传递给 NRI 插件,同时,接收 NRI 插件返回的执行结果,在 2.0 版本,NRI adaptaion 支持根据 NRI 插件的返回信息更新容器的信息(OCI Spec)。
1.0 版本
NRI 可以追溯到 2020 年 7 月 20 日发布在 containerd 社区的提案:Add Node Resource Interface design doc[1],大意是,现有的容器网络接口(CNI)在处理不同容器网络栈实现的时候做得很优雅,与传统 Hook 方式介入容器生命周期的方式不同,CNI 提供了安全的 API 注入 Container 生命周期。因此,该提案希望基于类似的思想提出一个用于管理节点资源的接口,处理逻辑位于 Create Container 和 Start Container 之间。
题外话:根据容器网络接口的命名,按理说,应该用容器资源接口(Container Resource Interface),简写 CRI,可能由于 CRI 已经被容器运行时接口(Container Runtime Interface)用了,所以才叫 NRI。(我猜的)
1.0[2] 版本 NRI 功能非常有限,仅用于管理节点的资源。实现方式类似于 OCI Hook,为每个 NRI 事件运行单独的插件实例,容器运行时通过标准输入和标准输出以 JSON 格式数据与插件交互。
Demo 体验
以 containerd 1.6.8 版本为例,体验 1.0 版本的 NRI。
git clone https://github.com/containerd/containerd.git cd containerd git checkout v1.6.8 make && sudo make install CONTAINERD_DIR=$(cat /lib/systemd/system/containerd.service | grep "ExecStart=" | awk -F= '{gsub("/containerd","",$2); print $2}') sudo cp bin/containerd* ${CONTAINERD_DIR}
NRI 仓库 1.0 版本分支中没有示例插件,README.md 的示例代码无法成功编译,因此可以使用 v2.0 中的示例代码:https://github.com/containerd/nri/tree/v0.2.0
git clone https://github.com/containerd/nri.git cd nri git checkout v0.2.0 cd examples/clearcfs sed -i '/result := r.NewResult(c.Type())/a \tlogrus.Infof("Invoke clearcfs ok!!")' main.go sed -i '/result := r.NewResult(c.Type())/a \tr.Spec.Annotations["qos.class"]="ls"' main.go sed -i 's/Debugf/Infof/g' main.go go build
1.0 版本启用 NRI[3],只需要在 containerd 配置文件中设置 NRI 插件二进制文件所在目录和各插件的配置文件即可,默认目录:
const ( // DefaultBinaryPath for nri plugins DefaultBinaryPath = "/opt/nri/bin" // DefaultConfPath for the global nri configuration DefaultConfPath = "/etc/nri/conf.json" // Version of NRI Version = "0.1" )
因此,只需要将编译好的 NRI 插件二进制文件拷贝到/opt/nri/bin
目录,同时在/etc/nri/conf.json
添加插件的配置文件:
sudo mkdir /opt/nri/bin sudo mkdir -p /etc/nri sudo cp clearcfs /opt/nri/bin sudo tee /etc/nri/conf.json <<- EOF { "version": "0.1", "plugins": [ { "type": "clearcfs" } ] } EOF
通过 crictl 启动容器:
tee container-config.yaml <<- EOF metadata: name: busybox image: image: busybox command: - busybox - sh - -c - echo busybox $(sleep inf) log_path: busybox.0.log linux: {} EOF tee pod-config.yaml <<- EOF metadata: name: nginx-sandbox namespace: default attempt: 1 uid: hdishd83djaidwnduwk28bcsb log_directory: /tmp linux: {} EOF sudo systemctl start containerd crictl pull registry.cn-hangzhou.aliyuncs.com/google_containers/pause:3.6 ctr -n k8s.io i tag registry.cn-hangzhou.aliyuncs.com/google_containers/pause:3.6 registry.k8s.io/pause:3.6 sudo crictl run container-config.yaml pod-config.yaml
结果验证:
sudo journalctl -xe -u containerd | grep -e "Invoke clearcfs ok" -e "clearing cfs"
查看 cpu quota 的值:
源码分析
在 NRI 插件中,实现了 Invoke 方法:
func (c *clearCFS) Invoke(ctx context.Context, r *types.Request) (*types.Result, error) { result := r.NewResult(c.Type()) r.Spec.Annotations["qos.class"] = "ls" logrus.Infof("Invoke clearcfs ok!!") if r.State != types.Create { return result, nil } switch r.Spec.Annotations["qos.class"] { case"ls": logrus.Infof("clearing cfs for %s", r.ID) control, err := cgroups.Load(cgroups.V1, cgroups.StaticPath(r.Spec.CgroupsPath)) if err != nil { returnnil, err } quota := int64(-1) return result, control.Update(&specs.LinuxResources{ CPU: &specs.LinuxCPU{ Quota: "a, }, }) } return result, nil }
通过 Request 携带的 Spec 信息得到容器的 CgroupsPath,读取容器的 Cgroups 文件,然后通过control.Update
方法修改 LinuxCPU.Quota 的值为-1。
1.0 版本中,NRI adaptation 能够传递给 NRI 插件的信息包括:
-
NRI 插件的配置信息
-
容器当前生命周期的状态(create,delete,update,pause,resume)
-
容器 ID
-
SandboxID
-
容器进程 Pid
-
Labels
-
精简版的容器运行时信息,主要内容包括
-
容器使用的资源
-
Namespaces
-
CgroupsPath
-
Annotations
type Spec struct { // Resources struct from the OCI specification // // Can be WindowsResources or LinuxResources Resources json.RawMessage `json:"resources"` // Namespaces for the container Namespaces map[string]string`json:"namespaces,omitempty"` // CgroupsPath for the container CgroupsPath string`json:"cgroupsPath,omitempty"` // Annotations passed down to the OCI runtime specification Annotations map[string]string`json:"annotations,omitempty"` }
2.0 版本
2.0 版本 NRI 只需要运行一个插件实例用于处理所有 NRI 事件和请求,容器运行时通过 unix-domain socket 与插件通信,使用基于 protobuf 的协议数据,和 1.0 版本相比拥有更高的性能,能够实现有状态的 NRI 插件。
Demo 体验
最新发布的 Containerd 版本集成了 NRI 2.0, NRI 仓库的示例程序也更完善。
# 回到 containerd 本地代码仓库 cd containerd git checkout main make && sudo make install CONTAINERD_DIR=$(cat /lib/systemd/system/containerd.service | grep "ExecStart=" | awk -F= '{gsub("/containerd","",$2); print $2}') sudo cp bin/containerd* ${CONTAINERD_DIR}
2.0 版本的配置文件和 1.0 版本有些不同:
sudo tee -a /etc/containerd/config.toml <<- EOF [plugins."io.containerd.nri.v1.nri"] config_file = "/etc/nri/nri.conf" disable = false plugin_path = "/opt/nri/plugins" socket_path = "/var/run/nri.sock" EOF sudo tee /etc/nri/nri.conf <<- EOF disableConnections: false EOF
NRI 插件二进制的默认目录更改为/opt/nri/plugins
。
2.0 版本的示例程序源代码位于 nri/plugins[4] 目录下(以 logger 为例):
cd nri cd plugins/logger go build -o 01-logger sudo mkdir /opt/nri/plugins sudo cp 01-logger /opt/nri/plugins
插件的配置文件路径为/etc/nri/conf.d
,文件名可以是id-basename.conf
和basename.conf
:
此外,NRI 并没有规定插件配置文件的格式,用户可以通过Configure
接口自定义实现。在 logger 示例,可以看到,解析的配置文件为 yaml 格式:
func (p *plugin) Configure(config, runtime, version string) (stub.EventMask, error) { log.Infof("got configuration data: %q from runtime %s %s", config, runtime, version) if config == "" { return p.mask, nil } oldCfg := cfg err := yaml.Unmarshal([]byte(config), &cfg) if err != nil { return0, fmt.Errorf("failed to parse provided configuration: %w", err) } p.mask, err = api.ParseEventMask(cfg.Events...) if err != nil { return0, fmt.Errorf("failed to parse events in configuration: %w", err) } if cfg.LogFile != oldCfg.LogFile { f, err := os.OpenFile(cfg.LogFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { log.Errorf("failed to open log file %q: %v", cfg.LogFile, err) return0, fmt.Errorf("failed to open log file %q: %w", cfg.LogFile, err) } log.SetOutput(f) } return p.mask, nil }
Logger 的配置项包括:
type config struct { LogFile string`json:"logFile"` Events []string`json:"events"` AddAnnotation string`json:"addAnnotation"` SetAnnotation string`json:"setAnnotation"` AddEnv string`json:"addEnv"` SetEnv string`json:"setEnv"` }
为 logger 插件配置 log 的存储路径:
sudo mkdir /etc/nri/conf.d sudo mkdir /var/run/containerd/nri sudo tee /etc/nri/conf.d/01-logger.conf <<- EOF logFile: /var/run/containerd/nri/logger.log EOF
重启 containerd,运行容器:
sudo systemctl restart containerd crictl pull registry.cn-hangzhou.aliyuncs.com/google_containers/pause:3.8 ctr -n k8s.io i tag registry.cn-hangzhou.aliyuncs.com/google_containers/pause:3.8 registry.k8s.io/pause:3.8 sudo crictl run container-config.yaml pod-config.yaml
除了在 containerd 启动的同时加载 NRI 插件,也支持手动运行插件(需要修改/etc/nri/nri.conf
的disableConnections
为 true):
/opt/nri/plugins/nri-logger -idx 01 -logFile /var/run/containerd/nri/logger.log
查看 log 文件:
cat /var/run/containerd/nri/logger.log
可以看到,logger 打印了不同接口函数从 NRI adaptation 得到的 Pod 和 Container 相关信息:
源码分析
对于 logger 插件,只需要实现 NRI 接口函数,例如,CreateContainer
:
func (p *plugin) CreateContainer(pod *api.PodSandbox, container *api.Container) (*api.ContainerAdjustment, []*api.ContainerUpdate, error) { dump("CreateContainer", "pod", pod, "container", container) adjust := &api.ContainerAdjustment{} if cfg.AddAnnotation != "" { adjust.AddAnnotation(cfg.AddAnnotation, fmt.Sprintf("logger-pid-%d", os.Getpid())) } if cfg.SetAnnotation != "" { adjust.RemoveAnnotation(cfg.SetAnnotation) adjust.AddAnnotation(cfg.SetAnnotation, fmt.Sprintf("logger-pid-%d", os.Getpid())) } if cfg.AddEnv != "" { adjust.AddEnv(cfg.AddEnv, fmt.Sprintf("logger-pid-%d", os.Getpid())) } if cfg.SetEnv != "" { adjust.RemoveEnv(cfg.SetEnv) adjust.AddEnv(cfg.SetEnv, fmt.Sprintf("logger-pid-%d", os.Getpid())) } return adjust, nil, nil }
2.0 版本的 NRI 插件可以通过CreateContainer
接口修改容器的 OCI Spec 内容,能被修改的范围定义在api.ContainerAdjustment
:
type ContainerAdjustment struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Annotations map[string]string`protobuf:"bytes,2,rep,name=annotations,proto3" json:"annotations,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` Mounts []*Mount `protobuf:"bytes,3,rep,name=mounts,proto3" json:"mounts,omitempty"` Env []*KeyValue `protobuf:"bytes,4,rep,name=env,proto3" json:"env,omitempty"` Hooks *Hooks `protobuf:"bytes,5,opt,name=hooks,proto3" json:"hooks,omitempty"` Linux *LinuxContainerAdjustment `protobuf:"bytes,6,opt,name=linux,proto3" json:"linux,omitempty"` }
-
容器的 Annotations
-
容器的 Mounts
-
容器的环境变量
-
容器的 OCI Hooks
-
容器使用的资源,定义在 api.LinuxContainerAdjustment
-
Devices
-
容器使用的 Linux 资源
-
Cgroups 路径
type LinuxContainerAdjustment struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Devices []*LinuxDevice `protobuf:"bytes,1,rep,name=devices,proto3" json:"devices,omitempty"` Resources *LinuxResources `protobuf:"bytes,2,opt,name=resources,proto3" json:"resources,omitempty"` CgroupsPath string`protobuf:"bytes,3,opt,name=cgroups_path,json=cgroupsPath,proto3" json:"cgroups_path,omitempty"` }
接口列表
除了Configure
和CreateContainer
,NRI 插件能实现的接口函数还包括:
func (p *plugin) Synchronize(pods []*api.PodSandbox, containers []*api.Container) ([]*api.ContainerUpdate, error) { dump("Synchronize", "pods", pods, "containers", containers) returnnil, nil } func (p *plugin) Shutdown() { dump("Shutdown") } func (p *plugin) RunPodSandbox(pod *api.PodSandbox) error { dump("RunPodSandbox", "pod", pod) returnnil } func (p *plugin) StopPodSandbox(pod *api.PodSandbox) error { dump("StopPodSandbox", "pod", pod) returnnil } func (p *plugin) RemovePodSandbox(pod *api.PodSandbox) error { dump("RemovePodSandbox", "pod", pod) returnnil } func (p *plugin) PostCreateContainer(pod *api.PodSandbox, container *api.Container) error { dump("PostCreateContainer", "pod", pod, "container", container) returnnil } func (p *plugin) StartContainer(pod *api.PodSandbox, container *api.Container) error { dump("StartContainer", "pod", pod, "container", container) returnnil } func (p *plugin) PostStartContainer(pod *api.PodSandbox, container *api.Container) error { dump("PostStartContainer", "pod", pod, "container", container) returnnil } func (p *plugin) UpdateContainer(pod *api.PodSandbox, container *api.Container) ([]*api.ContainerUpdate, error) { dump("UpdateContainer", "pod", pod, "container", container) returnnil, nil } func (p *plugin) PostUpdateContainer(pod *api.PodSandbox, container *api.Container) error { dump("PostUpdateContainer", "pod", pod, "container", container) returnnil } func (p *plugin) StopContainer(pod *api.PodSandbox, container *api.Container) ([]*api.ContainerUpdate, error) { dump("StopContainer", "pod", pod, "container", container) returnnil, nil } func (p *plugin) RemoveContainer(pod *api.PodSandbox, container *api.Container) error { dump("RemoveContainer", "pod", pod, "container", container) returnnil } func (p *plugin) onClose() { os.Exit(0) }
可以看到,NRI 插件可以在 Pod 和 Container 的生命周期加入自定义逻辑。
Pod 生命周期
-
创建 Pod
-
停止 Pod
-
删除 Pod
NRI 插件可使用的信息
-
ID
-
name
-
UID
-
namespace
-
labels
-
annotations
-
cgroup parent directory
-
runtime handler name
type PodSandbox struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Id string`protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` Name string`protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` Uid string`protobuf:"bytes,3,opt,name=uid,proto3" json:"uid,omitempty"` Namespace string`protobuf:"bytes,4,opt,name=namespace,proto3" json:"namespace,omitempty"` Labels map[string]string`protobuf:"bytes,5,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` Annotations map[string]string`protobuf:"bytes,6,rep,name=annotations,proto3" json:"annotations,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` RuntimeHandler string`protobuf:"bytes,7,opt,name=runtime_handler,json=runtimeHandler,proto3" json:"runtime_handler,omitempty"` Linux *LinuxPodSandbox `protobuf:"bytes,8,opt,name=linux,proto3" json:"linux,omitempty"` Pid uint32`protobuf:"varint,9,opt,name=pid,proto3" json:"pid,omitempty"`// for NRI v1 emulation }
Container 生命周期
-
创建容器 (*)
-
创建容器完成
-
启动容器
-
启动容器完成
-
更新容器 (*)
-
更新容器完成
-
停止容器 (*)
-
删除容器
NRI 插件可使用的信息
-
ID
-
pod ID
-
name
-
state
-
labels
-
annotations
-
command line arguments
-
environment variables
-
mounts
-
OCI hooks
-
linux
-
memory
-
CPU
-
Block I/O class
-
RDT class
-
limit
-
reservation
-
swap limit
-
kernel limit
-
kernel TCP limit
-
swappiness
-
OOM disabled flag
-
hierarchical accounting flag
-
hugepage limits
-
shares
-
quota
-
period
-
realtime runtime
-
realtime period
-
cpuset CPUs
-
cpuset memory
-
namespace IDs
-
devices
-
resources
type Container struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Id string`protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` PodSandboxId string`protobuf:"bytes,2,opt,name=pod_sandbox_id,json=podSandboxId,proto3" json:"pod_sandbox_id,omitempty"` Name string`protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` State ContainerState `protobuf:"varint,4,opt,name=state,proto3,enum=nri.pkg.api.v1alpha1.ContainerState" json:"state,omitempty"` Labels map[string]string`protobuf:"bytes,5,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` Annotations map[string]string`protobuf:"bytes,6,rep,name=annotations,proto3" json:"annotations,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` Args []string`protobuf:"bytes,7,rep,name=args,proto3" json:"args,omitempty"` Env []string`protobuf:"bytes,8,rep,name=env,proto3" json:"env,omitempty"` Mounts []*Mount `protobuf:"bytes,9,rep,name=mounts,proto3" json:"mounts,omitempty"` Hooks *Hooks `protobuf:"bytes,10,opt,name=hooks,proto3" json:"hooks,omitempty"` Linux *LinuxContainer `protobuf:"bytes,11,opt,name=linux,proto3" json:"linux,omitempty"` Pid uint32`protobuf:"varint,12,opt,name=pid,proto3" json:"pid,omitempty"`// for NRI v1 emulation }
创建容器时可修改的信息
-
annotations
-
mounts
-
environment variables
-
OCI hooks
-
linux
-
memory
-
CPU
-
Block I/O class
-
RDT class
-
limit
-
reservation
-
swap limit
-
kernel limit
-
kernel TCP limit
-
swappiness
-
OOM disabled flag
-
hierarchical accounting flag
-
hugepage limits
-
shares
-
quota
-
period
-
realtime runtime
-
realtime period
-
cpuset CPUs
-
cpuset memory
-
devices
-
resources
type ContainerAdjustment struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Annotations map[string]string`protobuf:"bytes,2,rep,name=annotations,proto3" json:"annotations,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` Mounts []*Mount `protobuf:"bytes,3,rep,name=mounts,proto3" json:"mounts,omitempty"` Env []*KeyValue `protobuf:"bytes,4,rep,name=env,proto3" json:"env,omitempty"` Hooks *Hooks `protobuf:"bytes,5,opt,name=hooks,proto3" json:"hooks,omitempty"` Linux *LinuxContainerAdjustment `protobuf:"bytes,6,opt,name=linux,proto3" json:"linux,omitempty"` }
更新容器时可修改的信息
容器被创建成功后,插件可以在以下时间请求更新容器的信息:
-
响应其他容器创建的请求时
-
响应任意更新容器的请求时
-
相应任意停止容器的请求时
-
单独发起更新请求
更新容器信息时,可以修改的信息包括:
-
resources
-
shares
-
quota
-
period
-
realtime runtime
-
realtime period
-
cpuset CPUs
-
cpuset memory
-
limit
-
reservation
-
swap limit
-
kernel limit
-
kernel TCP limit
-
swappiness
-
OOM disabled flag
-
hierarchical accounting flag
-
hugepage limits
-
memory
-
CPU
-
Block I/O class
-
RDT class
type ContainerUpdate struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields ContainerId string`protobuf:"bytes,1,opt,name=container_id,json=containerId,proto3" json:"container_id,omitempty"` Linux *LinuxContainerUpdate `protobuf:"bytes,2,opt,name=linux,proto3" json:"linux,omitempty"` IgnoreFailure bool`protobuf:"varint,3,opt,name=ignore_failure,json=ignoreFailure,proto3" json:"ignore_failure,omitempty"` }
参考资料
[1] Add Node Resource Interface design doc: https://github.com/containerd/containerd/pull/4411
[2] 1.0: https://github.com/containerd/nri/blob/main/README-v0.1.0.md
[3] 1.0 版本启用 NRI: https://github.com/containerd/containerd/blob/v1.6.8/vendor/github.com/containerd/nri/README.md
[4] nri/plugins: https://github.com/containerd/nri/tree/main/plugins