k8s 运行时containerd

318 阅读15分钟

什么是运行时

在Kubernetes(简称K8s)中,运行时通常指的是容器运行时(Container Runtime),它是负责管理镜像和容器生命周期的重要组件。Kubernetes通过Container Runtime Interface (CRI)与容器运行时交互,以管理镜像和容器
k8s官网介绍:kubernetes.io/zh-cn/docs/…

常见的运行时

Docker

Docker是最知名的容器运行时之一,它通过Docker Daemon来启动容器,并通过集成containerd、runc等多个组件来完成容器的生命周期管理
网址:docs.docker.com/reference/c…

Containerd

Containerd是一个更为轻量级的容器运行时,它专注于容器的生命周期管理,并向上为Docker Daemon提供gRPC接口
网址:containerd.io/docs/

CRI-O

CRI-O是专门为Kubernetes设计的容器运行时,它实现了CRI接口,可以直接与Kubernetes的Kubelet交互。CRI-O的目标是提供一个轻量级、高性能的容器运行时,它不包含Docker的上层编排功能,只专注于容器的运行时环境
网址:github.com/cri-o/packa…

Mirantis Container Runtime(MCR)

Mirantis Container Runtime(MCR) 是一种商用容器运行时,以前称为 Docker 企业版。 你可以使用 MCR 中包含的开源 cri-dockerd组件将 Mirantis Container Runtime 与 Kubernetes 一起使用
网址:docs.mirantis.com/mcr/20.10/i…

常用的两个运行时的区别

k8s 常用的两个运行时为 docker 和 Containerd,下边就来对比一下 docker 和 Containerd的区别。

如何选择运行时

k8s官网介绍 Kubernetes 1.24 通过 Dockershim 对 Docker 的支持已移除,新建节点的容器运行时请使用 Containerd,通过 Docker 构建的镜像可以继续使用。 Containerd 是更为稳定的运行时组件,支持 OCI 标准,但不支持 Docker API。

Containerd:调用链更短,组件更少,更稳定,占用节点资源更少。建议选择 Containerd。

Docker:在以下情况下,请选择 Docker 作为运行时组件:

  • 如需使用 Docker in Docker。
  • 如需在 TKE 节点使用 docker build/push/save/load 等命令。
  • 如需调用 Docker API。
  • 如需使用 Docker Compose 或 Docker Swarm。

Containerd 和 Docker 组件常用命令是什么?

Containerd 不支持 docker API 和 docker CLI,但是可以通过 cri-tool 命令实现类似的功能。

镜像相关

镜像相关功能DockerContainerd
显示本地镜像列表docker imagescrictl images
下载镜像docker pullcrictl pull
上传镜像docker push
删除本地镜像docker rmicrictl rmi
查看镜像详情docker inspect IMAGE-IDcrictl inspect IMAGE-ID

容器相关

容器相关功能DockerContainerd
显示容器列表docker pscrictl ps
创建容器docker createcrictl create
启动容器docker startcrictl start
停止容器docker stopcrictl stop
删除容器docker rmcrictl rm
查看容器详情docker inspectcrictl inspect
attachdocker attachcrictl attach
execdocker execcrictl exec
logsdocker logscrictl logs
statsdocker statscrictl stats

POD 相关

POD 相关功能DockerContainerd
显示 POD 列表crictl pods
查看 POD 详情crictl inspectp
运行 PODcrictl runp
停止 PODcrictl stopp

调用链区别有哪些?

Docker 作为 K8S 容器运行时,调用关系如下: kubelet --> docker shim (在 kubelet 进程中) --> dockerd --> containerd

Containerd 作为 K8S 容器运行时,调用关系如下: kubelet --> cri plugin(在 containerd 进程中) --> containerd

Stream 服务的区别

kubectl exec/logs 等命令需要 kubelet 在 apiserver 跟容器运行时之间建立流转发通道。

Stream 服务的原理是什么?

可以通过了解kubectl exec命令的原理来了解 CRI 的 Stream Service 是如何工作的:

  1. dockershim 或 containerd 在启动后会监听某个端口,用以运行 Stream 服务。

  2. 当执行 kubectl exec 等命令时,请求经过 kube-apiserver 找到 Pod 对应的节点,转发到 kubelet。

  3. 此时 kubelet 会请求 CRI-Service(dockershim 或 containerd-cri)的 GetExec 接口,CRI-Server 会为本次请求生成一个随机的 Token,记录后和 CRI Stream server 监听端口组合成 URL,返回给 kubelet。

  4. kubelet 将 kube-apiserver 发过来的 HTTP 请求升级为 websocket,并作为 apiserver 和 CRI Stream 之间的 proxy 转发数据。

