AI Infra 训练营
总览
  • Day 1 · 集群起步 + CNI
  • Day 2 · 控制面 + etcd
  • Day 3 · CRD + Operator + Webhook
  • Day 4 · 存储深度
  • Day 5 · 卷扩容 + 安全
  • Day 6 · 调度 + 可观测
  • Day 7 · Harbor + ArgoCD + Mesh
  • Day 8 · AI Infra
  • Day 9 · Triton + GPU
  • Day 10 · MIG + HPA + 量化
  • Day 11 · AI Agent 端到端
  • Day 12 · 灾备
  • Day 13 · Operator + 联邦 + Mesh + RAG
  • Day 14 · CKA / CKS + 总结
  • LLM 训练手册
  • RAG + Agent 手册
  • 推理优化手册
  • 上下文工程手册
  • Agent 开发手册
  • 面试深度复盘
  • 训练 v2 深度手册
  • 心智模型
  • 看懂命令输出
  • 容器网络底层
  • K8s 网络深入
  • DNS 全套
  • 故障排查方法论
  • 心智模型
  • 容器挂载完整指南
  • K8s Volumes 大全
  • PV/PVC/CSI 深入
  • NFS 深入
  • 分布式存储概览
  • 故障排查 runbook
HiHuo 主站
GitHub
总览
  • Day 1 · 集群起步 + CNI
  • Day 2 · 控制面 + etcd
  • Day 3 · CRD + Operator + Webhook
  • Day 4 · 存储深度
  • Day 5 · 卷扩容 + 安全
  • Day 6 · 调度 + 可观测
  • Day 7 · Harbor + ArgoCD + Mesh
  • Day 8 · AI Infra
  • Day 9 · Triton + GPU
  • Day 10 · MIG + HPA + 量化
  • Day 11 · AI Agent 端到端
  • Day 12 · 灾备
  • Day 13 · Operator + 联邦 + Mesh + RAG
  • Day 14 · CKA / CKS + 总结
  • LLM 训练手册
  • RAG + Agent 手册
  • 推理优化手册
  • 上下文工程手册
  • Agent 开发手册
  • 面试深度复盘
  • 训练 v2 深度手册
  • 心智模型
  • 看懂命令输出
  • 容器网络底层
  • K8s 网络深入
  • DNS 全套
  • 故障排查方法论
  • 心智模型
  • 容器挂载完整指南
  • K8s Volumes 大全
  • PV/PVC/CSI 深入
  • NFS 深入
  • 分布式存储概览
  • 故障排查 runbook
HiHuo 主站
GitHub
  • 存储专题

    • 存储心智模型:从一次 write() 到磁盘
    • 容器挂载完整指南:bind mount / volume / mount propagation / 多视角
    • K8s Volume 类型大全 + subPath / 多视角
    • PV / PVC / StorageClass / CSI 深入
    • NFS 深入:server / client / K8s NFS / 经典坑
    • 分布式存储概览:Ceph / Longhorn / Rook
    • 存储故障排查 runbook

容器挂载完整指南:bind mount / volume / mount propagation / 多视角

容器挂载是企业生产存储事故的头号来源。 应用看到的路径、宿主看到的路径、CSI driver 挂的位置、subPath、mount propagation —— 任何一个细节不对就是几小时排错。

这一篇把容器挂载的所有原语和视角问题讲透。

这篇要回答什么

  1. Docker -v /host:/container 和 --mount type=bind 有什么区别?
  2. mount propagation 是什么?rprivate / rshared / rslave 怎么选?
  3. 容器里 /data/foo,节点上实际在哪?
  4. K8s pod 看到的 /etc/config,节点上是哪个文件?
  5. tmpfs / emptyDir / hostPath 在容器和宿主各占多少空间?
  6. 为什么 multipath / Ceph / NFS volumes 必须开 Bidirectional?
  7. 容器删了,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 mountnamed volumetmpfs
数据位置宿主任意路径/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 是否可见。

4 种 propagation
模式含义
private完全隔离,节点上有新 mount 容器看不到
shared双向共享,节点 mount 容器看到、容器 mount 节点也看到
slave节点 → 容器单向(容器看到节点的、节点看不到容器的)
unbindable罕用

每个有"递归版"(前缀 r):rprivate / rshared / rslave —— 递归对子 mount 也应用。

Docker / containerd 默认
# Docker bind mount 默认 rprivate
docker run -v /mnt:/data nginx
# /data 是 private、节点上后续在 /mnt/subdir mount 一个 NFS、容器看不到
K8s mountPropagation 字段
volumeMounts:
  - name: data
    mountPath: /data
    mountPropagation: Bidirectional   # 等价 rshared
    # 默认值: None(等价 rprivate)
    # 还可: HostToContainer(等价 rslave)

K8s 三种选项:

K8sLinux含义
None(默认)rprivate隔离
HostToContainerrslave宿主 → 容器单向
Bidirectionalrshared双向

实战例子(为什么这个重要)

场景 - CSI driver pod
# 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
场景 - 监控代理看 host volumes
spec:
  containers:
    - name: node-exporter
      volumeMounts:
        - name: rootfs
          mountPath: /host
          mountPropagation: HostToContainer    # rslave
          readOnly: true
  volumes:
    - name: rootfs
      hostPath:
        path: /
反面 - 不开 propagation 导致 stale view
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:Nslave、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. 路径视角对照表(核心收藏)

ConfigMap

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 文件
Secret

类似 ConfigMap、但是 tmpfs(内存):

$ findmnt /var/run/secrets/...
TARGET                                          SOURCE  FSTYPE   OPTIONS
/var/lib/kubelet/pods/⟨uid⟩/volumes/.../secret  tmpfs   tmpfs    rw,relatime

写盘怕泄漏、所以 K8s 把 Secret 放 tmpfs。

emptyDir

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
PVC

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。

subPath

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。

详见 02-k8s-volumes.md.


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.mdK8s Volume 类型大全 + subPath
03-pv-pvc-storageclass.mdPV / PVC / CSI
04-nfs-deep.mdNFS
05-distributed-storage.mdCeph / Longhorn
06-storage-troubleshooting.md故障排查 runbook

相关命令

  • mount / umount —— Linux mount 基础
  • findmnt —— 看挂载树
  • nsenter —— 进容器 ns 看视角
  • crictl —— 找 pod 容器 PID
  • lsblk —— 看块设备
在 GitHub 上编辑此页
Prev
存储心智模型:从一次 write() 到磁盘
Next
K8s Volume 类型大全 + subPath / 多视角