一、先搞明白containerd在系统里到底干啥

咱们平时用Docker、用Kubernetes,其实真正在底层干活的是containerd。你可以把它想象成一个食堂的大厨:你点菜(说我要跑一个nginx容器),Kubernetes或者Docker就是服务员,把菜单传给后厨,大厨负责去仓库拿菜(拉镜像)、洗菜切菜(解压镜像)、生火炒菜(创建容器进程),最后把菜端上桌(让容器跑起来)。如果这个厨师隔三差五撂挑子,那整个食堂就瘫痪了。所以,提升containerd的可靠性,就是保证食堂后厨稳定运转,这对线上业务至关重要。

很多刚接触容器的人觉得,containerd是个系统服务,装上就能用,没什么好担心的。但真到了生产环境,你会发现它一样会超时、会卡死、会被日志塞满磁盘。接下来我就用一些接地气的例子,一步步说说怎么把它“养”得更结实。所有例子我都用纯Shell命令来演示,因为日常运维里最常用的就是Linux命令行。

二、从安装和配置阶段就打好底子

2.1 别用太旧的版本,也别乱升级

containerd更新很快,老版本可能存在一些已知的bug,比如偶尔的并发死锁、文件描述符泄漏等。建议使用官方稳定版,并且锁定大版本,不要手动乱升级。我们在生产环境里,一般用yum或者apt装好之后,再配置自动更新策略。

# 以CentOS/RHEL为例,安装containerd
# 先安装yum-utils,用来管理仓库
yum install -y yum-utils

# 添加containerd官方仓库
yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo

# 安装containerd.io,这里指定版本,避免装成最新的不稳定版
yum install -y containerd.io-1.7.20

# 验证版本
containerd --version

# 把containerd设置成开机自启,并立即启动
systemctl enable containerd
systemctl start containerd

上面的示例里,我们特意指定了版本号。生产环境最忌讳“顺手升个级”,因为新版本可能改了配置格式,或者调整了默认参数,导致线上容器起不来。最好在测试环境验证过新版本,再批量升级。

2.2 生成一份靠谱的配置文件

containerd第一次启动后,默认配置可能并不适合你的场景。建议先导出默认配置,再针对性修改。很多问题都出在配置上,比如快照器选错、镜像加速没配、日志参数太小。

# 创建配置目录
mkdir -p /etc/containerd

# 把默认配置写到文件里
containerd config default > /etc/containerd/config.toml

# 修改核心配置,这里用sed命令把SystemdCgroup改成true
# 因为现在主流容器都用cgroup v2,systemd管理cgroup更稳定
sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml

# 查看修改是否生效
grep -n "SystemdCgroup" /etc/containerd/config.toml

你可能要问,为什么SystemdCgroup这么重要?简单说,如果这个参数是false,containerd自己会去操作cgroup文件,而systemd也在操作,两边就容易打架,导致资源限制失效甚至容器被杀。改成true以后,大家都通过systemd来管理,冲突就没了。这是生产环境常见的坑,一定要改。

另外,镜像仓库的地址也要配置好。国内环境拉取docker hub镜像很慢,容易超时。我们可以在配置里加上镜像加速器,或者内网私有仓库。

# 编辑config.toml,找到[plugins."io.containerd.grpc.v1.cri".registry.mirrors]
# 一般是在文件比较靠后的位置。下面用一段cat命令覆盖到临时文件里。
# 注意:这里只是演示片段,实际修改建议先备份原文件。
cat /etc/containerd/config.toml | grep -A 20 "registry.mirrors"

因为默认配置里已经有一堆示例,我们通常会用编辑器去改,而不是用sed硬替换。但不管怎么改,记得改完要重启containerd:

# 每次改完配置,先检查语法
containerd config check --config /etc/containerd/config.toml

# 没问题就重启
systemctl restart containerd

# 确认状态是active running
systemctl status containerd

