『containerd 系列』【全】一文读懂 containerd 中的 NRI 机制

本文内容节选自 《containerd 原理剖析与实战》,本书正参加限时优惠内购,限时 69.9 元购买

本文介绍 containerd 中的一种可插拔的扩展机制 NRI。

1. NRI 介绍

NRI(Node Resource Interface),即节点资源接口。是 containerd 中位于 CRI 插件中的一种扩展机制。NRI 可以提供容器不同生命周期事件的接口,用户在不修改容器运行时源代码的情况下添加自定义逻辑。NRI 在 containerd 中的定位如图所示。

图片

图 containerd CRI Plugin 中的 NRI

类似于 CNI 插件机制,NRI 插件机制允许将第三方自定义逻辑插入兼容 OCI 的运行时,例如,可以在容器生命周期时间点执行 OCI 规定范围之外的操作,分配和管理容器的设备和其它资源,以及修改原始 OCI 信息。

NRI 本身与任何容器运行时的内部实现细节无关,containerd 社区的目标是希望在常用的OCI运行时(containerd和cri-o)中实现对于NRI插件的支持。

2. NRI 插件的工作流程

下面介绍 NRI 的工作流程 。一个正常的 CRI 请求流程如下图所示。

图片

图 容器信息在 CRI 调用中的转化过程

如图所示,通过 k8s 创建 Pod 时:

  1. kubelet 将 Pod 信息转化为 CRI 的请求信息调用 CRI 容器运行时,如 containerd 或者 cri-o。

  2. CRI 容器运行时将 CRI 请求信息转化为 OCI 格式,调用低级容器运行时,如 runc,kata 等。

  3. runc,kata 通过 OCI 格式将容器进程启动起来。  接下来介绍 NRI 在 CRI 的请求流程中是如何工作的,如下图所示。

图片

图 NRI 在 CRI 的请求流程中的工作流程

如图所示,NRI 插件工作在通过 OCI 调用低级容器运行时操作容器之前:

  1. CRI 容器运行时将 CRI 请求转化为 OCI Spec 之后通过 NRI adaptation 调用 NRI 插件,(NRI adaptation 的功能包括插件发现、启动和配置,将 NRI 插件与运行时 Pod 和容器的生命周期事件关联,NRI adaptation 也就是 NRI 插件的 client)。NRI adaptation 将 Container 和 Pod 的信息(OCI Spec 的子集)传递给 NRI 插件,同时接收 NRI 插件返回的信息来更新容 OCI Spec。

  2. NRI adaptation 通过 ttrpc 调用 NRI Plugin,NRI adaptation 作为 NRI 的 client,NRI 协议同样是基于 protobuf 的 ttRPC 接口。其中 ttrpc 交互的 socket 为 /var/run/nri/nri.sock

  3. NRI Plugins 作为 ttrpc 的Server,接受 NRI adaptation 的请求,处理相关的逻辑,并将相应返回给 NRI adaptation。

  4. containerd 通过 NRI adaptation 得到修改后的 OCI Spec,通过修改后的 OCI Spec 启动对应的低级容器运行时,如 kata、runc 等。

3. NRI 插件协议介绍

NRI 插件协议主要是定义了一套容器运行时与 NRI 插件交互的 API。API 共有两部分:

  1. TTRPC API: 是基于 protobuf 定义的容器运行时对接 NRI 插件的 API。containerd 基于 ttrpc 与 NRI 插件中的 stub 进行通信。stub 是 NRI 项目实现的代码框架,类似于 k8s scheduler framework 的扩展方式,基于该框架开发自己的 NRI 插件时,与 containerd 的交互将被 stub 组件接管。

  2. Stub Interface: 该接口用于自定义 NRI 插件时实现的接口,该接口是 NRI Plugin 中的 stub 与自定义逻辑交互的接口。自定义 NRI Plugin 逻辑可以实现其中的一个或多个有关Pod 以及 容器生命周期的接口。  NRI 插件协议如下图 所示。

图片

图 NRI Plugin 协议 API 

TTRPC API

