Containerd介绍
前言
- 早在2016年3月,Docker 1.11的Docker Engine里就包含了containerd,而现在则是把containerd从Docker Engine里彻底剥离出来,作为一个独立的开源项目独立发展,目标是提供一个更加开放、稳定的容器运行基础设施。和原先包含在Docker Engine里containerd相比,独立的containerd将具有更多的功能,可以涵盖整个容器运行时管理的所有需求。
- containerd并不是直接面向最终用户的,而是主要用于集成到更上层的系统里,比如Swarm, Kubernetes, Mesos等容器编排系统。
- containerd以Daemon的形式运行在系统上,通过暴露底层的gRPC API,上层系统可以通过这些API管理机器上的容器。
- 每个containerd只负责一台机器,Pull镜像,对容器的操作(启动、停止等),网络,存储都是由containerd完成。具体运行容器由runC负责,实际上只要是符合OCI规范的容器都可以支持。
- 对于容器编排服务来说,运行时只需要使用containerd+runC,更加轻量,容易管理。
- 独立之后containerd的特性演进可以和Docker Engine分开,专注容器运行时管理,可以更稳定。


Containerd前世今生
2013年docker公司在推出docker产品后,由于其对全球技术产生了一定的影响力,Google公司明显感觉到自己公司内部所使用的Brog系统江湖地位受到的威胁,希望Docker公司能够与自己联合打造一款开源的容器运行时作为Docker核心依赖,但Docker公司拒绝了;接着Google公司联合RedHat、IBM等公司说服Docker公司把其容器核心技术libcontainer捐给中立社区(OCI,Open Container Intiative),并更名为runC。
为了进一步遏制Docker在未来技术市场影响力,避免在容器市场上Docker一家独大,Google公司带领导RedHat、IBM等成立了CNCF(Cloud Native Computing Fundation)基金会,即云原生计算基金会。CNCF的目标很明确,既然在容器应用领域无法与Docker相抗衡,那就做Google更有经验的技术市场------大规模容器编排应用场景,Google公司把自己内部使用的Brog系统开源------Kubernetes,也就是我们今天所说的云原生技术生态。
2016年Docker公司推出了Docker Swarm,意在一统Docker生态,让Docker既可以实现容器应用管理,也可以实现大规模容器编排,经过近1年左右时间的市场验证后,发现在容器编排方面无法独立抗衡kubernetes,所以Docker公司于2017年正式宣布原生支持Kubernetes,至此,Docker在大规模容器编排应用市场败下阵来,但是Docker依然不甘心失败,把Docker核心依赖Containerd捐给了CNCF,依此说明Docker依旧是一个PaaS平台。
2020年CNCF基金会宣布Kubernetes 1.20版本将不再仅支持Docker容器管理工具,此事的起因主要也与Docker捐给CNCF基金会的Containerd有关,早期为了实现Kubernetes能够使用Docker实现容器管理,专门在Kubernetes组件中集成一个shim(垫片)技术,用来将Kubernetes容器运行时接口(CRI,Container Runntime Interface)调用翻译成Docker的API,这样就可以很好地使用Docker了,但是随着Kubernetes在全球技术市场的广泛应用,有更多的容器管理工具的出现,它们都想能够借助于Kubernetes被用户所使用,所以就提出标准化容器运行时接口,只要适配了这个接口就可以集成到Kubernetes生态当中,所以Kubernetes取消了对shim的维护,并且由于Containerd技术的成功,可以实现无缝对接Kubernetes,所以接下来Kubernetes容器运行时的主角是Containerd。
Containerd架构
架构图
Containerd设计的目的是为了嵌入到Kubernetes中使用,它是一个工业级的容器运行时,不提供给开发人员和终端用户直接使用,这样就避免了与Docker产生竞争,但事实上,Containerd已经实现大多数容器管理功能,例如:容器生命周期管理、容器镜像传输和管理、容器存储与网络管理等。

-
Containerd 采用标准的 C/S 架构
- 服务端通过 GRPC 协议提供稳定的 API
- 客户端通过调用服务端的 API 进行高级的操作
-
为了实现解耦,Containerd 将不同的职责划分给不同的组件,每个组件就相当于一个子系统(subsystem)。连接不同子系统的组件被称为模块。
-
Containerd 两大子系统为:
- Bundle : 在 Containerd 中,Bundle 包含了配置、元数据和根文件系统数据,你可以理解为容器的文件系统。而 Bundle 子系统允许用户从镜像中提取和打包 Bundles。
- Runtime : Runtime 子系统用来执行 Bundles,比如创建容器。
其中,每一个子系统的行为都由一个或多个模块协作完成(架构图中的 Core 部分)。每一种类型的模块都以插件的形式集成到 Containerd 中,而且插件之间是相互依赖的。
例如,上图中的每一个长虚线的方框都表示一种类型的插件,包括 Service Plugin、Metadata Plugin、GC Plugin、Runtime Plugin 等,其中 Service Plugin 又会依赖 Metadata Plugin、GC Plugin 和 Runtime Plugin。每一个小方框都表示一个细分的插件,例如 Metadata Plugin 依赖 Containers Plugin、Content Plugin 等。
常用插件
- Content Plugin : 提供对镜像中可寻址内容的访问,所有不可变的内容都被存储在这里。
- Snapshot Plugin : 用来管理容器镜像的文件系统快照。镜像中的每一个 layer 都会被解压成文件系统快照,类似于 Docker 中的
graphdriver
。 - Metrics : 暴露各个组件的监控指标。

架构缩略图
Containerd 被分为三个大块:Storage
、Metadata
和 Runtime

与其它容器运行时工具性能对比
这是使用 bucketbench 对 Docker、crio 和 Containerd 的性能测试结果,包括启动、停止和删除容器,以比较它们所耗的时间:

结论: Containerd 在各个方面都表现良好,总体性能优于 Docker
和 crio
。
Containerd安装
YUM方式安装
shell
# 获取阿里云YUM源
wget -O /etc/yum.repos.d/docker-ce.repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
# 查看YUM源中Containerd软件
yum list | grep containerd
containerd.io.x86_64 1.6.24-3.1.el7 docker-ce-stable
# 安装Containerd.io软件,即可安装Containerd
yum -y install containerd.io
# 使用rpm -qa命令查看是否安装
rpm -qa | grep containerd
containerd.io-1.6.24-3.1.el7.x86_64
# 设置containerd服务启动及开机自启动
systemctl enable containerd && systemctl start containerd
# 安装Containerd时ctr命令亦可使用,ctr命令主要用于管理容器及容器镜像等。
# 使用ctr命令查看Containerd客户端及服务端相关信息。
ctr version
Client:
Version: 1.6.24
Revision: 61f9fd88f79f081d64d6fa3bb1a0dc71ec870523
Go version: go1.20.8
Server:
Version: 1.6.24
Revision: 61f9fd88f79f081d64d6fa3bb1a0dc71ec870523
UUID: 82e60921-24d7-43c4-b146-102da9d56192
二进制方式安装
shell
# 在GitHub官网找到对应的安装包,并下载
mkdir containerd
cd containerd
wget https://github.com/containerd/containerd/releases/download/v1.6.24/cri-containerd-cni-1.6.24-linux-amd64.tar.gz
tar xf cri-containerd-cni-1.6.24-linux-amd64.tar.gz
rm -f cri-containerd-cni-1.6.24-linux-amd64.tar.gz
ls
cri-containerd.DEPRECATED.txt etc opt usr
# 查看etc目录,主要为containerd服务管理配置文件及cni虚拟网卡配置文件
ls etc
cni crictl.yaml systemd
# 查看opt目录,主要为gce环境中使用containerd配置文件及cni插件
ls opt
cni containerd
# 查看usr目录,主要为containerd运行时文件,包含runc
ls usr
local
# 查看containerd.service文件,了解containerd文件安装位置
cat etc/systemd/system/containerd.service
...
[Service]
ExecStartPre=-/sbin/modprobe overlay
# 查看此位置,把containerd二进制文件放置于此处即可完成安装。
ExecStart=/usr/local/bin/containerd
...
复制containerd运行时文件至系统
shell
# 查看宿主机/usr/local/bin目录,里面没有任何内容。
ls /usr/local/bin/
# 查看解压后usr/local/bin目录,里面包含containerd运行时文件
ls usr/local/bin/
containerd containerd-shim containerd-shim-runc-v1 containerd-shim-runc-v2 containerd-stress crictl critest ctd-decoder ctr
# 复制containerd文件至/usr/local/bin目录中,本次可仅复制containerd一个文件也可复制全部文件。
cp usr/local/bin/* /usr/local/bin/
ls /usr/local/bin/
containerd containerd-shim-runc-v1 containerd-stress critest ctr
containerd-shim containerd-shim-runc-v2 crictl ctd-decoder
添加containerd.service文件至系统
shell
# 查看解压后的etc/systemd/system/目录
ls etc/systemd/system/
containerd.service
# 复制etc目录中的所有文件到 /etc
cp -R etc/* /etc
# 查看containerd使用帮助
containerd --help
# 复制opt目录
cp -R opt/* /opt
# 上面安装containerd的所有操作,可以通过下面一个命令解决
tar xf cri-containerd-cni-1.6.24-linux-amd64.tar.gz -C /
生成containerd模块配置文件
生成默认模块配置文件
Containerd 的默认配置文件为 /etc/containerd/config.toml
,可以使用containerd config default > /etc/containerd/config.toml
命令创建一份模块配置文件
shell
# 创建配置文件目录
mkdir /etc/containerd
# 生成配置文件
containerd config default > /etc/containerd/config.toml
# 由于网络原因,此处被替换 sandbox_image = "k8s.gcr.io/pause:3.6"
sed -i 's/registry.k8s.io/k8s.gcr.io/' /etc/containerd/config.toml
继续修改/etc/containerd/config.toml
配置文件,修改其中相应的内容如下所示
toml
[plugins."io.containerd.grpc.v1.cri".registry]
config_path = ""
[plugins."io.containerd.grpc.v1.cri".registry.auths]
[plugins."io.containerd.grpc.v1.cri".registry.configs]
[plugins."io.containerd.grpc.v1.cri".registry.configs."harbor.mytest.com".tls]
insecure_skip_verify = true # 是否跳过安全认证
# harbor用户名和密码配置
[plugins."io.containerd.grpc.v1.cri".registry.configs."harbor.mytest.com".auth]
username = "admin"
password = "Harbor12345"
[plugins."io.containerd.grpc.v1.cri".registry.headers]
# 各种镜像源配置
[plugins."io.containerd.grpc.v1.cri".registry.mirrors]
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."docker.io"]
endpoint = ["https://docker.mirrors.ustc.edu.cn", "http://hub-mirror.c.163.com"]
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."gcr.io"]
endpoint = ["https://gcr.mirrors.ustc.edu.cn"]
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."k8s.gcr.io"]
endpoint = ["https://gcr.mirrors.ustc.edu.cn/google-containers/"]
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."quay.io"]
endpoint = ["https://quay.mirrors.ustc.edu.cn"]
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."harbor.mytest.com"]
endpoint = ["http://harbor.mytest.com"]
启动containerd服务
shell
systemctl enable containerd && systemctl start containerd
systemctl status containerd
# 查看已安装containerd服务版本
ctr version
Client:
Version: v1.6.24
Revision: 61f9fd88f79f081d64d6fa3bb1a0dc71ec870523
Go version: go1.20.8
Server:
Version: v1.6.24
Revision: 61f9fd88f79f081d64d6fa3bb1a0dc71ec870523
UUID: a9f9d1e7-8018-4c0b-a817-261505e876fd
安装runC
由于二进制包中提供的runC默认需要系统中安装seccomp支持,需要单独安装,且不同版本runC对seccomp版本要求一致,所以建议单独下载runC 二进制包进行安装,里面包含了seccomp模块支持
shell
# 在GitHub官网找到对应的安装包,并下载
wget https://github.com/opencontainers/runc/releases/download/v1.1.9/runc.amd64
# 安装runC
mv runc.amd64 /usr/local/sbin/runc
# 为runC添加可执行权限
chmod +x /usr/local/sbin/runc
# 使用runc命令验证是否安装成功
runc -v
runc version 1.1.9
commit: v1.1.9-0-gccaecfcb
spec: 1.0.2-dev
go: go1.20.3
libseccomp: 2.5.4
Containerd容器镜像管理
- docker使用docker images命令管理镜像
- 单机containerd使用ctr images命令管理镜像,containerd本身的CLI
- k8s中containerd使用crictl images命令管理镜像,Kubernetes社区的专用CLI工具
shell
# 获取命令帮助
ctr --help
ctr images
# 查看镜像
ctr images ls
# 下载镜像
# containerd支持oci标准的镜像,所以可以直接使用docker官方或dockerfile构建的镜像
# 这里ctr命令pull镜像时,不能直接把镜像名字写成`nginx:alpine`
ctr images pull --all-platforms docker.io/library/nginx:alpine
# 查看已下载容器镜像
ctr images ls
REF TYPE DIGEST SIZE PLATFORMS LABELS
docker.io/library/nginx:alpine application/vnd.docker.distribution.manifest.list.v2+json sha256:4c93a3bd8bf95412889dd84213570102176b6052d88bb828eaf449c56aca55ef 17.1 MiB linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8,linux/ppc64le,linux/s390x -
# 指定平台下载容器镜像
ctr images pull --platform linux/amd64 docker.io/library/nginx:alpine
# 镜像挂载(方便查看镜像中包含的内容)
# 把已下载的容器镜像挂载至当前文件系统
ctr images mount docker.io/library/nginx:alpine /mnt
ls /mnt
bin docker-entrypoint.d etc lib mnt proc run srv tmp var
dev docker-entrypoint.sh home media opt root sbin sys usr
# 卸载
umount /mnt
# 镜像导出
# 把容器镜像导出,--all-platforms,导出所有平台镜像
ctr i export --all-platforms nginx.img docker.io/library/nginx:alpine
ll -h
-rw-r--r--. 1 root root 114M Oct 8 14:25 nginx.img
# 镜像删除
# 删除指定容器镜像
ctr image rm docker.io/library/nginx:alpine
# 再次查看容器镜像
ctr images ls
# 镜像导入
ctr images import nginx.img
# 修改镜像tag
# 把docker.io/library/nginx:alpine 修改为 nginx:alpine
ctr images tag docker.io/library/nginx:alpine nginx:alpine
# 查看修改后的容器镜像
ctr images ls
REF TYPE DIGEST SIZE PLATFORMS LABELS
docker.io/library/nginx:alpine application/vnd.docker.distribution.manifest.list.v2+json sha256:4c93a3bd8bf95412889dd84213570102176b6052d88bb828eaf449c56aca55ef 17.1 MiB linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8,linux/ppc64le,linux/s390x -
nginx:alpine application/vnd.docker.distribution.manifest.list.v2+json sha256:4c93a3bd8bf95412889dd84213570102176b6052d88bb828eaf449c56aca55ef 17.1 MiB linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8,linux/ppc64le,linux/s390x -
# 修改后对容器镜像做检查比对
ctr images check
REF TYPE DIGEST STATUS SIZE UNPACKED
docker.io/library/nginx:alpine application/vnd.docker.distribution.manifest.list.v2+json sha256:4c93a3bd8bf95412889dd84213570102176b6052d88bb828eaf449c56aca55ef complete (9/9) 17.1 MiB/17.1 MiB true
nginx:alpine application/vnd.docker.distribution.manifest.list.v2+json sha256:4c93a3bd8bf95412889dd84213570102176b6052d88bb828eaf449c56aca55ef complete (9/9) 17.1 MiB/17.1 MiB true
Containerd容器管理
获取命令帮助
shell
# 获取ctr命令帮助
ctr --help
# 获取创建静态容器命令帮助
ctr container --help
# 获取动态容器命令帮助
ctr run --help
使用ctr container create
命令创建容器后,容器并没有处于运行状态,其只是一个静态的容器。这个 container 对象只是包含了运行一个容器所需的资源及配置的数据结构,例如: namespaces、rootfs 和容器的配置都已经初始化成功了,只是用户进程(本案例为nginx)还没有启动。需要使用ctr tasks
命令才能获取一个动态容器。
使用ctr run
命令可以创建一个静态容器并使其运行。一步到位运行容器。
查看容器
container表示静态容器,可用c缩写代表container
shell
ctr container ls
ctr c ls
查看任务
task表示容器里跑的进程, 可用t缩写代表task
shell
ctr task ls
ctr t ls
创建静态容器
shell
ctr c create docker.io/library/nginx:alpine nginx1
ctr c ls
CONTAINER IMAGE RUNTIME
nginx1 docker.io/library/nginx:alpine io.containerd.runc.v2
# 查看容器详细信息
ctr c info nginx1
静态容器启动为动态容器
shell
# 复制containerd连接runC垫片工具至系统,上面已经复制过了
ls usr/local/bin/
containerd containerd-shim containerd-shim-runc-v1 containerd-shim-runc-v2 containerd-stress crictl critest ctd-decoder ctr
cp usr/local/bin/containerd-shim-runc-v2 /usr/bin/
# 启动task,即表示在容器中运行了进程,即为动态容器
# -d表示daemon或者后台的意思,否则会卡住终端
ctr t start -d nginx1
# 查看容器所在宿主机进程,是以宿主机进程的方式存在的。
ctr t ls
TASK PID STATUS
nginx1 11845 RUNNING
# 查看容器的进程(都是物理机的进程)
ctr task ps nginx1
PID INFO
11845 -
11882 -
11883 -
11884 -
11885 -
# 物理机查看到相应的进程
ps -ef | grep 11845
root 11845 11826 0 14:44 ? 00:00:00 nginx: master process nginx -g daemon off;
101 11882 11845 0 14:44 ? 00:00:00 nginx: worker process
101 11883 11845 0 14:44 ? 00:00:00 nginx: worker process
101 11884 11845 0 14:44 ? 00:00:00 nginx: worker process
101 11885 11845 0 14:44 ? 00:00:00 nginx: worker process
root 11919 1463 0 14:47 pts/1 00:00:00 grep --color=auto 11845
进入容器操作
shell
# 为exec进程设定一个id,可以随意输入,只要保证唯一即可,也可使用$RANDOM变量。
ctr task exec --exec-id 1 nginx1 /bin/sh
# 下面是在容器中操作
ifconfig
lo Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.0.0.0
inet6 addr: ::1/128 Scope:Host
UP LOOPBACK RUNNING MTU:65536 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)
# 访问本地提供的web服务
curl 127.0.0.1
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...
直接运行一个动态容器
shell
# -d 代表dameon,后台运行,--net-host 代表容器的IP就是宿主机的IP(相当于docker里的host类型网络)
ctr run -d --net-host docker.io/library/nginx:alpine nginx2
ctr c ls
CONTAINER IMAGE RUNTIME
nginx1 docker.io/library/nginx:alpine io.containerd.runc.v2
nginx2 docker.io/library/nginx:alpine io.containerd.runc.v2
ctr t ls
TASK PID STATUS
nginx1 11845 RUNNING
nginx2 11996 RUNNING
# 进入容器
ctr task exec --exec-id $RANDOM -t nginx2 /bin/sh
# 下面是在容器中操作
ifconfig
ens33 Link encap:Ethernet HWaddr 00:0C:29:A0:0E:68
inet addr:192.168.91.160 Bcast:192.168.91.255 Mask:255.255.255.0
inet6 addr: fe80::8ef0:ab61:8b17:dc27/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:216402 errors:0 dropped:0 overruns:0 frame:0
TX packets:29464 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:294702543 (281.0 MiB) TX bytes:2497674 (2.3 MiB)
lo Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.0.0.0
inet6 addr: ::1/128 Scope:Host
UP LOOPBACK RUNNING MTU:65536 Metric:1
RX packets:32 errors:0 dropped:0 overruns:0 frame:0
TX packets:32 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:2592 (2.5 KiB) TX bytes:2592 (2.5 KiB)
# 为容器中运行的网站添加网站文件
echo "nginx2" > /usr/share/nginx/html/index.html
exit
# 在宿主机上访问网站
curl 192.168.91.160
nginx2
暂停容器
shell
ctr t ls
TASK PID STATUS
nginx1 11845 RUNNING
nginx2 11996 RUNNING
# 暂停容器
ctr t pause nginx2
# 再次查看容器状态,看到其状态为PAUSED,表示停止。
ctr t ls
TASK PID STATUS
nginx1 11845 RUNNING
nginx2 11996 PAUSED
# 在宿主机上访问网站,发现不可以访问到网站
curl 192.168.91.160
恢复容器
shell
# 使用resume命令恢复容器
ctr t resume nginx2
ctr t ls
TASK PID STATUS
nginx1 11845 RUNNING
nginx2 11996 RUNNING
curl 192.168.91.160
nginx2
停止容器
shell
# 使用kill命令停止容器中运行的进程,既为停止容器
ctr t kill nginx2
ctr t ls
TASK PID STATUS
nginx1 11845 RUNNING
nginx2 11996 STOPPED
删除容器
shell
# 必须先停止task或先删除task,再删除容器
ctr t delete nginx2
# 查看静态容器,确认其还存在于系统中
ctr c ls
CONTAINER IMAGE RUNTIME
nginx1 docker.io/library/nginx:alpine io.containerd.runc.v2
nginx2 docker.io/library/nginx:alpine io.containerd.runc.v2
# 删除容器
ctr c delete nginx2
ctr c ls
CONTAINER IMAGE RUNTIME
nginx1 docker.io/library/nginx:alpine io.containerd.runc.v2
Containerd使用私有容器镜像仓库 Harbor
Harbor准备
准备192.168.91.153节点,安装harbor,在Docker学习中已经详细介绍过
shell
echo "192.168.91.153 harbor.mytest.com" >> /etc/hosts
# Docker安装
wget -O /etc/yum.repos.d/docker-ce.repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
yum -y install --setopt=obsoletes=0 docker-ce-24.0.6-1.el7
cat << EOF > /etc/docker/daemon.json
{
"registry-mirrors": ["https://zwyx2n3v.mirror.aliyuncs.com"],
"insecure-registries": ["http://harbor.mytest.com"]
}
EOF
systemctl enable --now docker
# Harbor安装
cd /root
wget https://github.com/goharbor/harbor/releases/download/v2.9.0/harbor-offline-installer-v2.9.0.tgz
tar xf harbor-offline-installer-v2.9.0.tgz
cd harbor
cp harbor.yml.tmpl harbor.yml
# 修改配置文件,修改hostname为当前节点ip,并将https相关配置注掉
sed -i -e 's/reg.mydomain.com/harbor.mytest.com/' -e '13,18 s/^/#/' harbor.yml
./prepare
./install.sh
# 安装好之后,就可以通过docker compose启停了
配置Containerd使用Harbor仓库
Harbor主机名解析
在containerd宿主机上配置host
shell
# harbor.mytest.com建议用FQDN形式,如果用类似harbor这种短名,后面下载镜像会出问题
echo "192.168.91.153 harbor.mytest.com" >> /etc/hosts
修改配置文件
继续修改/etc/containerd/config.toml
配置文件,上面已经修改过,如果没有修改,需要修改并重启containerd
toml
[plugins."io.containerd.grpc.v1.cri".registry]
config_path = ""
[plugins."io.containerd.grpc.v1.cri".registry.auths]
[plugins."io.containerd.grpc.v1.cri".registry.configs]
[plugins."io.containerd.grpc.v1.cri".registry.configs."harbor.mytest.com".tls]
insecure_skip_verify = true # 是否跳过安全认证
# harbor用户名和密码配置
[plugins."io.containerd.grpc.v1.cri".registry.configs."harbor.mytest.com".auth]
username = "admin"
password = "Harbor12345"
[plugins."io.containerd.grpc.v1.cri".registry.headers]
# 各种镜像源配置
[plugins."io.containerd.grpc.v1.cri".registry.mirrors]
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."docker.io"]
endpoint = ["https://docker.mirrors.ustc.edu.cn", "http://hub-mirror.c.163.com"]
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."gcr.io"]
endpoint = ["https://gcr.mirrors.ustc.edu.cn"]
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."k8s.gcr.io"]
endpoint = ["https://gcr.mirrors.ustc.edu.cn/google-containers/"]
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."quay.io"]
endpoint = ["https://quay.mirrors.ustc.edu.cn"]
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."harbor.mytest.com"]
endpoint = ["http://harbor.mytest.com"]
ctr下载镜像
shell
# 下载容器镜像
# --platform linux/amd64 指定系统平台,也可以使用--all-platforms指定所有平台镜像
ctr images pull --platform linux/amd64 docker.io/library/nginx:latest
# 查看已下载容器镜像
ctr images ls
ctr上传镜像
上传到Harbor library私有项目
shell
# 重新生成新的tag
ctr images tag docker.io/library/nginx:latest harbor.mytest.com/library/nginx:latest
# 查看已生成容器镜像
ctr images ls
# 推送容器镜像至Harbor
# 先tag再push,因为我们harbor是http协议,不是https协议,所以需要加上`--plain-http`,`--user admin:Harbor12345`指定harbor的用户名与密码(还需要指定用户名和密码,不知道为什么)
ctr images push --platform linux/amd64 --plain-http -u admin:Harbor12345 harbor.mytest.com/library/nginx:latest
manifest-sha256:b2888fc9cfe7cd9d6727aeb462d13c7c45dec413b66f2819a36c4a3cb9d4df75: done |++++++++++++++++++++++++++++++++++++++|
config-sha256:61395b4c586da2b9b3b7ca903ea6a448e6783dfdd7f768ff2c1a0f3360aaba99: done |++++++++++++++++++++++++++++++++++++++|
elapsed: 0.5 s total: 9.7 Ki (19.3 KiB/s)

shell
# 下载已上传容器镜像
ctr images pull --plain-http harbor.mytest.com/library/nginx:latest
Containerd使用Harbor容器镜像仓库验证
nerdctl安装
nerdctl是containerd的命令行接口
shell
mkdir -p /etc/containerd/certs.d/harbor.mytest.com
cat > /etc/containerd/certs.d/harbor.mytest.com/hosts.toml << "EOF"
server = "http://harbor.mytest.com"
[host."http://harbor.mytest.com"]
capabilities = ["pull","resolve","push"]
skip_verify = true
EOF
systemctl restart containerd
wget https://github.com/containerd/nerdctl/releases/download/v1.5.0/nerdctl-1.5.0-linux-amd64.tar.gz
tar xf nerdctl-1.5.0-linux-amd64.tar.gz -C /usr/local/bin/
nerdctl version
WARN[0000] unable to determine buildctl version: exec: "buildctl": executable file not found in $PATH
Client:
Version: v1.5.0
OS/Arch: linux/amd64
Git commit: b33a58f288bc42351404a016e694190b897cd252
buildctl:
Version:
Server:
containerd:
Version: v1.6.24
GitCommit: 61f9fd88f79f081d64d6fa3bb1a0dc71ec870523
runc:
Version: 1.1.9
GitCommit: v1.1.9-0-gccaecfcb
nerdctl使用
shell
# 默认登录dockerhub
# 输入用户名和密码登陆dockerhub
nerdctl login
# 登出
nerdctl logout
# 使用nerdctl登录非安全的harbor
nerdctl login -u admin -p Harbor12345 harbor.mytest.com
# 查看本地镜像
nerdctl images | grep harbor.mytest.com
harbor.mytest.com/library/nginx latest 3c4c1f42a89e 5 hours ago linux/amd64 189.1 MiB 67.3 MiB
# 上传镜像
nerdctl push harbor.mytest.com/library/nginx:latest
# 删除镜像
nerdctl rmi harbor.mytest.com/library/nginx:latest
# 拉取镜像
nerdctl pull harbor.mytest.com/library/nginx:latest
# 再次查看本地镜像
nerdctl images | grep harbor.mytest.com
harbor.mytest.com/library/nginx latest 3c4c1f42a89e 8 seconds ago linux/amd64 189.1 MiB 67.3 MiB
基于nerdctl+buildkit+containerd构建容器镜像
buildkit介绍
buildkit是Docker公司开源的下一代镜像构建工具,支持OCI标准的镜像构建,可以通过Dockerfile制作容器镜像。
buildkit由两部分组成:
- buildkitd(服务端):负责镜像构建,目前支持runc和containerd作为镜像构建环境,默认是runc。
- buildkitctl(客户端):负责解析Dockerfile文件,并向服务端buildkitd发出构建请求。
相对于docker,buildkit具有以下优势:
- 更高效:支持并行的多阶段构建、更好的缓存
- 管理更安全:支持secret mount,无需root权限
- 更易于扩展:使用自定义中间语言LLB,完全兼容Dockerfile,也可以支持第三方语言。后台支持runc和containerd
buildkit安装
shell
wget https://github.com/moby/buildkit/releases/download/v0.12.2/buildkit-v0.12.2.linux-amd64.tar.gz
tar xf buildkit-v0.12.2.linux-amd64.tar.gz
mv bin/* /usr/bin/
# 没有之前的警告了
nerdctl version
Client:
Version: v1.5.0
OS/Arch: linux/amd64
Git commit: b33a58f288bc42351404a016e694190b897cd252
buildctl:
Version: v0.12.2
GitCommit: 567a99433ca23402d5e9b9f9124005d2e59b8861
Server:
containerd:
Version: v1.6.24
GitCommit: 61f9fd88f79f081d64d6fa3bb1a0dc71ec870523
runc:
Version: 1.1.9
GitCommit: v1.1.9-0-gccaecfcb
cat > /usr/lib/systemd/system/buildkit.socket << "EOF"
[Unit]
Description=BuildKit
Documentation=https://github.com/moby/buildkit
[Socket]
ListenStream=%t/buildkit/buildkitd.sock
SocketMode=0660
[Install]
WantedBy=sockets.target
EOF
cat > /usr/lib/systemd/system/buildkit.service << "EOF"
[Unit]
Description=BuildKit
Requires=buildkit.socket
After=buildkit.socket
Documentation=https://github.com/moby/buildkit
[Service]
ExecStart=/usr/bin/buildkitd --oci-worker=false --containerd-worker=true
[Install]
WantedBy=multi-user.target
EOF
systemctl enable --now buildkit.service
容器镜像构建实践
构建应用发布基础镜像
shell
mkdir /root/imgtest
cd /root/imgtest
cat > Dockerfile << "EOF"
FROM ubuntu:20.04
LABEL [email protected]
RUN apt update && apt -y install iproute2 ntpdate tcpdump telnet traceroute nfs-kernel-server nfs-common lrzsz tree openssl libssl-dev libpcre3 libpcre3-dev zlib1g-dev ntpdate tcpdump telnet traceroute gcc openssh-server lrzsz tree openssl libssl-dev libpcre3 libpcre3-dev zlib1g-dev ntpdate tcpdump telnet traceroute iotop unzip zip make && apt clean
EOF
# 构建镜像
nerdctl build -t harbor.mytest.com/library/ubuntu:20.04 .
nerdctl images | grep harbor.mytest.com
harbor.mytest.com/library/nginx latest 3c4c1f42a89e 31 minutes ago linux/amd64 189.1 MiB 67.3 MiB
harbor.mytest.com/library/ubuntu 20.04 31f9985e78b3 54 seconds ago linux/amd64 422.5 MiB 152.6 MiB
# 上传已构建基础镜像到harbor容器镜像仓库
nerdctl push harbor.mytest.com/library/ubuntu:20.04
构建JDK容器镜像
shell
mkdir /etc/buildkit
# 这里配置build时可以使用http的镜像源(Dockfile中FROM)
cat > /etc/buildkit/buildkitd.toml << "EOF"
debug = true
[registry."harbor.mytest.com"]
http = true
insecure = true
EOF
mkdir /etc/nerdctl
# 这里的配置可以用于开启调试,insecure_registry似乎没什么用
cat > /etc/nerdctl/nerdctl.toml << "EOF"
debug = false
debug_full = true
insecure_registry = true
EOF
systemctl restart buildkit.service
mkdir /root/jdktest
cd /root/jdktest
wget https://builds.openlogic.com/downloadJDK/openlogic-openjdk/8u392-b08/openlogic-openjdk-8u392-b08-linux-x64.tar.gz
cat > Dockerfile << "EOF"
# jdk 8 base image
FROM harbor.mytest.com/library/ubuntu:20.04
LABEL author="[email protected]"
ADD openlogic-openjdk-8u392-b08-linux-x64.tar.gz /usr/local/src
RUN ln -s /usr/local/src/openlogic-openjdk-8u392-b08-linux-x64 /usr/local/jdk
ENV JAVA_HOME /usr/local/jdk
ENV JRE_HOME $JAVA_HOME/jre
ENV CLASSPATH $JAVA_HOME/lib/:$JRE_HOME/lib/
ENV PATH $PATH:$JAVA_HOME/bin
RUN echo "export JAVA_HOME=/usr/local/jdk" >>/etc/profile
RUN echo "export TOMCAT_HOME=/apps/tomcat" >>/etc/profile
RUN echo "export PATH=$JAVA_HOME/bin:$JAVA_HOME/jre/bin:$TOMCAT_HOME/bin:$PATH" >>/etc/profile
RUN echo "export CLASSPATH=.$CLASSPATH:$JAVA_HOME/lib:$JAVA_HOME/jre/lib:$JAVA_HOME/lib/tools.jar" >> /etc/profile
RUN rm -f /etc/localtime && ln -snf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
EOF
# 构建镜像
nerdctl build -t harbor.mytest.com/library/ubuntu-jdk-base:8u392 .
# 推送镜像
nerdctl push harbor.mytest.com/library/ubuntu-jdk-base:8u392
基于jdk镜像构建tomcat镜像
shell
mkdir /root/tomcattest
cd /root/tomcattest
wget https://dlcdn.apache.org/tomcat/tomcat-8/v8.5.93/bin/apache-tomcat-8.5.93.tar.gz
cat > Dockerfile << "EOF"
# tomcat 8.5.93 base image
FROM harbor.mytest.com/library/ubuntu-jdk-base:8u392
LABEL author="[email protected]"
RUN mkdir -pv /apps /data/tomcat/webapps /data/tomcat/logs
ADD apache-tomcat-8.5.93.tar.gz /apps/
RUN ln -sv /apps/apache-tomcat-8.5.93 /apps/tomcat
RUN useradd -u 2023 tomcat && chown -R tomcat.tomcat /apps /data/tomcat
EOF
nerdctl build -t harbor.mytest.com/library/tomcat-base:8.5.93 .
nerdctl push harbor.mytest.com/library/tomcat-base:8.5.93
基于tomcat镜像构建业务应用镜像
shell
mkdir /root/apptest
cd /root/apptest
echo 'Hello,World!' > index.html
mkdir myapp
mv index.html myapp
tar czf myapp.tar.gz myapp
cat > run_tomcat.sh << "EOF"
#!/bin/sh
su - tomcat -c "/apps/tomcat/bin/catalina.sh run"
EOF
chmod +x run_tomcat.sh
# 运行一个tomcat容器,拷贝server.xml
# 运行并进入容器
nerdctl run -it harbor.mytest.com/library/tomcat-base:8.5.93 /bin/bash
find / -name server.xml
/apps/apache-tomcat-8.5.93/conf/server.xml
# 使用ctrl+p+q退出容器,但是不关闭容器
nerdctl ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
af81d0576109 harbor.mytest.com/library/tomcat-base:8.5.93 "/bin/bash" 25 seconds ago Up tomcat-base-af81d
nerdctl cp af81d0576109:/apps/apache-tomcat-8.5.93/conf/server.xml .
sed -i 's#appBase="webapps"#appBase="/data/tomcat/webapps"#' server.xml
cat > Dockerfile << "EOF"
# app
FROM harbor.mytest.com/library/tomcat-base:8.5.93
ADD server.xml /apps/tomcat/conf/
ADD myapp.tar.gz /data/tomcat/webapps/
ADD run_tomcat.sh /apps/tomcat/bin/
RUN chown -R tomcat.tomcat /apps /data/tomcat
EXPOSE 8080 8443
CMD ["/apps/tomcat/bin/run_tomcat.sh"]
EOF
nerdctl build -t harbor.mytest.com/library/tomcat-app:v1 .
# 运行容器
nerdctl run -p 8080:8080 --net=host -d harbor.mytest.com/library/tomcat-app:v1
curl 192.168.91.160:8080/myapp/
Hello,World!
Containerd Network管理
默认Containerd管理的容器仅有lo网络,无法访问容器之外的网络,可以为其添加网络插件,使用容器可以连接外网。CNI(Container Network Interface)
创建CNI网络
containernetworking /cni | CNI v1.1.1 |
---|---|
containernetworking /plugins | CNI Plugins v1.1.1 |
获取CNI工具源码
shell
# 使用wget下载cni工具源码包
wget https://github.com/containernetworking/cni/archive/refs/tags/v1.1.1.tar.gz
# 解压已下载cni工具源码包
tar xf cni-1.1.1.tar.gz
ls
cni-1.1.1
# 重命名已下载cni工具源码包目录
mv cni-1.1.1 cni
# 查看cni工具目录中包含的文件
ls cni
cnitool CONTRIBUTING.md DCO go.mod GOVERNANCE.md LICENSE MAINTAINERS plugins RELEASING.md scripts test.sh
CODE-OF-CONDUCT.md CONVENTIONS.md Documentation go.sum libcni logo.png pkg README.md ROADMAP.md SPEC.md
获取CNI Plugins(CNI插件)
shell
# 使用wget下载cni插件工具源码包
wget https://github.com/containernetworking/plugins/releases/download/v1.1.1/cni-plugins-linux-amd64-v1.1.1.tgz
# 创建cni插件工具解压目录
mkdir ./cni-plugins
# 解压cni插件工具至上述创建的目录中
tar xf cni-plugins-linux-amd64-v1.1.1.tgz -C ./cni-plugins
ls cni-plugins
bandwidth dhcp host-device ipvlan macvlan ptp static vlan
bridge firewall host-local loopback portmap sbr tuning vrf
准备CNI网络配置文件
准备容器网络配置文件,用于为容器提供网关、IP地址等。
shell
# 创建名为mynet的网络,其中包含名为cni0的网桥
cat > /etc/cni/net.d/10-mynet.conf <<-EOF
{
"cniVersion": "1.0.0",
"name": "mynet",
"type": "bridge",
"bridge": "cni0",
"isGateway": true,
"ipMasq": true,
"ipam": {
"type": "host-local",
"subnet": "10.66.0.0/16",
"routes": [
{ "dst": "0.0.0.0/0" }
]
}
}
EOF
cat > /etc/cni/net.d/99-loopback.conf <<-EOF
{
"cniVerion": "1.0.0",
"name": "lo",
"type": "loopback"
}
EOF
生成CNI网络
shell
wget -O /etc/yum.repos.d/epel.repo http://mirrors.aliyun.com/repo/epel-7.repo
yum -y install jq
# 进入cni工具目录
cd cni
# 必须在scripts目录中执行,需要依赖exec-plugins.sh文件,再次进入scripts目录
cd scripts
ls
docker-run.sh exec-plugins.sh priv-net-run.sh release.sh
# 执行脚本文件,基于/etc/cni/net.d/目录中的*.conf配置文件生成容器网络
CNI_PATH=/root/cni-plugins ./priv-net-run.sh echo "Hello World"
Hello World
# 在宿主机上查看是否生成容器网络名为cni0的网桥
ip a s
...
3: cni0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default qlen 1000
link/ether ba:41:52:db:f7:18 brd ff:ff:ff:ff:ff:ff
inet 10.66.0.1/16 brd 10.66.255.255 scope global cni0
valid_lft forever preferred_lft forever
inet6 fe80::b841:52ff:fedb:f718/64 scope link
valid_lft forever preferred_lft forever
# 在宿主机上查看其路由表情况
ip route
default via 192.168.91.2 dev ens33 proto static metric 100
10.66.0.0/16 dev cni0 proto kernel scope link src 10.66.0.1
192.168.91.0/24 dev ens33 proto kernel scope link src 192.168.91.160 metric 100
为Containerd容器配置网络功能
创建一个容器
shell
ctr images pull docker.io/library/busybox:latest
ctr run -d docker.io/library/busybox:latest busybox
ctr c ls
CONTAINER IMAGE RUNTIME
busybox docker.io/library/busybox:latest io.containerd.runc.v2
ctr t ls
TASK PID STATUS
busybox 1796 RUNNING
进入容器查看其网络情况
shell
ctr t exec --exec-id $RANDOM -t busybox sh
# 下面是在容器中操作
ip a s
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
exit
获取容器进程ID及其网络命名空间
shell
# 在宿主机中完成指定容器进程ID获取
pid=$(ctr tasks ls | grep busybox | awk '{print $2}')
echo $pid
1796
# 在宿主机中完成指定容器网络命名空间路径获取
netnspath=/proc/$pid/ns/net
echo $netnspath
/proc/1796/ns/net
为指定容器添加网络配置
shell
cd /root/cni/scripts
# 执行脚本文件为容器添加网络配置
CNI_PATH=/root/cni-plugins ./exec-plugins.sh add $pid $netnspath
# 进入容器确认是否添加网卡信息
ctr t exec --exec-id $RANDOM -t busybox sh
# 下面是在容器中操作
ip a s
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: eth0@if5: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
link/ether 86:49:0c:82:a7:50 brd ff:ff:ff:ff:ff:ff
inet 10.66.0.3/16 brd 10.66.255.255 scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::8449:cff:fe82:a750/64 scope link
valid_lft forever preferred_lft forever
# 在容器中ping容器宿主机IP地址,可以ping通
ping -c 2 192.168.91.160
# 在容器中ping宿主机所在网络的网关IP地址,可以ping通
ping -c 2 192.168.91.2
# 在容器中开启httpd服务
echo "containerd net web test" > /tmp/index.html
httpd -h /tmp
wget -O - -q 127.0.0.1
containerd net web test
exit
# 在宿主机访问容器提供的httpd服务
curl http://10.66.0.3
containerd net web test
Containerd容器数据持久化存储
把宿主机目录挂载至Containerd容器中,实现容器数据持久化存储
shell
# 创建一个静态容器,实现宿主机目录与容器挂载,src=/tmp 为宿主机目录,为容器中目录
ctr c create docker.io/library/busybox:latest busybox3 --mount type=bind,src=/tmp,dst=/hostdir,options=rbind:rw
# 运行用户进程
ctr t start -d busybox3 bash
# 进入容器,查看是否挂载成功
ctr t exec --exec-id $RANDOM -t busybox3 sh
# 下面是在容器中操作
ls /hostdir
systemd-private-31f3b5ea3fa949de83869a732a6ee572-chronyd.service-HZfOgQ
vmware-root_708-2998936538
vmware-root_716-2965513684
# 向容器中挂载目录中添加文件
echo "hello world" > /hostdir/test.txt
# 退出容器
exit
# 在宿主机上查看被容器挂载的目录中是否添加了新的文件,已添加表明被容器挂载成功,并可以读写此目录中内容。
cat /tmp/test.txt
hello world
与其它Containerd容器共享命名空间
当需要与其它Containerd管理的容器共享命名空间时,可使用如下方法。
shell
ctr tasks ls
TASK PID STATUS
busybox3 2137 RUNNING
busybox 1796 RUNNING
ctr container create --with-ns "pid:/proc/2137/ns/pid" docker.io/library/busybox:latest busybox4
ctr tasks start -d busybox4 bash
# 进入容器
ctr tasks exec --exec-id $RANDOM -t busybox3 sh
# 下面是在容器中操作
ps aux
PID USER TIME COMMAND
1 root 0:00 sh
15 root 0:00 sh
20 root 0:00 sh
25 root 0:00 ps aux
Docker集成Containerd实现容器管理
目前Containerd主要任务还在于解决容器运行时的问题,对于其周边生态还不完善,所以可以借助Docker结合Containerd来实现Docker完整的功能应用。
shell
# docker安装
wget -O /etc/yum.repos.d/docker-ce.repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
yum -y install docker-ce
# 修改配置
vim /usr/lib/systemd/system/docker.service
ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
# 将上面的修改为下面的
ExecStart=/usr/bin/dockerd --containerd /run/containerd/containerd.sock --debug
# 重新加载配置
systemctl daemon-reload
# 设置docker daemon启动并设置其开机自启动
systemctl enable docker && systemctl start docker
ps aux | grep docker
root 3519 0.3 4.9 1042976 49320 ? Ssl 09:35 0:00 /usr/bin/dockerd --containerd /run/containerd/containerd.sock --debug
# 使用docker运行容器
docker run -d nginx:latest
docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
d09731c82261 nginx:latest "/docker-entrypoint...." 15 seconds ago Up 14 seconds 80/tcp optimistic_hofstadter
# 使用ctr查看是否添加一个新的namespace,本案例中发现添加一个moby命名空间,即为docker使用的命名空间
ctr namespace ls
NAME LABELS
default
moby
# 查看moby命名空间,发现使用docker run运行的容器包含在其中
ctr -n moby c ls
CONTAINER IMAGE RUNTIME
d09731c82261ed63e3e362476de634c288991b8c76a668a26c5c300e7a4b3d69 - io.containerd.runc.v2
# 使用ctr能够查看到一个正在运行的容器,既表示docker run运行的容器是被containerd管理的
ctr -n moby t ls
TASK PID STATUS
d09731c82261ed63e3e362476de634c288991b8c76a668a26c5c300e7a4b3d69 3754 RUNNING
# 使用docker stop停止且使用docker rm删除容器后再观察,发现容器被删除
docker stop d09;docker rm d09
ctr -n moby c ls
CONTAINER IMAGE RUNTIME
ctr -n moby t ls
TASK PID STATUS