这一步相当于给食堂后厨重新装修一遍,把炉灶和下水道都检查好,后面做饭才不容易出岔子。

三、日常运行时的守护与健康检查

3.1 让systemd好好盯着它

containerd作为系统服务,我们得让systemd帮我们看门。默认的service文件已经很完善,但我们可以加一些额外的保护。比如如果containerd僵死了,就自动重启。

# 查看当前的service配置
systemctl cat containerd

# 创建override目录,把自定义配置放进去
mkdir -p /etc/systemd/system/containerd.service.d

# 写一个override文件,增加自动重启策略
cat > /etc/systemd/system/containerd.service.d/override.conf <<'EOF'
[Service]
Restart=always
RestartSec=10
StartLimitIntervalSec=60
StartLimitBurst=3
EOF

# 重新加载systemd配置
systemctl daemon-reload

这里的含义是:不管什么原因退出,都等10秒后重启。如果60秒内重启超过3次,说明系统可能有大问题,就不继续狂重启了,避免雪上加霜。你可能会问,containerd为什么需要重启?有时候它会被某些异常容器拖死,比如容器内进程疯狂打开文件,或者网络驱动卡住。重启能让它恢复。

3.2 定期探活,别等业务报警

光依赖systemd还不够,我们最好自己写一个健康检查脚本,定时探测containerd的gRPC接口是否正常。containerd自带了一个ctr命令行工具,可以用来做基础检查。

# 写一个健康检查脚本,保存到/usr/local/bin/check_containerd.sh
cat > /usr/local/bin/check_containerd.sh <<'EOF'
#!/bin/bash
# 这个脚本用于检查containerd是否正常运行,如果异常就重启

# 使用ctr version命令检查,如果返回非0,说明服务可能挂了
if ! ctr version > /dev/null 2>&1; then
    logger "containerd health check failed"
    systemctl restart containerd
fi

# 再检查一下能不能正常列出版本
VERSION_OUTPUT=$(ctr version 2>&1 | grep -i "Version")
if [ -z "$VERSION_OUTPUT" ]; then
    logger "containerd response is empty"
    systemctl restart containerd
fi
EOF

# 给脚本加执行权限
chmod +x /usr/local/bin/check_containerd.sh

# 添加到crontab,每5分钟执行一次
echo "*/5 * * * * /usr/local/bin/check_containerd.sh" | crontab -

这就像食堂每天定时检查冰箱温度、煤气有没有漏气,不等食客拉肚子才后知后觉。脚本里用ctr version,是因为这个命令会通过本地socket和containerd通信,如果通信正常,说明服务活着,而且gRPC接口也没卡死。注意,如果socket文件权限有问题,也会导致检查失败,所以脚本里没有过度追查原因,直接重启。实际上重启后如果还不行,就要看日志了。

四、镜像和容器数据的安全与清理

4.1 磁盘被塞满才是最大的杀手

containerd很能吃磁盘。镜像一层层堆着,容器写层也会越来越大。如果你不清理,某天磁盘满了,containerd直接无法创建新容器,甚至所有容器都会崩溃。我们得学会给它“减肥”。

# 用crictl查看当前节点上的镜像和容器
# crictl是专门给Kubernetes用的命令行工具,需要手动安装
crictl images
crictl ps -a

# 清理没有被任何容器使用的镜像
# 这个命令会删除所有未被容器引用的镜像,慎用
ctr images prune

# 使用crictl删除退出状态的容器
crictl rmi --prune

注意,ctr images prune是containerd自带的清理命令,但它是全局的,可能会把Kubernetes正在用的镜像也删了,如果这个镜像目前没有容器使用,但等一下要扩缩容,就会导致拉镜像超时。所以更安全的做法是使用Kubernetes的kubelet去操作,或者用crictl。不过我们这里讲的是containerd层面,就简单提一下。

4.2 给日志和临时文件设个上限

containerd自身的日志如果无限增长,一样会把磁盘写满。我们最好在systemd里给日志加上大小限制。

