解决 Docker "Unimplemented: unknown service containerd.services.leases.v1.Leases" 错误

事故现场:一次崩溃的滚动升级

作为一名 Arch Linux 用户,我习惯性地执行了 sudo pacman -Syu 将系统更新到最新。然而更新后,所有 Docker 容器都无法启动,命令行抛出如下错误:

Error response from daemon: Unimplemented: unknown service containerd.services.leases.v1.Leases
failed to start containers: a1000b-sdk-fad-2.3.0.4

检查版本信息,得到的却是两个看起来都很新的组件:

$ containerd --version
containerd github.com/containerd/containerd/v2 v2.3.0 2976f38ccbfcda5ef1364d63d60b0a304e4bf94a.m

$ docker version
Server: Engine:
  Version:          29.4.2
  ...

$ docker info | grep -i "containerd version"
 containerd version: N/A

docker info 中居然显示 containerd version: N/A!显然 Docker 已经无法正确识别 containerd 的服务了。

错误根因:版本“新”的错觉与 API 断裂

很多人第一反应是:Docker 29.x 太老了?或者是 containerd 2.x 太新了?
实际上,containerd 2.x 是“主动变新”的一方,Docker 29.x 还在使用旧接口

关键点在于:

  • containerd 从 1.5 开始引入了 leases(资源租约)服务,对应的 gRPC 服务名为 containerd.services.leases.v1.Leases,主要用于镜像层的生命周期管理(避免垃圾回收过早删除正在使用的快照)。
  • containerd 2.0 是一个包含破坏性变更的大版本,它彻底重命名或移除了旧版的 leases.v1.Leases 服务(改用新的服务结构)。
  • Docker Engine 29.x(以及当前的整个 29 系列)在启动容器和拉取镜像时,仍然会调用旧版的 leases.v1.Leases 接口,因为它的代码尚未适配 containerd 2.x 的新 API。

因此,当 Arch Linux 将 containerd 独立滚动升级到 2.x 后,Docker 29.x 尝试调用一个已经不复存在的 gRPC 服务,containerd 返回 Unimplemented,容器启动失败。docker info 中的 N/A 则是 Docker daemon 连接 containerd 失败后给出的默认显示。

一句话总结:containerd 2.x 是新版本,但 Docker 29.x 还在用旧版 containerd 的 API,两者出现了严重的版本脱节,导致 Docker 完全无法工作。

解决方案:降级 containerd 并锁定版本

由于 Docker 官方尚未发布支持 containerd 2.x 的版本,目前最稳妥、社区验证有效的方法是将 containerd 降级到 1.7.x(建议 1.7.22),然后锁定其版本,阻止后续滚动更新自动升级。

选择正确的降级版本

Arch Linux Archive 查看 containerd 历史版本发现,官方存档中 1.7.x 系列的最后一个版本是 containerd-1.7.22-1-x86_64.pkg.tar.zst(发布日期 2024-09-09)。该版本与 Docker 29.4.2 完全兼容,可以作为降级目标。

⚠️ 注意:存档中并不存在 1.7.25 等更高版本,不要选错。

方法一:使用 downgrade 工具(推荐)

对于 Arch Linux 用户,社区工具 downgrade 是最方便的降级助手,它能自动搜索本地缓存、归档站和镜像,并解决下载时可能出现的 SSL 兼容问题。

# 安装 downgrade
sudo pacman -S downgrade

# 执行降级
sudo downgrade containerd

在出现的交互式列表中,选择 1.7.22-1 版本,确认后工具会自动完成下载和安装。

方法二:手动下载并安装(绕过 SSL 错误)

如果因 OpenSSL 版本过高导致 pacman -U 从 archive 下载失败(出现 TLS connect error),可以使用 curl 临时跳过证书校验,然后本地安装。

# 手动下载包(-k 忽略证书错误,确保网络可信)
curl -k -O https://archive.archlinux.org/packages/c/containerd/containerd-1.7.22-1-x86_64.pkg.tar.zst

# 本地安装
sudo pacman -U containerd-1.7.22-1-x86_64.pkg.tar.zst

锁定 containerd 版本

降级完成后,必须立即编辑 /etc/pacman.conf,找到 IgnorePkg 所在行,修改为:

IgnorePkg = containerd

这样即使之后执行 pacman -Syu,containerd 也会被忽略,不会再被自动升级到 2.x。

重启 Docker 并验证

# 重启服务(containerd 会在 docker 之前被自动启动)
sudo systemctl restart docker

# 检查 containerd 版本是否正确识别
docker info | grep -i "containerd version"
# 期望输出: containerd version: 1.7.22

# 测试启动之前出问题的容器
docker start a1000b-sdk-fad-2.3.0.4

此时容器应该能够正常启动,错误彻底消失。

其他可能相关的环境问题

虽然绝大多数案例都是版本不兼容导致的,但以下情况也可能引发类似错误,排查时可以作为参考:

  • XFS 文件系统的 ftype=0:containerd 2.x 的 overlayfs 快照器要求 XFS 在格式化时带有 ftype=1,否则会加载失败并伴随 Unimplemented 或插件错误。如果问题出现在 XFS 分区上,可以检查 /var/lib/containerd 所在分区的 ftype 设置。
  • 服务启动顺序错误:更新后仅重启了 Docker 而未重启 containerd,导致旧进程残留,这时 Docker 仍会尝试连接旧的 socket,也可能触发相同的错误。重启整个服务栈即可解决。

总结:滚动更新中的依赖陷阱

Arch Linux 的滚动更新策略让我们能够迅速获得最新的上游软件,但也意味着不同的软件包可能以不同的节奏跨越大版本。containerd 2.x 的发布就是一个典型例子:它带来了架构重构,但下游的 Docker 尚未完成适配,直接升级就会导致整个容器栈瘫痪。

本次故障的修复路径清晰且经过大规模验证:

  1. 将 containerd 降级至最后一个兼容的 1.7.x 版本(如 1.7.22)。
  2. 锁定该包防止再次误升级。
  3. 等待未来 Docker 官方宣布支持 containerd 2.x 后,再解除锁定进行升级。

对于其他发行版的用户(如 Ubuntu),如果是通过 Docker 官方仓库安装的 Docker,通常 containerd 会作为 Docker 的依赖被一起锁定版本,不会突然升级到 2.x。但如果手动安装了新版的 containerd,同样会出现此问题,解决方案也是降级 containerd。

希望这篇博客能帮助你在面对同样的错误时快速定位问题,恢复 Docker 的正常运行。如果你在修复过程中遇到其他特殊情况,欢迎在评论区交流或查阅 Arch Wiki 及 Docker 官方文档获取最新信息。