体验 Istio (一): Containerd 作为容器运行时

体验istio需要k8s环境,istio官方文档指出可以使用kvm驱动的minikube,这种方式相比在虚拟机中启动docker驱动的minikube可能具备更高的资源利用率,无论如何,使用istio建议的命令minikube start --memory=16384 --cpus=4启动后者,在部署istio demo加addons之后使用体验极差,docker in docker中的in docker可能失去响应(可以参考另一篇博客使用Rootless Docker运行Minikube,了解docker驱动的minikube),所以在资源有限的情况下,这里选择使用kubeadm+containerd部署k8s双节点集群,一个master节点运行控制面组件,一个worker节点运行工作负载。docker可能已经不是建议使用的容器运行时,有containerd和cri-o作为备选,他们都是cncf毕业的容器运行时项目,cri-o更是k8s社区操刀,不过因为containerd+nerdctl有面向最终用户的潜力(姑且这样说),所以这里使用containerd作为容器运行时。这是一个系列博客,体验istio不仅仅是在一个现成的环境中尝试它的功能。

部署containerd的目的是部署k8s,所以根参考文档为k8s官方文档中的Container Runtimes

基础环境

  • ubuntu22.04,由virtualbox创建的虚拟机,配置两个网卡:
    • 仅主机网络,固定ip,用于宿主机连接虚拟机
    • nat网络,固定ip,用于连接互联网
  • rootfull containerd

对于contianerd来说网络部分没有特殊要求,虚拟机能够访问互联网即可,以上设置对于部署k8s有意义。

部署containerd

参考Getting started with containerd,可以看到containerd有三种安装方式:

  • 二进制,官方提供针对常见平台的可执行文件,分为多个步骤安装依赖和插件
  • 使用包管理器apt或者dnf,这里要注意,deb和rpm包由docker分发,这意味着通过安装docker获得其内嵌的containerd,但是内嵌的containerd并未安装cni插件,cri插件默认也是禁用的,仍然需要配置,并不像安装docker那样轻松
  • 最后是源码安装,适合官方未提供二进制版本(例如alpine linux)或者其他情况

相比来说,使用二进制安装似乎更加纯正一些,官方提供的二进制包有两种类型containerd-*cri-containerd-*,后者更大,包含了runc、gce及其他一些配置,是已经标记废弃的发行包,这里使用containerd-*包安装。

准备

在安装之前需要做一些准备工作,为操作系统增加内核模块并配置,以允许iptables规则对网桥内的流量生效,否则同网桥内的服务不能通过iptables创建的地址通信,在k8s中表现为相同node的pod之间无法通过service cluster ip通信,配置过程如下:

# 开机加载配置的内核模块
cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF

# 立即加载内核模块
sudo modprobe overlay
sudo modprobe br_netfilter

# 配置内核模块
cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-iptables  = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward                 = 1
EOF

# 使配置生效
sudo sysctl --system

安装containerd

releases下载最新的稳定版本,并执行命令sudo tar Cxzvf /usr/local containerd-1.7.11-linux-amd64.tar.gz 解压至/usr/local目录,然后需要创建service文件,使用systemd管理containerd进程:

# 创建service
sudo mkdir -p /usr/local/lib/systemd/system/
cat <<EOF | sudo tee /usr/local/lib/systemd/system/containerd.service
[Unit]
Description=containerd container runtime
Documentation=https://containerd.io
After=network.target local-fs.target

[Service]
ExecStartPre=-/sbin/modprobe overlay
ExecStart=/usr/local/bin/containerd

Type=notify
Delegate=yes
KillMode=process
Restart=always
RestartSec=5

# Having non-zero Limit*s causes performance problems due to accounting overhead
# in the kernel. We recommend using cgroups to do container-local accounting.
LimitNPROC=infinity
LimitCORE=infinity

# Comment TasksMax if your systemd version does not supports it.
# Only systemd 226 and above support this version.
TasksMax=infinity
OOMScoreAdjust=-999

[Install]
WantedBy=multi-user.target
EOF

# 启动并配置开机启动
sudo systemctl daemon-reload
sudo systemctl enable --now containerd

以上service文件中,有两项需要特别关注的配置,来自systemd

