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

K8s Volume 类型大全 + subPath / 多视角

K8s 提供了 十几种 volume 类型,每个有自己的语义、生命周期、视角问题。这一篇把它们全部覆盖——并对每一个都给"应用看到啥、节点 host 看到啥"的对照。

Volume 类型选错 = 数据丢失 / 性能踩坑 / 配置失效。

这篇要回答什么

  1. emptyDir / hostPath / configMap / secret / projected / downwardAPI / PVC / ephemeral 各是干什么的?
  2. subPath / subPathExpr 解决什么问题?什么时候用?
  3. configMap 改了为啥 pod 看不到?什么时候才能看到?
  4. Secret 是不是真的"加密"?K8s 给你的"安全感"有多少?
  5. projected volume 是怎么把 ConfigMap + Secret + token 合一起的?
  6. downwardAPI 怎么把 pod label / annotation 注入容器?
  7. CSI volume 在 K8s 是怎么运转的(简介,详见 03-pv-pvc-storageclass.md)

1. Volume 全景图

mindmap
  root((K8s Volume))
    临时类
      emptyDir
      tmpfs
      ephemeral
    Host 节点类
      hostPath
      Local PV
    配置类
      configMap
      secret
      downwardAPI
      projected
    持久类
      PVC (CSI)
      NFS
      iSCSI
      Ceph RBD/FS
    特殊
      gitRepo (废)

按生命周期分类(这是选型的根本):

类别生命周期例子
Pod 级跟 pod 同存亡emptyDir / configMap / secret / projected / downwardAPI
节点级跟节点同存亡(pod 走了数据还在)hostPath
集群级独立生命周期、跨 pod / 跨节点persistentVolumeClaim (CSI / NFS / Ceph)

第一原则:emptyDir / hostPath 不是"持久"

emptyDir pod 删 = 数据没。 hostPath 节点上跑 = 数据在;节点重启 / pod 重调度到别的节点 = 看不到旧数据。

要持久 → PVC。


2. emptyDir —— Pod 级临时空间

spec:
  volumes:
    - name: cache
      emptyDir: {}
  containers:
    - name: app
      volumeMounts:
        - name: cache
          mountPath: /cache

视角对照

视角路径
应用代码/cache/foo
容器 mount ns/cache/foo
节点 host/var/lib/kubelet/pods/⟨pod-uid⟩/volumes/kubernetes.io~empty-dir/cache/foo
物理盘节点根分区(默认)/ 或 tmpfs(medium: Memory)

medium: Memory 内存型 emptyDir

volumes:
  - name: shm
    emptyDir:
      medium: Memory
      sizeLimit: 1Gi

放内存里、不占盘 → 但占 pod 的 memory limit。

# 节点上看
$ findmnt /var/lib/kubelet/pods/⟨uid⟩/volumes/kubernetes.io~empty-dir/shm
TARGET                                SOURCE  FSTYPE  OPTIONS
/var/lib/kubelet/pods/.../shm         tmpfs   tmpfs   rw,relatime,size=1024000k

典型场景:

  • 共享内存(/dev/shm 替代)—— 多容器 IPC
  • 计算中间结果(不需持久)
  • 解压临时空间

sizeLimit —— 生产必加

emptyDir:
  sizeLimit: 5Gi

不设的话 pod 写满节点根分区 → 节点 NotReady。给所有 emptyDir 都加 sizeLimit——超过会被 evict。

反面教材

把 emptyDir 当持久缓存

spec:
  containers:
    - name: redis
      volumeMounts:
        - name: data
          mountPath: /data
  volumes:
    - name: data
      emptyDir: {}

Redis 数据放 emptyDir → pod 重启容器 OK;pod 删除 / 重调度 → 数据没。

业务"反正能从源数据重建"用 emptyDir 没问题(如缓存)。真要持久 → PVC。


3. hostPath —— 节点本地路径

spec:
  containers:
    - name: app
      volumeMounts:
        - name: logs
          mountPath: /app/logs
  volumes:
    - name: logs
      hostPath:
        path: /var/log/myapp
        type: DirectoryOrCreate

type 字段

type含义
"" (默认)不检查
DirectoryOrCreate不存在就创建(目录)
Directory必须存在、必须是目录
FileOrCreate不存在就创建(文件)
File必须存在、必须是文件
Socket必须是 Unix socket
CharDevice字符设备
BlockDevice块设备

hostPath 的本质 + 三个核心问题

hostPath = 容器直接挂宿主路径。