TTRPC API 中主要包含两类 API: Runtime Service 和 Plugin Service:

  1. Runtime Service: 顾名思义是在容器运行时中实现的 Service,NRI 项目抽象了 adaptation 的框架,该框架中封装集成了 Runtime Service 的 Server 实现,容器运行时中引用少量代码即可接入,当前 containerd 将 adaptation 框架集成在了 CRI Plugin 中。该接口的 Client 端在 NRI Plugin 中的 stub 中,Server 端位于 CRI Plugin 中的 adaptation 中。Runtime Service 是容器运行时暴露给 NRI Plugin 的公共接口,所有请求都是由插件发起的。该接口提供两个功能:插件注册、容器更新

  2. Plugin Service: 该接口是在 NRI Plugin 中实现的 Server。同样 NRI 项目抽象了 stub 框架,该框架封装了 Plugin Service 的 Server 实现。NRI stub 是用于开发自定义 NRI 插件的框架,用户开发自己的自定义 NRI 插件时,只需引用并集成 stub,实现自己的 Stub Interface 接口即可。Plugin Service 是 NRI 插件中暴露给容器运行时的接口。这个接口上的所有请求都是由 NRI adaptation 发起的。该接口提供以下功能:1. 配置插件。2. 获得已经存在的 pod 和容器的初始列表。3. 将插件hook到pod/container的生命周期事件中 4. 关闭插件。

📢 注意 adaptation 是 NRI 项目提供给容器运行时的适配库,供容器运行时 (如 CRI Plugin)用来集成 NRI 并与 NRI 插件交互。它实现了基本的插件发现、启动和配置。它还提供了必要的功能: 将 NRI 插件与容器运行时的 Pod 和 容器 的生命周期事件挂钩。多个 NRI 插件可以同时处理任何一个pod或容器的生命周期事件,adaptation 按照一定的顺序调用插件,并将多个插件的响应合并为一个。在合并响应时,当检测到多个插件对单个容器所做的任何改变有冲突时,会将该事件对应的错误返回给容器运行时。

stub 则是 NRI 项目提供给 NRI 插件开发者的另一个库,该库封装了许多实现 NRI 插件的底层细节。stub 负责通过和 adaptation 连接建立、插件注册、配置插件以及相关事件订阅。用户开发 NRI 插件,都是基于 stub 库来实现的。

Stub Interface

Stub interface 接口是用户自行实现 NRI 插件逻辑需要满足的接口,自定义 NRI 插件采用集成 stub 的方式扩展,stub 通过函数调用方式调用用户实现的 Stub Interface 接口,用户可以实现其中的一个或多个有关 Pod 以及 容器生命周期的接口。

Stub Interface 接口支持如表7.11 所示。

自定义插件需要实现的 Stub Interface 接口

类型函数名说明
插件相关Configure该接口使用插件给定的配置文件来配置插件,配置插件的格式由插件自行决定,配置插件的地址存放在 /etc/nri/conf.d/ 中
Shutdown该接口用于通知插件关闭服务
onClose插件关闭
Synchronize该接口用于同步插件中 PodSandbox 以及容器的状态,用于 Plugin 初次启动时,NRI 向插件发送现有pod和container的列表,插件对现有的 Pod 和容器进行必要的更新和修改,同时接口返回容器更新后的列表,容器可以修改的字段本节后面会介绍
Pod 相关RunPodSandbox该接口用于通知插件当前 Pod 处于 Start 阶段
StopPodSandbox该接口用于通知插件当前 Pod 处于 Stop 阶段
RemovePodSandbox该接口用于通知插件当前 Pod 处于 Remove 阶段
容器相关CreateContainer该接口是容器创建前的回调函数,此时 containerd 中容器还没创建,允许插件在该阶段对容器的 OCI Spec 中的部分配置信息进行更改。
PostCreateContainer该接口是容器创建完成后的回调接口
StartContainer该接口是容器启动前的回调接口
PostStartContainer该接口是容器启动后的回调接口
UpdateContainer该接口是容器更新前的回调接口,此时 containerd 中容器还没更新,允许插件在该阶段对容器的 OCI Spec 中的部分配置信息进行更改。
PostUpdateContainer该接口是容器更新后的接口
StopContainer该接口是容器停止后的接口,可以在此阶段更新任何现存的容器
RemoveContainer该接口是容器移除后的回调接口

