4

Containerd NRI 插件 - abin在路上

 1 year ago
source link: https://www.cnblogs.com/sctb/p/17094244.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Containerd NRI 插件

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: &quota, }, }) } return result, nil }

通过 Request 携带的 Spec 信息得到容器的 CgroupsPath,读取容器的 Cgroups 文件,然后通过control.Update方法修改 LinuxCPU.Quota 的值为-1。

1.0 版本中,NRI adaptation 能够传递给 NRI 插件的信息包括:

  • NRI 插件的配置信息

  • 容器当前生命周期的状态(create,delete,update,pause,resume)

  • 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.confbasename.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.confdisableConnections为 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"` }

除了ConfigureCreateContainer,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 插件可使用的信息

  • 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 插件可使用的信息

  • pod ID

  • state

  • labels

  • annotations

  • command line arguments

  • environment variables

  • mounts

  • OCI hooks

  • linux

  • memory

  • 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

  • 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"` }

更新容器时可修改的信息

容器被创建成功后,插件可以在以下时间请求更新容器的信息:

  1. 响应其他容器创建的请求时

  2. 响应任意更新容器的请求时

  3. 相应任意停止容器的请求时

  4. 单独发起更新请求

更新容器信息时,可以修改的信息包括:

  • 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

  • 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

图片

__EOF__


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK