容器 16:数据写到了哪一层,重建后还在吗
位置决定删除后的命运
容器可写层一般属于该容器对象;删掉并重新创建相同镜像的新容器,不应期待该层的修改自动出现在新容器中。bind mount 让宿主指定目录可见,数据的所有者和备份边界仍属于该外部目录;volume 有独立于单个容器的生命周期,删除容器不等于删除 volume。tmpfs 不把写入落到同一磁盘持久层,容器重建后内容不能作为持久状态。具体默认快照实现、匿名 volume 清理规则和挂载传播需以所选运行时与执行命令核对,不对所有系统给绝对保证。
同一个容器内路径背后可能是四种对象
应用对 /tmp/containers-state/state.txt 打开文件时,路径首先按该进程的根与 mount namespace 解析。如果没有更具体的挂载覆盖,写入可能落在运行时为该容器准备的可写快照;若运行参数将宿主自有目录 bind 到相同路径,写入转而触及外侧目录;若挂载了命名 volume,生命周期由 volume 对象管理;若挂的是 tmpfs,这份状态依赖其内存文件系统实例而不是镜像层。应用里同一行 open 无法告诉读者这四种情况是哪一种。要在运行中的实际进程检查 /proc/<pid>/mountinfo、运行时的挂载列表、相应的宿主源目录或 volume 名和一条自建的写入结果,才可以确定本次路径经过的挂载。
这也解释为什么 Dockerfile COPY /app/config ... 并不能保证进程后来一定读到镜像里的那份文件:一个 bind 或 volume 挂载到同一路径时,镜像内原有内容可能被覆盖而暂时不可见。Docker volume 文档还描述了空 volume 首次挂载到有内容的目标目录时可能发生的复制行为;它与已有内容的 volume 挂上去遮蔽原路径不是同一情形,实验应固定是否为新创建的空卷和目标目录初始内容,而不是把“挂了卷后看见的文件”统统归因于镜像层。无论哪一种情形,ls 只能回答当前视图看到什么,不能直接证明未来删除容器后的保存位置。
停止、删除、重建分别是什么动作
停止原容器使它的主进程结束,但容器对象及其可写层是否仍被保留,要看本次执行的具体生命周期动作;再启动同一个容器可能读回写在自身可写层的状态。docker rm 删除容器对象并最终释放其关联可写层,再从同一镜像新建另一个容器,两个容器 ID 必须分列,不能因路径相同就断言新容器复用了旧数据。命名 volume 若未被显式删除,可以在新容器上按同一 volume 名称重新挂载并读取;bind mount 则要重用原宿主目录。tmpfs 关联当前挂载实例,不是为跨重建准备的数据存储,即使某次重启行为取决于具体 runtime,也不能把它当持久化保障。
删除匿名 volume 与命名 volume 的规则还依删除命令的选项;不能见到 docker rm 返回 0 就宣布所有存储都清空。相反,旧容器一时删不掉也不说明业务数据已经得到可靠备份。实验命令必须分别记录 docker stop、docker rm、docker volume inspect 与宿主目录 stat 的真实输出,并在新容器中 GET 同一状态值,用容器 ID、挂载源和文件哈希关联“旧的写入”和“新的读取”。只写“stop 后还有、rm 后没了”而不标明源是层还是卷,不能作为通用的存储结论。
只读、属主与安全策略是独立前提
只读 bind mount 或卷挂载使写入被拒,即便容器内部 id 打印 root,也需要先检查挂载选项。可写 bind 目录但进程的 UID/GID 映射与目录所有者不匹配时,写入同样可能失败;外侧目录路径不存在、父目录不可遍历、LSM 拒绝或宿主文件系统为只读又是不同失败来源。05 的 UID 映射与 08 的安全约束在同一文件访问上同时生效,EACCES 一次错误不能独自指出是哪个层负责。实验应从自建空目录开始,固定模式位和 UID/GID,分别只改变读写选项与访问身份,记录失败时确切 errno、容器内外 stat、mountinfo 与安全日志是否可用;不可通过对共享宿主目录 chmod 777 把问题“修好”。
共用探针接到写入失败时,HTTP 处理线程如何报错取决于其代码路径;如果客户端出现连接中断或非 200,不应把 curl 的退出码和服务内部 exception 当作同一个值。正常案例应在一个有界的短字符串 POST 后再 GET,核对实际 HTTP 状态、响应与后端存储文件;只读拒绝案例要同时确认进程是否还在、后端源没有新增文件、是否存在先前成功的 state.txt。当前云端仅有普通进程的本地目录案例,没有这几类挂载与容器 UID 的对照,正文保留 VM 补跑前提。
tmpfs 的“内存”不等于绝不产生持久痕迹
tmpfs 不是写入镜像可写层,也不提供给新容器自动复用的命名卷对象;但说“只在内存里,绝不可能碰到磁盘”过于绝对。内核 tmpfs 文档说明其页面可能在系统配置允许时参与 swap,是否由 swap 等机制落到其他存储与实际节点条件有关;它也会计入相应内存使用,需要结合 memory cgroup 的计数观察。因而不要仅凭给敏感文件挂 tmpfs 就宣称没有信息泄漏风险,也不要在共享机器禁用 swap 只为使实验图表更干净。VM 中验证 tmpfs 重建后状态缺失时,应记录挂载实例、内存/swap 前提、进程退出与清理,别把“未使用宿主 bind 目录”当作“数据已经安全销毁”。
volume 与 bind mount 都要考虑一致性和备份:程序可能先写缓冲而未同步到实际后端,备份程序若在应用仍写入时直接复制文件,可能拿到不一致快照。证明“重建后能读回一个字符串”不等于有可靠恢复流程;备份应在专用环境定义停写或快照边界、保存原始源与校验哈希,恢复到独立目录/新卷后实际读取。30 篇将追踪 Kubernetes 的卷挂载与节点路径,16 只研究容器单节点在四类数据源上的写入归属,不提前将 StatefulSet 或 CSI 行为概括进来。
共用 probe_app.py 的 /state 返回自有 state.txt,可以复用同一 HTTP 接口写入固定短字符串。专用 VM 正例把它分别指向镜像可写层、自己创建的 bind 目录、命名 volume 和 tmpfs,记录写入后的响应,再按“重启原容器 → 删除并新建 → 重新挂载同一数据源”逐步比较。失败例把只读目录挂到 state-dir,应用写入应返回文件系统错误(当前探针会让处理该请求的线程报错,需以进程和 HTTP 实际输出来判断),并检查目录权限而不是随意 chmod 宿主共享目录。入口和清理要求记录所有被创建的目录与挂载名称;源码包只含同一探针,无虚构容器输出。
本机可在普通进程上检查自有目录写入,但缺容器运行时,不能据此证明可写层、volume 或重建行为。这一篇的容器侧持久化与备份恢复全部 NOT_RUN;执行前需固定容器 ID、镜像 digest、挂载路径、数据所有权和清理动作。
补跑记录应有四组互不混用的对象标识:镜像 digest 用于确认相同应用制品,容器 ID 用于区分停止重启与删除新建,volume 名或 bind 源路径用于定位持久数据,状态文件内容哈希用于判别是否真读到上轮写入。先 POST 一次固定短字符串,记录请求与后端字节,再分别执行重启、删除、重建,逐次 GET。测试 tmpfs 时只在 VM 自建容器上启用,负载和写入大小都有上限;结束后分别检查容器、命名卷、宿主 bind 目录和 tmpfs 挂载的清理结果。当前仓库没有真实 runtime,不能把这些预期步骤复制成原始实验输出。
用两次读取判断是否真的持久
第一轮在无挂载条件下启动 A,读 /state 确认初始状态,POST 合成值 lab-a,在 A 存活时再次 GET;停止并启动同一个 A 时再 GET,最后删除 A、从同一 digest 新建 B 再 GET。若 B 看不到 lab-a,这只支持“此路径本轮落在 A 的可写层且没有自动转移给 B”之类的受限解释,尚须结合 mountinfo 排除 A 实际写在匿名卷上的可能。第二轮在同一目标路径挂命名 volume,让 A 写入后删除 A,在另一个容器 ID 的 B 上挂同名 volume,再读取合成值并通过 docker volume inspect 确认是同一个卷对象。两轮若容器 ID 和挂载源不同,不能简单把结果排成一行“原容器重启后还在”。
bind 目录测试应同时在宿主已存在的自建空目录采样:写之前 stat 宿主目录,容器里 POST 后立即读宿主同名文件、记录哈希和数值所有者;删除容器后外侧再读一次。若宿主文件存在却新容器 GET 空值,先查新容器的挂载源是否仍指向这个目录,而不是宣布“bind mount 不持久”。tmpfs 分支要证明的是不会按卷身份在重建后自动复用,不能拿另一个容器也有 /tmp/containers-state 这串路径就断言它读的是同一文件系统对象。四组测试结束按容器、挂载、宿主自建目录顺序逐一核验归零,命名卷删除前先判断是否仍被本次创建的容器引用。
只读负例使用本次自建文件,先检查原有内容哈希,再以 readonly 选项挂入,发起一次写请求,记录客户端 HTTP 状态、应用日志与源文件哈希是否未变。若请求失败但源文件的内容变了,可能指向挂载配置没有按预期生效,应检查实际进程的 mountinfo;若只看到错误而没有后端取证,也可能只是路由、认证或探针本身出了错。这种对照不能在真实业务目录上通过重复写入来“确认只读”,而要在限定写入字节数的测试目录里完成。整个四组实验未在当前云端执行,页面与附件构建都不能充当这些检查的退出码。
| 现象 | 追溯对象 | 判断边界 |
|---|---|---|
| 删除重建丢数据 | 可写层、容器 ID | 不代表 volume 数据也丢失 |
| 文件仍存在 | bind/volume 的宿主位置 | 还要确认读的是同一挂载 |
| 写失败 | UID/GID、只读选项、LSM | 不能只归因于镜像格式 |
练习
- 先预测“停止再启动原容器”和“删除再创建同一镜像容器”对可写层里
state.txt的不同结果,在专用 VM 保存两个容器 ID 后实测。 - 用自己创建的 bind 目录预测宿主与容器看到的所有者,读写后检查原始宿主文件、重挂载与清理;不能修改共享目录所有权。
上一篇:15:多平台;下一部从 OCI bundle 与运行时的 create/start 生命周期对照镜像制品。扩展阅读:存储体系,不作为本篇实测。
系列总目录:从进程隔离到运行时与编排。




