☰
Docker容器存储全解析:分层机制、数据卷与磁盘清理实战
2026/10/27 3:54:12 网站建设 项目流程

1. 先把"存不下"这件事说清楚:容器存储到底分几层

docker 容器存储问题,十个里有八个最后都落到同一句抱怨上——"我也没往容器里放多少东西,磁盘怎么就满了"。这个直觉之所以经常骗人,是因为容器的磁盘占用和你看到的文件大小根本不是一张账本。镜像层、容器可写层、数据卷、绑定挂载进来的宿主机目录、容器日志、构建缓存,这六样东西在docker ps里全都看不见,但它们才真正吃掉你的容量。我在第一次遇到宿主机磁盘被吃满时,反复删容器、删镜像,docker system df显示空间确实降了,可df -h里的可用空间只回了不到三分之一,剩下的部分藏在被删除但仍被进程持有的文件句柄里,那是后话了。

这篇内容想解决的,是把"容器存储"从一团模糊的焦虑拆成一串可以按图索骥的具体问题:镜像和容器各自占哪些目录、什么数据该放卷、什么数据天生不该持久化、默认存储路径怎么迁走、日志怎么限制、空间异常时按什么顺序排查。适合已经能把容器跑起来、但被磁盘告警和"容器删了数据也没了"折腾过的人,也适合刚接手一套容器化环境、准备把存储策略定下来的同行。命令和参数我会给全,思路和取舍我也会讲清楚,因为哪一步没想明白,后面都要用事故补学费。

1.1 镜像层与容器可写层:写时复制到底付出了什么代价

要理解容器的磁盘账,先得接受一个事实:容器不是一台虚拟机,它没有一整块属于自己的磁盘。镜像由若干只读层叠起来,容器启动时在最上面加一个薄薄的可写层,所有写操作都落在这个可写层里。这套机制叫写时复制(Copy-on-Write),意思是文件原本在下层是只读的,你要改它,内核先把整个文件复制到上层,再在上层改。注意是整个文件,不是一个字节。

这就解释了一个反直觉的现象:你在容器里往一个 2GB 的数据文件尾部追加了一行,看起来只写了几十字节,实际磁盘上多了一整个 2GB 的副本。日志文件、SQLite 数据库、正在被写入的二进制文件,放在可写层里都是这个待遇。我见过一个服务把日志写在容器内的应用目录下,日志涨到 8GB,容器的 writable layer 就实打实占了 8GB,而宿主机上的日志轮转策略对它完全无效,因为这份文件根本不在宿主机的目录树里被管理。

可写层的生命周期也值得强调:容器删除,可写层跟着消失,数据一起蒸发。这不是缺陷,是设计。容器被当成"一次性计算单元",状态应该外置。所以判断一段数据该不该留在可写层,只需要问一句:这个容器被重建时,这份数据丢了会不会出事。会出事的一律外置,不会出事的(临时缓存、pid 文件、运行时生成的锁)留在可写层反而干净。

还有一种更容易被忽略的占用:镜像层本身。每次docker build都可能产生新的层,旧的中间层不会自动回收,构建缓存会一直堆在/var/lib/docker下。一个每周构建几十次的环境,构建缓存涨到几十 GB 是很平常的事,而且它不在docker images的列表里,只能靠docker system df里那一行 Build Cache 才看得见。

1.2 三种数据落盘方式:卷、绑定挂载、tmpfs

Docker 给你三条把数据搬出可写层的路,选错了会在权限、性能、可移植性上连本带利地还回来。

**数据卷(volume)**由 Docker 自己管理,物理位置在宿主机的/var/lib/docker/volumes/<卷名>/_data。它的优势是跨平台一致、可以用 Docker 的命令备份和迁移、在多个容器间共享,而且不依赖宿主机上某个具体路径存在。生产环境里凡是需要持久化的数据,我的默认选择都是卷。

**绑定挂载(bind mount)**直接把宿主机的一个目录映射进容器。它适合配置文件、开发时的源码热加载、以及需要和宿主机上其他程序共享的目录。代价是耦合了宿主机的目录结构,换一台机器路径就可能不存在,而且宿主机上的权限、属主、强制访问控制策略会原封不动地带进容器,权限问题的坑大多出在这里。

tmpfs挂在内存里,容器停止就没了。适合放会话缓存、临时解压目录、需要极低延迟又不该落盘的中间文件。它有个容易被忽略的好处:给/tmp挂一个带 size 限制的 tmpfs,可以防止某个失控进程写满整块磁盘。

方式物理位置生命周期典型用途主要风险
可写层/var/lib/docker/overlay2/<id>/diff随容器删除运行时临时文件写放大、随容器消失
数据卷/var/lib/docker/volumes/独立于容器数据库、上传文件忘记清理,越积越多
绑定挂载宿主机任意路径独立于容器配置文件、源码权限、路径耦合
tmpfs内存随容器停止缓存、临时目录吃内存,超限会写失败

选型上我自己的规矩很朴素:配置用绑定挂载只读挂进去,业务数据用卷,临时垃圾用 tmpfs,可写层只放"重建就重建、丢了也无所谓"的东西。这条线一划清,后面百分之八十的存储事故都不会发生。

1.3 存储驱动选型:为什么现在几乎只剩下 overlay2

早些年存储驱动是个需要认真比较的选项,aufs、devicemapper、btrfs、zfs 各有拥趸。今天的结论很干脆:主流发行版上就用 overlay2,别折腾。原因不复杂——overlay2 已经进了内核主线,不需要额外装内核模块,层数支持和性能都够用,社区和生态的默认适配也都在它身上。

docker info里能看到当前用的驱动。如果显示的是 vfs,那要留神,vfs 不做任何分层复用,每一层都完整复制一份,磁盘占用会成倍膨胀,只适合测试环境或者文件系统实在不支持的场合。devicemapper 的 loop-lvm 模式更是老古董,性能和容量都有硬伤,遇到就换成 overlay2。

overlay2 也有它的边界:层数太多会拖慢文件查找,所以 Docker 对镜像层数有上限约束,这也是为什么 Dockerfile 里要尽量合并 RUN 指令。另外 overlay2 依赖底层文件系统支持d_type,早年 XFS 如果不带ftype=1格式化就会出问题,现在新版本基本默认开启,但接手老机器时值得用xfs_info确认一下。这些细节平时用不上,一旦踩到就是启动直接失败,排查起来毫无头绪。

2. 上线前要定下来的几件事:存储方案怎么选

存储这块有个特点:事后补救的成本远高于事前想清楚。容器跑起来、数据写进去之后再改存储方式,意味着停机、迁移、校验,还得赌一把数据完整性。所以在按下docker run之前,有几件事值得花半小时定下来:哪些服务是有状态的、卷用什么命名规范、权限怎么对齐、容量按什么口径估。这半小时省下来的,往往是后面几个通宵。

2.1 有状态与无状态的分界线

我习惯把服务先分两类。无状态服务,比如前端静态资源、无状态的 API 网关,随便重建,存储上不需要任何持久化配置,日志走标准输出交给日志驱动处理就行。有状态服务,数据库、消息队列、对象存储、上传目录,必须把数据目录挂出来,而且要在编排文件里写死,不能靠人记。

这条线画错的典型症状是:数据库容器的数据目录没挂卷,跑了三个月,某次升级重建容器,数据没了。更隐蔽的版本是挂是挂了,但挂的是容器内路径的一个子目录,父目录下的其他数据照样丢。我的做法是把有状态服务的挂载点在编排文件里单独列一段,注释写清楚"此处数据不可丢",并且定期用脚本校验这些挂载点确实指向了卷或者宿主机目录,而不是静默地落在了可写层。

还有一类服务容易被误判成无状态:带本地缓存的搜索服务、需要保留索引的日志采集器。它们丢了数据不会立刻报错,但会引发长时间的重建和性能抖动。这类我也归到有状态一侧,给卷,图个安稳。

2.2 参数对照:-v 与 --mount 到底差在哪

-v和--mount做的是同一件事,但行为差异会在出问题时放大。-v是简写,如果宿主机路径不存在,Docker 会自动创建一个目录,你以为挂的是文件,结果挂了个空目录进去,应用读配置读了个寂寞。--mount更严格,源路径不存在直接报错,不会替你猜。

我现在的习惯是交互式试验用-v图快,写进编排文件、脚本、文档的一律用--mount。多打几个字,换来的是错误当场暴露而不是三天后才发现。

# 简写:宿主机路径不存在时会自动建目录,容易埋坑 docker run -d --name app1 -v /data/app/conf:/etc/app/conf:ro nginx:stable # 显式写法:源不存在直接报错,类型和只读属性一目了然 docker run -d --name app2 \ --mount type=bind,source=/data/app/conf,target=/etc/app/conf,readonly \ nginx:stable # 命名卷:不用关心宿主机路径,交给 Docker 管理 docker run -d --name app3 \ --mount type=volume,source=app_data,target=/var/lib/app \ nginx:stable

命名卷有个必须知道的行为:当卷是空的、且容器内目标路径已有内容时,Docker 会把镜像里那份内容复制进卷。这个"初始化"只发生一次。它对"镜像自带一份默认配置,用户第一次启动时想要这份默认值"的场景很友好,但在恢复备份时会坑人——你先docker run创建了卷(已经写入了默认内容),再往里面解压备份,旧文件和新文件混在一起,出现一堆莫名其妙的残留。正确顺序是先建空卷、不解压前不启动容器,或者解压前把卷清空。

2.3 权限与属主:容器里写文件为什么会 Permission denied

绑定挂载最常见的报错是Permission denied,原因是容器内进程的 UID 和宿主机目录的属主对不上。举例,宿主机目录属主是 UID 1000,容器里跑的应用是 UID 101(很多官方镜像会给应用建一个固定 UID 的用户),写文件自然被拒。这不是 Docker 的 bug,是 Linux 权限模型在起作用,容器不是权限的例外。

处理办法有几种,各有代价。最省事的是在宿主机上chown成容器内的 UID,缺点是换个镜像 UID 变了又得改;稍微讲究一点的是在编排文件里显式指定user,让容器以宿主机上某个确定 UID 运行,两边对齐;再进一步是启用用户命名空间映射,让容器内的 root 映射到宿主机上的普通用户,安全性更好但会带来卷属主的整体变化,不适合在所有环境一把梭。

# 查宿主机目录属主 stat -c '%u:%g %n' /data/app/uploads # 查容器内进程的 UID docker exec app1 id # 让容器以指定 UID 运行(编排文件里对应 user: "1000:1000") docker run -d --user 1000:1000 \ --mount type=bind,source=/data/app/uploads,target=/data/uploads \ myapp:1.0

提示:新建的绑定挂载目录,属主和权限一定要在上线前定好,别指望容器自己去改。容器启动时改宿主机目录权限,在多容器共享同一目录时极易互相覆盖。

另外,宿主机如果启用了强制访问控制策略,即使 UID 对上了也可能被拦。这类问题日志里通常有明确提示,看到就按提示给目录打上正确的安全上下文标签,不要图省事关掉整个策略。

3. 实操:把存储路径、日志和清理策略一次性配好

前面聊的是判断,这一段全是落地。一套容器环境想长期稳定,通常要做三件事:把数据目录从系统盘挪到独立盘、给容器日志加上限、把清理动作变成例行公事而不是救火。这三件事都有标准做法,照着做基本不会出问题,唯一的要求是别在生产高峰动。

3.1 迁移 /var/lib/docker 到独立数据盘

系统盘被容器占满是最常见的故障场景之一。镜像、容器可写层、卷、构建缓存默认都在/var/lib/docker,和系统日志、包管理器抢同一块盘。给它单独挂一块数据盘,是投入产出比最高的一步。

迁移过程本质是把目录整体搬走,然后告诉 Docker 新位置。核心是rsync的参数:-a保留权限时间,-H保留硬链接(overlay2 目录里硬链接很多,普通cp会把它们展开成多份,容量翻倍),-A保留 ACL,-X保留扩展属性,--numeric-ids避免跨机器时 UID 被映射错。

