上一篇【第18篇】Ingress Controller选型和实战——Nginx Ingress完全指南
下一篇【第20篇】ConfigMap——配置管理的正确姿势
摘要
容器有一个"健忘症"——重启就失忆,删了就彻底消失。你费劲写进去的日志没了、用户上传的图片蒸发了、数据库的数据全丢了……这谁受得了?K8s的解决方案是Volume——它是在Pod级别定义的"外接存储",跟容器生命周期解耦:容器挂了,Volume里的数据还在,新容器起来接着用。
这篇文章从"如果没有Volume会怎样"讲起,拆解K8s里最常用的几种Volume类型(emptyDir、hostPath、nfs),用表格帮你对比选型,然后演示两个实战利器——subPath(把单个文件挂进去而不是覆盖整个目录)和ConfigMap/Secret的挂载。读完这篇,你就知道"数据放哪儿"这个问题的答案了。
一、为什么容器需要Volume——“鱼的记忆只有七秒”
先感受一下容器"失忆"的痛:
【没有Volume——容器重启 = 数据丢失】 Pod启动 │ ▼ ┌─────────────────────────────────────┐ │ Container: nginx │ │ ─────────────────────────────── │ │ /var/log/nginx/access.log │ 用户请求写入的日志 │ /usr/share/nginx/html/index.html │ 网站文件 │ /tmp/uploaded/pic.jpg │ 用户上传的图片 │ │ │ "这些数据存在容器的文件系统里" │ │ "容器文件系统是临时的——跟鱼一样" │ └─────────────────────────────────────┘ │ │ Pod重启/容器崩溃 ▼ ┌─────────────────────────────────────┐ │ Container: nginx (新容器) │ │ ─────────────────────────────── │ │ /var/log/nginx/access.log → 空的! │ │ /usr/share/nginx/html/index.html → 没了!│ │ /tmp/uploaded/pic.jpg → 消失了! │ │ │ │ 😱 "我的数据呢?!!!" │ └─────────────────────────────────────┘有了Volume之后:
【有Volume——数据存在"外部",跟容器生命周期解耦】 ┌─────────────────────────────────────┐ │ Volume │ │ ┌─────────────────────────────┐ │ │ │ 真实的数据存储位置 │ │ │ │ • Node本地磁盘(emptyDir) │ │ │ │ • Node指定路径(hostPath) │ │ │ │ • NFS远程存储 │ │ │ │ • 云盘(AWS EBS/阿里云盘) │ │ │ │ │ │ │ │ Volume跟Pod同生命周期 │ │ │ │ Pod删了Volume才没 │ │ │ │ 容器重启:数据完好! │ │ │ └──────────┬──────────────────┘ │ │ │ 挂载到容器 │ │ ▼ │ │ ┌─────────────────────────────┐ │ │ │ Container │ │ │ │ /var/log/nginx/ → Volume │ │ │ │ 容器以为自己写的是本地磁盘 │ │ │ │ 实际上写的都是Volume │ │ │ └─────────────────────────────┘ │ └─────────────────────────────────────┘要点:Volume的生命周期跟Pod绑定,不是跟容器绑定。容器重启、重建——只要Pod没删,Volume就在。这跟你租房一个道理:容器是"租客"(来来去去),Volume是"房子"(一直在那儿)。租客搬走了,房子里的东西不会跟着消失。但房子拆了(Pod删除),Volume也就没了(至少emptyDir是这样)。
Volume和容器文件系统的对比
| 维度 | 容器文件系统 | Volume |
|---|---|---|
| 生命周期 | 跟容器绑定(重启就没了) | 跟Pod绑定(Pod删除才释放) |
| 容器间共享 | ❌ 各容器独立文件系统 | ✅ 同一Pod的容器可以共享 |
| 持久化 | ❌ 临时的 | 取决于Volume类型(hostPath/NFS可持久) |
| 性能 | 容器层写时复制,性能差 | 直接写底层存储,性能好 |
| 大小限制 | 受镜像层限制 | 取决于底层存储 |
二、常用Volume类型——从"临时"到"永久"
K8s支持几十种Volume类型,但日常用的就那几种。按"持久化程度"从低到高排列:
【Volume 持久化排行榜】 持久化程度 ▲ │ ┌─────────────────────────┐ │ │ 云盘 (AWS EBS/PD/disk) │ ← 最高:Pod删了数据还在,还能漂移到别的Node │ ├─────────────────────────┤ │ │ NFS / CephFS │ ← 高:网络存储,多Pod共享 │ ├─────────────────────────┤ │ │ hostPath │ ← 中:绑定Node磁盘,Pod删了数据还在(Node上) │ ├─────────────────────────┤ │ │ emptyDir │ ← 低:Pod删了就没,但容器重启数据还在 │ └─────────────────────────┘ │ └────────────────────────────────────────────► 共享能力2.1 emptyDir——“Pod级别的临时便签”
Pod启动时创建一个空目录,Pod内所有容器都能用。Pod删除时目录内容清空。
【emptyDir 典型使用场景】 ┌─────────────────────────────────────────────┐ │ Pod │ │ │ │ ┌───────────────┐ ┌──────────────────┐ │ │ │ nginx │ │ filebeat │ │ │ │ 写日志到 │ │ 读日志从 │ │ │ │ /var/log/ │ │ /logs/ │ │ │ └───────┬───────┘ └────────┬─────────┘ │ │ │ │ │ │ │ ┌──────────────┐ │ │ │ └─►│ emptyDir │◄──┘ │ │ │ (共享卷) │ │ │ └──────────────┘ │ │ │ │ 容器间共享文件、临时缓存、中间结果 │ └─────────────────────────────────────────────┘apiVersion:v1kind:Podmetadata:name:nginx-with-loggerspec:volumes:-name:shared-logsemptyDir:{}# 啥也不用配,创建个空目录containers:-name:nginximage:nginx:1.25volumeMounts:-name:shared-logsmountPath:/var/log/nginx# nginx日志写到这里-name:log-readerimage:busyboxcommand:["tail","-f","/logs/access.log"]volumeMounts:-name:shared-logsmountPath:/logs# 另一个容器从这里读readOnly:true# emptyDir 也可以用内存做存储(tmpfs)——极速,但Pod一挂全没volumes:-name:ram-diskemptyDir:medium:Memory# 用内存!读写极快sizeLimit:"256Mi"# 限制大小,防止把Node内存吃光要点:emptyDir是Pod级别的——Pod删了就没了。它的典型场景有三个:(1) 容器间共享文件(如日志代理收集业务日志),(2) 临时计算中间结果,(3) 用
medium: Memory做超高速读写缓存。但千万别拿emptyDir存数据库——Pod一删库就跑了!
2.2 hostPath——“直连Node磁盘”
把Node上的一个目录直接挂载到Pod里。Pod删除后,Node上的数据还在。
【hostPath——直接挂载Node目录】 Node-1 文件系统 ┌──────────────────────────────────────┐ │ /data/k8s/ │ │ ├── logs/ │ │ │ ├── app.log │ │ │ └── error.log │ │ └── config/ │ │ └── nginx.conf │ └──────────────┬───────────────────────┘ │ hostPath 挂载 ▼ ┌──────────────────────────────────────┐ │ Pod-1 │ │ /var/log/app → hostPath:/data/logs │ │ /etc/nginx → hostPath:/data/config│ └──────────────────────────────────────┘apiVersion:v1kind:Podmetadata:name:hostpath-demospec:volumes:-name:host-logshostPath:path:/data/k8s/logs# Node上的绝对路径type:DirectoryOrCreate# 目录不存在就创建-name:host-confighostPath:path:/etc/kubernetes/ssltype:Directory# 必须已存在containers:-name:appimage:myapp:latestvolumeMounts:-name:host-logsmountPath:/var/log/app-name:host-configmountPath:/etc/ssl/certsreadOnly:truehostPath的type参数很重要:
| type值 | 含义 | 安全性 |
|---|---|---|
""(空) | 不检查,啥都能挂 | ⚠️ 最低 |
DirectoryOrCreate | 目录存在就用,不存在就创建 | 较安全 |
Directory | 目录必须已存在 | ✅ 推荐 |
FileOrCreate | 文件存在就用,不存在就创建 | 较少用 |
File | 文件必须已存在 | 较少用 |
Socket | 必须是Unix Socket | 特殊场景 |
要点:hostPath是一把双刃剑——好用,但危险。同一个hostPath可能被多个Pod同时写,造成文件冲突。更严重的是,如果Pod被调度到另一个Node上,新Pod访问不到原来Node上的数据。hostPath只适合两类场景:(1) DaemonSet(如日志收集agent,每个Node一个),(2) 要访问Node系统文件的监控/管理工具。
2.3 NFS——“网络共享硬盘”
NFS卷让多个Pod跨Node共享同一个文件系统——这是hostPath做不到的。
【NFS——多Node多Pod共享】 ┌──────────────────┐ │ NFS Server │ │ 192.168.1.100 │ │ /exports/data │ └────────┬─────────┘ │ 网络挂载 ┌─────┼─────┐ │ │ │ ▼ ▼ ▼ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ Node-1 │ │ Node-2 │ │ Node-3 │ │ ┌──────┐ │ │ ┌──────┐ │ │ ┌──────┐ │ │ │Pod-A │ │ │ │Pod-B │ │ │ │Pod-C │ │ │ │/data │ │ │ │/data │ │ │ │/data │ │ │ └──────┘ │ │ └──────┘ │ │ └──────┘ │ └──────────┘ └──────────┘ └──────────┘ │ │ │ └─────────────┴─────────────┘ 都挂载同一个 NFS 目录 /data 里看到的是同一份数据apiVersion:v1kind:Podmetadata:name:nfs-demospec:volumes:-name:shared-datanfs:server:192.168.1.100# NFS服务器地址path:/exports/data# NFS导出路径readOnly:falsecontainers:-name:appimage:myapp:latestvolumeMounts:-name:shared-datamountPath:/data三种常用Volume快速对比
| 类型 | 持久性 | 多Pod共享 | 跨Node | 性能 | 生产就绪? |
|---|---|---|---|---|---|
| emptyDir | Pod删了就没 | ✅ 同Pod内共享 | ❌ 绑定Node | 高(本地磁盘/内存) | 临时数据 ✅ |
| hostPath | Pod删了还在 | ⚠️ 同Node可共享 | ❌ 绑定Node | 高(本地磁盘) | 仅DaemonSet ✅ |
| NFS | 独立于Pod | ✅ 任意Pod | ✅ 跨Node | 中(受网络影响) | ✅ 但需维护NFS服务器 |
三、Volume的挂载方式——两种模式
3.1 普通挂载——整个目录
# Volume挂到容器的/mnt/data目录# 容器里 /mnt/data 之前的内容会被Volume"覆盖"(看不到原来的了)volumes:-name:myvolemptyDir:{}containers:-name:appvolumeMounts:-name:myvolmountPath:/mnt/data# 挂到这个路径3.2 subPath——“只挂一个文件,别把整个目录盖了”
这是Volume使用中最容易误解也最实用的技巧。默认挂载会把目标目录整个替换成Volume内容,但有时候你只想把一个文件(比如配置文件)塞进去:
apiVersion:v1kind:Podmetadata:name:subpath-demospec:volumes:-name:config-volumeconfigMap:name:app-config# ConfigMap里有 nginx.confcontainers:-name:nginximage:nginx:1.25volumeMounts:-name:config-volumemountPath:/etc/nginx/nginx.conf# ❌ 错误!会把整个/etc/nginx/覆盖subPath:nginx.conf# ✅ 正确!只替换nginx.conf这一个文件【subPath 的作用——挂单个文件 vs 覆盖整个目录】 没有 subPath(默认行为): 有 subPath: ┌────────────────────────┐ ┌────────────────────────┐ │ 容器 /etc/nginx/ │ │ 容器 /etc/nginx/ │ │ ├── nginx.conf ← 覆盖 │ │ ├── nginx.conf ← 替换 │ │ ├── mime.types ← 没了!│ │ ├── mime.types ✅ 还在 │ │ ├── modules/ ← 没了!│ │ ├── modules/ ✅ 还在 │ │ └── conf.d/ ← 没了!│ │ └── conf.d/ ✅ 还在 │ └────────────────────────┘ └────────────────────────┘ 整个目录被Volume内容替换掉了 只替换了nginx.conf一个文件 其他文件全丢失! 其他文件完好无损要点:subPath是"精准替换"——它把Volume里的一个文件/目录挂到容器的指定路径,不影响该路径下的其他文件。这在配置注入场景是标配用法:你只想替换
nginx.conf,不想把整个/etc/nginx/目录清空。
四、挂载ConfigMap和Secret——配置文件注入
Volume的一大用途是把ConfigMap和Secret挂载成文件——应用不用改代码,直接读文件就行。
apiVersion:v1kind:Podmetadata:name:configmap-volume-demospec:volumes:-name:app-configconfigMap:name:myapp-configitems:# 选择性挂载某些key-key:app.propertiespath:application.properties# key→文件名的映射-key:log4j.xmlpath:log4j2.xmldefaultMode:0644# 文件权限containers:-name:appimage:myapp:latestvolumeMounts:-name:app-configmountPath:/app/config# ConfigMap内容变成文件夹里的文件readOnly:true# 挂载后,容器里看到的文件结构:# /app/config/# ├── application.properties ← app.properties 的内容# └── log4j2.xml ← log4j.xml 的内容# 应用代码里直接读 /app/config/application.properties 即可Secret的挂载一模一样——把configMap换成secret:
volumes:-name:tls-certssecret:secretName:tls-secretitems:-key:tls.crtpath:cert.pem-key:tls.keypath:key.pemdefaultMode:0600# 私钥权限必须是600containers:-name:appvolumeMounts:-name:tls-certsmountPath:/etc/ssl/certsreadOnly:true要点:用Volume挂载ConfigMap/Secret有一个好处——热更新。ConfigMap/Secret内容改了之后,kubelet会在一定时间内同步更新挂载的文件(默认约60秒)。而用环境变量注入的配置项则不会自动更新,必须重启Pod。
五、PV/PVC简介——“动态存储管理”
前面讲的emptyDir和hostPath都有个致命问题:Pod删了数据可能就没了(emptyDir),或者换Node了数据访问不到(hostPath)。生产环境需要的是"Pod删了数据还在、Pod漂到哪都能访问"的存储——这就是PersistentVolume(PV)和PersistentVolumeClaim(PVC)。
【PV/PVC 概念——存储的"预售模式"】 K8s管理员 K8s用户(你) ──────── ─────────── 创建 PV(仓库里的存储单元) 创建 PVC(申请存储) ┌─────────────────┐ ┌─────────────────┐ │ PV-1: 100Gi │◄────────│ "我要10Gi存储" │ │ NFS:/exports/pv1 │ 绑定 │ PVC: myapp-data │ ├─────────────────┤ └─────────────────┘ │ PV-2: 50Gi │ │ │ AWS EBS: vol-xxx│ │ Pod引用PVC ├─────────────────┤ ▼ │ PV-3: 200Gi │ ┌─────────────────┐ │ Ceph RBD: img-1 │ │ Pod │ └─────────────────┘ │ volumes: │ │ - name: data │ PV = 预先准备的存储资源 │ persistentVolumeClaim: PVC = 用户的存储需求单 │ claimName: myapp-data └─────────────────┘PVC模式的优势在于"关注点分离":管理员管存储(PV),开发者只管用(PVC),双方不用互相了解对方的配置细节。
要点:本节只是PV/PVC的一个预告——Volume的本质是"给Pod挂存储",PV/PVC把这个概念提升到集群级别的"存储抽象层"。后续文章会有专门的PV/PVC深入篇,包括StorageClass动态创建、访问模式(RWO/ROX/RWX)等。
六、Volume的常用命令
# 查看Pod的Volume挂载情况kubectl describe pod my-pod|grep-A20"Volumes:"kubectl describe pod my-pod|grep-A10"Mounts:"# 进入容器看挂载点kubectlexec-itmy-pod --df-hkubectlexec-itmy-pod --mount|grep/data# 在容器里写文件测试Volume是否工作kubectlexec-itmy-pod --touch/data/test.txt kubectlexec-itmy-pod-clog-reader --ls/logs# 另一个容器看共享Volume# 删除Pod看数据是否还在kubectl delete pod my-pod# 如果是emptyDir → 数据没了# 如果是hostPath → 数据在Node上# 如果是PV/PVC → 数据在存储后端本篇小结
Volume让容器从"临时工"变成"有产者":
- 核心价值:容器文件系统是临时的,Volume把数据放在容器外部——容器重启数据还在,Pod删除才释放
- 三种常用类型:emptyDir(临时共享,Pod删除即清)、hostPath(绑定Node,持久但绑定节点)、NFS(网络共享,多Node可用)
- subPath妙用:精准替换单个文件而不是覆盖整个目录——配置文件注入标配
- ConfigMap/Secret挂载:配置和密钥变成文件,应用直接读,支持热更新
- PV/PVC预告:生产环境存储管理的基础,PVC让你把存储当资源申请
下一篇咱们聊ConfigMap——配置管理的正确打开方式。应用配置不再硬编码在镜像里,支持热更新、多环境切换、版本管理。
上一篇【第18篇】Ingress Controller选型和实战——Nginx Ingress完全指南
下一篇【第20篇】ConfigMap——配置管理的正确姿势