3 个生产坑:

  1. 节点之间内容不一致:pod 调度到不同节点看到的 /var/log/myapp 完全不同
  2. 节点重启 / 维护:pod 飘到别的节点 = 看到空目录或别的数据
  3. 安全风险:容器写宿主路径 = 容器可以影响宿主(特别 /etc / /proc / /var/lib/...)

生产 99% 场景不要 hostPath。例外:

  • 监控 agent 看节点 /proc /sys(需要 readOnly)
  • CSI driver pod 看节点 /var/lib/kubelet(需要 propagation)
  • 节点级日志收集(fluentd / promtail 读 /var/log/containers)

替代方案:Local PV

要"本地存储 + K8s 调度感知" → 用 Local PV(详见 03-pv-pvc-storageclass.md):

# Local PV - 比 hostPath 安全多了
apiVersion: v1
kind: PersistentVolume
metadata:
  name: local-pv-m1
spec:
  capacity:
    storage: 100Gi
  volumeMode: Filesystem
  accessModes: [ReadWriteOnce]
  persistentVolumeReclaimPolicy: Retain
  storageClassName: local-storage
  local:
    path: /mnt/disks/ssd1
  nodeAffinity:                            # ← 关键:绑定到特定节点
    required:
      nodeSelectorTerms:
        - matchExpressions:
            - key: kubernetes.io/hostname
              operator: In
              values: [m1]

差别:

  • hostPath:任意节点都尝试 mount /var/log/myapp(即使没有这个目录 / 内容不一样)
  • Local PV:K8s 知道这个数据只在 m1、把 pod 调度到 m1

4. configMap —— 配置文件投射

两种使用方式

投射为文件
spec:
  containers:
    - name: app
      volumeMounts:
        - name: cfg
          mountPath: /app/config
  volumes:
    - name: cfg
      configMap:
        name: my-config
        items:                              # 可选:选 ConfigMap 里的特定 key
          - key: app.yaml
            path: application.yaml          # 改个文件名
          - key: log.conf
            path: log/app.log.conf          # 子目录

容器里:

$ kubectl exec app -- ls /app/config
application.yaml
log/
$ kubectl exec app -- cat /app/config/application.yaml
# ConfigMap 里 app.yaml 的内容
注入环境变量
containers:
  - name: app
    env:
      - name: DB_HOST                       # 单个 key
        valueFrom:
          configMapKeyRef:
            name: my-config
            key: db.host
    envFrom:                                # 批量注入
      - configMapRef:
          name: my-config

envFrom 把 ConfigMap 所有 key 都变成环境变量。注意 key 名必须合法环境变量名(不能含 . -)。

视角对照

volumes:
  - name: cfg
    configMap:
      name: my-config
视角路径
应用代码/app/config/app.yaml
容器 mount ns/app/config/app.yaml
节点 host/var/lib/kubelet/pods/⟨uid⟩/volumes/kubernetes.io~configmap/cfg/app.yaml
实际存储节点 tmpfs 文件(在内存里)

ConfigMap 更新与同步(反直觉)

K8s 的 kubelet 周期性同步 ConfigMap 到 pod。默认 sync 周期 1 分钟:

# 改 ConfigMap
$ kubectl edit configmap my-config

# Pod 里多久能看到?
$ kubectl exec app -- cat /app/config/app.yaml
# 0-60 秒内更新

subPath 时永远不更新

volumeMounts:
  - name: cfg
    mountPath: /etc/app.yaml
    subPath: app.yaml                    # ← subPath 时不会自动更新

subPath 用 bind mount 单个文件 → ConfigMap 更新时 bind 源是新文件、但 bind 目标没重新指向。

修法:

  1. 不用 subPath——挂整个目录
  2. 或者依赖 Helm chart 重启 pod(annotation hash)

详见 §8 subPath 章节。

应用要"感知"配置更新怎么办

K8s 不会通知应用 ConfigMap 改了。三种做法:

  1. 应用自己 watch 文件(inotify)—— 多数应用不支持
  2. 应用 SIGHUP reload(nginx / sshd 风格)—— 但 K8s 不会发 SIGHUP
  3. 改 ConfigMap → rollout restart deployment(最常见)
# 改 ConfigMap 之后
kubectl rollout restart deployment my-app

Helm chart 的高级套路:

spec:
  template:
    metadata:
      annotations:
        checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}

ConfigMap 改 → annotation 变 → 强制 rolling update。


5. secret —— 配置敏感数据

用法跟 ConfigMap 几乎一样

volumes:
  - name: tls
    secret:
      secretName: my-tls
      items:
        - key: tls.crt
          path: server.crt
        - key: tls.key
          path: server.key
          mode: 0400                        # 只 owner 可读
env:
  - name: DB_PASSWORD
    valueFrom:
      secretKeyRef:
        name: db-creds
        key: password

Secret 的"安全感" —— 真相

$ kubectl get secret my-secret -o yaml
apiVersion: v1
kind: Secret
data:
  password: cGFzc3dvcmQxMjM=                # ← base64 编码!

$ echo cGFzc3dvcmQxMjM= | base64 -d
password123

Secret = base64 编码、不是加密。任何有 secret get 权限的人都能解码。

K8s 提供的"安全感":

机制提供
RBAC限制谁能 get secret
挂载用 tmpfs不落盘磁盘(看下面)
etcd 加密etcd 里 Secret 加密存储(需要显式开)
绝对没有的base64 不是加密

etcd 加密 —— 生产必开

# /etc/kubernetes/encryption-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources:
      - secrets
    providers:
      - aescbc:
          keys:
            - name: key1
              secret: <32 字节 base64 编码的 key>
      - identity: {}                        # 兜底(明文)

启动时 apiserver --encryption-provider-config=...。

不开 etcd 加密 → 拿到 etcd 备份 = 拿到所有 Secret。K8s 默认不开。

详见 kubeadm.md。

视角对照

volumes:
  - name: tls
    secret:
      secretName: my-tls
视角路径
应用代码/etc/tls/server.crt
节点 host/var/lib/kubelet/pods/⟨uid⟩/volumes/kubernetes.io~secret/tls/server.crt
物理存储tmpfs(内存里、不落盘)
$ findmnt /var/lib/kubelet/pods/⟨uid⟩/volumes/kubernetes.io~secret/tls
TARGET                                SOURCE  FSTYPE  OPTIONS
/var/lib/kubelet/.../tls              tmpfs   tmpfs   rw,relatime

K8s 故意把 Secret 放 tmpfs → 节点崩溃 / 重启 = 内存里 secret 没了 = 不会泄漏到盘。

Sealed Secret / External Secret —— 真正的"加密"方案

Sealed Secret:用集群里的 controller 公钥加密后存 git 仓库。只有 controller 能解开成 K8s Secret。

apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
  name: my-secret
spec:
  encryptedData:
    password: AgBxK4...                      # 加密的数据、可以放公开 git

External Secret:Secret 实际存 Vault / AWS Secrets Manager / GCP Secret Manager,K8s 通过 controller 拉到 cluster 里。

生产推荐其中一种。


6. projected —— 多源合一

把 configMap + secret + downwardAPI + token 投射到同一个目录。

volumes:
  - name: all-config
    projected:
      sources:
        - configMap:
            name: app-config
            items:
              - key: app.yaml
                path: config/app.yaml
        - secret:
            name: tls
            items:
              - key: tls.crt
                path: tls/server.crt
        - downwardAPI:
            items:
              - path: meta/labels
                fieldRef:
                  fieldPath: metadata.labels
        - serviceAccountToken:
            path: token
            expirationSeconds: 3600
            audience: api

容器里:

$ kubectl exec app -- ls /etc/all-config/
config/
meta/
tls/
token

$ kubectl exec app -- cat /etc/all-config/config/app.yaml
# ConfigMap 内容

现代 K8s 已经默认用 projected token

$ kubectl exec my-pod -- mount | grep serviceaccount
... /var/run/secrets/kubernetes.io/serviceaccount type tmpfs (...) ...

$ kubectl exec my-pod -- ls /var/run/secrets/kubernetes.io/serviceaccount/
ca.crt
namespace
token

K8s 1.20+ 默认 ServiceAccount token 用 projected——含 audience + expiration、自动刷新。

老版(自动挂的 secret-based token)已 deprecated。


7. downwardAPI —— 把 pod 元数据注入容器

让容器访问自己的 pod 信息(label / annotation / 资源 limit 等)。

两种用法

作为环境变量
env:
  - name: POD_NAME
    valueFrom:
      fieldRef:
        fieldPath: metadata.name
  - name: POD_NS
    valueFrom:
      fieldRef:
        fieldPath: metadata.namespace
  - name: POD_IP
    valueFrom:
      fieldRef:
        fieldPath: status.podIP
  - name: NODE_NAME
    valueFrom:
      fieldRef:
        fieldPath: spec.nodeName
  - name: CPU_LIMIT
    valueFrom:
      resourceFieldRef:
        containerName: app
        resource: limits.cpu
作为文件
volumes:
  - name: pod-info
    downwardAPI:
      items:
        - path: labels
          fieldRef:
            fieldPath: metadata.labels        # 整个 label map
        - path: annotations
          fieldRef:
            fieldPath: metadata.annotations
        - path: cpu-limit
          resourceFieldRef:
            containerName: app
            resource: limits.cpu

可暴露的字段

来源可用字段
metadataname / namespace / uid / labels / annotations
specnodeName / serviceAccountName
statuspodIP / podIPs / hostIP
resourcelimits.cpu / limits.memory / requests.cpu / requests.memory / limits.ephemeral-storage

典型用例

# 应用日志 / metric 标记 pod 名(用 downwardAPI POD_NAME)
# 应用根据 limit 自动调 GC heap (Java -Xmx 用 CPU_LIMIT)
# 应用知道自己是哪个 ns (POD_NS) 决定行为

8. subPath / subPathExpr —— 最容易踩的坑

没 subPath 的默认行为:挂整个目录

volumeMounts:
  - name: cfg
    mountPath: /etc/nginx
volumes:
  - name: cfg
    configMap:
      name: nginx-config

/etc/nginx 整个被替换成 ConfigMap 里的内容。

$ kubectl exec nginx -- ls /etc/nginx
nginx.conf            # 来自 ConfigMap
fastcgi_params        # 来自 ConfigMap
# 但是 /etc/nginx/conf.d/* /modules-enabled/* 都看不见了!被遮盖

典型问题:nginx 镜像里 /etc/nginx/conf.d/default.conf 被 ConfigMap 遮盖、nginx 启动失败找不到。

subPath:只挂 volume 里的一个子路径到一个特定文件

volumeMounts:
  - name: cfg
    mountPath: /etc/nginx/nginx.conf       # 挂成一个文件、不是目录
    subPath: nginx.conf                     # ConfigMap 里的 nginx.conf
volumes:
  - name: cfg
    configMap:
      name: nginx-config

容器内:

$ ls /etc/nginx/
nginx.conf            # 来自 ConfigMap(subPath 挂的)
conf.d/                # 原镜像里的、保留
modules-enabled/       # 原镜像里的、保留
fastcgi_params         # 原镜像里的

只替换了 nginx.conf、其它原镜像内容保留。

subPath 的多视角

视角路径
应用/etc/nginx/nginx.conf
容器 mount/etc/nginx/nginx.conf 是一个 bind mount
节点 host/var/lib/kubelet/pods/⟨uid⟩/volume-subpaths/<vol>/<container>/<idx>

注意第三个——subPath 在节点上的存储位置和直接挂 volume 不一样:

$ ls /var/lib/kubelet/pods/⟨uid⟩/volume-subpaths/
cfg/                   # 一个目录
  └── nginx/0/         # 容器名/index
       └── nginx.conf  # ← subPath 挂的源

::: caution subPath 的两大坑

坑 1:subPath 时 ConfigMap 更新不同步

volumeMounts:
  - name: cfg
    mountPath: /etc/nginx.conf
    subPath: nginx.conf
# 改 ConfigMap
kubectl edit configmap my-config

# Pod 内 cat /etc/nginx.conf
# 内容**没变**!

K8s 不会更新 subPath 挂的文件。pod 重启之后才会。

修:

  • 不用 subPath,整个目录挂(但要解决"遮盖"问题)
  • 或者依赖 deployment 重启(Helm checksum annotation)

坑 2:subPath 必须存在

volumeMounts:
  - name: cfg
    mountPath: /etc/nginx/nginx.conf
    subPath: nginx.conf     # ConfigMap 里没这个 key

→ pod 启动失败:

Error: failed to start container: 
  failed to mount: no such file or directory

修:确认 ConfigMap 里 key 存在。

:::

subPathExpr —— 用变量做 subPath

env:
  - name: POD_NAME
    valueFrom:
      fieldRef:
        fieldPath: metadata.name
volumeMounts:
  - name: logs
    mountPath: /app/logs
    subPathExpr: $(POD_NAME)              # ← 用变量
volumes:
  - name: logs
    hostPath:
      path: /var/log/k8s

效果:每个 pod 挂到 /var/log/k8s/⟨pod-name⟩/(不互相覆盖)。

适合 StatefulSet 多副本 + 节点本地日志收集。


