跳到主要内容

2 篇博文 含有标签「运维」

查看所有标签

Docker 卷挂载后是空的?etcd 数据全在可写层,容器重建即丢

· 阅读需 6 分钟

磁盘清理时用 docker ps -as 巡检各容器可写层,etcd 容器赫然 385M——而它挂载的数据卷 etcd_data 里只有 8K。当时磁盘清理没敢动它:一旦 recreate,Milvus 的全部元数据就随可写层一起蒸发。

在开发 AI客服 时遇到此问题——7×24小时AI客服,快速解答产品使用问题;它的知识库检索跑在 Milvus 上,而 Milvus 的元数据全靠这个 etcd。

TL;DR

compose 的 volumes: 只负责「把卷挂到容器某个路径」,应用往哪写由它自己的参数决定。etcd 没设 data-dir 时默认写到工作目录下的 default.etcd——挂载点 /etcd 它根本没用,385M 数据全在可写层,docker compose up -d 的任何一次重建都会把它带走。修复用 etcd 原生迁移路径:在线快照 → restore → 停机换数据 → 补 ETCD_DATA_DIR → 带卷重建,零丢失,全程停机不到两分钟。

问题现象

compose 里的 etcd 服务看起来是「正确」的——卷挂了:

  etcd:
image: quay.io/coreos/etcd:v3.5.5
environment:
- ETCD_AUTO_COMPACTION_MODE=revision
- ETCD_LISTEN_CLIENT_URLS=http://0.0.0.0:2379
# ... 没有任何 data-dir 相关配置
volumes:
- etcd_data:/etcd

但两个数字对不上:

$ docker ps -as --format "{{.Names}}\t{{.Size}}" | grep etcd
rag-service-etcd-1 385MB (virtual 199MB) ← 可写层 385M

$ docker exec rag-service-etcd-1 du -sh /etcd
8K /etcd ← 挂载的卷是空的

$ docker exec rag-service-etcd-1 ls -la / | grep etcd
drwx------ 3 root root 4096 default.etcd ← 数据在这:容器根目录

根因

挂载 ≠ 使用。 volumes: etcd_data:/etcd 只保证「卷被挂到 /etcd 这个路径」,至于 etcd 往哪写,由它自己的 --data-dir 参数决定。etcd 的缺省 data-dir 是工作目录下的 default.etcd——这份 compose 既没设 ETCD_DATA_DIR 环境变量,也没传 --data-dir 命令行参数,于是 etcd 在容器根目录自建了 default.etcd,把全部数据写进了可写层。挂载点 /etcd 从第一天起就是空的。

这类「卷是摆设」最阴险的地方在于平时完全无症状:服务正常跑、数据正常读写、监控全绿。只有当你需要 docker compose up -d --force-recreate(换配置、回收可写层、迁移宿主机)的那一刻,可写层被整体丢弃,数据才消失——而那通常是你在做紧急运维、最不需要第二起事故的时候。

解决方案

用 etcd 原生的快照迁移路径,在线快照保证一致性,短暂停机只出现在最后的数据切换。

步骤 1:在线一致性快照

docker exec rag-service-etcd-1 sh -c \
'ETCDCTL_API=3 etcdctl --endpoints=http://127.0.0.1:2379 snapshot save /tmp/etcd-snap.db'
docker cp rag-service-etcd-1:/tmp/etcd-snap.db /root/etcd-snap.db

snapshot save 对运行中的 etcd 是安全的(不走文件复制,走 Raft 后端快照),不停机、不锁写。

步骤 2:restore 成目标 data-dir 结构

docker run --rm -v /root:/host quay.io/coreos/etcd:v3.5.5 \
etcdctl snapshot restore /host/etcd-snap.db --data-dir /host/etcd-restored

restore 会生成带完整 member/ 结构的数据目录(快照本身不能直接当 data-dir 用)。

步骤 3:停机切换,数据入卷

