ARTICLE DETAIL

资讯详情

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

深入理解docker diff:容器文件系统变更排查实战

深入理解docker diff:容器文件系统变更排查实战 你一定遇到过这种情况监控报警说容器磁盘占用涨了或者线上服务莫名其妙多了个文件再或者镜像打好后总觉得哪里不对。这时候最想干的一件事就是掀开容器看一眼里面到底被改了什么。docker diff就是干这个用的。它能把容器相对镜像的文件差异全部列出来新增了什么、删除了什么、改了什么一目了然。这篇文章我会从底层的文件系统原理讲起再给出一线排查时常用的操作步骤和踩坑记录覆盖运行中容器、已停止容器、Dockerfile 构建、安全审计等场景。不管你是刚摸 Docker 没多久还是已经被容器运维折腾过一阵子这轮读完基本就能把docker diff用透。1. 先搞懂 docker diff 在看什么容器文件系统变化的底层逻辑很多人第一次跑docker diff会觉得很神奇我明明没有做任何快照或者备份操作Docker 凭什么知道容器里哪些文件不一样了这背后其实是镜像分层和容器可写层的设计在起作用搞清楚这套机制docker diff的输出你才能真正读懂。1.1 镜像层和容器层为什么 diff 能成立Docker 镜像不是一个大整文件而是由很多只读层叠加出来的。你可以把它想成一本不断往上贴便签的书底层便签是基础系统往上每一条 RUN、COPY、ADD 指令都会形成新的便签层。启动容器时Docker 会在这些只读便签的顶上再贴一层完全可写的空白便签这一层叫容器层也叫可写层。你往容器里写文件、改配置、装软件所有变化都只发生在这一层可写层上底层镜像完全不动。这就意味着 Docker 做 diff 时不需要去比对“整个文件系统”它只需要扫描这个可写层跟“最接近的镜像层”之间的差异。底层机制用的是存储驱动的能力比如 overlay2 会维护一个白名单和变更记录Docker 再把这些变更按照路径和类型整理出来就是你看到的那一列输出。理解了这个设计有几个关键结论就顺理成章了。第一个结论是容器被删除后可写层也就没了所有未提交的修改全部消失。第二个结论是docker commit把一个容器保存成新镜像时本质上就是把可写层的变化打包成新的镜像层。第三个结论是docker diff能在任意时刻帮你预览这次 commit 到底会固化哪些变化。1.2 输出里的 A / C / D 分别代表什么docker diff的输出格式非常简洁每行一个路径前面带一个状态标记。这三个标记分别是 A、C、D全称对应 Added、Changed、Deleted。通俗点说A 表示这个文件或目录是容器里新增的镜像里原本没有。C 表示这个文件或目录的属性或内容发生了变化。D 表示镜像里原本存在的文件或目录在容器里被删除了。举个例子假设我跑了个基于nginx:alpine的容器进去改了/etc/nginx/conf.d/default.conf然后又往/usr/share/nginx/html下新增了一个info.html顺手删掉了/etc/nginx/conf.d/example.conf。那么docker diff的结果大概率长这样C /etc/nginx/conf.d/default.conf A /usr/share/nginx/html/info.html D /etc/nginx/conf.d/example.conf很多人在刚开始看输出时有个误解如果看到某个目录状态是 C就以为目录下所有文件都变了。其实不是。当 diff 输出显示某个目录本身是 C 时通常只是这个目录的元数据发生了变化常见的是权限变了、属主变了、修改时间变了。真正的文件级变化会单独列出来。所以说docker diff看的是“路径级变化”不是“目录递归变化”。1.3 diff 显示到目录就停了路径含义要分清上面提到目录状态 C 不代表里面所有文件都变了但另一个更常见的情况是你增加了一个文件diff 输出可能同时出现两行一行是新增文件本身一行是它所在的目录被标记为 C。这是因为父目录的元数据比如目录大小、修改时间也跟着变了。看到这种成对出现的记录别紧张这是正常现象。还有一点需要注意docker diff输出的路径是容器内的绝对路径不是相对于某个工作目录的路径。所以输出里的/app就是根目录下的 app 目录别把它理解成上下文路径。另外docker diff需要的参数是容器 ID 或容器名不认镜像名。你拿镜像名去跑会直接报错提示找不到容器这一点很多人第一次用都会栽一下。2. 上手实操docker diff 命令的常规用法和参数理解了原理接下来就是实际使用。这一节我会把docker diff的常用姿势和容易出错的细节过一遍包括针对运行中容器和已停止容器的操作、跟其他 Docker 命令的配合方法以及在docker compose环境下的使用方式。2.1 基本命令针对运行中容器和已退出容器docker diff的语法非常简单docker diff CONTAINERCONTAINER 可以是容器 ID 的前几位也可以是完整容器名。可以用docker ps -a先看一眼当前有哪些容器再决定对哪个执行 diff。它能作用的对象不只是正在运行的容器已经停止的容器同样可以。因为容器的可写层在容器停止后依然保留只要容器还没被docker rm删掉diff 就一直可以做。实操里最常见的一个动作是这样的docker ps -a docker diff myapp如果容器很多记不住名字可以用容器 ID 的前 4 到 6 位比如docker diff a1b2c3。Docker 会自己匹配。如果你的 shell 里有多个容器 ID 前缀相同Docker 会报错提示你写得更具体一点。关于命令本身docker diff不带任何额外参数也不支持过滤、排序、输出格式调整。它就是一个“看一眼”的工具。如果你需要对结果做更复杂的筛选通常的做法是把输出接到grep、awk或者sort上。比如只想看新增的文件可以执行docker diff myapp | grep ^A只想看被删除的就改成docker diff myapp | grep ^D在容器层文件非常多的时候这种过滤非常实用。我习惯把 diff 结果重定向到文件里再做进一步分析避免终端被刷屏还能留档备查。2.2 和 docker inspect / docker exec 配合做交叉验证docker diff告诉你“哪里变了”但要回答“为什么变了”“是谁改的”还需要配合其他命令交叉验证。首先是docker inspect。当你对某个变化路径有疑问比如想知道这个路径是不是挂载卷、或者这个文件的挂载来源是什么就可以用 inspect 看容器详情。尤其是 hits 到/etc/hosts、/etc/hostname、/etc/resolv.conf这类文件时它们本身是 Docker 自动挂载进去的被 diff 标记为 C 非常正常并不是被恶意篡改。其次是docker exec。diff 显示/opt/app/logs下面新增了一堆日志文件你想看一眼具体内容那就直接进容器操作docker exec -it myapp sh ls -l /opt/app/logs tail -n 50 /opt/app/logs/app.log还有一种场景是 diff 显示某个系统文件内容变了但你想对比一下跟镜像原来的内容差别。此时可以用docker cp把这个文件从容器里拷出来如果同一个文件在镜像和容器里各拷一份再用diff命令对比操作上完全可以。不过镜像里的文件不太好直接取一般可以先用docker run --rm 镜像名 cat /xxx/yyy把原始内容打出来再看容器里相同路径的内容两个一对比问题就浮出水面了。2.3 在 docker compose 环境里怎么用很多项目现在都用docker compose管理服务容器名字会带项目前缀。比如项目叫mystack服务叫web容器名往往就是mystack-web-1。这种情况下你要 diff 的就是这个带前缀的容器名。操作方式很简单先列出所有容器docker compose ps拿到准确的容器名后再执行docker diff mystack-web-1如果你只想从 compose 服务名层面操作可以先把容器名解析出来。比如docker compose ps -q web这条命令会输出 web 服务对应的容器 ID拿这个 ID 去 diff 即可。还有一个小技巧在 compose 项目里很多服务会挂载本地目录作为卷。前面说过挂载卷里的文件变化不会进容器层所以 diff 往往只能看到非挂载路径的变化。如果 diff 结果意外没什么东西但服务行为明显有变化先检查是不是所有变化都发生在挂载卷里。3. 典型实战场景从“看到变化”到“定位问题”docker diff单独看是查文件变更放在实际场景里其实是个非常好用的定位工具。这一节聊几个我经常用到 diff 的场景包括线上容器文件被改、Dockerfile 构建和镜像瘦身、以及安全审计方向。3.1 排查线上容器内文件被改得莫名其妙有一次线上服务报警说某台机器的容器磁盘占用快速上涨。我第一反应是日志在疯狂输出但df -h看到占用增长之后再进容器看日志目录文件并没有想象中那么大。后来我用docker diff一跑发现问题根本不在日志目录而是某个临时目录下多了一个几百 MB 的临时文件是应用的一个 bug 导致缓存没有清理。这类问题的排查思路可以总结为三步。第一步先跑docker diff看整体变更范围拿到所有新增、修改、删除的路径。第二步重点关注体积敏感路径比如/tmp、/var、/opt、/home下的新增文件用docker exec进去按文件大小排序快速定位大文件。第三步处理完之后再看一次docker diff确认可写层恢复到了一个干净状态。实际操作中我会用一个组合命令来定位容器里的大文件docker exec myapp sh -c find / -type f -size 50M -exec ls -lh {} \;一旦找到可疑的大文件再用 diff 对照确认它确实是容器层新增的。这时你就知道这个文件不是镜像里自带的而是容器运行过程中产生出来的排查方向瞬间清晰。3.2 在 Dockerfile 构建和镜像瘦身时用 diff 辅助排查docker diff不只能用在运行中的容器上。你在调试 Dockerfile 的时候也可以利用它的能力。常见的做法是用基础镜像先启动一个容器手动执行你在 Dockerfile 里写的命令然后用docker diff看看这些命令到底改了哪些文件判断有没有产生预期之外的垃圾文件或者有没有装了不该装的东西。举个例子你写了一个安装脚本想确定它到底往系统里塞了哪些文件。启动容器后依次执行脚本执行完跑一遍docker diff输出里所有 A 开头的路径就是脚本新增的。对于那些你根本没想到会被创建的文件就可以在 Dockerfile 里用清理步骤把它们删掉或者调整安装方式从源头避免产生。反过来如果你在做镜像瘦身同样可以用 diff 思路做减法。先把一个完整容器里的业务跑通然后看 diff 输出里哪些文件在运行后根本没被用到哪些依赖实际上可以裁剪。配合docker history可以定位是哪一条构建指令引入了这些冗余文件。这里要提醒一句在 Dockerfile 的 RUN 指令里不要把docker diff当常规手段用。因为构建过程中的中间容器会自动清理你是没法直接对构建上下文执行 diff 的。更稳妥的思路是在本地起一个容器复现构建步骤再借助 diff 做分析。3.3 安全审计发现可疑写入与异常配置变更容器安全里有一个常见问题某个镜像可能带了后门或者在启动过程中被外部脚本注入了异常配置。docker diff可以作为一手审计工具帮你快速判断一个容器相对镜像发生了哪些可疑变更。我试过这样一个场景有个容器启动后服务一直报错排查了很久都找不到原因。后来我对它跑了一次 diff发现/etc/crontab被改了多了几条定时任务。顺着 crontab 里的记录很快就找到了一个被植入的恶意脚本。这个脚本做得很隐蔽如果不是 diff 暴露了文件变更可能还要折腾更久。安全审计时重点关注这几个位置的变化/etc/passwd、/etc/shadow、/etc/sudoers账户和权限配置。/etc/crontab、/var/spool/cron计划任务。/root/.ssh、/home/*/.sshSSH 密钥。/etc/profile、/etc/bash.bashrc、/etc/ld.so.preload启动加载类配置。/usr/bin、/usr/sbin、/bin、/sbin系统命令目录。一旦 diff 结果出现这些路径的变更就要提高警惕。你可以用前面介绍的docker inspect和docker exec做进一步核实。在生产环境里也可以把 diff 输出定期落盘作为容器运行记录的一部分后期审计时再看。4. docker diff 的边界和容易踩的坑再顺手的工具也有自己的边界。docker diff能帮你看清可写层变化但它不是万能的有些东西它不会显示有些情况它显示的内容会“误导”你。这一节把这些边界和坑集中整理一下。4.1 为什么挂载卷里的文件看不到变化这是刚接触 diff 的人最容易困惑的问题。明明容器里的/data目录文件一直在变但docker diff什么都查不出来。原因在于挂载卷volume 或 bind mount不走容器可写层它直接映射宿主机目录。既然是宿主机目录容器层里自然不存在这些文件diff 扫描范围内当然不会出现它们。你可以用一个简单的实验验证。启动一个挂载本地目录的容器mkdir -p /tmp/testdata docker run -d --name vol-test -v /tmp/testdata:/data nginx:alpine docker exec vol-test touch /data/hello.txt docker diff vol-test你会发现 diff 输出很可能为空或者只有一些系统层面的微小变化。但宿主机上/tmp/testdata/hello.txt确实创建了。所以当你的应用数据大部分在挂载卷里时docker diff并不能帮助你了解业务数据变化它的定位是“镜像与容器可写层的差异”。如果确实需要监控宿主机目录变化应该从宿主机层面入手比如用inotifywait之类的工具监控绑定的目录。这个思路跟docker diff是互补关系两者结合起来才能覆盖完整的文件变化视图。4.2 时间戳和权限变化也会触发 C结果看起来很大第二类容易让人误判的场景是容器启动后什么都没动但docker diff输出了一堆 C。这些 C 往往是目录或文件的元数据变化比如 atime、mtime、权限、属主等。容器启动时很多应用会创建临时目录、锁文件或者改变某些系统目录的访问时间。对于 Nginx、MySQL、Redis 这类服务来说启动后 diff 出现一批新文件和目录其实很正常那是程序初始化写进去的配置文件、socket 文件、pid 文件等。判断这些 C 是否有问题有一个常用技巧记录容器启动瞬间的 diff 基线等运行一段时间后再跑一次 diff把两次结果做对比。增加出来的路径才是运行过程中真正发生变化的部分。单纯看一次 diff 很难区分“启动时初始化产生”和“运行中被篡改”的区别。不过要注意docker diff本身没有“比较两个时间点”的能力你只能通过保存不同时刻的 diff 输出再用本地的diff命令去对比两个结果文件。这也是前面建议把 diff 输出重定向到文件的一个原因。4.3 diff 与 docker commit 的关系提交前必须先看 diffdocker commit可以把一个容器的当前状态保存为新镜像但它不会告诉你这个容器跟原镜像差了多少内容。所以在每次 commit 之前先跑一次docker diff是非常有价值的操作。我自己踩过一次坑一个开发环境容器原本只想把应用代码变更提交成新镜像结果 commit 完之后镜像体积暴涨。后来一查发现容器里堆积了大量编译缓存和临时文件全都跟着 commit 进入了新镜像。如果提交前先跑一次docker diff看到那一堆 A 路径就不会犯这种错误。如果你确定要 commit我建议按照下面的顺序操作先跑docker diff输出结果保存。检查 diff 里的 A 路径是否有明显的垃圾文件比如编译缓存、依赖目录、日志文件。进入容器清理无用文件对应路径会变成 D 或消失。再次执行docker diff确认变更范围符合预期后再 commit。这条流程下来镜像基本会干净很多。5. 常见问题速查与排查技巧最后这部分把平时高频遇到的问题汇总成速查表再把两三个我实际用下来最有用的排查技巧分享出来。后面你再遇到类似情况可以直接对照着排查。5.1 高频问题速查表现象可能原因处理思路提示Error: No such container参数写成了镜像名或容器已被删除用docker ps -a确认容器存在diff 结果为空所有变更都在挂载卷里或容器相对镜像没有任何变化检查挂载卷或确认操作是否写入了可写层出现大量 C 目录目录元数据变化或镜像内容本身被覆盖结合时间戳和进程日志判断是否异常/etc/hosts、/etc/hostname显示 CDocker 自动管理文件正常现象不需要处理diff 结果很大输出刷屏容器可写层文件太多重定向到文件再分析不要直接看终端容器时间显示 UTCdiff 没发现/etc/localtime变化有些镜像的时区通过环境变量处理未实际写文件用docker inspect查询环境变量或用挂载卷覆盖时区文件diff 之后想还原某个文件docker diff不支持回滚用docker cp从镜像原始层拷贝文件覆盖这些问题是群里同行问得最多的大部分都能通过“先看 diff再 inspect最后 exec 进去确认”三步法解决。我自己排查时基本也是这个顺序。5.2 配合文件时间戳和进程定位的排查技巧最后一个技巧也算是我个人最常用的一套组合拳。当你看到 diff 里有可疑的新增文件又想知道它是谁创建的时候可以用时间戳来锁定时间窗口。思路是这样的先用docker diff拿到可疑路径。再用docker exec查看这个文件的时间信息。然后去容器的系统日志或应用日志里找对应时间点的操作记录。实在不行用docker top查看当前进程列表结合strace之类工具做进一步追踪。举一个实际例子有个容器里莫名其妙多了一个/var/tmp/kworker文件文件名伪装成内核线程的名字看起来像是挖矿程序。我通过ls -l /var/tmp/kworker看到创建时间然后去系统日志里找那个时间点的登录和进程执行记录最后发现是一个容器内服务启动了外部命令导致的下发脚本。整个过程里docker diff是第一棒没有它我根本不知道这个文件是什么时候冒出来的。所以说docker diff本身不复杂但它在排查链路里的位置非常关键。它是所有深度调查的起点相当于给了你一张“变化清单”。后面接什么工具都可以前提是你手里有这张清单。我个人在实际操作中的体会是docker diff一定要养成习惯性使用的意识。不只是出问题的时候才想起来跑每次部署新版本、每次跑完一个未知脚本、每次 commit 之前都应该顺手 diff 一下。它花不了几秒钟但往往能帮你省下后面好几个小时的排障时间。尤其在多人协作的项目里谁改了什么、改在哪diff 结果比聊天记录可靠得多。如果你刚开始接触容器我建议先拿一个 nginx 容器做实验启动后改点文件、装个包、删个东西然后用docker diff观察输出。多做几次你对“容器层”这个概念的理解会变得特别直观。
返回列表