# 1. 停服务,确保没有写入 systemctl stop docker docker.socket containerd # 2. 预演一次,看看要搬多少、有没有权限问题 rsync -aHAX --numeric-ids --dry-run /var/lib/docker/ /data/docker/ # 3. 正式搬迁,建议加上进度显示 rsync -aHAX --numeric-ids --info=progress2 /var/lib/docker/ /data/docker/ # 4. 核对两边文件数量和总大小 du -sh /var/lib/docker /data/docker # 5. 改配置:/etc/docker/daemon.json # { # "data-root": "/data/docker", # "log-driver": "json-file", # "log-opts": { "max-size": "100m", "max-file": "3" } # } # 6. 启动并确认新路径生效 systemctl start docker docker info | grep -i "docker root dir" # 7. 观察几天没问题后再删旧目录

有两个细节容易翻车。一是旧目录不要立刻删,Docker 里有些元数据和挂载状态在切换瞬间还没稳定,留几天观察是廉价的保险。二是/etc/docker/daemon.json必须是合法 JSON,多一个逗号 Docker 就起不来,改完先systemctl start docker看状态,别急着重启整台机器。

3.2 日志轮转:容器日志把磁盘吃满的经典事故

容器日志默认走json-file驱动,位置在/var/lib/docker/containers/<容器ID>/<容器ID>-json.log,默认没有任何大小上限。一个打日志比较勤的服务,一天写几 GB 很正常,如果它同时往标准输出和标准错误狂打,磁盘见底只是时间问题。

限制要配置在 daemon 层面,让所有容器继承默认值,而不是逐个容器加参数——逐个加,总有一天会漏。

{ "log-driver": "json-file", "log-opts": { "max-size": "100m", "max-file": "3" } }

这组参数的意思是单个日志文件最大 100MB,最多保留 3 个,也就是单个容器日志上限 300MB。量级怎么定?按峰值日志量估:假设某服务峰值每秒写 200KB 日志,一天约 17GB,那 100MB×3 相当于只保留了几分钟的历史,排查问题根本不够用。这种情况下正确的做法不是把上限调到 20GB,而是把日志级别降到 warn、把详细日志交给专门的日志采集组件外送,本地只留个尾巴。日志存储和日志分析的职责分开,各自才好控制。

注意:日志驱动和轮转参数只在容器创建时生效。改完daemon.json并重启 Docker,已存在的容器不会自动应用新参数,需要重建容器。想临时验证,可以先docker inspect看某个老容器的HostConfig.LogConfig,十有八九还是空的。

3.3 清理策略与 docker system df 的正确读法

docker system df是看存储账本的第一入口,加-v能看到每一项的具体条目。它给出四行:Images、Containers、Local Volumes、Build Cache。新手常犯的错是只盯第一行,看到镜像不大就以为没问题,实际上构建缓存和卷才是大头。

列含义常见膨胀原因清理命令
Images镜像层总占用,含未被引用的频繁构建、多版本 tag 并存docker image prune -a
Containers容器可写层占用应用往容器内写日志、缓存docker container prune
Local Volumes卷占用卷随容器删除但卷本身保留docker volume prune
Build Cache构建缓存每次构建产生新层,旧层不回收docker builder prune

清理命令里最危险的是docker volume prune,它删的是没有被任何容器引用的卷。听起来安全,实际上"没被引用"不等于"没用"——你把一个容器停掉准备明天恢复,它的卷在此期间可能就是未引用状态,一执行就没了。我的规矩是:卷的删除永远手工确认,先docker volume ls列出来,看清楚名字再逐个删,绝不在生产机上跑带-a的批量清理。

# 只看不删,先弄清楚要清理什么 docker system df -v # 清理停止的容器、悬空镜像、未使用的网络,不碰卷 docker system prune # 清构建缓存,保留最近 7 天内的 docker builder prune --filter "until=168h" # 卷相关:先列出再决定,别用 -a docker volume ls --filter dangling=true docker volume rm <卷名>

3.4 卷的备份与恢复:三个命令级别的手艺

卷由 Docker 管理,位置在/var/lib/docker/volumes下,但不要直接去那个目录里cp。直接复制文件在有写入的情况下可能拿到撕裂状态,而且跨机器的属主、扩展属性容易丢。标准做法是起一个临时容器,把卷和目标目录都挂进去,在容器里打包。

