三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

【Kubernetes从入门到精通】第19篇:Volume——容器数据的“不动产“

【Kubernetes从入门到精通】第19篇:Volume——容器数据的“不动产“

上一篇【第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:true

hostPathtype参数很重要:

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性能生产就绪?
emptyDirPod删了就没✅ 同Pod内共享❌ 绑定Node高(本地磁盘/内存)临时数据 ✅
hostPathPod删了还在⚠️ 同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让容器从"临时工"变成"有产者":

  1. 核心价值:容器文件系统是临时的,Volume把数据放在容器外部——容器重启数据还在,Pod删除才释放
  2. 三种常用类型:emptyDir(临时共享,Pod删除即清)、hostPath(绑定Node,持久但绑定节点)、NFS(网络共享,多Node可用)
  3. subPath妙用:精准替换单个文件而不是覆盖整个目录——配置文件注入标配
  4. ConfigMap/Secret挂载:配置和密钥变成文件,应用直接读,支持热更新
  5. PV/PVC预告:生产环境存储管理的基础,PVC让你把存储当资源申请

下一篇咱们聊ConfigMap——配置管理的正确打开方式。应用配置不再硬编码在镜像里,支持热更新、多环境切换、版本管理。


上一篇【第18篇】Ingress Controller选型和实战——Nginx Ingress完全指南
下一篇【第20篇】ConfigMap——配置管理的正确姿势


← 返回列表