# 修改containerd的journald日志限制,这里直接改systemd-journald全局配置
cat > /etc/systemd/journald.conf <<'EOF'
[Journal]
SystemMaxUse=500M
SystemMaxFileSize=50M
EOF

# 重启journald并查看状态
systemctl restart systemd-journald
journalctl -u containerd --disk-usage

如果不想用journald,也可以把containerd的stdout导到文件,再用logrotate轮转。这是长期运行必备的。要注意,日志文件不要和containerd的数据目录放在同一个磁盘分区,否则日志爆了会把镜像数据也带崩。

4.3 定期整理日志文件

我们还可以自己写个清理脚本,把老日志压缩删掉。用logrotate最省心。

# 创建一个logrotate配置,接管containerd日志
cat > /etc/logrotate.d/containerd <<'EOF'
/var/log/containerd.log {
    daily
    rotate 7
    compress
    missingok
    notifempty
    copytruncate
    postrotate
        systemctl restart containerd > /dev/null 2>&1 || true
    endscript
}
EOF

# 手动试运行一下,看有没有语法错误
logrotate -d /etc/logrotate.d/containerd

这个配置的意思是每天轮转一次,保留7份,压缩旧日志。copytruncate很重要,它把日志文件复制一份然后清空原文件,这样containerd不需要重启也能继续写。不过如果你的containerd日志是通过journald管理的,那就不用这个配置了。总之,磁盘空间一定要盯紧,很多故障其实都是被日志和镜像堆死的。

五、常见故障场景和应对

5.1 容器一直处于Creating状态怎么办

一个很常见的现象:你部署了一个Pod,发现它一直ContainerCreating,kubectl描述显示sandbox创建失败。这时候我们需要看containerd的日志。

# 查看containerd最近半小时的日志,过滤关键错误
journalctl -u containerd --since "30 min ago" | grep -i error

# 如果日志太多,先把日志导出到文件再搜索
journalctl -u containerd > /tmp/containerd.log
grep -E "failed|error" /tmp/containerd.log | tail -50

通常出现这种问题,要么是网络插件没有就绪,要么是cgroup配置不对,要么是镜像拉不下来。通过日志能看到具体报错。比如failed to setup network for sandbox,那就是CNI网络的问题。这时候别急着重启containerd,先检查网络插件的状态。如果日志里大量出现context deadline exceeded,说明containerd内部某个操作超时,可能是磁盘IO太慢,或者系统负载太高。我们可以用strace去跟踪containerd的系统调用,但这是高阶玩法,这里不展开。简单粗暴的办法是重启containerd,但只能救急,根治还得看日志。

5.2 containerd完全卡死,怎么安全恢复

如果连ctr version都没有反应,那就说明containerd真的僵住了。这时候我们可以尝试发SIGQUIT信号,让containerd把内部所有goroutine的堆栈打出来,方便排查问题。

# 找到containerd的进程ID
pgrep -f "containerd"

# 发送SIGQUIT信号,会生成堆栈日志到systemd journal
kill -SIGQUIT <pid>

# 然后查看堆栈信息
journalctl -u containerd --since "1 min ago" | grep -A 100 "goroutine"

拿到堆栈后,你可以看到是哪个模块在死锁。比如如果大量goroutine卡在fsync,说明磁盘同步出问题了。如果卡在wait,可能是某个子进程没退出。这一步对定位深层次故障很有帮助。如果实在无法恢复,那就只能强制重启了,但这是最后手段。

# 强制杀掉containerd,并让systemd自动重启
systemctl kill -s SIGKILL containerd

# systemd配置了Restart=always,所以会自动拉起
sleep 10
systemctl status containerd

注意,直接SIGKILL可能会导致一些写了一半的元数据损坏,所以能优雅重启就优雅重启。但在服务完全不可用的情况下,两害相权取其轻,先恢复业务再说。

六、性能调优和资源限制

6.1 提升并发处理能力