# 备份:把 app_data 卷打成 tar.gz 放到当前目录 docker run --rm \ --mount type=volume,source=app_data,target=/src,readonly \ --mount type=bind,source="$(pwd)",target=/backup \ alpine:3.19 \ tar czf /backup/app_data-$(date +%Y%m%d).tar.gz -C /src . # 恢复:先建一个空卷,再解压进去(顺序很重要) docker volume create app_data_restore docker run --rm \ --mount type=volume,source=app_data_restore,target=/dst \ --mount type=bind,source="$(pwd)",target=/backup,readonly \ alpine:3.19 \ sh -c "cd /dst && tar xzf /backup/app_data-20240101.tar.gz" # 备份前最好让应用处于静止状态,或者在应用层做一次一致性导出

这里有个我踩过的坑值得单独说:数据库类的卷,直接打包文件系统是不可靠的。数据库在运行时,页缓存里的数据和磁盘上的文件不一致,打出来的包可能起不来。正确姿势是先用数据库自己的导出工具做逻辑备份,再考虑文件级备份作为兜底;如果必须文件级备份,就先停写或者用文件系统快照。文件级备份看起来快、看起来省事,恢复失败一次就够记一辈子。

4. 空间异常排查实录:从现象到根因的路径

磁盘告警时最忌讳的是上来就docker system prune -a。删完之后空间是回来了,但根因没找到,下个月同一时间再告警一次,而且删掉的东西里可能有别人正在用的镜像。我一般的顺序是:先确认是哪一块在涨,再确认是哪个容器,再确认是文件还是句柄,最后才动手清。这个顺序能把误删的概率压到很低。

4.1 删了文件磁盘不降:被进程占用的句柄

df -h显示用了 95%,du -sh加起来只有 60%,这中间 35% 的差额几乎总是同一个原因:有文件被删了,但仍有进程持有它的句柄,inode 没释放,空间自然不回收。容器场景下这个进程可能是容器里的应用,也可能是宿主机上的日志采集器。

# 找出被删除但仍被占用的文件 lsof +L1 2>/dev/null | head -50 # 按占用大小排序,看谁最狠 lsof +L1 2>/dev/null | awk '{print $7, $1, $2, $9}' | sort -rn | head -20

找到之后有两条路:重启对应进程,或者手工清空句柄(> /proc/<pid>/fd/<fd>,仅对部分场景有效且风险较高)。生产上我倾向重启或滚动替换那个容器,干净、可控。

这类问题的根因通常是应用自己删日志文件:日志组件按大小切分文件,删掉旧的,但写日志的进程还开着老文件的 fd,空间就一直挂着。解决办法是让应用支持 reopen(收到信号后重新打开文件),或者干脆别让应用自己管文件,把日志打到标准输出交给日志驱动,落盘和轮转交给 Docker。

4.2 overlay2 目录里那堆看不懂的目录名

/var/lib/docker/overlay2下面是一堆 64 位十六进制命名的目录,每个对应一个层或者一个容器的可写层。直接ls完全看不出谁是谁,但按大小排序之后,最大的那几个往往就是问题所在。

# 按占用排序,找最大的层 du -sh /var/lib/docker/overlay2/* 2>/dev/null | sort -rh | head -10 # 把目录名映射回容器:diff 目录对应哪个容器 docker ps -aq | while read id; do dir=$(docker inspect "$id" --format '{{.GraphDriver.Data.UpperDir}}') size=$(du -sh "$dir" 2>/dev/null | cut -f1) echo "$size $id $(docker inspect "$id" --format '{{.Name}}')" done | sort -rh | head -15

这段脚本解决的是"我知道有个容器占了很多,但不知道是哪个"的经典困境。跑完通常能立刻定位到一个容器,进去看它的可写层里到底堆了什么。我遇到过的情况包括:应用把日志写到容器内的相对路径、临时上传文件没清理、崩溃转储文件、包管理器缓存没删。这些都能通过改配置或挂卷解决,改完重建容器即可。

overlay2/l这个目录也值得知道,它存的是层之间引用的短符号链接,方便 lowerdir 参数不至于超长。它不属于任何单个容器,别手动去删。

4.3 inode 用满与磁盘配额