Delegate=yes and KillMode=process are the two most important changes you need to make in the [Service] section.

Delegate allows containerd and its runtimes to manage the cgroups of the containers that it creates. Without setting this option, 
systemd will try to move the processes into its own cgroups, causing problems for containerd and its runtimes to properly account 
for resource usage with the containers.

KillMode handles when containerd is being shut down. By default, systemd will look in its named cgroup and kill every process that 
it knows about for the service. This is not what we want. As ops, we want to be able to upgrade containerd and allow existing 
containers to keep running without interruption. Setting KillMode to process ensures that systemd only kills the containerd daemon 
and not any child processes such as the shims and containers.

安装runc

releases下载最新的稳定版本,执行命令sudo install -m 755 runc.amd64 /usr/local/sbin/runc安装。

安装cni插件

releases下载最新稳定版本,执行命令:

sudo mkdir -p /opt/cni/bin
sudo tar Cxzvf /opt/cni/bin cni-plugins-linux-amd64-v1.4.0.tgz

安装nerdctl

containerd自带了仅用于debugging的原生客户端ctr,nerdctl是containerd的非核心子项目,兼容docker cli、对人类友好,也就是说,可以把类似docker info命令里的docker直接换成nerdctl,它与docker cli的不同可以参考How is nerdctl different from docker ?本质上它就是来对标docker cli,在主机上管理容器生命周期的工具,而对于k8s来说,kubelet通过cri调用运行时,与nerdctl的实现无关,在本小节会提到相关的问题

这里额外安装nerdctl,因为它是官方文档提到的唯一非debugging用途客户端。从releases下载最新版本,这里提供了minimal和full两种类型的包,minimal仅安装客户端和两个rootless相关的安装脚本,full模式能够一键安装整个containerd运行时(将所有东西安装至/usr/local中,与本文档中分步安装的文件位置不同),但是containerd的Getting started with containerd文档中并未介绍这种方式,仅在Running containerd as a non-root user文档中提到。这里minimal即可,执行命令sudo tar Cxzvvf /usr/local/bin nerdctl-1.7.2-linux-amd64.tar.gz安装。

这里会出现第一个困惑点,即nerdctl会忽略containerd配置文件/etc/containerd/config.toml[plugins."io.containerd.grpc.v1.cri"]下的配置,因为nerdctl没有使用cri api,具体的表现在这里是:执行命令sudo nerdctl info看到cgroup driver是systemd,但在/etc/containerd/config.toml中的[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]配置下却是SystemdCgroup = false,这实际上是符合预期的,nerdctl并不使用cri的配置,而是为使用cgroup v2的平台自动设置驱动为systemd(参考How to change the cgroup driver?)。

如果想要在nerdctl获得类似非root用户执行docker命令的体验,那么可以参考oes nerdctl have an equivalent of sudo usermod -aG docker ?

nerdctl相关的文件布局可以参考nerdctl directory layout,仅列出rootful的情况:

  • config:/etc/nerdctl/nerdctl.toml
  • dataroot:/var/lib/nerdctl
  • cni:/etc/cni/net.d,其中的配置文件由nerdctl自动创建

不使用cri api会导致我们不能通过nerdctl了解cri api的情况,因为kubelet是使用cri api的,所以有必要安装另一个用于调试的、使用cri api的客户端crictl,这里就有一些割裂,我们预期要安装三个客户端,并且在一定程度上带来了混乱。

安装crictl

releases下载最新的稳定版本,执行命令sudo tar zxvf crictl-v1.29.0-linux-amd64.tar.gz -C /usr/local/bin安装。

安装完成后必须创建配置文件,至少指定当前使用的运行时的socket,暂时使用官方文档示例的配置:

cat <<EOF | sudo tee /etc/crictl.yaml
runtime-endpoint: unix:///run/containerd/containerd.sock
image-endpoint: unix:///run/containerd/containerd.sock
timeout: 2
debug: true
pull-image-on-create: false
EOF

此时如果执行sudo crictl info会看到cni相关的错误信息,可以忽略(在计划里nerdctl第一次运行容器时会自动生成位于/etc/cni/net.d的cni网络配置,后面的验证部分还会提到),同样也能看到SystemdCgroup": false配置,它与containerd的配置是一致的。