9. persistentVolumeClaim —— 持久存储

spec:
  containers:
    - name: app
      volumeMounts:
        - name: data
          mountPath: /data
  volumes:
    - name: data
      persistentVolumeClaim:
        claimName: my-pvc

详见 03-pv-pvc-storageclass.md。这里只看视角:

视角路径
应用/data/foo.dat
容器 mount/data/foo.dat
节点 CSI mount 点/var/lib/kubelet/plugins/.../mount/foo.dat
节点 bind 给 pod/var/lib/kubelet/pods/⟨uid⟩/volumes/kubernetes.io~csi/⟨pv-name⟩/mount/foo.dat
底层云盘 / NFS server / Ceph RBD

PVC 是两层 bind——CSI 先挂到全局位置、kubelet 再 bind 给 pod。

CSI volume 在节点上的实际位置

$ ls /var/lib/kubelet/plugins/⟨csi-driver⟩/
... global mount 点

$ ls /var/lib/kubelet/pods/⟨pod-uid⟩/volumes/kubernetes.io~csi/
... 每个 PVC 的 bind 点

排查 PV 挂载问题 → 看这两个位置。

Generic Ephemeral Volume(K8s 1.21+)

如果你想要"PV 的能力 + emptyDir 的生命周期":

spec:
  volumes:
    - name: scratch
      ephemeral:
        volumeClaimTemplate:
          spec:
            accessModes: [ReadWriteOnce]
            storageClassName: fast-ssd
            resources:
              requests:
                storage: 50Gi

K8s 自动创建一个 PVC(pod 删则删)+ 用 StorageClass 动态供应 PV。

适合"临时大数据处理"——比 emptyDir 大、自动清理、性能可配。


10. 其它 volume 类型(简介)

nfs —— 直接挂 NFS

volumes:
  - name: shared
    nfs:
      server: nfs.example.com
      path: /exports/shared
      readOnly: false

不经过 PVC、直接挂。生产慎用——节点上 NFS server 挂了所有 pod 卡 D 状态。

详见 04-nfs-deep.md。

iscsi / cephfs / rbd —— 直接挂分布式存储

volumes:
  - name: data
    iscsi:
      targetPortal: 10.0.0.100:3260
      iqn: iqn.2024-01.com.example:storage
      lun: 0

K8s 1.30+ 这些 in-tree provisioner 已 deprecated → 都改用 CSI driver。

gitRepo —— 已废弃

不要用。用 init container git clone 到 emptyDir 替代。

csi —— 未来

所有外部存储都通过 CSI 接入。详见 03-pv-pvc-storageclass.md。


11. 完整 volume 类型对照表

Volume生命周期多 pod 共享持久物理位置
emptyDirPod同 pod 容器❌节点 / tmpfs
emptyDir{medium:Memory}Pod同 pod 容器❌节点内存
hostPath节点✅节点级节点指定路径
configMapPod同 pod❌tmpfs(节点)
secretPod同 pod❌tmpfs(节点)
projectedPod同 pod❌tmpfs(节点)
downwardAPIPod同 pod❌tmpfs(节点)
persistentVolumeClaim独立✅(看 accessMode)✅CSI / NFS / ...
ephemeral (Generic)Pod同 pod❌(pod 删则删)CSI dynamic
nfs (in-tree)集群✅✅NFS server
csi (volume)Pod同 pod看 CSICSI driver

12. 反面教材合集

反面 1:ConfigMap 当 hot-reload 配置中心

# 改 ConfigMap 期望应用立刻看到
kubectl edit configmap my-config

K8s 同步周期默认 1 分钟、应用不会自动感知。要 hot-reload 用 Vault / Consul / 应用层 watch。

K8s ConfigMap 是 "启动时配置 + 偶尔更新" 的工具、不是 hot-reload 配置中心。

反面 2:把 Secret 当 KMS 用

type: Opaque
data:
  api-key: bm90LXNlY3JldA==              # base64

base64 不是加密。真敏感用:

  • HashiCorp Vault + Vault Agent Injector / External Secrets
  • Sealed Secrets(git 里也安全)
  • 云 KMS + Secrets Manager(AWS / GCP / Aliyun)

K8s Secret 是"分发凭证给 pod"的方式、不是"安全存储凭证"的方案。

反面 3:用 subPath 挂目录

volumeMounts:
  - name: data
    mountPath: /data
    subPath: subdir/                      # ← 期望挂目录

