容器挂载完整指南:bind mount / volume / mount propagation / 多视角
容器挂载是企业生产存储事故的头号来源。 应用看到的路径、宿主看到的路径、CSI driver 挂的位置、subPath、mount propagation —— 任何一个细节不对就是几小时排错。
这一篇把容器挂载的所有原语和视角问题讲透。
这篇要回答什么
- Docker
-v /host:/container和--mount type=bind有什么区别? - mount propagation 是什么?
rprivate/rshared/rslave怎么选? - 容器里
/data/foo,节点上实际在哪? - K8s pod 看到的
/etc/config,节点上是哪个文件? - tmpfs / emptyDir / hostPath 在容器和宿主各占多少空间?
- 为什么 multipath / Ceph / NFS volumes 必须开
Bidirectional? - 容器删了,volume 数据还在吗?
1. 三种容器挂载机制
graph TB
subgraph BindMount[bind mount]
B1[宿主某目录]
B2[容器某路径]
B1 ---|"映射"| B2
B3["特点: 双向、改一边都生效"]
end
subgraph NamedVolume[named volume]
V1[Docker 管理目录<br>/var/lib/docker/volumes/...]
V2[容器某路径]
V1 ---|"映射"| V2
V3["特点: Docker 管生命周期、可命名"]
end
subgraph TmpfsMount[tmpfs mount]
T1[内存中]
T2[容器某路径]
T1 -.in memory.-> T2
T3["特点: 在内存里、容器停就没"]
end
style BindMount fill:#e1f5ff
style NamedVolume fill:#ffe1f5
style TmpfsMount fill:#fff4e1
Docker 命令对比
# 1. bind mount(宿主路径直接挂)
docker run -v /host/data:/container/data nginx
docker run --mount type=bind,source=/host/data,target=/container/data nginx
# 2. named volume(Docker 管理)
docker volume create mydata
docker run -v mydata:/container/data nginx
docker run --mount type=volume,source=mydata,target=/container/data nginx
# 3. tmpfs(内存中)
docker run --tmpfs /tmp nginx
docker run --mount type=tmpfs,target=/tmp,tmpfs-size=100m nginx
-v vs --mount 怎么区分
docker run -v /host/data:/container/data ... # 看 `:` 前是否含 `/`
# 含 / → bind mount
# 不含 → named volume
docker run --mount type=bind,source=... # 显式声明类型、更清晰
docker run --mount type=volume,source=...
--mount 更现代、推荐。-v 的 magic 行为是历史包袱。
三种方式对比
| 维度 | bind mount | named volume | tmpfs |
|---|---|---|---|
| 数据位置 | 宿主任意路径 | /var/lib/docker/volumes/... | 内存 |
| 容器停 | 数据保留 | 数据保留 | 数据消失 |
| 跨容器共享 | ✅ | ✅ | ❌ |
| 性能 | 取决于宿主文件系统 | 同左 | 超快(内存) |
| 备份难度 | 中(自己管) | 易(docker volume) | 不适用 |
| 适合场景 | 配置文件 / 开发挂代码 | 数据库 / 持久化 | secrets / 临时计算 |
2. bind mount 详解 —— Linux 原生概念
bind mount 不是 Docker 发明的——是 Linux mount(2) 的功能。
# 宿主上自己玩 bind mount
mkdir /data
mount --bind /var/log /data
# 现在 /data 看到的内容 = /var/log
ls /data # 等同 ls /var/log
# 改一边另一边可见
touch /data/test.log
ls /var/log/test.log # 存在!
umount /data # 解除 bind
ls /data # 空
Docker bind mount 就是用这个机制——把宿主某目录"映射"到容器内的 mount namespace。
多视角图(关键)
flowchart TB
subgraph HostFS[宿主真实文件系统]
HostPath["/var/log/myapp/<br>(真实存在)"]
end
subgraph HostNS[节点 root mount namespace]
Host1["/var/log/myapp/foo.log"]
end
subgraph ContNS[容器 mount namespace]
Cont1["/app/logs/foo.log"]
end
HostFS --> HostNS
HostNS -.bind mount.-> ContNS
style HostFS fill:#e1ffe1
style HostNS fill:#e1f5ff
style ContNS fill:#ffe1f5
容器里写 /app/logs/foo.log、节点上立刻在 /var/log/myapp/foo.log 出现。同一份数据、两个视角。
反面:容器内写、宿主看不到
docker run -d --name myapp nginx
docker exec myapp sh -c 'echo "in container" > /tmp/x.txt'
# 宿主上找
find / -name x.txt 2>/dev/null
# /var/lib/docker/overlay2/.../merged/tmp/x.txt ← 在容器 overlay 层
没 bind mount 时——容器写的文件在 overlay 上层(容器删了就没)。要持久化必须显式 bind。
3. /proc/$PID/mounts —— 看容器视角的挂载
容器有自己的 mount namespace、所以看到的 mount 表不一样:
# 宿主上看节点 root ns 的 mount
$ cat /proc/1/mounts | wc -l
145
# 看某容器的 mount
$ PID=$(crictl inspect $(crictl ps -q --name my-pod) | jq '.info.pid')
$ cat /proc/$PID/mounts | wc -l
25 ← 比节点少很多
# 看具体 mount
$ cat /proc/$PID/mounts | head -10
overlay / overlay rw,relatime,lowerdir=...,upperdir=...,workdir=... 0 0
← 这是 overlay 根
proc /proc proc rw,nosuid,nodev,noexec,relatime 0 0
tmpfs /dev tmpfs rw,nosuid,size=65536k,mode=755 0 0
sysfs /sys sysfs ro,nosuid,nodev,noexec,relatime 0 0
# 后面是 bind mount 来的 volumes:
/dev/sda3 /etc/hosts ext4 ... ← K8s 给的 /etc/hosts
/dev/sda3 /etc/hostname ext4 ... ← K8s 给的 /etc/hostname
/dev/sda3 /etc/resolv.conf ext4 ... ← K8s 给的 DNS 配置
/dev/sda3 /app/config configMap ... ← ConfigMap 挂的
K8s pod 默认挂上来一堆系统配置文件(hosts / hostname / resolv.conf)— 这些都是 bind mount、节点上有真实文件。
用 findmnt 看更清晰
$ nsenter -t $PID -m findmnt
TARGET SOURCE FSTYPE OPTIONS
/ overlay overlay rw,relatime
├─/dev tmpfs[/dev/null] tmpfs rw,nosuid
├─/sys sysfs sysfs ro
├─/sys/fs/cgroup cgroup cgroup ro
├─/proc proc proc rw
├─/etc/resolv.conf /dev/sda3[/var/lib/kubelet/.../resolv.conf]
├─/etc/hostname /dev/sda3[/var/lib/kubelet/.../hostname]
├─/etc/hosts /dev/sda3[/var/lib/kubelet/.../hosts]
├─/app/config /dev/sda3[/var/lib/kubelet/.../configmap]
└─/data /dev/sdb1[/mnt/data]
/dev/sda3[/var/lib/kubelet/.../...] 这种形式说明 是 bind mount:sda3 上的某子路径被 bind 到容器的路径。
4. 节点上找容器文件的真实位置
经典场景:节点磁盘满、想知道是哪个 pod 占的。
# K8s pod 的数据存在
/var/lib/kubelet/pods/⟨pod-uid⟩/
# 看具体目录结构
$ ls /var/lib/kubelet/pods/abc-uid/
containers/ ← 容器日志 / 状态
etc-hosts ← /etc/hosts
plugins/ ← CSI 用
volumes/ ← 重点:所有 volume
$ ls /var/lib/kubelet/pods/abc-uid/volumes/
kubernetes.io~configmap/
kubernetes.io~secret/
kubernetes.io~empty-dir/
kubernetes.io~projected/
kubernetes.io~csi/ ← CSI volume
kubernetes.io~nfs/ ← NFS volume
找占空间最多的 pod volume
$ du -sh /var/lib/kubelet/pods/*/volumes/* 2>/dev/null | sort -hr | head
20G /var/lib/kubelet/pods/abc/volumes/kubernetes.io~empty-dir/cache
5G /var/lib/kubelet/pods/def/volumes/kubernetes.io~csi/pvc-xyz
1G /var/lib/kubelet/pods/ghi/volumes/kubernetes.io~configmap/app-config
# 反查 abc 是哪个 pod
$ kubectl get pods -A -o jsonpath='{range .items[*]}{.metadata.uid} {.metadata.namespace}/{.metadata.name}{"\n"}{end}' | grep "^abc"
abc... default/my-noisy-app
emptyDir 占 20G 就是这个 pod 写爆了的。
5. Mount Propagation —— 必学
mount propagation 决定一个 mount namespace 里的新 mount、对其他 namespace 是否可见。
| 模式 | 含义 |
|---|---|
private | 完全隔离,节点上有新 mount 容器看不到 |
shared | 双向共享,节点 mount 容器看到、容器 mount 节点也看到 |
slave | 节点 → 容器单向(容器看到节点的、节点看不到容器的) |
unbindable | 罕用 |
每个有"递归版"(前缀 r):rprivate / rshared / rslave —— 递归对子 mount 也应用。
# Docker bind mount 默认 rprivate
docker run -v /mnt:/data nginx
# /data 是 private、节点上后续在 /mnt/subdir mount 一个 NFS、容器看不到
volumeMounts:
- name: data
mountPath: /data
mountPropagation: Bidirectional # 等价 rshared
# 默认值: None(等价 rprivate)
# 还可: HostToContainer(等价 rslave)
K8s 三种选项:
| K8s | Linux | 含义 |
|---|---|---|
None(默认) | rprivate | 隔离 |
HostToContainer | rslave | 宿主 → 容器单向 |
Bidirectional | rshared | 双向 |
实战例子(为什么这个重要)
# CSI driver pod
spec:
containers:
- name: csi-driver
volumeMounts:
- name: kubelet-dir
mountPath: /var/lib/kubelet
mountPropagation: Bidirectional # ← **必须**!
volumes:
- name: kubelet-dir
hostPath:
path: /var/lib/kubelet
spec:
containers:
- name: node-exporter
volumeMounts:
- name: rootfs
mountPath: /host
mountPropagation: HostToContainer # rslave
readOnly: true
volumes:
- name: rootfs
hostPath:
path: /
volumeMounts:
- name: data
mountPath: /data
# propagation: None(默认)
volumes:
- name: data
hostPath:
path: /mnt
节点上验证 propagation
$ cat /proc/self/mountinfo | grep /data
123 45 0:67 / /data rw,relatime shared:42 - tmpfs tmpfs rw
^^^^^^^^^^
propagation 信息
| 字符 | 含义 |
|---|---|
shared:N | 在 share group N(双向) |
master:N | slave、master 是 N |
| 没标记 | private |
6. K8s pod 内的特殊 mount
K8s 给每个 pod 自动挂的东西:
$ kubectl exec test-pod -- mount | head
overlay on / type overlay (rw,relatime,lowerdir=...,upperdir=...,workdir=...)
proc on /proc type proc (rw,nosuid,nodev,noexec,relatime)
tmpfs on /dev type tmpfs (rw,nosuid,size=65536k,mode=755)
devpts on /dev/pts type devpts (rw,nosuid,noexec,relatime,...)
sysfs on /sys type sysfs (ro,nosuid,nodev,noexec,relatime)
# 系统配置(来自节点 bind mount)
/dev/sda3 on /etc/hosts type ext4 (rw,relatime)
/dev/sda3 on /etc/hostname type ext4 (rw,relatime)
/dev/sda3 on /etc/resolv.conf type ext4 (rw,relatime)
# 服务账户 token(projected)
tmpfs on /var/run/secrets/kubernetes.io/serviceaccount type tmpfs
# 用户定义的 volumes
/dev/sdb1 on /data type ext4 (rw,relatime)
Service Account Token —— 投射卷
$ kubectl exec test-pod -- ls /var/run/secrets/kubernetes.io/serviceaccount/
ca.crt
namespace
token
$ kubectl exec test-pod -- cat /var/run/secrets/kubernetes.io/serviceaccount/token | head -c 50
eyJhbGciOiJSUzI1NiIsImtpZCI6IjFs...
这是 pod 用来调 apiserver 的 token。是 tmpfs(不会落盘)。
详见 02-k8s-volumes.md projected 章节。
7. 路径视角对照表(核心收藏)
K8s:
volumes:
- name: app-config
configMap:
name: my-config
containers:
- name: app
volumeMounts:
- name: app-config
mountPath: /app/config
四个视角:
| 视角 | 路径 |
|---|---|
| 应用代码 | /app/config/foo.yaml |
| 容器 mount ns | /app/config/foo.yaml |
| 节点 bind 源 | /var/lib/kubelet/pods/⟨uid⟩/volumes/kubernetes.io~configmap/app-config/foo.yaml |
| 物理盘 | 节点根分区的某 ext4 文件 |
类似 ConfigMap、但是 tmpfs(内存):
$ findmnt /var/run/secrets/...
TARGET SOURCE FSTYPE OPTIONS
/var/lib/kubelet/pods/⟨uid⟩/volumes/.../secret tmpfs tmpfs rw,relatime
写盘怕泄漏、所以 K8s 把 Secret 放 tmpfs。
K8s:
volumes:
- name: cache
emptyDir: {}
| 视角 | 路径 |
|---|---|
| 应用代码 | /cache/foo |
| 容器 mount ns | /cache/foo |
| 节点 bind 源 | /var/lib/kubelet/pods/⟨uid⟩/volumes/kubernetes.io~empty-dir/cache/foo |
| 物理盘 | 节点根分区(或 medium: Memory 时是 tmpfs) |
emptyDir.medium: Memory 改成内存盘(会占用 pod 的 memory limit):
volumes:
- name: shm
emptyDir:
medium: Memory
sizeLimit: 1Gi
K8s:
volumes:
- name: data
persistentVolumeClaim:
claimName: my-pvc
| 视角 | 路径 |
|---|---|
| 应用代码 | /data/foo |
| 容器 mount ns | /data/foo |
| 节点 CSI mount 点 | /var/lib/kubelet/plugins/.../mount/foo |
| 节点 bind 给 pod | /var/lib/kubelet/pods/⟨uid⟩/volumes/kubernetes.io~csi/<pvname>/mount/foo |
| 底层设备 | 云盘 / NFS server / 块设备 |
K8s PV 是两层 bind——CSI 先挂到全局位置、kubelet 再 bind 给 pod。
K8s:
volumeMounts:
- name: data
mountPath: /etc/config.yaml
subPath: config.yaml # ← 只挂 volume 里某子文件
| 视角 | 路径 |
|---|---|
| 应用代码 | /etc/config.yaml(是文件、不是目录) |
| 节点 bind 源 | /var/lib/kubelet/pods/⟨uid⟩/volumes/.../config.yaml |
subPath 只挂 volume 里的某个子路径到容器某个特定位置。常见用法:把 ConfigMap 里一个 key 挂成 /etc/<file> 而不替换整个 /etc。
8. 反面教材
反面 1:bind mount 覆盖了容器内已有内容
docker run -v /tmp/empty:/usr/bin nginx
# nginx 启动失败:找不到 /usr/bin/nginx
bind mount 遮盖目标路径的原有内容。/usr/bin 被替换成空目录、nginx 二进制找不到。
修:永远 mount 到不存在或允许覆盖的路径。
反面 2:subPath 不感知 ConfigMap 更新
volumeMounts:
- name: config
mountPath: /etc/app.conf
subPath: app.conf # ← 用了 subPath
# 更新 ConfigMap、pod 看不到新内容!
subPath 时 ConfigMap / Secret 更新不自动同步——直到 pod 重启。
修:
- 不用 subPath,整个目录挂
- 或者依赖 pod 重启(Helm 模板里加 annotation hash)
反面 3:容器挂宿主 /proc、/sys、/var/run/docker.sock
volumes:
- name: docker-sock
hostPath:
path: /var/run/docker.sock
容器拿到 docker.sock = 容器能控制宿主 docker daemon = 宿主 root。CI / 监控工具有时这么做、但业务 pod 绝对不能。
K8s 安全审计的常见红线项。
反面 4:emptyDir 在 OOM 后丢
volumes:
- name: cache
emptyDir: {}
pod 用 emptyDir 当"持久缓存"。pod 重启容器 → 数据保留;pod 删除 → 数据没。
K8s 节点 reboot / 调度 / OOM evict → pod 重新创建 → 视为新 pod → emptyDir 空。
要"真持久" → PVC。
反面 5:mountPropagation 漏配让 CSI 不工作
# CSI driver pod
volumeMounts:
- name: kubelet-dir
mountPath: /var/lib/kubelet
# mountPropagation: 没配!(默认 None)
业务 pod 的 PVC mount 失败、报错:
MountVolume.WaitForAttach failed for volume "pvc-xxx" : timeout
实际是 CSI 在容器里 mount 了、但节点 ns 看不到。修:加 mountPropagation: Bidirectional。
详见 §5。
反面 6:hostPath + Local PV 没固定节点
volumes:
- name: data
hostPath:
path: /mnt/data
pod 调度到不同节点 → 看到不同节点的 /mnt/data(数据完全不一样)。
要"绑定到特定节点的本地存储" → 用 Local PV + nodeAffinity,详见 03-pv-pvc-storageclass.md.
反面 7:tmpfs 占满 pod memory
volumes:
- name: shm
emptyDir:
medium: Memory # ← tmpfs
resources:
limits:
memory: 1Gi
tmpfs 写满 = 占 pod memory limit。1Gi limit + tmpfs 写 800Mi → pod OOMKilled、即使应用本身没占多少。
修:
sizeLimit限制 tmpfs 大小:emptyDir.sizeLimit: 200Mi- 或者用磁盘 emptyDir(默认)
9. 排查 cheatsheet
"容器看不到我挂的卷"
flowchart TD
Start[容器看不到 volume] --> S1{1. volume 在<br>pod spec 里?}
S1 -->|否| F1[加 volumes + volumeMounts]
S1 -->|是| S2{2. mountPath 正确?}
S2 -->|否| F2[改 mountPath]
S2 -->|是| S3{3. mount Propagation<br>对吗?}
S3 -->|None 但要双向| F3[改 Bidirectional]
S3 -->|对| S4{4. 节点 bind 源<br>真有数据?}
S4 -->|否| F4[CSI / hostPath 问题]
S4 -->|是| S5[查 subPath / readOnly]
关键命令
# 1. 看 pod 实际挂的 volumes
kubectl describe pod my-pod | grep -A 30 Mounts
# 2. 看容器视角的 mount
PID=$(crictl inspect $(crictl ps -q --name my-pod) | jq '.info.pid')
nsenter -t $PID -m findmnt
# 3. 看节点上对应的 bind 源
ls /var/lib/kubelet/pods/⟨pod-uid⟩/volumes/
# 4. 看 mount propagation
cat /proc/$PID/mountinfo | grep <path>
# 找 shared/master/private 标记
# 5. 文件级权限
nsenter -t $PID -m ls -la /data
# 容器内文件 owner / mode 是不是匹配应用预期
"节点磁盘满 - 是不是 pod 占的"
# 哪个 pod 的 volume 占空间
du -sh /var/lib/kubelet/pods/*/volumes/* 2>/dev/null | sort -hr | head
# emptyDir 是不是写爆
du -sh /var/lib/kubelet/pods/*/volumes/kubernetes.io~empty-dir/* | sort -hr | head
# overlay 层(镜像)是不是太多
du -sh /var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/* | sort -hr | head
crictl rmi --prune
10. 下一步
| 篇 | 内容 |
|---|---|
| 00-storage-mental-model.md | 存储心智模型 |
| 本篇 | 容器挂载 + mount propagation |
| 02-k8s-volumes.md | K8s Volume 类型大全 + subPath |
| 03-pv-pvc-storageclass.md | PV / PVC / CSI |
| 04-nfs-deep.md | NFS |
| 05-distributed-storage.md | Ceph / Longhorn |
| 06-storage-troubleshooting.md | 故障排查 runbook |
相关命令
- mount / umount —— Linux mount 基础
- findmnt —— 看挂载树
- nsenter —— 进容器 ns 看视角
- crictl —— 找 pod 容器 PID
- lsblk —— 看块设备