ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Docker与Containerd集成:从底层分工到排障实战

Docker与Containerd集成:从底层分工到排障实战 你用docker run跑起一个容器看起来一切风平浪静。但如果你只把 Docker 当成一个黑盒迟早会被生产环境里的报错教育一顿——比如服务起不来、socket 提示 permission denied、Docker Desktop 在 Windows 上死活检测不到虚拟化。这系列文章讲 Containerd 讲到第 9 篇这次专门聊 Docker 和 Containerd 的集成。这篇内容适合三类人背着线上环境的运维、被 Docker Desktop 启动错误卡住的新手、以及想彻底搞明白“镜像和容器到底谁在管”的开发者。我会把安装配置、底层分工、命令对照、常见排障一次讲透附上我在真实环境里踩过的坑和验证过的命令。1. 重新认识 Docker 与 Containerd 的分工1.1 一个 docker 命令背后到底发生了什么很多人的认知停留在“docker 命令 容器”实际上一个docker run背后是一条完整的调用链。你输入的指令先由 docker CLI 转换成 HTTP 请求发送给 dockerdDocker daemondaemon 负责解析镜像、创建网络、分配资源然后把最核心的容器生命周期管理交给 containerdcontainerd 再调用 runc 去内核里创建命名空间和 cgroup。这个链路很好理解docker CLI 是前台接待dockerd 是值班经理containerd 是仓库调度员runc 才是那个真正搬箱子的工人。一旦生产环境出问题我建议你先画一遍这条链路脑子里立刻会清楚“该看日志的是 dockerd 还是 containerd 还是内核”。为什么要拆这么多层因为每一层都可以独立替换和升级。早期 Docker 确实把容器操作全塞进 daemon 里结果 daemon 一挂整个机器上的容器全失控。现在这个分层设计就是为了让运行时管理更健壮也让 Kubernetes 这类调度系统可以不用 docker daemon 直接对接 containerd。1.2 containerd 凭什么能叫工业级容器运行时containerd 从 Docker 项目中剥离出来后很快被 CNCF 接收成为事实上的容器运行时标准。它负责镜像传输、镜像存储、容器执行、网络插件管理并且实现了 Kubernetes 的 CRIContainer Runtime Interface接口。它不提供面向用户的 CLI也没有复杂的 API 层专注做底层的容器管理。这带给运维的价值是依赖少、崩溃面小、资源开销低而且稳定。生产环境中Kubernetes 节点可以直接把 runtime 指定为containerd节点上跑几十上百个容器都不用担心 daemon 成为瓶颈。对比来看Docker 更像一个“工具集合”它把构建镜像、推送镜像、启动容器、编排 Compose 全部集成在一起方便开发者使用。而 containerd 只关心“把容器稳定跑起来”这一件事。Kubernetes 选择 containerd正是看中了它的工业级定位。1.3 两种主流集成模式别再混为一谈很多人问“我到底该学 Docker 还是 containerd”其实更重要的是理解集成方式。目前主流有两条路径第一种Docker 自带 containerd。装 Docker Engine 的时候会把 containerd 作为依赖一起装上dockerd 通过内部接口与 containerd 通信。这是绝大多数开发者和运维最熟悉的方式docker ps、docker compose都正常工作你几乎感知不到 containerd 的存在但它确实在背后干活。第二种独立部署 containerd 并用 nerdctl 操作。这种场景通常在 Kubernetes 节点上甚至一些轻量容器平台。你可以直接安装 containerd用 nerdctl 执行nerdctl run、nerdctl pull等命令命令风格和 docker 高度一致但完全绕开了 docker daemon。这样节省了 daemon 的资源消耗也减少了故障点。这两种模式不是二选一。生产节点上推荐独立 containerd开发机和传统运维环境保留 Docker两者可以共存。2. 从零搭建 Docker 与 Containerd 环境2.1 Linux 上最稳的安装姿势在 Ubuntu 或 CentOS 这类 Linux 服务器上最快的安装方式是用 Docker 官方脚本curl -fsSL https://get.docker.com | sh systemctl enable --now docker执行完之后有一个容易被忽略的重点检查 containerd 是否被 docker 自动拉起。systemctl status containerd如果这个服务没有运行Docker 的容器操作也会跟着出问题。因为 Docker Engine 集成 containerd 之后containerd 不再是一个可选项而是引擎运行的底座。如果你更喜欢手动安装可以在 Ubuntu 上依次装好containerd.io、docker-ce、docker-ce-cli、docker-buildx-plugin、docker-compose-plugin这几个包。注意安装顺序先装 containerd 再装 docker 也没问题关键是版本要匹配。拿 CentOS 这类 yum 体系来说也一样用yum install安装同一套版本即可。装完先别急着拉镜像立刻验证一次 docker daemon 和 containerd 之间的通信docker info sudo ctr version看到docker info里能显示 Storage Driverctr version能返回 containerd 版本说明集成链路已经通了。2.2 Windows 和 macOS 的 Docker Desktop 集成坑Docker Desktop 的界面做得再友好本质也是要调起一套 Linux 虚拟机或者 WSL2 后端。这里最容易翻车的就是这个报错Virtualization support not detected Docker Desktop failed to start because virtualisation support wasnt detected“虚拟化支持未检测到”的原因通常有四种主板 BIOS 里的虚拟化功能没开、Windows 的 Hyper-V 功能未启用、WSL2 没有正确安装或者和其他虚拟机软件比如老版本 VMware/VirtualBox冲突。排查顺序建议是打开任务管理器切到“性能”标签看 CPU 一栏里的“虚拟化”是否为“已启用”如果显示“已禁用”重启进 BIOS/UEFI把 Intel VT-xIntel 平台或 SVM ModeAMD 平台打开管理员权限运行 PowerShell依次执行dism /online /enable-feature /featurename:Microsoft-Hyper-V-All /all /norestart wsl --install wsl --set-default-version 2如果还不行管理员 CMD 里执行bcdedit /set hypervisorlaunchtype auto然后重启。绝大多数 Docker Desktop 无法启动的问题都能在这几步里解决。核心思路就是Docker Desktop 本身是运行在 Windows 之上的消费者底层的虚拟化平台没准备好它什么也干不了。2.3 一条命令确认集成是否真的生效我知道很多人装完环境就焦虑“我到底装成功没有”。其实不用慌一条命令就能见分晓docker info | grep -i runtime标准的 Docker Engine 在集成了 containerd 和 runc 之后Runtimes列表里一定包含runc。如果显示的是其他运行时说明这台主机的底层运行时被替换过。再执行一次systemctl status containerd看到 active (running) 状态就说明整个集成链路目前是健康的。还有一个更底层的验证方式直接查看系统中的 containerd socketls -l /run/containerd/containerd.sock这个 socket 是 containerd 暴露给 dockerd 或者 nerdctl 的接入点。它存在并且权限正确说明 containerd 已经准备好接收指令。3. 深入配置让 Docker 正确指挥 Containerd3.1 daemon.json 里到底能调什么Docker 的配置集中在/etc/docker/daemon.json很多启动问题都能在这里找到答案。我见过太多人一上来就抄配置改坏了甚至直接写崩因为不了解每一项的含义。一个生产环境常用的最小配置长这样{ registry-mirrors: [https://your-mirror.example.com], log-driver: json-file, log-opts: { max-size: 50m, max-file: 5 }, storage-driver: overlay2 }registry-mirrors镜像加速地址解决拉取慢的问题log-driver和log-opts限制容器日志体积否则/var/lib/docker/containers会被日志文件撑爆storage-driver统一使用 overlay2这需要内核模块overlay和br_netfilter支持改完配置后重启systemctl daemon-reload systemctl restart docker systemctl status docker这里要强调一个经验daemon.json里别乱开实验性的 containerd-snapshotter 参数。某些新版本把 containerd-snapshotter 作为可选特性想开启containerd原生镜像快照但稳定性和兼容性在不同内核上表现不一致。如果你没有充足测试时间别在生产上拿这个实验参数冒险。3.2 镜像拉取慢、安装失败怎么根治如果你刚装完 Docker 就遇到docker pull mysql:8.0卡半天或者直接超时大概率是镜像源问题。镜像加速器的本质是帮你从距离更近的缓存仓库拉取镜像不是让网络变快。配置方式就是在daemon.json里加一组registry-mirrors然后重启 docker。注意不要只配一个地址多配几个挂了一个还有备用的。我这里提供一个真实的“安装 MySQL 失败”案例排查过程。当时用户执行docker run -d -p 3306:3306 --name mysql8 -e MYSQL_ROOT_PASSWORDroot mysql:8.0结果提示repository docker.io/library/mysql not found或者404 Client Error。这不是版本不存在而是拉取路径或者镜像源出了问题。我的建议是先检查docker search mysql能否返回正常内容再检查docker pull mysql:8.0.36是否比mysql:8.0更稳定因为大版本 tag 可能指向最新子版本有时某些镜像源同步不及时如果加速器失效立刻换一个新的镜像源并重启 docker同理docker 安装 redis 主从这类问题多数也是卡在镜像下载而不是配置上。先把基础镜像下载环节跑通再谈主从配置顺序不能反。3.3 用 nerdctl 直接操作 containerd你会什么时候必须用 nerdctl最常见的就是 Kubernetes 节点上手头没有 docker CLI只有 containerd 的场景。这时候想排查节点上的容器状态总不能干瞪眼。nerdctl 是 containerd 项目推出的兼容命令几乎复刻了 docker CLI 的操作方式还支持 compose。你可以把它理解成“没有 daemon 层的 docker”。举几个常用对照# 查看容器列表 nerdctl ps # 拉取镜像 nerdctl pull nginx:alpine # 启动容器 nerdctl run -d --name web -p 80:80 nginx:alpine容器在 containerd 里的管理和在 docker 里有细微差别。最明显的一点是nerdctl 默认创建的 namespace 是default而 Kubernetes 里用k8s.io这个 namespace。所以排查节点容器时要记住加-n k8s.io否则你会发现自己什么容器都看不到。这个细节坑了我不少时间现在写出来希望你避免。不过更常见的场景是Docker 和 containerd 都装了用户搞不清该用哪个。我的建议是日常开发继续用 docker CLI排查底层状态时用ctr或nerdctl验一下双通道配合心里才踏实。4. 容器管理实操与关键细节4.1 docker 与 nerdctl 常用命令对照很多读者在看完上一节后会问“我到底该背哪套命令”实际上两套命令高度相似关键是知道在什么环境下用哪个。我整理了一份常用对照表建议保存备用操作场景docker 命令nerdctl 命令查看运行中的容器docker psnerdctl ps查看所有容器docker ps -anerdctl ps -a拉取镜像docker pull nginxnerdctl pull nginx构建镜像docker build -t app .nerdctl build -t app .创建并启动容器docker run -d --name web -p 80:80 nginxnerdctl run -d --name web -p 80:80 nginx查看容器日志docker logs webnerdctl logs web进入容器docker exec -it web shnerdctl exec -it web sh停止容器docker stop webnerdctl stop web删除容器docker rm webnerdctl rm web查看镜像列表docker imagesnerdctl images清理悬空资源docker system prunenerdctl system prune注意两处区别一是在 containerd 环境下没有 daemonnerdctl直接和 containerd socket 通信资源占用更小二是 nerdctl 在多用户环境下要处理 namespace非defaultnamespace 的容器必须显式指定。理解这两点你在两套工具之间切换就不会慌。4.2 网络与端口映射最容易忽视的细节容器网络是新手踩坑重灾区。先说一个最常见的错误认知认为在宿主机上能 ping 通容器 IP。默认 bridge 网络模式下容器和宿主机在不同网段宿主机往往 ping 不通容器这个不算故障别浪费时间调整。真正需要关注的是端口映射。执行docker run -d --name web -p 80:80 nginx:alpine流量走向是宿主机 80 端口 → dockerd 的 iptables 规则 → 容器 80 端口。这个过程中如果容器内服务监听的是 127.0.0.1 而不是 0.0.0.0外面就永远访问不到。我排查过很多 Nginx、MySQL、Redis 容器外部访问失败的问题查到最后都是容器内服务绑定地址写死了环回地址。另外多容器之间通信推荐手动创建 networkdocker network create app-net docker run -d --name mysql --network app-net mysql:8.0 docker run -d --name web --network app-net -p 80:80 nginx:alpine这样web容器里可以直接用mysql这个主机名访问数据库不用记 IP。这是 Compose 能自动处理的逻辑手动用 docker 命令时很容易被忽略。4.3 数据持久化欠的账迟早要还容器是即用即弃的。多少人踩过这个坑用docker run -d mysql:8.0跑了几天想升级镜像直接docker rm -f mysql然后再docker run起一个新的结果数据全没了。原因很简单容器没有挂载持久化目录数据全部写进了容器可写层容器一删数据跟着报废。正确姿势是把数据目录挂出来docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDroot \ -v mysql_data:/var/lib/mysql \ --restartalways \ mysql:8.0这里的-v mysql_data:/var/lib/mysql是命名卷Docker 会把数据放到宿主机的/var/lib/docker/volumes/mysql_data目录下。--restartalways保证机器重启后容器自动拉起避免服务器断电后数据库服务静默消失。同样思路也适用于 Redisdocker run -d --name redis \ -p 6379:6379 \ -v redis_data:/data \ --restartalways \ redis:7 redis-server --appendonly yesRedis 的持久化文件会写在容器内/data挂载卷以后就不会因为容器重建丢数据。做 Redis 主从时从节点容器同样要挂载数据卷并且注意--restartalways。5. 排障宝典几个让新手头疼的错误5.1 permission denied while trying to connect to the Docker api这是出现频率最高的错误没有之一。你输入任何 docker 命令都会收到类似permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock原因就是你当前的 Linux 用户不在docker用户组里没有权限访问 docker.sock 这个 socket 文件。解决办法sudo usermod -aG docker $USER newgrp docker执行完重新登录一次终端再试docker ps就能通。注意newgrp docker是临时切换当前 shell 的用户组避免为了生效反复注销登录。如果你是在桌面版 Linux 上用 sudo 装的 docker这种问题几乎必现直接按这个流程处理。另外一个更“暴力”的方式是sudo chmod 666 /var/run/docker.sock我极其不推荐。把 socket 权限开放给所有用户很容易让任意用户获得宿主机的 root 权限这是典型的安全隐患。老老实实加用户组比改权限稳妥得多。5.2 磁盘空间不足导致 docker 服务启动失败很多人在生产环境碰上docker: failed to start docker application container engine第一反应就去查版本其实大部分是磁盘满了。镜像、容器、卷、日志全部堆在/var/lib/docker下空间一满dockerd 直接拒绝启动。排查步骤systemctl status docker journalctl -u docker --since -10m df -h du -sh /var/lib/docker如果发现/var/lib/docker占用异常高先看是不是日志文件过大du -sh /var/lib/docker/containers/*日志多通常是因为没有配置max-size我建议回到 3.1 节的配置把每个容器的日志限制在 50MB 以内并且只保留 5 份轮转文件。然后清理悬空镜像和停止状态的中间层docker system prune -a docker volume prune这里提醒一句docker system prune -a会把所有未被使用的镜像全部删掉执行前先确认没有需要保留的镜像。一开始就用配置文件把日志兜住比事后清理更省心。5.3 升级 Docker 或重启后容器全不见有用户反馈升级 Docker 版本后重启主机发现docker ps -a显示所有容器都消失了吓出一身冷汗。其实这大多是容器状态从“运行中”变成了“exited”因为升级过程会停掉 docker 服务容器进程被回收。正确做法是升级前先确认容器的重启策略。如果你在创建容器时加了--restartalwaysdocker 服务启动后会自动把容器拉起来中间体验“容器消失”的时间很短。没加重启策略的话就要手动docker start $(docker ps -aq)一次性恢复所有停止容器。所以生产环境的容器创建时就该带上--restartalways尤其像 MySQL、Redis、Nginx 这类常驻服务。这是一个最小投入、最大收益的操作习惯。5.4 容器内数据库服务启动失败或外部无法访问装 MySQL 和 Redis 时除了镜像拉取另一个高频问题是服务起来后外部无法连接。排查思路从外到内检查端口映射是否生效docker ps检查宿主机防火墙是否放行端口ufw status或firewall-cmd --list-all检查容器内服务监听地址进入容器docker exec -it mysql8 mysql -uroot -p验证服务本身没问题拿 MySQL 举例容器内my.cnf如果配置了bind-address127.0.0.1即便是-p 3306:3306映射了端口外部也无法访问。需要把绑定地址改成0.0.0.0或注释掉。Redis 同理protected-mode yes默认开启不绑定 IP 的情况下外部连接会被拒绝。5.5 镜像仓库认证与加速器失效问题拉私有仓库镜像时常常遇到unauthorized: authentication required这是因为本地没有正确的登录凭证。解决方式docker login your-registry.example.com登录凭证会保存在~/.docker/config.json里。注意这个文件可能会被 CI/CD 系统或者容器内拷贝里面有明文令牌保管好别泄露。如果之前配置的镜像加速器突然失效拉镜像失败不要慌。检查daemon.json的registry-mirrors换一个新的加速地址重启 docker 再试。实在下载失败的镜像可以先执行docker rmi 镜像名清掉不完整的缓存再重新拉取避免被残留层干扰。6. 我的经验与推荐组合6.1 开发机、测试机、生产机分别怎么配如果是本地开发机装 Docker Desktop 或者 Linux 上的 Docker Engine 就够用。它自带 compose、构建工具、图形化界面省事。底层集成 containerd 是标配不需要你额外介入。如果是测试环境或者 mini 服务器比如 N100 这类低功耗小主机建议直接用 Docker Engine 跑服务资源开销比 Docker Desktop 小太多。跑十来个容器完全没压力前提是镜像别都无脑拉 latest时间久了镜像体积会给你上一课。生产环境建议走 Kubernetes 默认的 containerd 运行时。节点上不装 Docker CLI 也没关系用crictl和nerdctl管理容器在镜像构建打包这台机器上保留 Docker。这种组合的好处是开发端的 docker 生态照样用生产端的故障面被压制到最小。6.2 我日常最常用的几个顺手命令除了常规的docker ps、docker logs我几乎每次排查都会用到这几个# 检查镜像和容器占用了多少空间 docker system df # 查看某个容器的详细元信息 docker inspect -f {{.State.Status}} {{.HostConfig.RestartPolicy.Name}} container-name # 查看容器的重启次数和退出码 docker inspect -f {{.RestartCount}} {{.State.ExitCode}} container-name # 查看 containerd 版本与运行时行为 sudo ctr version # 清理已被容器不再使用的网络避免 iptables 规则堆积 docker network prunedocker system df几乎是我上服务器后的第一道检查指令它能快速告诉你镜像、容器、卷、日志缓存四类资源各自占了多大空间一眼定位磁盘压力来源。6.3 零坑自检清单部署容器前过一遍结合实际经验我总结了一份部署前的自检清单每一条都是花真金白银买来的教训Docker 和 containerd 服务状态是否都是 activerunning/etc/docker/daemon.json是否配置了镜像加速和日志大小限制容器存储路径/var/lib/docker所在磁盘是否剩余 20% 以上空间当前用户是否已在 docker 用户组socket 权限是否合理所有常驻服务是否配置了--restartalways数据类容器是否挂载了命名卷或 bind mount端口映射是否和宿主机防火墙规则匹配容器内服务监听地址是否为0.0.0.0是否绑定了环回地址镜像 tag 是否明确避免latest漂移导致意外升级这套清单看起来很细但每一条都对应一个真实事故。对照着走一遍能躲开绝大多数新手会遇到的问题。最后再分享一个小技巧我在排查 containerd 集成问题时总是先在宿主机的/run/containerd/containerd.sock上用ctr看一眼 namespace。很多时候你以为 docker 里看不到容器是镜像坏了实际是容器跑到了另一个 namespace 下面。先把 namespace 这个概念刻在脑子里排查效率会提升一大截。
返回列表