ARTICLE DETAIL

资讯详情

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

Docker镜像与容器核心操作:分层机制、数据持久化与排障命令详解

Docker镜像与容器核心操作:分层机制、数据持久化与排障命令详解 1. 镜像操作前先弄明白“镜像分层”这一底层机制很多人学 Docker 到第二篇就卡住了pull、run、exec、logs命令能背但真遇到“容器里改的文件去哪了”“镜像为什么删不掉”“mysql 一重启数据就没了”就彻底懵。原因很统一——镜像和容器这两个概念没想透。这篇既然是“镜像与容器核心操作命令”专题我就不客气地默认你会装、会启动 Docker直接拿这一章先把底层机制捋顺。1.1 镜像不是“安装包”容器也不是“解压结果”最常见的说法是“镜像像模板容器像实例”。这个类比不算错但特别容易误导。你买一台新电脑第一次装 Windows 就是“镜像刻录”装完系统改了壁纸、装了软件这台电脑的状态已经和镜像文件不一样了——这才是容器。真正的差别在于镜像只读容器多了一层可写层。你在容器里写的任何文件、装的任何软件都落在这一层薄薄的可写层上。一旦容器被删除docker rm这层东西就全部消失而你 pull 下来的镜像本体依然原封不动地躺在磁盘里。我把三者职责用表格拆开后面所有命令都会围绕这张表展开对象是否可写生命周期典型用途被删除后数据去向镜像只读拉取后长期存在模板、分发、版本管理删除镜像时清理容器可写创建后运行/停止/删除跑起来的进程环境先删容器才能删镜像数据卷可写独立于容器存在持久化数据、共享文件卷不随容器删除这张表我建议你打印出来贴在工位。很多初学者把数据直接写进容器可写层图了一时方便后面容器一重建全部归零那才是真正的“生产事故预习”。1.2 一层一层叠出来的镜像才是 pull 输出有“拉取层”的原因镜像另一个关键设计是分层存储。你执行docker pull nginx:1.27-alpine时看到的输出不是一整块文件下载而是“Pull complete”一行接一行每一行对应一个镜像层。这背后是联合文件系统技术各个镜像层被叠加成“看起来”是一个完整的文件系统。可这么设计图什么两个理由一是能复用你本地如果已经有一个基础镜像层再拉另一个基于相同基础层的镜像时相同的层直接复用不需要重新下载二是能省磁盘每个层都只保存“这个层相对上一层做了哪些改动”而不是每拉一个新镜像就完整存一份整个文件系统。所以我强烈建议你学会看docker history。比如想搞清楚nginx:1.27-alpine这个镜像到底由什么拼出来直接执行docker history nginx:1.27-alpine输出里会列出每一层的创建指令从基础镜像alpine到设置环境变量再到复制配置、暴露端口、设定启动命令。这些层当初就是由 Dockerfile 里的一行行指令生成的。一条指令生成一层镜像的“厚度”基本等于 Dockerfile 的行数密度。这也是我后面要讲“为什么不要写一长串 RUN 命令”的前置知识——层越多打包和拉取越慢不必要的中间层需要清理时才更麻烦。搞懂分层后你再回头看那句“容器是镜像的一个运行实例”会有新的理解镜像是叠好的多层结构容器只是在这堆层上方额外叠了一个可写层进程就跑在这个联合起来的文件系统里。2. 镜像增删查改管好本机镜像的四个核心命令镜像相关命令看起来就那么几个真正用得明白的人很少。主要因为大家默认把镜像当成“文件”处理而没意识到镜像是有 tag、有 digest、有继承关系、还会产生悬空态的特殊对象。2.1 pull 命令与 tag 机制latest 标签背后藏着最大的坑拉镜像的命令基本就一句docker pull nginx:1.27-alpine注意这里nginx是镜像仓库名1.27-alpine是 tag。nginx:latest当然也能拉但我几乎从不在生产环境用 latest原因很现实latest 是个“移动标签”镜像仓库维护者随时可以把 latest 指向新版本。你今天拉到的 latest 和明天拉到的 latest可能是完全不同的东西更麻烦的是集群里不同节点在“不同时间点”拉了 latest版本不一致的问题会直接变成一个说不清的故障。如果你只记住 tag 还不够更可靠的是记住 digest。每个镜像内容经过哈希计算会得到一个唯一的摘要值表示内容和那份镜像一一对应。这样你每次拉取都确保是同一份内容docker pull nginxsha256:2dc8c3e3c6cbe8d2394b5f6c5d5d3ec04b8f6b1f3d7e9c1d14a0b06c0a5e2bdigest 怎么查用docker images --digests就能看到每个镜像的 digest。值得提醒的是普通人日常开发用 tag 就好但对“部署必须可重复、出问题必须可回溯”的场景锁定 digest 是最稳妥的。这是从“能跑”走向“可靠部署”的一小步。另外关于拉取速度国内网络环境下第一次拉镜像经常慢到怀疑人生官方仓库的连通性也不稳定。我建议你在 Docker CLI 里配置 registry mirror也就是镜像加速服务。配置路径通常是 Docker 设置里的 Docker Engine 配置项加入类似下面的内容{ registry-mirrors: [ https://docker.mirrors.example.com ] }注意要用你所用云服务商提供的、合规的加速器地址。配置完重启 Docker再拉镜像速度通常会有明显改善。2.2 查镜像images、inspect、history 各干各的活docker images最常用但也最容易扫一眼就关掉。它能告诉你本地有哪些镜像、tag 是什么、镜像 ID、以及占用磁盘大小docker images --digests如果镜像特别多可以过滤出只想看的docker images | grep nginx docker images --filter danglingtrue第二种过滤命令里的danglingtrue指的是悬空镜像。什么是悬空镜像当你反复用同一个 tag 拉取新版本旧镜像虽然内容还在但已经没有 tag 指向它了就变成none:none。这些镜像没人主动引用却依然占着磁盘空间。所以看到一堆none不要慌这是镜像更新过程中的正常产物后面清理掉就行。docker image inspect是拿镜像底层元数据的命令。你想知道镜像的架构、操作系统、暴露端口、环境变量、入口命令它可以一次性全给出来。排查问题的时候特别有用比如容器起不来你想确认镜像里的CMD到底是什么直接看docker image inspect nginx:1.27-alpine输出是一个长 JSON建议配合jq用docker image inspect nginx:1.27-alpine | jq .[0].Config.Cmddocker history则用来追溯镜像层的来源。我前面说过每一个镜像层对应构建时的一条指令比如你拿到第三方镜像想知道它里面加了什么history能非常直观地告诉你更新依赖、装软件、改配置、设置启动命令一层层像“考古”一样展开。这也是判断“这个镜像靠不靠谱”的快速手段。一个镜像 history 里全是乱七八糟的RUN拼接哪怕它在仓库里下载量很高你也要保持警惕。2.3 删镜像rmi 与 image prune 的正确组合删除镜像的命令很简单docker rmi nginx:1.27-alpine但实际删除时最常遇到的报错是Error response from daemon: conflict: unable to remove repository reference nginx:1.27-alpine (must force) - container意思是已有容器在引用这个镜像必须先删容器再删镜像。我不建议你直接加-f强制删除那样容易把正在运行的容器底层镜像文件删掉造成更诡异的问题。正确做法是先看有哪些容器依赖这个镜像docker ps -a | grep nginx把相关容器停止并删除后再执行rmi。本地镜像攒多了之后单条删除太麻烦用批量清理命令docker image prune默认会清掉所有悬空镜像而保留有 tag 引用的镜像。想更激进一点把没有被容器使用的镜像全部干掉的docker image prune -a但注意-a会把“已经不存在容器引用”的镜像一口气清理掉。如果你有个镜像是特意留着备用但暂时没用就别用这个命令。如果你只想清理某段时间之前产生的悬空镜像加个 filterdocker image prune --filter until168h这里168h表示七天内没有被引用且已悬空超过七天的镜像。这套组合下来磁盘基本告别“莫名满了”的状态。3. 容器从创建到删除的全生命周期操作细节镜像只是原料真正干活的容器才是主角。一个容器从诞生到退休要经历好几个状态命令也远比“run 一下”多得多。这个生命周期掌握不了后面排障、清理、部署都会处处踩坑。3.1 run 实际上是 create、start、attach 三步组合很多人以为docker run nginx:1.27-alpine是个单一操作其实它背后是三步docker create创建容器、docker start启动容器、必要时再attach到容器终端。官方拆开的目的就是给你更细的控粒度想创建配置好但先不启动可以用 create已经创建的容器等时机到了再启动。所以run本质上是个“组合命令”。我们先看最常见的完整形态docker run -d --name web-server -p 8080:80 --restart unless-stopped nginx:1.27-alpine拆开解释-d后台运行不加这个你会在前台不断看到 nginx 访问日志终端一关容器就跟着退。--name web-server给容器起名字。不加的话 Docker 会随机给一个“prickly_benz”之类的滑稽名字排查时很难受。-p 8080:80把宿主机 8080 端口映射到容器内 80 端口。注意容器内有自己的网络隔离不映射的话宿主机访问不到容器里的 nginx。--restart unless-stopped容器退出时自动重启除非是你手动停的。这是生产环境非常常用的重启策略。还有一个初学者必踩的坑交互式命令要用-it。比如想临时跑一个 alpine 容器进去敲命令docker run -it alpine sh-i保持标准输入打开-t分配一个伪终端。两个参数必须成对出现少了-t你会发现 shell 没有交互提示符少了-i你敲命令输入不进去。一定要先记住“交互就 -it后台就 -d”这能省掉不少时间。3.2 stop、kill、pause、restart停止容器有不同的力度容器运行期间你常见的操作对象可以从docker ps看到。但要小心docker ps默认只会列出状态为 Up 的容器加了-a才能看到那些已经退出和刚创建但没启动的容器。docker ps -a输出里STATUS一列很关键有Up、Exited、Created、Paused、Restarting等状态。对状态的拿捏决定了你用哪条停止命令命令实际行为结束信号适用场景docker stop优雅停止先 SIGTERM宽限期后 SIGKILL正常下线服务docker kill立即停止直接 SIGKILL进程卡死、无响应docker pause冻结进程暂停容器内所有进程临时挂起不结束容器docker restart重启容器先 stop 再 start应用状态异常时尝试恢复举个直白的例子docker stop相当于你给进程发了“请交接完手头工作再下班”的通知处理完信号后才会退出docker kill相当于“马上断电”进程连清理机会都没有。生产环境里能用 stop 就别用 kill尤其数据库这类需要刷盘保存状态的服务直接 kill 很容易坏数据。docker pause平时容易被忽略但在做备份或排查时非常有用。比如你要对容器内文件打一个一致性快照直接执行docker pause mysql-container容器进程全部冻结住文件不再变化打完快照再docker unpause mysql-container恢复。不比 stop/start 优雅得多。3.3 删除容器rm、prune 以及批量清理想删容器要分两步先确认已经停止再执行docker rm web-server如果容器还在运行直接删会报错提示你容器未停止。这时候多一个-f可以强制停止并删除不过我同样建议少用——强制删除会把 stop 该做的 SIGTERM 省掉对数据库之类服务不友好。一个实用技巧是删容器和停容器连在一起docker rm -f $(docker ps -aq)ps -aq拿到所有容器 ID包括停止的rm -f强删。这是开发环境清理专用生产环境慎用。当你有一堆已经停止的僵尸容器躺在那儿时最省事的docker container prune它会问你是否确认删除所有停止状态的容器。加上-f跳过确认。这里有个细节值得说明prune 默认只清除停止的容器运行中的容器不会被误杀所以相对安全。4. 进容器排障与日志定位线上问题不再靠重启解决容器最让人头疼的时刻莫过于启动后“秒退”或者服务假死。初学者第一反应是删掉重建但成熟的排障思路应该是先进容器的世界看进程、看日志、看资源占用。这一节我分享的正是日常调试工具集。4.1 exec 与 attach 两种“进容器”方式差距很大现在有一个正在后台运行的容器我想进去查看或者操作它大多数人第一句就是docker execdocker exec -it web-server sh这条命令的含义是在正在运行的容器web-server里额外启动一个新进程sh并把这个进程绑定到当前终端。因为它是“额外”启动的进程所以你在里面敲什么命令都不会影响容器的主进程退出这个 shell 也不会导致容器停止。docker attach则完全不同它不是开新进程而是把当前终端“贴”到容器主进程的标准输入、输出和错误流上。你执行的docker attach web-server就像直接端坐在这个容器的主进程面前看到的是主进程打出的日志按CtrlC发送给主进程的 SIGINT 信号有可能直接终止容器主进程容器立刻退出。所以日常排障我只会用exec。attach这种“绑定主进程”的用法更适合在主进程本身有交互式终端、需要手动操作它的场景里。你如果分不清就记住一句话exec 是“进去新开一个 shell”attach 是“连上正在运行的主进程”。4.2 “启动即退出”排查链路从 logs 找 root cause最典型的问题场景你docker run了一个 mysql 容器结果容器刚启动就变成 Exited(1)。此时很多人立刻删掉容器重新 run同一条命令跑十遍结果还是失败。真正高效的排查链路是这么走的第一步先看容器当前状态docker ps -a确认容器确实没起来拿到容器 ID 或名字。第二步看日志docker logs mysql-container这一步八成能直接给你答案。比如 mysql 启动失败的日志里通常会出现“Cant start server: Bind on TCP/IP port... Permission denied”或者“Initialize different directory”这些信息能快速定位是端口冲突还是数据目录权限不对。我见过太多人因为 mysql 失败就反复重装最后用docker logs看一眼发现不过是宿主机 3306 端口被占了。第三步如果日志提示了但问题不好判断才考虑进容器或查资源。比如它提示内存不足那就跑一句docker stats去看整个主机的容器内存占用情况。这里要特别强调一个基本功容器内没有 systemd。你没法用systemctl status那一套去看服务为什么没启动日志机制也完全不同。容器里的主进程就是“前台进程”日志全部走标准输出docker logs是唯一的日志入口。这个思维转过来容器排障效率立刻上一个台阶。4.3 实时状态监控stats、top、events 怎么搭容器运行起来之后想看它是否健康、资源占用多少我有三件套推荐。docker logs加参数可以跟踪输出docker logs -f --tail 200 web-server-f表示持续输出--tail指从头文件末尾开始算多少条。开发阶段调试 nginx 或应用这个命令我把当“顶替 tail -f”用。docker top查看容器内实际运行的进程docker top web-server它展示的是宿主机视角下属于这个容器 PID 命名空间内的进程列表。比如 nginx 容器里到底起了一个 master 进程还是几个 worker这里一目了然也能快速发现容器里是不是被塞进了额外的进程。docker stats是资源监控利器docker stats输出是实时刷新的表格显示每个容器的 CPU 百分比、内存占用和限制、网络 IO、磁盘 IO。没有它你很难判断某个容器的内存为什么会持续涨。docker events则是看容器生命周期的“事件流”docker events --filter containerweb-server容器创建、启动、停止、销毁、被 OOM 杀掉都会在这里留下一条事件记录。我之前排查过一台机器上容器频繁消失的问题单纯看ps和logs都看不出结构最后是docker events里连续几行 OOM kill 事件把真凶暴露了——内存限制设得太小容器运行几分钟就被系统杀掉。5. 容器资源限制与数据持久化部署真正可用服务的分水岭前四节的命令能让你把容器“跑起来”并且“排好障”。但真实部署一个能用很久的服务还差两块硬骨头资源隔离的边界、数据持久化的策略。这也是网上大量“容器跑一天就挂”“容器 rm 之后数据全没”的根源。5.1 资源限制内存、CPU 及容器资源隔离的边界“容器资源隔离”不是一句口号而是靠内核能力实现的体现在 Docker 参数上就是内存和 CPU 的限制。一个完全没做资源限制的容器理论上可以吃光宿主机的全部内存和 CPU甚至拖垮宿主机上的其他服务。这不是危言耸听线上真见过跑了个 Java 容器把整台机器吃爆的。给容器限制内存docker run -d --name app -m 512m --memory-swap 1g nginx:1.27-alpine-m 512m设定内存上限 512 MB--memory-swap 1g设定内存加交换分区总共 1 GB。限制 CPUdocker run -d --name app --cpus 1.5 nginx:1.27-alpine表示容器最多使用 1.5 个 CPU 核。也可以精确配额docker run -d --name app --cpu-shares 1024 nginx:1.27-alpine--cpu-shares默认值是 1024表示相对权重数值越大在这个宿主机上抢占 CPU 的优先级越高。多容器共处一台机器时这个参数比--cpus更灵活。设置限制之后docker stats能看到限制是否生效。内存限制有一个副作用要提前知道容器内存超过限制时系统可能直接给容器发 OOM kill容器就这样“莫名”退出。5.2 持久化容器可以被随时销毁但数据不行容器这个概念的天然属性就是“可以被随时销毁重建”。如果你把数据写在容器可写层里容器删了数据跟着没了。生产环境的 mysql、redis、业务文件绝不能依赖容器可写层。两种主流持久化方式bind mount 和 volume。bind mount 直接将宿主机某个目录映射进容器docker run -d --name mysql-server -v /my/own/datadir:/var/lib/mysql -e MYSQL_ROOT_PASSWORD123456 mysql:8.0/my/own/datadir是宿主机目录/var/lib/mysql是容器内 mysql 的数据目录。目录映射的好处是你能直接用宿主机文件工具管理但坏处也很明显宿主机目录的权限和路径需要你完全把控跨机器迁移时配置里的路径到处要改。我日常更推荐 volume它由 Docker 管理不依赖具体路径docker volume create mysql-data docker run -d --name mysql-server -v mysql-data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD123456 mysql:8.0volume 的优势在于位置由 Docker 统一管理备份和迁移有专门命令权限处理更安全而且它独立于容器生命周期——删掉容器、重建容器挂到同一个 volume 上数据照样在。这里统计一下刚踩过的典型坑很多人为了让“数据持久化”把宿主机的一个普通目录映射进容器却忽略了目录权限。比如上面 mysql 容器宿主机目录如果权限是 755 且属主不是 mysql 对应的 uid容器内 mysqld 就可能没有写入权限直接启动失败。遇到这个情况先别想着重装看一下docker logs里的权限报错处理宿主目录权限即可。5.3 docker cp 只能救急不能当网盘用还有一个高频命令docker cp用来在宿主机和容器之间拷贝文件docker cp ./backup.sql mysql-server:/tmp/backup.sql把容器里的文件拷到宿主机docker cp mysql-server:/var/log/mysql/error.log ./error.log它的使用前提是“容器还在”。我建议只把它当救急工具比如临时调个配置、拉出一个日志不应作为日常数据交换方案。日常数据的交换和持久化一定要用 volume 或者 bind mount 提前设计好。容器一旦删除或重建docker cp 这条路立即失效而 volume 从来不会因此受影响。还有一个小提醒docker cp不支持跨宿主机拷贝也不支持同步它只是单个文件或目录的“一次性搬运”。想让容器和数据文件在机器之间流转正确的思路是构建镜像、推送镜像、再在新机器上拉取运行数据则用 volume 或专门的存储方案解决。把 cp 当网盘用早晚会在数据同步上吃亏。说实话这套内容我自己也是踩了无数坑才理顺的。最初我也习惯“容器坏了就删掉重开”后来被 mysql 数据清空狠狠教育过才开始认真研究镜像分层和 volume 持久化。现在我把最常用的经验总结成一句话送给你镜像负责定义“怎么运行”容器负责承担“当前运行”数据卷负责留住“不能丢的东西”日志则负责告诉你“到底发生了什么”。这四个角色分清楚了Docker 的核心操作命令自然会各就各位。
返回列表