Containerd NRI 外掛

2023-02-06 06:01:21

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)

  • 容器 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.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 外掛可使用的資訊

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


更新容器時可修改的資訊

容器被建立成功後,外掛可以在以下時間請求更新容器的資訊:

  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

  • 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