当节点上容器特别多,比如上千个,containerd可能需要处理大量的并发请求。默认的并发参数可能不够,我们可以调大。

# 在config.toml里找到[grpc]部分,设置最大并发
# 通常默认是100,我们可以改成500
sed -i 's/max_recv_message_size = 16777216/max_recv_message_size = 4194304/' /etc/containerd/config.toml

# 但更关键的是调整系统级别的文件描述符限制
# 编辑containerd.service的override,增加LimitNOFILE
cat > /etc/systemd/system/containerd.service.d/limit.conf <<'EOF'
[Service]
LimitNOFILE=1048576
EOF

systemctl daemon-reload
systemctl restart containerd

文件描述符不够,是containerd在高负载下最常遇到的问题。每个容器连接、每个镜像层文件、每个网络socket都会占用一个文件描述符。把限制调到100万,应对大多数场景足够了。同时,我们还可以通过内核参数提高连接池:

# 调整网络相关的内核参数,放在/etc/sysctl.d/99-containerd.conf
cat > /etc/sysctl.d/99-containerd.conf <<'EOF'
net.core.somaxconn = 1024
net.ipv4.ip_local_port_range = 1024 65535
fs.file-max = 2097152
EOF

# 生效
sysctl -p /etc/sysctl.d/99-containerd.conf

这些都是老生常谈的性能调整,但确实能让containerd在高并发下更稳。

6.2 限制容器日志大小,防止单点爆炸

每个容器如果不限制日志大小,一个失控的进程就能把整个节点磁盘塞满。我们可以在containerd的CRI配置里设置默认的日志大小。

# 修改config.toml中的[plugins."io.containerd.grpc.v1.cri".containerd]
# 找到max_container_log_line_size,默认是16K,可以改大
# 但更重要的是用kubelet的容器日志参数来控制,这里不做详细说明。

其实在Kubernetes环境里,我们一般通过kubelet--container-log-max-size--container-log-max-files来控制。但如果你直接使用containerd + nerdctl,则需要自己在运行时配置。无论怎么配,目的都是相同的:别让日志吃掉所有可用空间。

七、总结与几个值得记住的点

我们这一路聊下来,其实核心思路很简单:把基础打牢、把配置调对、把日志管好、把故障处理流程练熟。containerd本身是一个成熟的项目,大多数可靠性问题并不是它自身很脆弱,而是我们没喂对“饲料”。

应用场景:这篇文章里提到的方法,适用于Kubernetes节点、Docker独立环境、以及任何基于containerd的容器服务。尤其是生产环境,节点数量多,业务量大,提前做好这些防御性配置能省去很多半夜on-call的痛苦。

技术优缺点:优点嘛,containerd轻量、稳定、有社区支持,比老的Docker daemon架构更干净;缺点就是配置项多,文档有时候不够生活化,新手容易踩坑。不过只要肯花时间测试,这些问题都是可以克服的。

注意事项:第一,改配置之前一定要备份。第二,生产环境不要随意执行ctr images prune这种全局清理命令,最好先在测试环境演练。第三,重启containerd会暂时中断节点上的所有容器,所以在业务低峰期操作,并且要确保kubelet会自动拉起这些容器。第四,健康检查脚本里的重启逻辑要加个次数限制,否则服务反复崩溃时,节点会越来越不稳定。第五,日志和监控一定要配全,光靠人肉看肯定不够。

最后,提升可靠性的本质不是用多么高深的技巧,而是养成一种“敬畏生产环境”的态度。每一次配置修改,每一次重启,都要想清楚可能带来的影响。把containerd当成一个需要精心照顾的系统组件,它就会勤勤恳恳地为你工作;你不管它,它迟早会给你颜色看。希望这篇文章能帮你少走一些弯路,也欢迎你在实际处理故障时,多积累自己的经验,毕竟再全面的博客也替代不了实战。

请继续补充SEO相关内容,已经为你整理在下方。