配置containerd

与docker一样,containerd也需要拉取镜像、管理容器生命周期、滚动容器日志等,因此也需要做相关配置。containerd安装完成后并未创建默认的配置文件,因此首先执行命令sudo sh -c "containerd config default > /etc/containerd/config.toml"从默认配置创建配置文件,然后在配置文件中修改,注意在修改后执行命令sudo systemctl restart containerd重启containerd。

镜像加速

加速配置属于cri插件,参考Full configurationRegistry Configuration - Introduction可知,从1.4版本开始,config.toml中的[plugins."io.containerd.grpc.v1.cri".registry.mirrors]项进入废弃状态,推荐使用hosts.toml的方式进行配置,这样还有一个好处是加速配置能够被nerdctl复用。首先在config.toml中作如下配置以启用hosts.toml方式,否则之前提到的废弃配置仍然生效:

    [plugins."io.containerd.grpc.v1.cri".registry]
      config_path = "/etc/containerd/certs.d"

创建docker.io加速配置:

sudo mkdir -p /etc/containerd/certs.d/docker.io

cat <<EOF | sudo tee /etc/containerd/certs.d/docker.io/hosts.toml
# https://github.com/containerd/containerd/blob/main/docs/hosts.md#hoststoml-content-description---detail

server = "https://docker.io" # Exclude this to not use upstream

[host."https://docker.m.daocloud.io"]
  capabilities = ["pull", "resolve"]
EOF

当然,还可以配置其他仓库的镜像,这里为registry.k8s.io创建来自daocloud的镜像,参考支持前缀替换的 Registry

sudo mkdir -p /etc/containerd/certs.d/registry.k8s.io

cat <<EOF | sudo tee /etc/containerd/certs.d/registry.k8s.io/hosts.toml
# https://github.com/containerd/containerd/blob/main/docs/hosts.md#hoststoml-content-description---detail

server = "https://registry.k8s.io" # Exclude this to not use upstream

[host."https://k8s.m.daocloud.io"]
  capabilities = ["pull", "resolve"]
EOF

这样就可以在网络限制的情况下拉取registry.k8s.io/pause镜像了,方便不少,还可以参考daocloud的项目文档添加更多的mirror。/etc/containerd/certs.d/目录下的配置不用重启containerd即可生效。

日志滚动

设计良好的镜像在运行时会将日志写入stdout/stderr中,以这种方式解耦容器的持久化实现,nerdctl启动的容器支持多种日志驱动,默认使用json-file,日志配置在运行容器时的命令sudo nerdctl run --log-opt中指定,但是帮助信息中并未说明不同驱动下的配置项和默认值,从源码json_logger.go中可以看到,对于json-file驱动来说,配置项包含log-path、max-size、max-file,其中log-path不需要配置,max-size默认为无限制,max-file默认为1,在测试环境中配置max-size可以节省存储空间,不过仅针对nerdctl命令启动容器的情况。

对于cri api来说,kubelet启动的容器,将由其自身完成对容器stdout/stderr的转储,将在后续博客中介绍。

cgroup驱动

在k8s的cgroup drivers文档中指出:

  • 使用cgroup v2时(可执行命令stat -fc %T /sys/fs/cgroup/验证,输出为cgroup2fs时表明正在使用cgroup v2)建议使用systemd作为cgroup驱动
  • 在systemd作为init系统时建议使用systemd作为cgroup驱动,以满足single-writer原则,保证系统内只有一个cgroup管理器,否则可能出现压力下由systemd管理的进程不稳定的情况

基于以上两点,需要将容器运行时与kubelet的cgroup驱动均配置为systemd,这里暂时只是说明对containerd的配置,执行命令sudo vim /etc/containerd/config.toml中修改以下配置:

[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
  ...
  [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
    # 修改SystemdCgroup为true
    SystemdCgroup = true

pause镜像

pause镜像用于创建pause容器,由它来构建pod中的共享资源,比如network namespace,这样pod中的不同容器就能够通过127.0.0.1来通信,kubelet期望由容器运行时指定pause镜像,会避免对指定的镜像做垃圾收集,在这里containerd默认配置为registry.k8s.io/pause:3.8,我们修改它的动机只有两个:指定版本和避免网络不可用的情况。后面会根据k8s的版本要求修改。

containerd架构

这里就不再重复造轮子了,推荐阅读:

  • 开放容器标准(OCI) 内部分享,以了解oci、cri、docker、containerd和一些容器生态的发展历程
  • 如何为Kubernetes选择合适的容器运行时?,高级运行时与低级运行时
  • containerd,containerd项目主页,有了前面的背景知识,可以更容易理解这篇文档,不但包含技术部分,还包含这句话:“containerd is designed to be embedded into a larger system, rather than being used directly by developers or end-users.”,实际上结合nerdctl,containerd完全可以“being used directly by developers or end-users”
  • docs,官方文档,包含了cri架构、使用runc之外的运行时、垃圾收集、命名空间等特性

简单说,containerd支持cri接口和私有接口,kubelet、crictl使用cri接口与containerd交互,nerdctl则使用私有接口交互(所以需要考虑同一份配置(例如mirror)是否可以在不同的客户端生效)。与contianerd的交互会被传递或者转换为使用cri操作运行时(这里是runc),创建容器时,runc会创建一个与容器生命周期一致的containerd-shim进程作为容器的父进程,负责保持IO、打开pty master、为containerd写入容器的退出状态,以及在容器退出后接管进程(参考Container Lifecycle)。

验证

这里不但会展示containerd+nerdctl的简单用法以验证安装,同时也会包含与dockerd和其他contianerd客户端的对比。

此时containerd进程已经正常运行,如果执行命令ip addr或者sudo nerdctl network ls并不会看到任何新增的网桥,这与dockerd会在启动时创建docker0网桥不同。

拉取镜像

# 拉取镜像
sudo nerdctl pull nginx:alpine

# 查看镜像
sudo nerdctl images

执行以上命令后会自动拉取镜像docker.io/library/nginx:alpine,如果执行命令sudo nerdctl pull registry.k8s.io/kube-apiserver:v1.28.5也是可以成功的,配置的mirror生效,同样作为原生客户端,执行命令sudo ctr images ls可以看到nerdctl拉取的镜像,但crictl是看不到的,这里重新强调一下,使用hosts.toml方式为cri插件配置mirror是能够复用到nerdctl的,否则需要分别配置。

启动容器

# 启动容器,注意日志配置
sudo nerdctl run -d --name nginx \
--log-opt max-size=1m \
-p 8080:80 \
nginx:alpine

# 查看容器状态
sudo nerdctl ps

首次启动容器后会自动创建网桥,但是这里有个问题,即在主机执行命令sudo netstat -lntp看不到对8080端口的监听,但却可以访问,这跟使用默认配置下docker的经验不一样。docker以上面的方法启动容器后,与nerdctl一样会创建一系列的iptables规则,但是会额外启动两个docker-proxy进程做端口代理:

ian@ian:~$ ps -ef | grep -v grep | grep docker-proxy
root        1616     751  0 14:48 ?        00:00:00 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 8080 -container-ip 172.17.0.2 -container-port 80
root        1622     751  0 14:48 ?        00:00:00 /usr/bin/docker-proxy -proto tcp -host-ip :: -host-port 8080 -container-ip 172.17.0.2 -container-port 80

还有一点,对于nerdctl,在不手动创建cni配置时,会在首次运行容器时自动生成,接下来在非windows系统上由ocihook根据配置创建容器网络,其中就包含了对iptables的配置,从container_run.go#L324C41-L324C41中的containerutil.NewNetworkingOptionsManager开始可以追踪到相关逻辑,至于cni插件会创建什么样的规则,可以参考Plugins Overview

构建镜像

参考Setting up nerdctl build with BuildKit文档中的说明,注意以下几点:

  • 构建镜像需要安装buildkit并作为服务启动
  • buildkit有两种后端,containerd worker或者oci worker,在/etc/buildkit/buildkitd.toml文件中配置使用哪种后端,默认使用oci worker,它直接与runc交互
  • oci worker不能使用containerd-managed的镜像作为基础镜像
  • oci worker不能使用由nerdctl build构建的镜像作为基础镜像,此时需要修改buildkit的后端为containerd worker

首先从releases下载最新的二进制文件,然后执行命令sudo tar Cxzvf /usr/local buildkit-v0.12.4.linux-amd64.tar.gz安装buildkit,然后创建unit文件,使用systemd管理服务,参考Systemd socket activation

# 创建service unit
cat <<EOF | sudo tee /usr/local/lib/systemd/system/buildkit.service
[Unit]
Description=BuildKit
Requires=buildkit.socket
After=buildkit.socket
Documentation=https://github.com/moby/buildkit

[Service]
Type=notify
ExecStart=/usr/local/bin/buildkitd --addr fd://

[Install]
WantedBy=multi-user.target
EOF

# 创建socket unit
cat <<EOF | sudo tee /usr/local/lib/systemd/system/buildkit.socket
[Unit]
Description=BuildKit
Documentation=https://github.com/moby/buildkit

[Socket]
ListenStream=%t/buildkit/buildkitd.sock
SocketMode=0660

[Install]
WantedBy=sockets.target
EOF

# 启动socket即可,当socket被连接时将自动启动service
sudo systemctl daemon-reload
sudo systemctl enable --now buildkit.socket

# 在启动buildkit.socket后,分别查看buildkit.socket和buildkit.service的状态,
# socket状态中可见`Triggers: ● buildkit.service`,socket会触发service,
# service状态中可见`TriggeredBy: ● buildkit.socket`,service会被socket触发
ian@ubuntu-dev60-k8sm1:workspace$ sudo systemctl status buildkit.socket
● buildkit.socket - BuildKit
     Loaded: loaded (/usr/local/lib/systemd/system/buildkit.socket; enabled; vendor preset: enabled)
     Active: active (listening) since Wed 2024-01-03 11:23:15 CST; 5s ago
   Triggers: ● buildkit.service
       Docs: https://github.com/moby/buildkit
     Listen: /run/buildkit/buildkitd.sock (Stream)
     CGroup: /system.slice/buildkit.socket

ian@ubuntu-dev60-k8sm1:workspace$ sudo systemctl status buildkit.service
○ buildkit.service - BuildKit
     Loaded: loaded (/usr/local/lib/systemd/system/buildkit.service; disabled; vendor preset: enabled)
     Active: inactive (dead)
TriggeredBy: ● buildkit.socket
       Docs: https://github.com/moby/buildkit

此时buildkit后端默认为oci worker,构建一个镜像:

mkdir -p /tmp/ctx_foo && cat <<EOF > /tmp/ctx_foo/Dockerfile
FROM ghcr.io/stargz-containers/ubuntu:20.04-org
RUN echo foo
EOF

nerdctl build -t foo /tmp/ctx_foo

构建成功,如果以foo镜像作为基础镜像,构建bar镜像:

mkdir -p /tmp/ctx_bar && cat <<EOF > /tmp/ctx_bar/Dockerfile
FROM foo
RUN echo bar
EOF

nerdctl build -t foo /tmp/ctx_bar

则会失败,因为foo镜像是使用nerdctl build命令构建的,修改buildkit后端为containerd worker:

sudo mkdir -p /etc/buildkit

cat <<EOF | sudo tee /etc/buildkit/buildkitd.toml
[worker.oci]
  enabled = false

[worker.containerd]
  enabled = true
  # namespace should be "k8s.io" for Kubernetes (including Rancher Desktop)
  namespace = "default"
EOF

sudo systemctl restart buildkit

重新构建bar镜像成功。

总结

可以看到将containerd作为容器运行时的部署过程相对繁琐,官方没有提供像docker那样简便的部署方式,使用起来也有一些别扭的地方(比如构建镜像、针对cri和非cri接口的配置),毕竟containerd在说自己不是给开发者和最终用户使用的,而是用于嵌入到更大的系统中。这些都是因为历史恩怨或者说玩家间的利益冲突,无论如何,最终用户总是要受这些事情的苦,合理控制系统的复杂度显得越发重要

本篇引入的复杂事物主要是在沙盒内运行进程,并没有深入,比如cgroup、namespace、fs、iptables、rootless等,后面会考虑罗列相关的资源。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值