如何在 Containerd 中使用并配置 Stream 服务?

Docker API 本身提供 stream 服务,Dockershim 位于 kubelet 内部有默认的配置 "127.0.0.1:0"。

Containerd 的 stream 服务需要单独配置:

[plugins.cri]
  stream_server_address = "127.0.0.1"
  stream_server_port = "0"
  enable_tls_streaming = false

K8S 1.11 前后版本配置区别是什么?

Containerd 的 stream 服务在 K8S 不同版本运行时场景下配置不同。

在 K8S 1.11 之前: Kubelet 不会做 stream proxy,只会做重定向。即 Kubelet 会将 containerd 暴露的 stream server 地址发送给 apiserver,并让 apiserver 直接访问 containerd 的 stream 服务。此时,您需要给 stream 服务转发器认证,用于安全防护。

在 K8S 1.11 之后: K8S 1.11 引入了 kubelet stream proxy,使 containerd stream 服务只需要监听本地地址即可。

容器 Exec 的区别

Docker 和 Containerd 在 Exec 的实现上略有区别,区别主要在 Execsync 的实现上,也即执行单条命令的情况。

因此 kubectl exec命令在不指定参数-it时 和 pod lifecycle 中的 ExecProbe 在 Runtime 类型不同的节点上表现可能稍有不同。

kubectl exec 的区别

docker exec 时会以当前 exec 首进程结束为 exec 结束的标志。

CRI exec 会以当前 exec 中所有进程结束为本次 exec 结束。

例如 kubectl exec <pod-id> -- bash -c "nohup sleep 10 &" 命令,在 Runtime 是 Docker 的节点上会在两秒左右结束;而在 Containerd 的节点上需要等到 sleep 进程退出后才能结束。

pod exec probe 的区别

kubelet 在实现 exec probe 时使用了 CRI Runtime 的 ExecSync 接口,因此 exec probe 和 kubectl exec # no -t -i 的表现一致,也即:

在 Docker 节点上,exec probe 如果残留子进程仍会正常退出。

在 Containerd 节点上,exec probe 会等到 probe 中所有的进程退出再结束。

区别导致的影响主要出现在 pod lifecycle 的 postStartHook 和 preStopHook 中,如果在 hook 中使用 exec probe 并且出现残留子进程的情况,在 containerd 的节点上可能会遇到 Pod 长期卡在 containerCreating 状态。原因是 kubelet 在 syncPod 时会逐个容器拉起,并执行 probe,如果某个 probe 因上述原因阻塞住,会导致后续容器无法启动。

在 ExecProbe 中拉起子进程并退出父进程属于 K8S 中未定义的行为,具体表现可能会和运行时版本、种类相关,因此建议尽量不要在 probe 执行过于复杂的操作。

容器网络的区别

在正常情况下,Pod 中的容器会共享同一个 Network Namespace,因此 Pod 需要在创建 Sandbox 容器时将网络准备好。为了更好的说明区别,我们简单介绍下 Pod 网络初始化的流程:

  1. kubelet 调用 CRI Runtime(dockershim 或 containerd)创建 Pod 的 Sandbox 容器。

  2. CRI Runtime 调用底层 docker 或 containerd 创建 pause 容器(此时 pause 进程还没启动,但已经初始化 Network Namespace)。

  3. CRI Runtime 调用 CNI 执行在 Network Namespace 中创建 veth 并加入 cbr0 网桥等网络初始化操作。

  4. CRI Runtime 启动 pause 容器,pause 进程被拉起。

  5. kubelet 继续调用 CRI Runtime 执行创建容器等后续操作。

Docker 和 containerd 在创建 pause 容器并初始化 Network Namespace调用 CNI 初始化 veth 这两步有区别。

创建 pause 容器

Containerd 是为了 kubernetes 设计的 CRI Runtime,没有独立的网络模块;但 Docker 在设计时带有自己的网络功能,因此 docker 在创建 Pause 容器时,会进行 Docker 特有的网络设置。该设置导致和 Containerd 最大的区别是在不使用 IPv6 的情况下,Docker 会将容器 Network Namespace 中内核参数net.ipv6.conf.all.disable_ipv6 设置为1,也即关闭容器内的 ipv6 选项。