有时候df -h显示还有空间,但就是写不进去,报No space left on device。这时候看df -i。容器环境极容易把 inode 用光——大量小文件是 inode 杀手,比如缓存目录里每个请求一个文件、日志按分钟切分、包管理器缓存里成千上万个小文件。overlay2 的每个层都带一套完整目录结构,层数多了 inode 消耗也翻倍。

现象首要怀疑验证命令
有空间但写不进去inode 耗尽df -i
du和df差异巨大已删除文件被占用lsof +L1
单个容器占用巨大可写层写了大量数据按 UpperDir 统计
卷越积越多卷随容器删除但未清理docker volume ls
构建后磁盘暴涨构建缓存未回收docker system df -v

inode 耗尽没有优雅解法,只能清。优先清构建缓存和包管理器缓存,这类文件数量多、价值低。长期方案是给容器挂的卷设置配额,或者在挂载 tmpfs 时加size限制,把失控写入约束在可控范围内。很多文件系统支持项目配额,配置稍麻烦,但对多租户环境是值得的。

4.4 常见问题速查表

把上面这些零散经验收成一张表,出问题时按顺序对一遍,比从头推理快得多。

现象最可能原因处理动作
重启容器后数据全没了数据写在可写层改挂卷,后续所有数据外置
容器内写文件报 Permission deniedUID 与宿主机目录属主不一致对齐 UID 或调整目录属主
改了 daemon.json 日志配置不生效只对新建容器生效重建老容器
卷里出现一堆不该有的默认文件空卷初始化时复制了镜像内容恢复备份前清空卷
磁盘满了但不知道谁占的先查四类占用docker system df -v
删文件后空间不回文件句柄未释放lsof +L1定位后重启进程

5. 性能与容量:容易被忽略的细节

存储这块最后悔的事,往往是当初只考虑了"能不能存下",没考虑"存得够不够快""换算成钱合不合算"。一个跑在 overlay2 可写层上的数据库,写入性能可能只有直连卷的一半;一个把所有上传文件都放本地盘的架构,扩容时只能整机扩,成本曲线很难看。这些不属于故障,属于设计债,早还便宜。

5.1 卷的性能差异:本地盘、网络存储、可写层

同一份数据,放在可写层、本地卷、网络卷上,性能差着量级。可写层最差,因为每次首次写文件都要 copy-up,改大文件等于复制大文件。本地卷接近宿主机裸盘性能,只是多一层目录映射,几乎无损耗。网络存储看实现,块存储挂成本地盘之后当本地卷用,性能较好;文件级网络存储(NFS 之类)在小文件随机写上会明显劣化,元数据操作开销大,数据库这类场景基本不适合。

我自己的判断标准是看 IO 模式。顺序读写为主的(日志归档、大文件上传),网络存储能接受;随机小 IO 密集的(数据库、消息队列索引),一律本地盘,宁可让数据跟着机器走,靠复制和故障转移保证高可用,而不是靠共享存储。这个取舍听着激进,但在实际项目里比"所有数据都放共享存储"稳得多,因为共享存储一旦抖动,所有依赖它的服务同时受影响。

还有一个细节:容器内目录的层级也会影响性能。挂载点越深,层越复杂,overlay2 的路径解析成本越高,虽然量级很小,但在极高频的小文件访问场景下能测出差别。这事不值得为了性能去改目录结构,但值得知道它存在,避免在压测出现微小差异时找错方向。

5.2 镜像瘦身与构建缓存管理

镜像体积直接决定两件事:拉取时间和磁盘占用。一个 2GB 的镜像,在十台机器上就是 20GB,每台机器还有历史版本。控制体积的手段不复杂,难在坚持。

多阶段构建是最有效的一招:编译环境和运行环境分离,最终镜像里只有运行时产物,编译工具链、源码、中间文件统统留在前一个阶段。

# 构建阶段:带完整工具链 FROM golang:1.22 AS builder WORKDIR /src COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -o /out/app ./cmd/app # 运行阶段:只要二进制和必要配置 FROM alpine:3.19 RUN adduser -D -u 10001 appuser WORKDIR /app COPY --from=builder /out/app /app/app USER appuser ENTRYPOINT ["/app/app"]