docker stop rag-service-etcd-1
rm -rf /var/lib/docker/volumes/etcd_data/_data/* # 卷是空的,清掉挂载残留
cp -a /root/etcd-restored/. /var/lib/docker/volumes/etcd_data/_data/

步骤 4:补上缺失的配置,带卷重建

  etcd:
environment:
- ETCD_DATA_DIR=/etcd # ← 缺的就是这一行
# ...
volumes:
- etcd_data:/etcd
docker compose up -d etcd    # 配置变更触发 recreate,新容器以卷为 data-dir

步骤 5:验证

docker exec rag-service-etcd-1 etcdctl --endpoints=http://127.0.0.1:2379 endpoint health
du -sh /var/lib/docker/volumes/etcd_data/_data # 数据应在卷里
docker ps -as | grep etcd # 可写层应回落到 KB 级

本次修复后:可写层 385M → 8KB,数据落卷 123M,上游 Milvus 自动重连、元数据读写正常。

注意事项

  • 给 stateful 容器(etcd/postgres/redis/minio)做巡检时,把「可写层大小 vs 卷大小」当固定对账项:docker ps -as 的 SIZE 一栏异常膨胀而卷很空,几乎必然是数据没落卷。
  • 判断「数据在哪」要追进程实际读写的路径docker top 看参数、进容器找数据目录),不能只看 compose 有没有 volumes 行——有挂载不等于被使用。
  • 单节点 etcd 的快照 restore 会重建 member 元数据,只适用于单节点场景;多节点集群的迁移请走成员变更流程。
  • 同类排查方法论见 容器日志吃满服务器磁盘?docker system df 的 reclaimable 是误报——docker ps -as 可写层观察是同一把刀;挂载路径被覆盖的另一形态见 Docker Volume 覆盖 Bind Mount

常见问题

Docker 挂载卷后目录是空的怎么回事?

两种常见原因:一是空卷挂载遮盖了镜像内原有目录(Docker 的既有行为);二是应用的数据目录参数根本没指向挂载点,数据写进了容器可写层——本文的 etcd 案例就是这种。用 du 分别看卷目录和容器可写层的大小,一眼可辨。

etcd 数据怎么安全迁移到新的 data-dir?

etcdctl snapshot save 在线做一致性快照(不停机),etcdctl snapshot restore --data-dir 生成带 member 结构的目标目录,停容器后把数据放到新位置、以新 data-dir 启动,最后 endpoint health 加上游重连验证。全程停机只在切换那一段。

compose 里挂了卷为什么没生效?

volumes: 只把卷挂到容器的某个路径;应用写哪里由它自己的配置决定——etcd 看 data-dir,postgres 看 PGDATA,redis 看配置文件里的 dir。挂载点和应用配置对不上,卷就只是个空目录,数据全在可写层,重建即丢。

CCLEE

独立开发者,24年电商行业实战经验,专注将AI能力落地于真实商业场景。

合作咨询

容器日志吃满服务器磁盘?docker system df 的 reclaimable 是误报

· 阅读需 7 分钟

告警邮件:生产服务器根分区用到 81%(30G/40G,超过 80% 红线)。第一反应是查 docker system df——它显示 images 一栏 RECLAIMABLE 100% (7),看起来删掉镜像就能回血;但这 7 个镜像全在运行,真按提示 docker image prune -a 就是生产事故。

在开发 AI运营 时遇到此问题——基于大语言模型的智能分析平台,自动洞察市场趋势、用户行为与销售数据;出问题的是跑它 Airflow 数据管道的 Docker 服务器。

TL;DR

磁盘超红线时别信 docker system df 的 RECLAIMABLE——它是「无容器引用空间」的估算口径,active 镜像照样标 100%。正确姿势:sudo du -xh -d1 / 逐层找大头。这次的三大隐形消耗都不在业务数据里:容器可写层里无限累积的 task 日志(Dockerfile 没给日志配卷)、包管理器缓存(npm + pnpm 共 ~4G)、只增不删的备份脚本(每次全量克隆从不清理)。对应清理:缓存直删、可写层 force-recreate 回收、备份脚本加 TTL 剪枝——81% 回落到 64%。

问题现象

$ df -h /
Filesystem Size Used Use% Mounted on
/dev/vda1 40G 30G 81% /

$ docker system df
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 7 7 4.2GB 100% (7) ← 全是运行中镜像
Containers 5 5 810MB 0%

docker system df 的提示去做文章(清理镜像)无从下手——RECLAIMABLE 100% 但 ACTIVE 也是 7/7。真正的空间去哪了,docker system df 完全没体现:

sudo du -xh -d1 / | sort -rh | head
# 部分输出:
# 2.7G /root/.npm ← npm 缓存
# 1.1G /root/.local/share/pnpm ← pnpm store
# 857M /root/backups-git ← 备份工作区,只增不删
# (overlay2 内藏:airflow scheduler 可写层 468M + dag-processor 257M)

根因

三类消耗在「业务数据」视角里全部隐形。

容器可写层吃日志。 Airflow 的 task 日志写在容器内部、没挂外置卷——scheduler 可写层 468M、dag-processor 257M,约两周累积 780M,随时间单调增长。镜像层不变,变的都是可写层;docker system df 把它归在 Containers 的 SIZE 里,混在 810M 的总数里毫无存在感。

包管理器缓存只进不出。 频繁部署/构建的服务器上,~/.npm(2.7G)和 pnpm store(1.1G)持续累积,没有人会主动去清。

备份脚本只增不删。 备份脚本每次全量克隆仓库工作区推 GitHub,旧的克隆目录从不剪枝——857M 里大部分是历史重复。更隐蔽的是孤本:某些备份目录对应的分支从未推送成功,直接删有风险,需要先核对。

docker system df 的 RECLAIMABLE 是按「没有运行容器引用」估算的统计口径,active 镜像也可能显示 100% reclaimable(本次 7/7 全如此)——它是误报来源,不是清理依据。

解决方案

步骤 1:du 逐层定位,先看再删

sudo du -xh -d1 / | sort -rh | head        # 根分区逐层
sudo du -xh -d1 /var/lib/docker | sort -rh | head # Docker 目录下钻
docker ps -as --format "table {{.Names}}\t{{.Size}}" # 各容器可写层

du -x 不跨文件系统,避开 proc/sys 噪音和 overlay 混淆;docker ps -as 的 SIZE 列直接暴露每个容器的可写层大小——这是定位「日志写进容器」类问题的关键一条。

步骤 2:缓存直删

npm cache clean --force        # 或直接 rm -rf ~/.npm/_cacache
pnpm store prune # 只清未引用的包

缓存类共回收 ~5.1G,无风险、可随时重下。

步骤 3:可写层 force-recreate 回收

docker compose up -d --force-recreate   # 可写层随旧容器删除而回收

本次回收 ~780M。两个前提:选低峰期(服务会重启);容器内还有用要先捞出来——比如 docker cp 把 task 日志拷出,否则随容器一起没了。

步骤 4:备份脚本加 TTL,防复发

# 备份目录保留 7 天,超过自动剪
find /root/backups-git -maxdepth 1 -type d -mtime +7 -exec rm -rf {} +

把剪枝加进备份脚本尾部(本例 github-backup-push.sh 已部署);删除前核对未推送的孤本分支(git ls-remote 对比),确认无独有提交再删——本次 325M 孤本经确认后清理。

清理完成后:81% → 64%,缓存/剪枝/可写层三处合计回收 ~6.7G。

注意事项

  • docker system df 的 RECLAIMABLE ≠ 可删空间:active 镜像也可能标 100%(本次 7 个全在用)。清理决策以 du + docker ps -as 实测为准。
  • force-recreate 会重启服务且回收可写层——需要留存的日志先 docker cp 出来;根治是给日志配外置卷 + 保留期轮转,可写层回收只是这一次的止血。
  • 删备份目录前必核对:有没有从未推送成功的孤本分支,删之前 git ls-remote 确认。
  • 磁盘红线告警要配「定位命令」一起给,收到告警的人第一件事是 du -xh -d1 /,而不是猜。
  • 同一台服务器更早的「磁盘 93% + CPU 160%」排查复盘见 2 核 7G 服务器 Docker 资源黑洞三步排查

常见问题

docker system df 的 RECLAIMABLE 显示 100% 能直接删吗?

不能。RECLAIMABLE 是「当前无容器引用」空间的估算口径,运行中的 active 镜像也可能被标 100% reclaimable——实测 7 个镜像全在用仍显示如此。它回答的是「理论上有多少未被引用」,不回答「能不能删」。动手前用 du -xh -d1docker ps -as 实测定位。

docker ps 的 SIZE 和 docker system df 的 SIZE 有什么区别?

docker ps -as 的 SIZE 是单个容器可写层的大小;docker system df 是镜像/容器/卷/缓存的分类汇总。容器内日志无限累积只体现在可写层(docker ps -as 能看到),不会体现在任何 image size 上——这就是「镜像看起来不大、磁盘却在涨」的原因。

Docker 服务器磁盘满了怎么清理?

分三类:包管理缓存直删(npm cache clean --forcepnpm store prune),无风险;容器可写层用 docker compose up -d --force-recreate 回收(会重启服务,日志先备份);备份、日志、克隆目录这类加 TTL 定期剪枝防复发。任何删除动作前,先用 du -xh -d1 / 确认大头位置。

CCLEE

独立开发者,24年电商行业实战经验,专注将AI能力落地于真实商业场景。

合作咨询