同样的 Pod 在 Docker 的节点上只开启了 IPv4,而在 Containerd 的节点上会同时开启 IPv4 和 IPv6。

同时开启 IPv4 和 IPv6 的情况中,DNS 解析可能会同时发出v4、v6两个版本的包。在某些情况,业务如果需要频繁进行 DNS 解析,可能会触发 DNS 解析库的 Bug(取决于 Pod 业务的实现时的依赖)。在 Containerd 节点上,可以通过给 Pod 添加 init container 来针对 Pod 关闭 IPv6 设置。代码如下:

apiVersion: v1
kind: Pod
...
spec:
  initContainers:
  - image: busybox
    name: sysctl
    command: ["sysctl", "-w", "net.ipv6.conf.all.disable_ipv6=1"]
    securityContext:
      privileged: true
...

调用 CNI

两者在调用 CNI 上没有实质区别。

对比项DockerContainerd
谁负责调用 CNIKubelet 内部的 docker-shimContainerd 内置的 cri-plugin
如何配置 CNIKubelet 参数 --cni-bin-dir--cni-conf-dirContainerd 配置文件(toml): [plugins.cri.cni] bin_dir = "/opt/cni/bin" conf_dir = "/etc/cni/net.d"

容器日志的区别

对比项DockerContainerd
存储路径如果 Docker 作为 K8S 容器运行时,容器日志的落盘将由 docker 来完成,保存在类似/var/lib/docker/containers/$CONTAINERID目录下。Kubelet 会在/var/log/pods 和 /var/log/containers下面建立软链接,指向/var/lib/docker/containers/$CONTAINERID该目录下的容器日志文件。如果 Containerd 作为 K8S 容器运行时, 容器日志的落盘由 Kubelet 来完成,保存至/var/log/pods/$CONTAINER_NAME目录下,同时在/var/log/containers目录下创建软链接,指向日志文件。
存储大小Pod 中的每个容器,docker 默认会保留 100MB*10 = 1G 日志Pod 中的每个容器,containerd 默认会保留 10MB*5 = 50MB 日志
配置参数在 docker 配置文件中指定:
"log-driver": "json-file",
"log-opts": {"max-size": "100m","max-file": "5"}
方法一:在 kubelet 参数中指定:
--container-log-max-files=5
--container-log-max-size="100Mi"
方法二:在 KubeletConfiguration 中指定:
"containerLogMaxSize": "100Mi",
"containerLogMaxFiles": 5,
把容器日志保存到数据盘把数据盘挂载到 “data-root”(缺省是 /var/lib/docker)即可。创建一个软链接 /var/log/pods 指向数据盘挂载点下的某个目录。在 TKE 中选择“将容器和镜像存储在数据盘”,会自动创建软链接 /var/log/pods

containerd 运行时的使用

参考地址:github.com/containerd/…

安装 Containerd

下载
cri-containerd-cni-1.6.14-linux-amd64.tar.gz压缩包中已经按照官方二进制部署推荐的目录结构布局好。 里面包含了systemd配置文件,containerd以及cni的部署文件。 下载并解压缩到系统的根目录/中:

[root@test01v ~]# wget https://github.com/containerd/containerd/releases/download/v1.6.14/cri-containerd-cni-1.6.14-linux-amd64.tar.gz
[root@test01v ~]# tar -zxvf cri-containerd-cni-1.6.14-linux-amd64.tar.gz -C /

注意经测试cri-containerd-cni-1.6.4-linux-amd64.tar.gz包中包含的runc在CentOS 7下的动态链接有问题,这里从runc的github上单独下载runc,并替换上面安装的containerd中的runc

[root@test01v ~]# wget https://github.com/opencontainers/runc/releases/download/v1.1.2/runc.amd64

配置containerd文件

[root@test01v ~]# mkdir -p /etc/containerd
[root@test01v ~]# containerd config default > /etc/containerd/config.toml

修改/etc/containerd/config.toml
1). 将sandbox_image = "registry.k8s.io/pause:3.6" 修改为 sandbox_image = "registry.aliyuncs.com/google_containers/pause:3.9"
2). SystemdCgroup = false 修改为 SystemdCgroup = true

一个参考配置 ``

# 禁用指定的插件
disabled_plugins = []

# 指定要导入的配置文件或插件
imports = []

# 这个分数决定了当系统内存不足时,Containerd进程被内核选中进行内存回收的概率。分数越高,被选中的概率越大,
oom_score = -998

