Docker架构拆解:从docker run到镜像分层与内核隔离 敲下docker run之后看着终端吐出一串容器 ID几乎所有人都经历过那个能用但心里没底的阶段。平时跑得好好的一旦碰上容器起不来、端口映射不生效、镜像把磁盘吃到报警就只能靠搜索引擎和反复重试硬扛。问题的根源往往不在命令记不熟而在于对 Docker 基本架构没有建立起一张清晰的图谁在调用谁、请求从客户端发出去之后经过哪几个进程、镜像的分层是怎么落盘的、容器和宿主机内核之间到底是什么关系。这套架构本身并不玄乎它本质上把 Linux 内核里几项平时很少被直接摆弄的能力——namespace 负责隔离、cgroups 负责资源约束、联合文件系统负责分层——包装成了一套顺手好用的接口外面再套一个常驻的守护进程和一个命令行客户端。这篇是专题第一篇只讲架构本身组件怎么分层、请求怎么流转、一次运行之后宿主机上到底多了哪些东西。既适合已经会敲命令、想把黑盒拆开看一眼的人也适合刚接触容器、想从第一天就把概念理顺的人。1. 先把这张架构图读懂Docker 由哪些部件拼起来1.1 为什么很多人学 Docker 会卡在会用但不懂我见过太多人的学习路径是这样的装好环境照着教程敲docker run、docker ps、docker exec一周之内常用命令就都熟了然后学习就停在这里。这套流程在顺利的时候毫无破绽可一旦出问题麻烦就来了。容器启动后立刻退出你会去猜是不是命令写错了宿主机磁盘突然少了二十个 G你不知道该删哪个目录两个容器之间网络不通你只能一个个参数往回试。这种参数试错法效率极低因为你面对的是一个由四五个进程串起来的调用链问题可能出现在链条上的任何一环而你在链条外面盲猜。懂架构带来的第一个直接好处是你知道了排查的入口在哪。docker ps显示容器 Exited你要看的是容器主进程的退出码和日志docker info报存储驱动异常你要看的是overlay2目录和挂载情况容器里ping不通外部你要区分是容器网络、宿主机转发还是上游的问题。第二个好处是资源估算有依据知道镜像是分层共享的、容器的可写层是独立的你就能理解为什么拉十个基于同一个基础镜像的容器磁盘占用远小于十个完整副本。第三个好处是选型判断什么时候该用容器、什么时候老老实实上虚拟机答案藏在内核共享这一条里而这条正是架构的根基。所以我不建议一上来就背命令。先把这张图在脑子里画出来后面所有命令都只是在这张图上的某个节点做操作学起来会快得多也扎实得多。1.2 从客户端到内核四层调用链拆解把 Docker 拆开看它是一条从用户终端一直延伸到内核特性的链路中间隔着好几个进程。很多人以为docker命令就是 Docker其实它只是一个瘦客户端真正的活儿是后面几个进程干的。层级组件主要职责是否常驻第一层命令行客户端解析参数、把操作翻译成接口请求发给守护进程否命令结束即退出第二层守护进程接收请求、管理镜像与容器元数据、分配网络和存储、对外暴露接口是随服务启动第三层容器运行时管理器拉取镜像层、管理容器生命周期、把运行任务派发给底层运行时是第四层底层运行时与内核创建命名空间、配置资源限制、挂载根文件系统、执行容器主进程否按需拉起这张表是整篇文章的骨架后面所有内容都是往里填细节。这里先强调一个经常被误解的点命令行客户端和守护进程之间不是简单的本地调用而是通过一套接口协议通信。这个设计意味着客户端可以跑在完全不同的机器上通过配置环境变量把请求指向远程守护进程这也是很多人在本地调试远程环境时用到的能力。同样因为这条链路是网络通信所以守护进程的监听方式和权限配置会直接影响你能不能连上这一点在后面讲排查的时候还会提到。另一件值得先说清楚的事是第三层和第四层的关系。早期版本的守护进程里直接内置了容器执行逻辑后来这部分被剥离成一个独立的运行时管理器守护进程通过接口把活儿派给它它再去调用更低层的运行时。这么拆的目的是让管理和执行解耦上层可以专注镜像分发、容器编排、资源调度这些偏业务的事下层专注把进程真正跑起来这件事。这套分层也带来了一个很实际的好处——下层运行时遵循统一的容器运行时规范理论上换一个符合规范的实现也能跑不必绑死在某一个上面。1.3 容器和虚拟机的边界到底在哪理解架构绕不开这个对比因为它决定了容器的能力上限。虚拟机是通过虚拟化技术模拟出一整套硬件然后在上面装一个完整的操作系统内核应用跑在这个独立内核之上。容器没有自己的内核容器里的进程和宿主机上的普通进程共用同一个内核只是被命名空间隔在了不同的视图里、被资源控制限制在了不同的配额里。这个差别带来的后果是很具体的。启动速度上虚拟机要经历引导加载、内核初始化、系统服务启动这一长串流程容器只需要创建几个命名空间、设置好配额、切换根目录然后执行进程量级差得很明显。资源开销上虚拟机每个实例都有一份完整的内核内存占用和系统服务开销容器几乎没有额外的基础开销因为内核是共享的。但代价也在这里隔离强度不一样。容器内的进程在内核眼里依然是普通进程一旦出现内核层面的漏洞边界就可能被突破而且容器不能跑跟宿主机内核差异很大的系统比如在一种内核上直接跑另一套内核体系的容器是做不到的。我自己总结的一句话是容器解决的是应用交付和运行环境一致性的问题虚拟机解决的是强隔离和多系统并存的问题。这两个目标不一样所以不存在谁替代谁很多生产环境里实际上是两种技术叠着用的。理解了这条边界你就明白为什么有些场景一上手就感觉不对劲——比如想在一个容器里跑需要加载特定内核模块的东西或者想跑一套完全不同体系的系统那本来就不是容器擅长的事。2. 镜像、容器、仓库三个词撑起整套模型2.1 镜像是只读的层叠积木镜像这个词容易让人联想到一个完整的、不可分割的文件包实际恰恰相反镜像是分层的而且每一层都是只读的。构建过程中每执行一步写操作就会产生一个新层新层记录的是相对上一层的变化量而不是完整快照。底下的基础层可能在几十个镜像之间共享上层只存放你这一步新增或修改的东西。这种设计的价值在磁盘和传输上体现得最明显。假设你本地已经有一份基础镜像再拉一个基于同一个基础的其他镜像时公共的底层已经存在只需要下载差异部分。这也是为什么很多镜像看起来体积庞大但实际拉取速度并不慢——公共层被复用掉了。用docker history可以直观看到每个镜像由哪些层构成、每层大致多大我经常用它来判断一个镜像是不是被喂得太胖。理解分层的另一个价值在于理解构建缓存。构建时如果某一层的内容没有变化这一层以及它下面的所有层都可以直接复用缓存不用重新执行。这一条直接决定了写构建文件的正确姿势变动频率低的内容放前面变动频繁的放后面。很多人抱怨构建慢其实是把拷贝源代码这种每次都变动的操作放在了安装依赖之前导致依赖安装缓存永远失效。注意镜像层一旦生成就不可修改。所谓修改镜像里的文件其实是在上层新增记录底层始终保持原样这个机制就是后面讲写时复制的根基。2.2 容器是加了可写层的活进程容器不是镜像的解压副本它的真实结构是若干只读镜像层 一个薄薄的可写层再加上一组运行时配置命名空间、资源配额、网络设置、挂载点。这个可写层在容器启动时才创建容器被删除时一起销毁。这就解释了一堆日常困惑。为什么容器里装了个软件、重启容器之后还在因为重启不等于删除可写层还在。为什么删掉容器重建之后装的东西全没了因为可写层跟着容器一起被销毁了新容器拿到的是一个全新的空可写层。所以在容器里手动改配置、手动装依赖本质上都是在临时层上做实验一旦容器生命周期结束这些改动就烟消云散了。真要持久化只有两条路把改动固化到镜像里改构建文件重新构建或者把需要保留的数据放到挂载的卷上。这里还有个反直觉的细节可写层不是整份拷贝。当容器要修改一个位于只读层的文件时系统并不会把整个层复制一份而是只把这一个文件复制到可写层然后对副本做修改。这个机制叫写时复制。它的好处是容器启动几乎瞬间完成、磁盘占用极小代价是频繁大量写小文件时会有额外的复制开销。提示写密集型应用数据库、日志量大的服务不要把数据目录留在容器可写层一是性能有损耗二是数据会随容器删除一起丢。2.3 仓库是层片的物流中心仓库负责存储和分发镜像但它的组织方式跟普通文件服务器不一样它按层来存。一次拉取的实际过程大致是先拿到描述这个镜像的清单清单里列出了它包含哪些层以及每层的唯一摘要值然后再按需逐层下载下载完成后用摘要值做校验确认内容完整。这个摘要校验机制很关键。镜像标识除了可读的名字和标签还有一个基于内容算出来的摘要。标签是可以被重新指向的同一个标签在不同时间可能对应不同内容而摘要一旦确定就对应唯一内容。所以在需要严格保证每次部署的镜像完全一致的场景里用摘要而不是标签来指定镜像是更稳妥的做法。这个细节在生产环境里很重要我见过因为标签被覆盖导致不同机器拉到的镜像不一致、排查了半天的案例。拉取时还有一点值得注意多个层是可以并行下载的所以你会在终端看到好几条进度条同时推进。层是按需下载的如果你本地已经有某一层就不会重复下载。这也意味着清缓存要谨慎——docker system prune这类命令会删掉未被使用的镜像和层删完下次再拉就得重新走网络。3. 一次 docker run 的完整旅程3.1 客户端先做了什么从你按下回车那一刻开始算。客户端要做的第一件事是解析参数把-p、-v、-e、--memory这些选项整理成结构化的请求体同时判断要连的是本地守护进程还是远程地址。判断依据来自环境变量和默认配置如果指向的是本地通常走的是本地套接字文件。接下来客户端把请求发给守护进程然后就开始等回包。这里有个容易被忽略的点默认情况下运行命令加不加后台参数行为差别很大。不加后台参数时客户端会把容器的标准输入输出接管过来你的终端实际上被挂到了容器主进程的输出管道上所以你才能实时看到日志。加了后台参数之后客户端拿到容器标识就退出了容器继续在后台跑。这也解释了另一个常见现象前台运行容器时按组合键退出容器也跟着停了。因为终端连接断开容器的标准输入被关闭主进程读到结束信号就退出了。想让容器在后台长期跑除了加后台参数还要保证容器里有一个持续运行的前台进程这是很多人写构建文件时踩的坑——把服务配成后台守护模式启动结果容器一启动就退出因为主进程认为没事可做就结束了。3.2 守护进程的调度与元数据落地守护进程收到请求后第一步是确认镜像在不在本地。不在就先触发拉取流程这是一个可能耗时较长的阶段很多人以为命令卡住了其实是在下载。镜像就位之后守护进程开始准备运行环境这一步做的事情相当多而且顺序有讲究。先分配容器标识并写入元数据这些信息落在守护进程的数据目录下包括容器的配置文件、主机的解析文件、以及后面会用到的各种状态记录。然后创建可写层也就是在存储驱动里为这个容器准备一个独立的上层目录。接着处理网络如果用了默认的桥接模式就要在虚拟网桥上分配地址、配置容器内部的网络接口、写入转发规则。如果指定了端口映射还要在宿主机上登记映射关系。最后处理挂载把指定的卷或者宿主机目录挂到容器的目标路径上。这些准备工作全部完成后守护进程才把启动这个容器的任务交给下一层。这里有个设计上很聪明的地方容器的运行状态和守护进程本身是解耦的。也就是说守护进程重启正在运行的容器不会跟着死掉。这一点在升级守护进程或者调整配置的时候非常重要也是很多人第一次重启服务时惊讶地发现容器还活着的原因。3.3 底层运行时与内核能力真正上场任务传到底层运行时之后画风就从管理切换到执行了。底层运行时的职责非常聚焦按照规范把进程创建出来。具体动作包括准备容器的根文件系统视图也就是把若干只读层和一个可写层组合成一个完整的目录树、设置挂载点、创建各个维度的命名空间、写入资源配额、切换根目录、降权到指定用户最后执行容器的主进程。命名空间是隔离的核心。不同维度的命名空间负责不同的隔离面进程维度让容器内看不到宿主机上的其他进程网络维度给容器一套独立的网卡和地址挂载维度让容器有一套独立的目录树主机名与域维度让容器有自己的主机名进程间通信维度隔离共享内存和消息队列用户维度做身份映射。这几个维度组合起来容器内看出去就像一台独立的小机器但实际上跑的还是宿主机内核上的普通进程。资源限制靠的是另一套内核机制负责给进程组设定配额。能限制的维度包括内存上限、处理器时间片权重、磁盘读写速率、进程数量上限等。这里有个实操细节值得强调内存上限设置之后容器内进程超出限制会被直接终止而不是像有些系统那样先触发回收再慢慢处理。所以给容器设内存限制时要留出余量尤其是带缓存的中间件设得太紧会出现莫名其妙的进程退出。3.4 启动之后的常驻角色与退出回收容器主进程跑起来之后链路上还有一个不起眼但很关键的角色中间层进程。它的作用是承上启下把容器进程和上层管理器隔开。这个设计解决了一个实际问题——上层管理器重启或者升级的时候容器进程不会受到牵连。你可以把中间层理解成给每个容器配的一个看护人它负责转发输入输出、上报退出状态、在容器主进程退出后做收尾。容器退出时会经历一轮回收。主进程退出中间层捕获退出码并上报守护进程更新容器状态并记录下来。这时容器进入已停止状态但可写层、网络配置、元数据都还在所以还能查看日志、还能重启。只有执行删除操作可写层和网络资源才会被真正释放。所以磁盘被容器占满时光停容器是没用的得把不再需要的容器删掉。提示容器退出码是最重要的排查线索之一。进程被强制终止通常对应一个特定的退出码正常退出是零主动终止是另一个值被内存限制干掉又是另一种表现。先看退出码再看日志顺序别搞反。4. 动手验证把架构从概念变成看得见的东西4.1 环境准备与版本自查理论讲完接下来最有效的学习方式是自己动手把这条链路摸一遍。准备一台能装容器的机器云主机、本地虚拟机都行注意内核版本别太老很多新特性依赖较新的内核。装好之后第一件事不是跑容器而是看版本。docker version这个命令的输出信息量很大值得逐段看。它会分成客户端和服务端两大块如果只看到客户端部分、服务端部分报连接错误说明客户端连不上守护进程这时候要检查服务是否在跑、当前用户有没有权限。服务端部分会列出一串版本信息包括引擎版本、运行时管理器版本、底层运行时版本还有初始化进程的版本。这一串恰好就是前面讲的分层结构在版本号上的体现——引擎、管理器、运行时各是各的可以分别升级。docker info这个命令输出的是运行环境的全貌我重点关注几项。存储驱动那一行告诉你用的是哪种分层文件系统主流是overlay2。资源控制驱动那一行告诉你配额是怎么接管的现在多数是交给系统服务管理器。资源控制版本那行区分新旧两套机制目录结构完全不同。内核版本和操作系统那几行则决定了你能用哪些特性。这些信息在排查问题时是必看的尤其换了一台机器之后第一件事就是先看这几行。4.2 用进程视角看容器它真的只是一个进程前面反复强调容器就是普通进程这一点可以用最朴素的方式验证。先跑一个长期运行的容器docker run -d --name demo-test busybox sleep 3600然后在宿主机上看进程树ps -ef | grep -E dockerd|containerd|shim|sleep 3600 | grep -v grep你会看到一条清晰的父子链条最上层是守护进程下面是运行时管理器再下面是容器的中间层进程最下面那个sleep 3600就是容器的主进程它的父进程正是中间层进程。这一刻容器就是被特殊配置过的进程这句话从概念变成了看得到的事实。再往下挖一层看这个进程在各个隔离维度上的归属PID$(docker inspect -f {{.State.Pid}} demo-test) ls -l /proc/$PID/ns/ ls -l /proc/1/ns/对比两组输出里的 inode 编号你会发现容器进程在进程、网络、挂载、主机名等多个维度上都和宿主机的初始进程不同说明它处在一套独立的视图里。而在用户维度上可能相同也可能不同取决于启动时有没有开启用户映射。这一步做完你就把抽象的隔离变成了可以眼睛看的具体证明。还有一个可以用来观察的细节在容器里执行ps你会发现自己就是编号为 1 的进程看不到宿主机上其他任何东西。这不是因为它真的占了开机第一号进程的位置而是因为在那个独立的进程视图里它就是第一个。这个错觉恰恰说明了命名空间的作用——它给进程提供了一套自洽的、只属于自己的编号体系。4.3 验证资源约束配额不是说说而已跑一个带内存限制的容器docker run -d --name demo-mem --memory256m busybox sleep 3600先确认容器里的感知docker exec demo-mem cat /sys/fs/cgroup/memory.max再看宿主机上对应的控制文件路径通常形如/sys/fs/cgroup/system.slice/docker-容器标识.scope/里面的各项文件对应不同维度的限制。老版本内核用的是另一套目录结构路径和文件名都不一样所以看之前先用docker info确认版本。用户在容器里看到的限制值和宿主机上控制文件里的值是一致的这点很关键——它不是靠容器内的工具模拟出来的而是内核真的在按这个数值执行。可以再进一步给容器写一个大内存程序观察它被强制性终止时容器状态的变化和退出码。这种亲手触发一次的体验比看十篇文档都记得牢。注意验证资源限制会产生真实的资源消耗和进程终止别在生产环境上做。另外有些系统的内存控制参数还有更细的维度比如交换空间的上限默认值可能会影响结果判断。4.4 验证分层与写时复制这一节是我认为最有价值的一次验证因为它能彻底改变你对镜像的理解方式。先启动一个容器docker run -d --name demo-layer busybox sleep 3600 docker inspect -f {{.GraphDriver.Data.MergedDir}} demo-layer docker inspect -f {{.GraphDriver.Data.UpperDir}} demo-layer第一个路径是容器最终看到的合并视图第二个路径是那层可写层的真实位置。现在做两件事一是在容器内新建一个文件二是在容器内修改一个原本就存在的文件docker exec demo-layer sh -c echo hello /newfile.txt docker exec demo-layer sh -c echo changed /etc/hostname然后去可写层目录里看sudo ls -la $UPPERDIR sudo find $UPPERDIR -name *.txt -o -name hostname你会发现新文件直接出现在可写层里而那个被修改的旧文件也出现在了可写层里——这就是写时复制的现场证据修改一个只读层中的文件时系统先把这个文件复制到可写层然后对副本动手底层始终保持原样。再确认一下挂载关系mount | grep overlay输出里能看到一个挂载项的选项部分列出了若干下层目录、一个上层目录、一个工作目录。那一串下层目录就是镜像的各层从下往上叠最上面那层是容器的可写层。这个输出基本就是镜像分层 容器可写层这句话在系统层面的完整呈现我第一次看到的时候确实有种豁然开朗的感觉。最后一步删掉容器再去看那个路径docker rm -f demo-layer可写层连同容器里做的一切改动一起消失了。这个演示把容器是一次性的这句话落实到了文件系统层面从此以后在容器里手动改配置这件事你会做得非常谨慎。5. 存储与网络架构里最容易踩坑的两块5.1 分层存储与数据卷的本质区别存储这块的困惑大多来自一个误解把容器的可写层当成持久化的磁盘用。前面对比过可写层跟着容器生命周期走删了容器就没了。真正的持久化方案是数据卷和宿主机目录挂载它们的实现路径跟可写层完全不是一回事。数据卷由守护进程统一管理通常落在数据目录下的一块独立区域它被直接挂到容器的目标路径上不参与分层也不受写时复制影响。宿主机目录挂载更直接把宿主机上的一个路径映射进容器容器里的写入直接落到宿主机磁盘上。两种方式各有取舍卷的管理更省心、跨平台一致性更好宿主机目录挂载对文件权限和路径的理解更直观、便于和宿主机上的其他工具协作。存储方式数据位置生命周期适用场景容器可写层随容器创建的上层目录跟容器同生共死临时文件、可丢弃的中间产物数据卷由守护进程管理的独立区域独立于容器需显式清理数据库数据、需要跨容器共享的状态宿主机目录挂载宿主机指定路径完全由宿主机管理配置文件、日志落盘、与其他工具协作还有一个常见的坑在宿主机目录挂载时如果容器内进程的运行用户对宿主机路径没有写权限就会出现容器里明明能读、一写就报错的情况。根因不在容器而在于两边身份映射没有对齐。我的做法是提前把目标目录的归属和权限设置好或者在启动时显式指定运行用户别等到出了问题再猜。5.2 网络模型与端口映射的真实路径默认情况下容器接入的是守护进程维护的一个虚拟网络通过一个虚拟网桥与宿主机连通容器在这个网段里分到一个地址。容器之间在同一条虚拟网络上可以直接用地址互访宿主机也能通过这个地址访问到容器。这套结构落地下来是一组虚拟网卡、一个网桥设备和一批转发规则。端口映射是问得最多的部分。指定映射之后宿主机上的某个端口收到的流量会被转到容器的目标端口。这条路径实际上有两套机制同时存在一套是内核层面的地址转换规则负责把发往宿主机端口的包改写到容器地址另一套是宿主机上的一个代理进程它监听那个端口并把流量转发进去。多数情况下走的是前一套代理进程作为补充存在。理解这一点有助于排查映射不生效时先确认规则有没有被写进去再确认是否有其他程序占了同一个端口最后才怀疑容器内部服务。docker run -d --name demo-net -p 18080:80 busybox sleep 3600 sudo iptables -t nat -L -n | grep 18080 ps -ef | grep docker-proxy | grep -v grep另外还有几个容易忽略的点。容器内部的服务必须监听在所有地址上而不是只监听本地回环否则外部转发进来的流量会被拒绝这在配置中间件和应用服务器的时候特别常见。容器之间的互访如果依赖容器名解析需要它们处在同一条自定义网络上默认网络里默认的解析行为不同。还有一点是关于端口冲突的宿主机端口被占用时容器会直接启动失败报错信息里会明确指出是绑定失败别去容器日志里找原因那里什么都没有。6. 常见问题与排查实录6.1 一张问题速查表架构清楚了排查就有了方向。下面这张表是我自己整理的高频问题清单按先看哪里排序能省掉大量盲目尝试。现象优先检查的位置常见根因处理方向命令报连接失败客户端与服务端的连接配置服务未启动、权限不足、地址配置错误检查服务状态、当前用户权限、连接地址容器启动后立刻退出容器主进程与退出码主进程被配成后台模式、缺少前台进程、启动参数错误查看日志与退出码改为前台运行主进程端口映射不生效宿主机转发规则与端口占用服务只监听回环、端口冲突、规则未写入改监听地址、换端口、核查规则磁盘占用持续增长镜像层、容器可写层、日志文件未清理的停止容器、悬空镜像、日志未轮转清理无用容器与镜像、配置日志轮转容器内改的文件重启后消失容器生命周期是否包含删除改动落在可写层容器被重新创建改为构建进镜像或用卷持久化内存限制下进程莫名被杀资源配额与退出码配额设得过紧、缓存占用被计入放宽配额、区分缓存与真实用量构建很慢、缓存不命中构建文件里的指令顺序高变动操作放在了前面调整顺序变动少的放前面这张表可以直接当检查清单用。实际排查的时候我一般遵循一个原则先确认问题发生在链路的哪一层再深入那一层看细节。比如容器起不来先看容器状态和退出码这是最上层的信息退出码指向资源问题再去看配额指向文件缺失再去看挂载。这样一条线索往下走比同时检查五六个地方高效得多。6.2 几条从踩坑里换来的心得第一条心得不要用标签来判断镜像是否一致。标签是可以被覆盖的同一个标签今天和明天可能指向不同内容。需要严格控制版本的时候用内容摘要来固定镜像部署脚本里也不要图省事只写标签。这个坑我吃过一次两台机器拉同名标签的镜像跑出来的行为不一样查了很久才发现是镜像不是同一份。第二条心得容器里不要做手工维护。今天进去改个配置明天进去装个工具看着方便实际上是在给未来埋雷。所有改动要么写进构建文件重新构建要么通过挂载把配置从外部注入要么用启动参数覆盖。这样容器才真正是可丢弃、可重建的出问题时直接把旧的删掉起一个新的而不是进去手忙脚乱地修。第三条心得先分清是容器问题还是应用问题。很多容器起不来最后发现是应用自己的配置错了容器只是忠实地把错误暴露出来。判断方法很简单把同样的启动命令在宿主机上直接跑一遍如果也失败那就不是容器的问题。这个动作只需要一分钟却能省掉大量在容器层面瞎折腾的时间。第四条心得关注资源限制和实际用量的差距。设了内存上限之后一定要在真实负载下观察一段时间的内存曲线看看峰值离上限还有多少余量。有些应用启动时占用很低跑一段时间之后缓存涨上来就被限制干掉了表现出来就是跑了几小时突然没了非常难查。第五条心得清理要养成习惯但也要谨慎。停止的容器、悬空镜像、构建缓存都会占用磁盘定期清理是必要的。但清理命令的杀伤力不小特别是带全部清理参数的那种一不小心把还要用的镜像删了重新拉取得等半天。我的做法是先列出要清理的对象看一眼确认没问题再执行删除而不是无脑上强度最高的那一条。专题一到这里架构的骨架基本立起来了客户端负责表达意图守护进程负责组织资源运行时负责把进程真正跑起来内核用隔离和配额把这些进程关进各自的笼子里。存储和网络是这套架构落地时接触面最广的两块也是最容易出问题的地方所以单独拎出来讲了一节。如果后面还要往下走我建议的路径是先去看构建过程把分层和缓存吃透再去看多容器之间的网络关系那时候你会发现所有新的概念其实都挂在这张架构图上只是长出了新的枝叶。我个人的习惯是每换一台新机器部署环境第一件事不是急着跑业务容器而是先用docker version和docker info把这几行信息看一遍确认存储驱动、资源控制驱动、内核版本都符合预期。这个动作看起来多余但正是它帮我提前发现了不止一次的环境差异问题——有的机器存储驱动不是默认的那一种有的资源控制还是旧版本这些问题在业务跑起来之后才暴露排查成本会高得多。花两分钟做这件事比事后花两个小时定位要划算。