可以看到,自定义插件可以在 Pod 和 Container 的生命周期中插入自定义的逻辑,用于修改容器的 OCI Spec 配置信息。

4. NRI Pod 生命周期事件

NRI Pod 生命周期事件如下图所示, NRI 插件可以订阅如下 Pod 生命周期,同时可对 Pod 信息进行修改:

  • 创建 Pod

  • 停止 Pod

  • 移除 Pod

图片

图 NRI Pod 生命周期事件

5. NRI Container 生命周期事件

NRI Container 生命周期事件如下图所示, NRI 插件可以订阅如下 Container 生命周期,同时可对 Container 信息进行修改:

  • 创建容器

  • 创建容器后

  • 开始容器

  • 容器启动后

  • 更新容器

  • 更新容器后

  • 停止容器

  • 移除容器

图片

图 NRI 订阅的 Container 生命周期事件

6. containerd 中启用 NRI 插件

可以在 containerd 配置文件中启用或禁用 NRI 的支持。默认情况下 NRI 插件是禁用的。可以通过编辑 containerd 配置文件(默认情况下是 /etc/containerd/config.toml)中的 [plugins."io.containerd.nri.v1.nri"] 部分来启用,并将 disable = true 更改为 disable =false。  启用后,containerd NRI 的配置文件如下。

 [plugins."io.containerd.nri.v1.nri"]
    disable = false
    disable_connections = false
    plugin_config_path = "/etc/nri/conf.d"
    plugin_path = "/opt/nri/plugins"
    plugin_registration_timeout = "5s"
    plugin_request_timeout = "2s"
    socket_path = "/var/run/nri/nri.sock"

配置文件的相关参数如下。

  • disable: 是否禁用 NRI 插件,默认值 true,禁用 NRI 插件。

  • disable_connections: 是否禁用外部插件的主动连接。默认值是 false, 即默认允许外部 NRI 插件的连接。启用外部连接后, containerd 会主动监听 socket_path 指定的 socket,允许外部插件进行插件注册。

  • plugin_config_path: 该路径是查找 NRI 插件配置文件的地方,默认为 /etc/nri/conf.d。NRI 插件的名称应该与插件名称保持一致,如 NRI 插件名称为 01-logger,则插件对应的配置文件为 01-logger.conf 或 logger.conf。注意 NRI 插件的配置文件不像 CNI ,NRI 插件没有特定的格式,是由 NRI 插件自行定义的格式。

  • plugin_path:  NRI 插件存放的路径,默认路径为 /opt/nri/plugins。NRI 插件启用后,containerd 会在该路径查找 NRI 插件进行自动注册。注意插件的命名要符合一定的规则, 命名格式要按照 idx-pluginname的格式,其中 idx 为 两位字符,范围是 00-99 的数字对应的字符串,如 01-logger

  • plugin_registration_timeout: 插件注册的超时间,默认是 5s。

  • plugin_request_timeout: 插件请求的超时时间,默认是 2s。

  • socket_path: containerd 和 NRI 插件交互的 socket 地址,默认是 /var/run/nri/nri.sock

    NRI 插件的启动有两种方式,一种是 containerd 自动启动一种是 NRI 插件外部运行

  1. containerd 预注册启动: 当 containerd 的配置文件中启用 NRI 插件后,NRI Adaptation 实例化时,即会自动启动 NRI 插件。这种方式需要将 NRI 插件放在 plugin_path 中,默认路径为 /opt/nri/plugins,并将 NRI 插件所需的配置文件放在 plugin_config_path 中,默认路径为 /etc/nri/conf.d。当 containerd 启动时就会自动加载并运行 /opt/nri/plugins 路径下的 NRI 插件,并获取 /etc/nri/conf.d 下插件对应的配置文件对插件进行配置。

  2. NRI 插件外部启动: 这种这种方式下 NRI 插件进程可以由 systemd 创建,或者运行在 Pod中。只要保证NRI插件可以通过 NRI socket 和 containerd 进行通信即可,默认的 NRI socket路径为/var/run/nri/nri.sock。NRI 插件启动后,会通过 NRI Socket 向 NRI Adaptation 中注册自己。使用 NRI 插件外部启动方式时,一定要确保 NRI 配置中的 disable_connections 为 false。