# 指定插件目录的路径。插件目录用于存放自定义插件
plugin_dir = ""
required_plugins = []

# 用于指定容器的根目录、存储状态的路径、临时文件的存储路径
root = "/var/lib/containerd"
state = "/run/containerd"
temp = ""

# 配置文件的格式版本,如语法和结构
version = 2

[debug]
  address = ""
  format = ""
  gid = 0
  level = ""
  uid = 0

# git上插件列表
# https://github.com/containerd/containerd/blob/main/docs/PLUGINS.md
# https://github.com/containerd/containerd/tree/main/plugins

[plugins]

  #(CRI)交互相关的设置
  [plugins."io.containerd.grpc.v1.cri"]
    # 设备所有权是指控制对设备的访问权限和属性的能力
    device_ownership_from_security_context = false 
	
    # disable_apparmor = false时Containerd将启用AppArmor进行安全策略的检查和应用
    disable_apparmor = false

    # disable_cgroup = false时containerd会利用cgroup的机制对容器进行资源的限制、隔离和统计等操作
    disable_cgroup = false

    # 禁用hugetlb(大页内存)控制器
    disable_hugetlb_controller = true

    # 不禁用proc挂载,容器内的进程能够访问系统相关的运行时信息,例如查看进程信息、系统资源使用情况等
    disable_proc_mount = false
	
    # 禁用TCP服务,避免容器内不必要的TCP连接
    disable_tcp_service = true

    # 不启用SELinux
    enable_selinux = false
	
    # 不启用TLS
    enable_tls_streaming = false

    # 不启用无特权的网络诊断
    enable_unprivileged_icmp = false
	
    # 不启用无特权端口
    enable_unprivileged_ports = false
	
    # 不会忽略由镜像定义的卷,确保容器正常使用卷相关的功能
    ignore_image_defined_volumes = false

    # 最大并发下载数量为20,如下载镜像或者其他相关文件
    max_concurrent_downloads = 20

    # 容器日志行的最大大小为16384字节
    max_container_log_line_size = 16384
	
    # 网络命名空间(netns)挂载不在状态目录下进行,确保网络配置的独立性和安全性,避免对状态目录的不必要影响或者干扰。
    netns_mounts_under_state_dir = false
	
    # 不限制OOM(内存溢出)分数调整,意味着容器内的进程可以根据系统默认或者自定义的策略自由调整其OOM分数,从而影响在内存紧张情况下自身被杀死的优先级
    restrict_oom_score_adj = false
	
    # 指定容器沙箱使用的镜像,用于在容器启动时创建一个基本的运行环境。
    sandbox_image = "registry.k8s.io/pause:3.6"
	
    # 容器可以使用的 SELinux 安全类别的数量
    selinux_category_range = 1024

    # 每 10 秒收集一次容器的资源使用情况
    stats_collect_period = 10

    # 表示如果流服务器在指定时间内没有接收到任何数据,则会自动关闭连接。这有助于防止长时间空闲的连接占用不必要的资源,确保系统资源的有效利用
    stream_idle_timeout = "4h0m0s"

    # 只有本地进程可以连接到流服务器
    stream_server_address = "127.0.0.1"

    # 用于指定流服务器监听的端口。设置为 "0" 表示让操作系统自动选择一个可用的端口
    stream_server_port = "0"

    # false表示 containerd 不使用 systemd 来管理 cgroups,而是自行管理
    systemd_cgroup = false

    # true表示containerd 容忍缺少 hugetlb 控制器管理大页内存的分配
    tolerate_missing_hugetlb_controller = true

    # 用于指定 seccomp配置文件的路径,设置为空字符串表示不对容器内的系统调用进行限制
    unset_seccomp_profile = ""
 
    [plugins."io.containerd.grpc.v1.cri".containerd]
      #snapshotter: 指定快照器插件,用于管理容器的文件系统快照。常见的快照器有 overlayfs 和 btrfs
      snapshotter = "overlayfs"    

  # 是containerd的一个内部插件,用于配置容器的重启策略。interval指containerd会在等待指定时间后再尝试重新启动该容器
  [plugins."io.containerd.internal.v1.restart"]
    interval = "10s"

  # 指定镜像仓库的认证信息
  [plugins."io.containerd.grpc.v1.cri".registry.configs."0.0.0.0:8888".auth]
    username = "test"
    password = "test"