subPath 挂的单个路径(文件或目录)。挂目录"看起来 work"、但和直接挂 volume 区别不大、还有缺点(不自动更新)。

目录就直接挂 volume、文件用 subPath。

反面 4:在 ReadWriteOnce PV 上扩多副本

spec:
  replicas: 3
  template:
    spec:
      volumes:
        - name: data
          persistentVolumeClaim:
            claimName: my-pvc            # RWO

RWO PVC 只能给一个 node 上一个 pod用。3 副本调度到不同节点 → 第 2/3 副本起不来:

MountVolume failed: volume is already mounted by node X

修:

  • 要多副本 + 共享存储 → 用 RWX(NFS / CephFS / EFS)
  • 要多副本 + 独立存储 → 用 StatefulSet + volumeClaimTemplate(每个 pod 自己 PVC)

反面 5:emptyDir 没 sizeLimit + 应用写爆

volumes:
  - name: cache
    emptyDir: {}                          # 没 sizeLimit

应用日志 / 缓存写满 → 节点根分区满 → 节点 DiskPressure → kubelet 驱逐 pod / 节点 NotReady。

永远加 sizeLimit:

emptyDir:
  sizeLimit: 5Gi

K8s 超过 sizeLimit 会驱逐这个 pod(不会让节点撑爆)。

反面 6:hostPath 没 readOnly 暴露宿主

volumes:
  - name: rootfs
    hostPath:
      path: /
containers:
  - volumeMounts:
      - name: rootfs
        mountPath: /host
        # 没设 readOnly

容器拿到宿主根目录 + 可写——等于宿主 root。CI / 监控类有时这么做、绝对加 readOnly:

volumeMounts:
  - name: rootfs
    mountPath: /host
    readOnly: true

业务 pod 永远不要挂 /。

反面 7:用 gitRepo volume

volumes:
  - name: code
    gitRepo:
      repository: https://github.com/...

gitRepo 已 deprecated。安全问题 / 不可维护。

替代:init container clone 到 emptyDir:

initContainers:
  - name: clone
    image: alpine/git
    command: ["git", "clone", "https://...", "/code"]
    volumeMounts:
      - name: code
        mountPath: /code
volumes:
  - name: code
    emptyDir: {}

13. 排查 cheatsheet

"Pod 起不来、ContainerCreating 卡"

$ kubectl describe pod my-pod
Events:
  Warning  FailedMount  ...  Unable to attach or mount volumes: unmounted volumes=[data], unattached volumes=[data]: timed out waiting for the condition

逐层排查:

# 1. PVC 状态
$ kubectl get pvc my-pvc
# 应 Bound

# 2. PV 状态
$ kubectl get pv

# 3. 节点上看是否真挂
$ ssh m4 'ls /var/lib/kubelet/pods/⟨uid⟩/volumes/'
$ ssh m4 'findmnt | grep ⟨pod-uid⟩'

# 4. CSI driver 日志
$ kubectl logs -n kube-system ⟨csi-driver-pod⟩

# 5. K8s events 看具体错
$ kubectl get events --sort-by=".lastTimestamp" | tail -20

"configMap 改了 pod 没生效"

# 1. 用了 subPath 吗?
$ kubectl get pod my-pod -o yaml | grep subPath
# 用了 subPath → 不会自动更新

# 2. 没用 subPath、看 sync 周期
# kubelet 默认 1 分钟 sync

# 3. 触发 rolling restart
$ kubectl rollout restart deploy my-deploy

"Secret 看起来变了应用没用上"

# 1. Secret 真改了吗
$ kubectl get secret my-secret -o yaml

# 2. Pod 里看到的是不是新值
$ kubectl exec pod -- cat /etc/secret/token

# 3. 应用是不是缓存了
# (启动时读、改了不感知 → 重启 pod)

14. 下一步

篇内容
00-storage-mental-model.md存储心智模型
01-container-volumes.md容器挂载完整指南
本篇K8s Volume 类型大全
03-pv-pvc-storageclass.mdPV / PVC / CSI 深入(下一篇必读)
04-nfs-deep.mdNFS
05-distributed-storage.mdCeph / Longhorn
06-storage-troubleshooting.md故障排查 runbook

相关命令

  • kubectl —— get pv/pvc/configmap/secret
  • findmnt —— 看 pod 实际挂载
  • nsenter —— 进 pod 看容器视角
在 GitHub 上编辑此页
Prev
容器挂载完整指南:bind mount / volume / mount propagation / 多视角
Next
PV / PVC / StorageClass / CSI 深入