配套的两个小动作收益很大:写一份.dockerignore把.git、node_modules、测试数据、本地构建产物排掉,避免它们进入构建上下文拖慢传输并污染层缓存;把变化频率低的指令放前面(依赖安装在前,源码复制在后),让层缓存在日常开发中真正命中。

# 看每一层多大,找出真正的大头 docker history --no-trunc --format "{{.Size}}\t{{.CreatedBy}}" myapp:1.0 | head -20 # 构建时不使用缓存,验证从零构建是否真的能成功 docker build --no-cache -t myapp:1.0 . # 定期清构建缓存,保留最近几天 docker builder prune --filter "until=72h"

这里有个容易被误用的习惯:为了"省空间"把--no-cache当成日常构建参数。它会让每次构建都从零开始,缓存全废,磁盘短期是干净了,但构建时间和镜像层重复率都会上升。正确用法是只在验证构建正确性或者怀疑缓存污染时用。

5.3 容量规划的一点经验算法

容量这事我不想给"预留 30%"这种拍脑袋数字,给个能算的口径更有用。一台容器宿主机的磁盘,按这几项加起来再乘个安全系数:

  • 镜像总量:现有镜像层去重后的实际占用,按docker system df的第一行取值,再考虑未来半年的版本增长,通常按 1.5 倍估。
  • 容器可写层:单个容器按 100–500MB 估,如果是写日志到容器内的应用另算。
  • 卷:按业务数据增长曲线估,日志类按日增量乘保留天数。
  • 日志:单容器上限 × 容器数量,即max-size × max-file × 容器数。
  • 构建缓存:按镜像总量的 1–2 倍估,取决于构建频率。
  • 系统与其他:操作系统、软件包、临时文件,留 20GB 起步。

举个具体例子:50 个容器,镜像去重后 25GB,单容器可写层按 300MB 估是 15GB,卷 100GB,日志 300MB × 50 = 15GB,构建缓存按 30GB 估,系统留 20GB,加起来 205GB,乘 1.3 的安全系数约 267GB。这个数比"买块 500G 的盘吧"靠谱得多,也更容易跟非技术同事解释为什么需要这个容量。

提示:算完之后记得单独看 inode 上限。容量够但 inode 不够,一样写不进去,而且加盘不一定能解决,得看文件系统的 inode 分配策略。

6. 我在实际项目里攒下的几条硬经验

存储这块的坑有个共同点:它们都不会立刻报错,而是静静积累,直到某天以一个看似无关的故障形式爆发。所以比起记住多少命令,我更看重几条能落到日常操作里的习惯。

第一条,任何容器在写编排文件时,先问一句"这个容器的数据丢了要不要紧"。要紧就立刻挂卷,不要"先跑起来看看,回头再加"。回头往往是很久以后,而且是数据丢了以后。

第二条,日志配置写在 daemon 层面,不写在单个容器上。逐个容器加参数这种做法的失败率接近百分之百,因为总会有人新加容器时忘了加。

第三条,卷的清理永远手工确认。docker volume prune我基本不用,改成先docker volume ls --filter dangling=true看一眼,确认名字不是"备用""待恢复"这类之后再逐个删。批量清理节省的那点时间,抵不上一次误删带来的损失。

第四条,把docker system df -v加进例行巡检,每周看一次。这个动作花不了一分钟,但能在磁盘告警之前发现异常增长的趋势。等到 95% 再排查,选择余地就很小了,只能先删东西腾空间,问题根因反而没时间找。

第五条,迁移/var/lib/docker这类操作,rsync的-H参数千万别省。overlay2 目录里硬链接非常多,用普通复制会把它们展开成独立文件,容量翻倍不说,Docker 起来之后层引用关系也可能出问题。我见过有人用cp -r搬完,新目录比旧目录大了将近一倍,还以为是复制出错。

最后一条也是最重要的:数据库这类有状态服务,文件级备份只是兜底,主力备份一定是它自己的逻辑导出。文件系统层面的备份在运行中打出来看着是完整的 tar 包,恢复起来才发现数据不一致,这种事故很难在恢复演练之前被发现。所以备份做完一定要演练一次恢复,哪怕只是恢复到一台测试机上验证能起来。没演练过的备份,只能算半个备份。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询