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)。
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 格式資料與外掛互動。
以 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 版本 NRI 只需要執行一個外掛範例用於處理所有 NRI 事件和請求,容器執行時通過 unix-domain socket 與外掛通訊,使用基於 protobuf 的協定資料,和 1.0 版本相比擁有更高的效能,能夠實現有狀態的 NRI 外掛。
最新發布的 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
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
}
建立容器 (*)
建立容器完成
啟動容器
啟動容器完成
更新容器 (*)
更新容器完成
停止容器 (*)
刪除容器
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