[timeouts]
  "io.containerd.timeout.bolt.open" = "0s"

  # 当进行shim的清理操作超时时间、加载操作超时时间、关闭操作超时时间
  "io.containerd.timeout.shim.cleanup" = "5s"
  "io.containerd.timeout.shim.load" = "5s"
  "io.containerd.timeout.shim.shutdown" = "3s"
  
  # 查询或者等待任务状态的操作在2s内没有完成,就会被判定为超时
  "io.containerd.timeout.task.state" = "2s"

启动服务

配置containerd开机启动,并restart
[root@test06v ~]# systemctl enable containerd --now
[root@test06v ~]# systemctl restart containerd
使用crictl测试一下,确保可以打印出版本信息并且没有错误信息输出:
[root@test06v ~]# crictl version
Version:  0.1.0
RuntimeName:  containerd
RuntimeVersion:  v1.6.14
RuntimeApiVersion:  v1

可下载安装 nerdctl ,nerdctl兼容 docker CLI,适用于从 docker 转到 containerd 的使用习惯

wget https://github.com/containerd/nerdctl/releases/download/v1.0.0/nerdctl-1.0.0-linux-amd64.tar.gz
tar -xzvf nerdctl-*-linux-amd64.tar.gz -C /usr/local/bin/

使用eg:
nerdctl run --name redis redis:alpine

使用crictl

以下是 crictl 的一些常用命令及其描述和举例:

命令描述举例
crictl run创建并运行一个新的容器crictl run --rm --runtime io.containerd.runc.v2 --name test busybox sh
crictl start启动一个已经创建的容器crictl start <container-id>
crictl stop停止一个正在运行的容器crictl stop <container-id>
crictl restart重启一个容器crictl restart <container-id>
crictl kill强制终止一个容器crictl kill <container-id>
crictl delete删除一个容器及其相关的资源crictl delete <container-id>
crictl inspect查看一个容器的详细信息crictl inspect <container-id>
crictl ps列出所有容器,包括运行中和已停止的容器crictl ps
crictl logs查看指定容器的日志输出crictl logs <container-id>
crictl images列出本地存储的镜像crictl images
crictl pull拉取指定镜像到本地crictl pull nginx:latest
crictl rmi删除本地的指定镜像crictl rmi nginx:latest
crictl config配置crictl的相关参数,如运行时端点和镜像端点crictl config --set runtime-endpoint=unix:///run/containerd/containerd.sock
crictl info显示crictl的版本信息和配置信息crictl info
crictl version显示crictl支持的API版本crictl version
crictl stats显示容器的资源使用情况crictl stats <container-id>
crictl events显示容器的事件日志crictl events
crictl exec在运行的容器中执行命令crictl exec -it <container-id> sh
crictl attach附加到正在运行的容器的标准输入、输出和错误crictl attach <container-id>
crictl port-forward将本地端口转发到容器内的端口crictl port-forward <container-id> 8080:80
crictl cp复制文件或目录到或从容器crictl cp <container-id>:/etc/passwd .
crictl create创建一个新的容器但不启动它crictl create --runtime io.containerd.runc.v2 --name test busybox sh
crictl checkpoint创建容器的检查点crictl checkpoint <container-id>
crictl restore从检查点恢复容器crictl restore <checkpoint-id>
crictl update更新容器的某些属性,如资源限制等crictl update --resources.limits.cpu=1000 <container-id>
crictl pause暂停一个容器的运行crictl pause <container-id>
crictl unpause恢复一个被暂停容器的运行crictl unpause <container-id>
crictl top查看容器内运行的进程信息crictl top <container-id>
crictl diff显示容器文件系统的变更crictl diff <container-id>
crictl wait等待容器达到特定状态,如运行或停止crictl wait <container-id>
crictl load加载镜像存档文件到本地镜像存储库crictl load -i my-image.tar
crictl save将本地镜像存储为存档文件crictl save nginx:latest -o my-image.tar
crictl bundle创建一个 OCI bundlecrictl bundle /path/to/bundle
crictl mount显示容器的挂载信息crictl mount <container-id>
crictl unmount卸载容器的挂载点crictl unmount <container-id>
crictl stats显示容器的资源使用情况crictl stats <container-id>
crictl events显示容器的事件日志crictl events
crictl exec在运行的容器中执行命令crictl exec -it <container-id> sh
crictl attach附加到正在运行的容器的标准输入、输出和错误crictl attach <container-id>
crictl port-forward将本地端口转发到容器内的端口crictl port-forward <container-id> 8080:80
crictl cp复制文件或目录到或从容器crictl cp <container-id>:/etc/passwd .