7. containerd 插件示例

containerd NRI 项目中提供了多个 NRI 插件 详细参见 github.com/containerd/nri/tree/main/plugins,本文以 logger 示例进行演示。首先下下载并编译相关插件二进制,命令如下。

git clone https://github.com/containerd/nri
cd nri
make

执行成功后会在 build/bin/ 目录下生成对应的 NRI 插件二进制,如下。

root@zjz:/code/src/nri/build/bin# ls
device-injector  differ  hook-injector  logger  template  v010-adapter

接下来分别通过 NRI 插件外部启动和 containerd 预注册方式启动两种方式进行演示。

1. NRI 插件外部启动方式启动

首先以外部启动方式启动,外部 NRI 启动方式直接启动二进制文件即可,命令如下:

build/bin/logger -idx 00 -log-file /var/run/containerd/nri/logger.log

其中:

  • 通过 -idx 指定注册给 adaptation 的 index,该项为插件的 index 值,adaptation,会按照 index 递增的顺序,依次调用多个插件。

  • 通过 -log-file 指定日志输出的路径,若不指定,logger 插件将会打印到 stdout,此处打印到指定路径 /var/run/containerd/nri/logger.log  打开另一个 terminal 窗口, 执行命令如下。

tail -f /var/run/containerd/nri/logger.log

可以看到 logger 插件已经能够正常打印日志了,如下图所示。

图片

  图 NRI logger 插件 日志打印结果   

2. containerd 预注册方式启动

该方式与之前配置不同的是要将 NRI 插件和 NRI 插件的配置文件放在指定的路径下 。

注意以下命令的操作是在 nri 插件编译完成之后进行的。

cp build/bin/logger /opt/nri/plugins/01-logger
tee /etc/nri/conf.d/01-logger.conf <<- EOF
logFile: /var/run/containerd/nri/logger.log
EOF

通过 systemctl重启 containerd。

systemctl restart containerd

注意需要重启 containerd,因为 containerd 在初次启动时才会进行加载 NRI 插件。

查看 logger 日志,可以看到 NRI logger 插件已经成功注册。

root@zjz:~# tail -f /var/run/containerd/nri/logger.log
time="2023-06-10T15:40:28+08:00" level=info msg="Subscribing plugin 01-01-logger (01-logger) for events RunPodSandbox,StopPodSandbox,RemovePodSandbox,CreateContainer,PostCreateContainer,StartContainer,PostStartContainer,UpdateContainer,PostUpdateContainer,StopContainer,RemoveContainer"
time="2023-06-10T15:40:28+08:00" level=info msg="Started plugin 01-01-logger..."

8. NRI 插件的应用

NRI 插件的出现可以补充 Kubernetes 对于节点层面资源管理功能的不足,如CPU编排、内存分层、缓存管理、IO管理等。使用 NRI 插件可以将 kubelet的 Resource Manager 下沉到 CRI Runtime层进行管理。kubelet当前不适合处理多种需求的扩展,在 kubelet层增加细粒度的资源分配会导致 kubelet和 CRI 的界限越来越模糊。而 NRI 插件,则是在 CRI 生命周期间做调用与修改,更适合做资源绑定和节点的拓扑感知。并且在 CRI 内部做插件定义和修改,可以做到上层 Kubenetes 不感知的情况下做适配。

以上内容节选自新书 《containerd 原理剖析与实战》

最后,附上本书的购买链接,新书刚刚上架原价 109,限时优惠内购 69.9 元

  • 24
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值