ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Docker容器架构全解析:从客户端到runc的完整链路

Docker容器架构全解析:从客户端到runc的完整链路 第一篇我决定不急着讲命令先把docker-container-architecture这个题目拆透。很多人早就把docker run敲得飞起镜像、容器、网络都能玩个大概但遇到“Docker Desktop 闪退”“容器起来了但网络不通”“想换 containerd 又不知道和 Docker 什么关系”这些问题时就明显感觉到脑子里缺了一张图。这张图就是 Docker 的容器架构。这篇内容是 Docker 容器生态系列的开篇适合两类人看一是刚接触容器、想系统理解 Docker 而不仅仅是背命令的读者二是被 Windows 下 Docker Desktop 各种报错、Linux 下 Docker 服务突然起不来折腾过想搞清楚“到底哪一层出了问题”的同学。能理解架构才能真正拥有排障能力而不是靠百度猜答案。1. 先把容器生态的坐标系搭起来1.1 容器到底是什么共享内核的轻量“隔离舱”聊架构之前必须先定义容器。容器的本质不是虚拟机它没有自己独立的内核。一个容器进程跑在宿主机内核之上通过 Linux 内核提供的 namespace、cgroups 等机制给自己划出一块相对隔离的运行空间。namespace 负责“看不见”让进程以为自己是独立系统里唯一的主角cgroups 负责“用不超”限制这个进程能用多少 CPU、内存和磁盘 IO。你可以把容器想象成集装箱。集装箱内部货物摆放随意但外部尺寸统一、装卸标准固定容器也一样内部跑什么应用、什么依赖都由镜像决定对外却用同一套标准化接口构建、分发、运行。这也是“容器生态”这个词的由来围绕容器这个标准单元生长出了镜像仓库、容器运行时、编排系统、存储网络插件等一系列配套生态。1.2 Docker 在容器生态里的位置Docker 不等于容器但 Docker 是容器生态里最重要的入口。从用户视角看Docker 封装了底层细节你只需要docker run、docker build、docker compose up就能把应用跑起来。可在 Docker 内部整个执行链路是分层的最上层是用户交互的 CLI 客户端中间是常驻后台的 daemon下面还有 containerd、containerd-shim、runc 这一串组件。理解了这条链路你才能看懂很多常见现象。比如为什么 Docker Desktop 在 Windows 上启动失败时提示是“virtualization support not detected”因为 Docker Desktop 在 Windows 上需要一个轻量虚拟机来跑 Linux 内核这个虚拟化后端依赖 WSL2 或 Hyper-V。再比如为什么docker logs能拿到容器日志因为容器进程的 stdout/stderr 并没有直接消失而是被 shim 进程一直握在手里。1.3 为什么第一篇要死磕架构后面整个系列都会围绕 Docker 展开镜像怎么构建、网络怎么打通、数据卷怎么挂、Compose 怎么编排、生产环境怎么选运行时。这些内容如果脱离了架构来学很容易变成“背参数”。比如你知道-v是挂载目录但不知道挂载目录和容器可写层的本质区别遇到数据丢失依然一脸懵你知道docker stop能停容器但不知道它和docker kill最终都通过 containerd 向容器主进程发信号就无法理解为什么有些容器 stop 半天停不下来。所以第一篇先把地基打牢。下面我直接按“客户端 → dockerd → containerd → shim → runc → 容器进程”这条主链路把 Docker 容器架构完整拆一遍。2. Docker容器架构全景拆解2.1 docker CLI 与 dockerd客户端/服务端模式Docker 采用典型的客户端/服务端架构。你平时敲的docker命令本质是一个轻量级 CLI 客户端它本身不创建容器只负责把命令翻译成 HTTP 请求发给常驻后台的 Docker daemon。daemon 的进程名是dockerd在 Linux 上默认监听 Unix socket/var/run/docker.sock如果需要远程管理也可以配置 TCP 端口比如 2375明文或 2376TLS。这个设计带来什么结果CLI 和 daemon 可以不在同一台机器上。你可以让本机的docker客户端连接远程服务器的 dockerd只要网络可达、有权限。但默认情况下普通用户没有访问/var/run/docker.sock的权限所以刚装完 Docker 时直接敲docker ps经常会报permission denied while trying to connect to the Docker daemon socket。这正是架构带来的权限边界客户端能不能调用 daemon由 socket 文件权限决定。在 Windows/macOS 上Docker Desktop 把这个模型包装得更隐蔽一些真正的 dockerd 跑在一个轻量 Linux 虚拟机WSL2 或 Hyper-V 后端里本机的 CLI 通过 Windows 命名管道连接过去。所以你重启 Docker Desktop本质是重启了虚拟机里的整套 Docker 服务。2.2 containerd容器生命周期的实际管理者从 Docker 1.11 开始Docker 把容器生命周期管理能力从 dockerd 里抽出来交给了 containerd。containerd 最早是 Docker 公司内部组件后来捐给 CNCF现在已经是云原生领域的事实标准运行时管理者。dockerd 本身并不直接启动容器进程。它把“创建容器、启动容器、停止容器、销毁容器”这些脏活累活通过 gRPC 接口委托给 containerd。containerd 负责的事情包括镜像的拉取和存储、镜像层挂载、容器 OCI spec 生成、进程生命周期管理、cgroups 资源限制的配置等。换句话说containerd 是容器领域的“项目经理”它不亲手创建隔离环境但所有任务都经它调度。对应到进程你在 Linux 机器上会看到一个名为containerd的常驻进程。它默认监听/run/containerd/containerd.sock。如果你想绕过 Docker 直接操作它可以用ctr命令如果机器上装了 Kubernetes还会用到crictl追求 Docker 体验又想脱离 dockerd 的人可以用nerdctl。这些都是后话但你能看到 containerd 在生态中的底层地位。2.3 runc底层真正“造容器”的进程如果说 containerd 是项目经理那runc就是真正下场干活的施工队。runc 是 OCIOpen Container Initiative运行时规范的参考实现它做的事情非常底层根据一份 JSON 格式的容器配置调用 Linux 内核能力创建 namespace配置 cgroups准备 rootfs然后exec出容器进程。Docker 刚起步时用的其实是 LXC后来自己研发了 libcontainer 库再后来 libcontainer 演化成了 runc。runc 是一个极其轻量的二进制它不常驻后台而是在需要创建容器时被拉起来执行一次容器启动成功后就退出。这一点很关键runc 其实是一个“一次性执行工具”。这也是 Docker 架构设计最精妙的地方之一。把“创建”和“管理”分离runc 只负责把容器进程拉起来后续对容器的管理交给更上层的组件。2.4 containerd-shim容易被忽略的关键缓冲层runc 启动完容器进程就退出了那容器进程不就变成孤儿进程了吗这时候containerd-shim出场。每个容器都有一个对应的 shim 进程它被 containerd 拉起再让 shim 去调用 runc 创建容器。runc 退出后shim 就成了容器进程的直接父进程。shim 存在的意义至少有两点。第一它持有容器进程的 stdiodocker attach和docker logs才能继续读到容器的标准输入输出第二它把容器进程与 dockerd/containerd 解耦即使 dockerd 或 containerd 重启容器进程也不会受影响shim 还可以向 containerd 汇报容器退出状态。你可以把 shim 理解为“驻场监理”施工队 runc 盖完楼撤了监理 shim 留在现场随时向上汇报这栋楼容器的状态。所以一个运行中的容器在宿主机进程表里至少能看到三个相关进程containerd、containerd-shim、容器进程本身。3. 一条 docker run 命令的完整旅程3.1 从输入命令到 dockerd 收到请求现在我带你走一遍完整的docker run -d --name nginx -p 8080:80 nginx:1.26。命令敲下去CLI 先检查本地配置把参数解析成创建容器的请求通过 socket 发给 dockerd。这一步如果 socket 不通就会报我们都很熟悉的Cannot connect to the Docker daemon。dockerd 收到请求后并不马上创建容器。它先做了一系列校验镜像是否存在、端口有没有冲突、名字是否占用、资源限制参数是否合理。校验通过后dockerd 会把任务下发给 containerd。从这里开始用户能感知的“Docker”就已经退居幕后了。很多人以为docker run是 dockerd 亲自 fork 出来的其实不是。dockerd 更像一个 API 网关和功能聚合层它负责镜像构建、网络管理、数据卷挂载、日志驱动等而把纯容器生命周期操作交给了 containerd。3.2 镜像检查和拉取Layer 是原材料如果本地不存在nginx:1.26镜像dockerd 会先让 containerd 去镜像仓库拉取镜像。镜像是分层存储的每一层都是一个只读的 Layer。拉取的时候containerd 会做 sha256 校验确认每一层都完整。之后containerd 将这些只读层按顺序挂载成一个合并后的 rootfs。你可能会问为什么镜像要分层因为分层能极大提升磁盘利用率和传输效率。你本地有一个基于 Ubuntu 的镜像再拉一个同样基于 Ubuntu 的另一个镜像公共的 Ubuntu 层就不需要重复存储。这也是 Docker 生态里“镜像复用”的基础。如果你看docker pull的输出能看到Pull complete是按层出现的而不是一个整体就是这个原因。3.3 runc 创建隔离环境namespace 与 cgroupscontainerd 确认 rootfs 就绪后会先启动一个containerd-shim进程再由 shim 调用 runc。runc 读取 OCI 配置调用 Linux 内核能力完成容器隔离环境的创建。这里需要展开讲两类核心机制。第一是 namespace。Linux 内核给进程提供了多种隔离视图Docker 默认会创建以下几类命名空间隔离内容影响PID进程号容器内第一个进程 PID 是 1NET网络栈容器有独立的网卡、IP、路由IPC进程间通信隔离 System V IPC 和 POSIX 消息队列UTS主机名容器可以有自己的 hostnameMNT挂载点容器拥有独立的文件系统挂载视图USER用户 ID容器内 root 和宿主机 root 不是同一个账号CGROUPcgroup 根目录容器内看不到宿主机完整的 cgroup 树通过 namespace容器内进程“以为”自己独占了一台机器。这里面最容易被忽略的是 PID namespace容器内进程 1 就是你的应用进程它没有 systemd、没有 init 系统所以一些依赖 systemd 的服务在容器里是起不来的。第二是 cgroups。namespace 解决“看得见”的问题cgroups 解决“抢资源”的问题。runc 会根据配置创建一组 cgroup 控制文件写入 CPU 份额、内存上限、IO 权重等限制。如果你在docker run里加了-m 512m --cpus 1那最终落地就是 cgroup 配置里的一组数字。3.4 可写层、存储驱动与容器数据runc 把 rootfs 准备好后还会在只读镜像层之上叠加一个可写层。容器运行时的所有文件写入默认都发生在这一层。这种机制叫“写时复制”如果镜像里有一个文件容器想修改它存储驱动会先把文件从只读层复制到可写层再修改副本镜像本身纹丝不动。这带来两个重要结论。第一容器被删除时可写层也会被删除容器里产生的所有未持久化文件都会消失。第二如果你想保留数据要么用数据卷挂载宿主机目录要么用 bind mount 把宿主机路径直接映射进容器。不理解可写层就无法真正理解docker volume为什么是容器持久化的关键。存储驱动的选择也在这里体现。现代 Linux 发行版上Docker 默认使用overlay2它把多个只读层和一个可写层合并成一个视图。如果你用 Docker Desktop在 macOS 上可能走的是 virtiofs 和专门的虚拟磁盘方案但概念是相通的。3.5 网络与端口映射的架构介入点容器启动时如果没有指定网络模式会默认加入bridge网络。Docker 会在宿主机上创建一个虚拟网桥docker0并为每个容器分配一个虚拟网卡和一个内部 IP。容器访问外网时数据包从容器网卡发出经过 docker0再由宿主机 iptables 做 SNAT 转换外网访问容器时通过-p 8080:80映射的宿主机端口iptables DNAT 把流量转发给容器 IP 的 80 端口。这里要注意端口映射并不是容器网络的一部分而是宿主机 iptables/nftables 规则。所以 Linux 上 Docker 服务启动失败时如果 iptables 规则不干净或 FORWARD 链默认策略是 DROP容器网络就会莫名其妙地不通。这也是后面排障里最高频的问题之一。4. 容器生态里的关键选型Docker 不是唯一答案4.1 Docker、containerd、Podman 与 nerdctl 的边界理解了 Docker 的架构后你再看容器生态里的各种工具就会觉得豁然开朗。Docker功能最完整CLI 体验最好适合开发机、单机场景。但它引入了一个常驻 dockerd而 dockerd 这个“API 网关”对生产环境来说不是必需的。containerd 直用如果你用ctr直接操作 containerd性能更好组件更少但命令不够友好主要面向开发者和 Kubernetes。nerdctl目标是提供和 Docker CLI 几乎一致的体验但底层直接驱动 containerd不需要 dockerd。适合想摆脱 Docker 但又不想改习惯的人。Podman无 daemon 架构每个容器由一个独立子进程管理支持 rootless。它兼容 Docker 命令在很多 Linux 发行版上被作为 Docker 的替代方案。这些工具并不互斥它们共用 OCI 镜像规范和容器运行时生态是通的。4.2 为什么 Kubernetes 最终选择 containerdKubernetes 早期通过 dockershim 调用 Docker再由 Docker 调用 containerd链路长、稳定性差。后来 Kubernetes 社区意识到dockershim 这层“翻译官”并没有提供 Kubernetes 真正需要的额外价值于是从 1.24 版本起彻底移除 dockershim转而直接通过 CRIContainer Runtime Interface对接 containerd 或 CRI-O。现在你回头看 Docker 架构就会明白这次整合的必然性Kubernetes 需要的是一个能管理镜像、镜像层、容器生命周期的运行时containerd 本身就具备这些能力。Docker 的价值更多在开发体验和构建工具链而生产集群运行时containerd 已经足够。这也是容器生态“向底层收敛”的典型例子。4.3 Docker Compose单机编排与多服务协同容器生态里除了单个容器还有一个非常重要的层次多容器编排。docker compose就是 Docker 提供的单机多容器编排工具。它通过一个 YAML 文件描述服务、网络、依赖关系和数据卷然后由 Docker CLI 调用 dockerd 批量创建容器。Compose 的底层仍然走同一套架构每个服务都会被转成容器由 containerd 管理生命周期。但它补上了 Docker 原生命令的短板——把复杂环境定义一个文件里实现可重复部署。常见的 MySQL 主从、Redis 主从、面板类应用几乎都能用 Compose 一次性拉起一套环境。这也是你在各种部署教程里频繁看到docker-compose.yml的原因。4.4 Docker Desktop、WSL2 与虚拟化后端Docker Desktop 是目前 Windows 和 macOS 上体验最好的 Docker 运行方案。在 Windows 上Docker Desktop 有两种后端一种是基于 WSL2一种是基于 Hyper-V。WSL2 是默认且推荐的方式因为它启动更快、内存占用更小。但 WSL2 本身就是一层轻量虚拟机它对宿主机的虚拟化能力有硬性要求。如果在 BIOS 里关闭了 VT-x/AMD-V或者 Windows 功能里没有启用“虚拟机平台”和“适用于 Linux 的 Windows 子系统”Docker Desktop 启动时就会直接报virtualization support not detected或类似错误。理解了架构你就知道这不是 Docker 本身坏了而是它的虚拟化底座没准备好。5. 动手验证把架构“看”出来5.1 进程树视角一条链路上的真实进程在一台 Linux 机器上运行一个容器后用下面的命令查看进程ps -ef | grep -E dockerd|containerd|shim|runc|nginx | grep -v grep你会看到类似这样的结果dockerd一个进程containerd一个进程然后每个容器对应一个containerd-shim进程以及容器内的业务进程。如果你在启动瞬间抓取进程还可能看到一闪而过的runc进程。这正是理论架构在现实中的投影。如果一台机器上有 10 个容器你会发现 containerd-shim 恰好有 10 个这就是“一个容器一个 shim”的最好证明。5.2 命名空间与 cgroups 验证找到容器进程的 PID 后可以查看它所属的 namespacelsns -p PID输出里会列出 PID、NET、MNT、UTS 等 namespace每一类都有一个唯一的 inode 编号。你会发现容器进程的 namespace 和宿主机其他进程明显不同而且同一个容器里的多个进程共享同一组 namespace。再看资源限制cat /proc/PID/cgroup在 cgroup v2 系统上你会看到容器进程落在/system.slice/docker-id.scope这样的路径下。如果你想看得更直观用systemd-cgls可以查看完整的 cgroup 树能明显看到 Docker 为每个容器建的独立子目录。5.3 用 docker version / docker info 验证客户端与Server分离docker version是验证客户端/服务端架构最直接的办法。它会分成Client和Server两段分别显示版本。如果本机 CLI 连接的是远程 daemonServer 段显示的就会是远端信息。docker info同样能说明问题它返回的是 daemon 侧的系统状态包括存储驱动、内核版本、cgroup 版本、镜像数量、容器数量等。如果你在 Docker Desktop 里看到 Server 段的 Operating System 是 Linux而 Client 是 Windows不要奇怪因为 daemon 确实跑在虚拟化的 Linux 环境里。5.4 配置镜像加速器与常见安装注意事项镜像加速器的配置位置在/etc/docker/daemon.json。国内常用的是阿里云容器镜像服务提供的加速地址、中科大公共镜像加速地址等。配置格式如下{ registry-mirrors: [ https://your-mirror.example.com ] }改完后重启 Dockersudo systemctl restart docker要提醒的是镜像加速器解决的是“公共镜像拉取超时、速度慢”的问题不会改变镜像内容也不会绕过任何鉴权。生产环境如果对镜像可靠性要求高更推荐自建镜像仓库。Linux 安装 Docker 后如果服务起不来常见检查点是systemctl status docker和journalctl -u docker。如果日志里出现 iptables 相关错误检查net.ipv4.ip_forward是否开启、br_netfilter 模块是否加载。记住架构理解到位后你是顺着链路找问题而不是乱试命令。6. 常见问题与排查技巧实录6.1 Docker Desktop 闪退/虚拟化未启用这是 Windows 用户问得最多的问题。错误提示通常是 “Docker Desktop failed to start because virtualisation support wasnt detected”。原因几乎都出在两点BIOS 虚拟化开关没开或 Windows 的“虚拟机平台/WSL”功能没启用。排查顺序打开任务管理器 → 性能 → CPU看“虚拟化”是否显示“已启用”。如果显示未启用进 BIOS 开启 Intel VT-x 或 AMD-V。在“启用或关闭 Windows 功能”里勾选“适用于 Linux 的 Windows 子系统”和“虚拟机平台”。重启后执行wsl --status确认 WSL 版本为 2。最后再启动 Docker Desktop。还有一个常见报错是failed to connect to the docker api at npipe:////./pipe/docker-desktop-linux多半是 Docker Desktop 的后端虚拟机没起来或者服务还在启动中。不要反复重启客户端先看 WSL 状态再尝试在管理员 PowerShell 里执行wsl --shutdown后重新启动。6.2 Linux 下 Docker 服务启动失败Linux 上systemctl start docker失败先看 daemon 日志journalctl -u docker --no-pager -n 100常见三类原因网络相关iptables/br_netfilter 未加载、存储驱动相关文件系统不支持 overlay2、权限/配置相关daemon.json 语法错误。比如在 CentOS 7 上如果内核太老或文件系统是 XFS 但没开启 ftypeoverlay2 就会失败需要改用 vfs 驱动。排查时先用dockerd --debug前台启动日志会直接打到终端比 journalctl 更直观。看到哪个模块报错再针对性地查那个模块而不是直接卸载重装。6.3 连接 Docker daemon 权限错误permission denied while trying to connect to the Docker daemon socket是刚装完 Linux Docker 最经典的问题。原因是你当前用户不在 docker 组里。执行sudo usermod -aG docker $USER newgrp docker之后重新打开终端再试。生产环境对 docker 组要谨慎放权因为 docker 组里的人基本等同于拥有 root 权限——他们可以挂载宿主机目录到容器里再改文件。6.4 容器网络不通容器里ping 8.8.8.8不通但docker exec能进容器通常不是容器本身的问题。先看宿主机sysctl net.ipv4.ip_forward如果输出为 0容器 NAT 转发就不会生效。另外很多系统加固脚本会把 iptables FORWARD 链默认策略改成 DROP导致容器无法访问外网。可以把 FORWARD 链策略改回 ACCEPT或者给 Docker 相关规则放行。容器之间互相访问不通则要看是不是自定义 bridge 网络没建好或者服务只监听了 127.0.0.1。网络排查一定要有“分层”意识先容器内、再宿主机、再外部链路。6.5 镜像拉取失败与加速器配置镜像拉取超时最直接的办法是配置镜像加速器前面已经写过配置方法。如果配置后仍失败检查 daemon 是否真的加载了新配置docker info | grep -A 5 Registry Mirrors有些时候问题是镜像 tag 不存在或仓库名打错看完整错误信息不要看到 “timeout” 就只想到加速器。docker pull失败和docker run本地找不到镜像时的报错信息很像但前者会明确出现 pull 相关字样仔细看能省很多时间。6.6 容器退出码与 OOM 排查容器启动后立刻退出先看状态docker ps -a docker inspect container-id | grep -E ExitCode|OOMKilled|Error docker logs container-idExitCode 0通常意味着主进程正常退出ExitCode 137很可能是被 kill常见原因是 OOM 或手动 killOOMKilled: true说明容器内存超限被 cgroups 杀掉。遇到 OOM优先检查应用内存配置而不是无脑加-m上限。注意查看docker inspect里的State字段它能让你从架构角度判断“容器进程根本起不来”还是“容器起来了但被系统杀掉”。下面把六类高频问题整理成速查表问题典型现象排查方向常见解决Docker Desktop 无法启动提示 virtualisation support not detectedBIOS 虚拟化开关、Windows 功能BIOS 开启 VT-x/AMD-V启用 WSL2Docker 服务启动失败systemctl start 失败journalctl 日志、dockerd --debug加载 br_netfilter、开启 ip_forward权限错误permission denied on socket用户是否在 docker 组usermod -aG docker容器无外网ping 不通公网 IP宿主机 ip_forward、iptables FORWARD 链开启转发、调整 FORWARD 策略镜像拉取慢/超时pull 卡住或失败daemon.json 配置配置镜像加速器容器启动即退出ExitCode 非 0、OOMKilled trueinspect、logs调整内存限制、修复应用启动命令最后分享一个我自己的习惯排容器问题时我很少一上来就看 Docker 命令而是先ps -ef找出容器进程再lsns -p和cat /proc/PID/cgroup确认隔离状态最后才回到docker logs和docker inspect。这套“从内核到用户态”的顺序正是基于 docker-container-architecture 这条链路来的。你在实际操作里也可以试试把这套流程跑一遍比盲目搜报错信息靠谱得多。
返回列表