Filesystem ของ container จะหายไปทันทีที่ container ถูกแทนที่ อะไรก็ตามที่ต้องอยู่รอด ไม่ว่าจะเป็นไฟล์ database, ไฟล์ที่ผู้ใช้อัปโหลด หรือ log ของ message broker จึงต้องเก็บไว้ใน volume ที่อยู่นอก Pod ใน Kubernetes สิ่งนี้คือ persistent volume ซึ่งออกแบบโดยแยกปัญหาเป็นสองฝั่ง PersistentVolume (PV) แทนพื้นที่เก็บข้อมูลจริง ส่วน PersistentVolumeClaim (PVC) คือคำขอใช้พื้นที่จากฝั่งแอป แล้วมี StorageClass คอยเชื่อมสองฝั่งให้อัตโนมัติ เพื่อให้ disk ถูกสร้างขึ้นเมื่อมีคนขอ
บทความนี้ครอบคลุม volume แบบชั่วคราว, การ bind ระหว่าง PV กับ PVC, static กับ dynamic provisioning, CSI driver ทำอะไรจริง ๆ, access mode และวิธีทำ storage แบบ ReadWriteMany ด้วย NFS
Volume ที่เกิดและตายไปพร้อม Pod
ไม่ใช่ทุก volume ที่ถาวร กลุ่มนี้ผูกกับอายุของ Pod:
emptyDir: directory ว่างที่ถูกสร้างตอน Pod เริ่มทำงาน อยู่รอดเมื่อ container ใน Pod restart แต่ถูกลบเมื่อ Pod ถูกลบ เหมาะกับพื้นที่ทำงานชั่วคราวและการแชร์ไฟล์ระหว่าง container ใน Pod เดียวกัน ถ้าตั้งmedium: Memoryจะใช้ RAM แทน diskconfigMap,secret,projected: นำ config และ credential มาเป็นไฟล์ให้ container อ่านhostPath: mount directory จาก node เข้ามา ข้อมูลอยู่บน node นั้นเครื่องเดียว และเปิดให้ Pod เข้าถึง filesystem ของ host ได้ ควรใช้กับ agent ระดับ node อย่าง log collector เท่านั้น ไม่ใช่กับข้อมูลของแอป
volumes:
- name: scratch
emptyDir:
sizeLimit: 1Giถ้าข้อมูลต้องอยู่นานกว่า Pod ต้องใช้ PersistentVolume
PersistentVolume และ PersistentVolumeClaim
PV เป็น object ระดับคลัสเตอร์ (cluster-scoped) ที่อธิบาย storage จริง เช่น cloud disk, NFS export หรือ Ceph volume มีขนาด, access mode และ reclaim policy กำกับ
PVC เป็นคำขอที่อยู่ใน namespace เช่น "ขอพื้นที่ 20Gi ที่ node เดียวอ่านเขียนได้" Kubernetes จะหา PV ที่ตรงเงื่อนไข (ขนาดพอ, access mode และ StorageClass ตรงกัน) แล้ว bind ทั้งสองเข้าด้วยกันแบบหนึ่งต่อหนึ่ง จากนั้น Pod อ้างถึงแค่ claim:
apiVersion: v1
kind: Pod
metadata:
name: app
spec:
containers:
- name: app
image: ghcr.io/example/app:2.0.0
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: app-dataชั้นที่คั่นกลางนี้คือหัวใจของการออกแบบ developer ขอ storage ได้โดยไม่ต้องรู้ว่าข้างหลังเป็น EBS, SAN หรือ NFS และ manifest ชุดเดียวกันใช้ได้กับหลายคลัสเตอร์
เมื่อ claim ถูกลบ PV จะเป็นอย่างไรขึ้นอยู่กับ persistentVolumeReclaimPolicy:
| Policy | เมื่อลบ PVC | ใช้เมื่อ |
|---|---|---|
Delete | ลบทั้ง PV และ disk จริง | ข้อมูลที่ทิ้งได้หรือสร้างใหม่ได้ง่าย เป็นค่าเริ่มต้นของ dynamic provisioning |
Retain | PV เปลี่ยนเป็น Released และเก็บ disk ไว้ให้ admin จัดการเอง | ข้อมูลที่เสียไปโดยไม่ตั้งใจไม่ได้ |
Policy Recycle แบบเก่าถูก deprecate แล้ว ข้อมูลสำคัญควรใช้ Retain หรือกำหนดไว้ที่ StorageClass
Static provisioning: admin เตรียม PV ไว้ล่วงหน้า
แบบ static, admin สร้าง storage และเขียน PV ไว้ก่อน แล้ว PVC ค่อยมา bind ทีหลัง:
apiVersion: v1
kind: PersistentVolume
metadata:
name: reports-nfs
spec:
capacity:
storage: 50Gi
accessModes: ["ReadWriteMany"]
persistentVolumeReclaimPolicy: Retain
nfs:
server: nfs.internal.example.com
path: /exports/reports
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: reports
namespace: analytics
spec:
accessModes: ["ReadWriteMany"]
storageClassName: "" # don't use dynamic provisioning
volumeName: reports-nfs # bind to this specific PV
resources:
requests:
storage: 50Giบรรทัด storageClassName: "" สำคัญ ถ้าไม่ใส่ default StorageClass จะพยายามสร้าง volume ใหม่แทนที่จะ bind กับ PV ที่เตรียมไว้
Static provisioning ใช้ได้ดีถ้ามี share อยู่ไม่กี่อัน แต่ขยายไม่ไหว ทุกครั้งที่ต้องการ disk ใหม่ต้องเปิด ticket ให้ admin และ PV ที่เตรียมไว้ล่วงหน้าก็มีแต่จะไม่พอหรือไม่ก็เหลือทิ้งไว้เปล่า ๆ
Dynamic provisioning ด้วย StorageClass
StorageClass คือเทมเพลตสำหรับสร้าง volume ตามคำขอ เมื่อ PVC ระบุ class ใด provisioner ของ class นั้นจะสร้าง disk และ PV ให้อัตโนมัติ ขนาดพอดีกับที่ขอ
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-ssd
annotations:
storageclass.kubernetes.io/is-default-class: "true"
provisioner: ebs.csi.aws.com
parameters:
type: gp3
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: app-data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: fast-ssd
resources:
requests:
storage: 20GiField ที่ต้องเข้าใจ:
provisioner: CSI driver ที่สร้าง volume เช่นebs.csi.aws.com,pd.csi.storage.gke.ioหรือdobs.csi.digitalocean.comparameters: ค่าเฉพาะของแต่ละ driver เช่นชนิด disk, IOPS หรือ filesystemvolumeBindingMode: WaitForFirstConsumer: รอสร้าง disk จนกว่า Pod ที่ใช้ claim นี้จะถูก schedule เพราะ cloud disk ผูกกับ zone ถ้าสร้างก่อนที่ scheduler จะเลือก node อาจได้ disk ไปอยู่ใน zone ที่ Pod รันไม่ได้ block storage แบบ zonal จึงควรใช้โหมดนี้allowVolumeExpansion: true: ขยาย claim ได้ด้วยการแก้spec.resources.requests.storageขยายได้อย่างเดียว ลดขนาดไม่ได้- Default class: PVC ที่ไม่ระบุ
storageClassNameจะได้ class ที่มี annotation เป็น default
ดู class ที่มีด้วย kubectl get storageclass แล้วใช้ kubectl get pvc,pv ดูการ bind ถ้าใช้ StatefulSet ไม่ต้องเขียน PVC เองเลย เพราะ volumeClaimTemplates จะสร้างให้ Pod ละหนึ่งอัน ตามที่อธิบายไว้ในบทความ Kubernetes workloads
CSI: Kubernetes คุยกับ storage อย่างไร
Container Storage Interface (CSI) คือสัญญากลางระหว่าง Kubernetes กับระบบ storage ผู้ผลิต storage เขียน CSI driver ที่ implement ชุด gRPC call ตามมาตรฐาน แล้ว Kubernetes ก็ใช้งานได้โดยไม่ต้องมีโค้ดของ vendor อยู่ใน Kubernetes เลย plugin ของ cloud แบบ in-tree รุ่นเก่าก็ถูกย้ายไปเป็น CSI driver หมดแล้ว
CSI driver ส่วนใหญ่มีสองส่วน:
- Controller plugin มักรันเป็น Deployment พร้อม sidecar อย่าง external-provisioner และ external-attacher ดูแล
CreateVolume,DeleteVolumeและControllerPublishVolumeซึ่งคือการ attach disk เข้ากับ node - Node plugin รันเป็น DaemonSet บนทุก node ดูแล
NodeStageVolumeที่ format device (ถ้าจำเป็น) แล้ว mount ไว้ที่ staging path บน node และNodePublishVolumeที่ mount ต่อเข้าไปใน directory ของ Pod
ลองไล่ dynamic claim ตั้งแต่ต้นจนจบ:
- สร้าง PVC ที่ระบุ StorageClass
- ถ้าเป็น
WaitForFirstConsumerจะยังไม่มีอะไรเกิดขึ้น จนกว่า Pod ที่ใช้ claim จะถูก schedule ลง node - external-provisioner เรียก
CreateVolumeแล้ว PV ถูกสร้างและ bind กับ PVC - attacher เรียก
ControllerPublishVolumeเพื่อ attach disk เข้า node ที่ถูกเลือก - kubelet เรียก
NodeStageVolumeและNodePublishVolumeจากนั้น container ก็เห็น volume ที่mountPath
เมื่อ Pod ค้างอยู่ที่ ContainerCreating พร้อม error เรื่อง volume ลำดับนี้บอกได้ว่าต้องไปดูที่ component ไหน ส่วนบทบาทของ scheduler และ kubelet ดูเพิ่มได้ใน Kubernetes architecture นอกจากนี้ CSI ยังเปิดให้ใช้ VolumeSnapshot (snapshot.storage.k8s.io/v1) กับ driver ที่รองรับ ซึ่งมีประโยชน์มากกับการ backup
Access mode
| Mode | ชื่อย่อ | ความหมาย |
|---|---|---|
ReadWriteOnce | RWO | อ่านเขียนได้จาก node เดียว ในแต่ละช่วง Pod หลายตัวบน node นั้นใช้ร่วมกันได้ |
ReadOnlyMany | ROX | อ่านอย่างเดียวได้จากหลาย node |
ReadWriteMany | RWX | อ่านเขียนได้จากหลาย node พร้อมกัน |
ReadWriteOncePod | RWOP | อ่านเขียนได้จาก Pod เดียวในทั้งคลัสเตอร์ (เฉพาะ CSI) |
หลายคนเข้าใจว่า RWO คือ "Pod เดียว" แต่จริง ๆ ข้อจำกัดอยู่ที่ node เดียว ถ้าต้องการรับประกันว่ามีคนเขียนได้คนเดียวจริง ๆ ให้ใช้ ReadWriteOncePod
Mode ที่ใช้ได้ขึ้นกับชนิดของ storage Block storage (EBS, Persistent Disk, DigitalOcean Volumes, Ceph RBD) attach ได้ทีละเครื่อง จึงได้แค่ RWO ส่วน file storage (NFS, EFS, Azure Files, CephFS) แชร์ผ่าน network จึงทำ RWX ได้
NFS กับ storage แบบ ReadWriteMany
NFS (Network File System) คือการ export directory จากเซิร์ฟเวอร์ แล้วให้หลายเครื่อง mount ใช้พร้อมกัน นี่จึงเป็นหนึ่งในวิธีที่นิยมที่สุดในการได้ ReadWriteMany เช่น เมื่อ web app หลาย replica ต้องใช้ directory อัปโหลดเดียวกัน
รูปแบบที่ใช้กันบ่อยมีสามแบบ:
- NFS server ที่มีอยู่แล้ว + static PV แบบตัวอย่างก่อนหน้า ง่าย แต่ทุก share ต้องสร้างด้วยมือ
- NFS server ที่มีอยู่แล้ว + dynamic provisioning โดยใช้ csi-driver-nfs ซึ่งสร้าง subdirectory บน share ให้ทุกครั้งที่มี PVC ใหม่
- NFS server ที่รันอยู่ในคลัสเตอร์เอง nfs-server-provisioner (ปัจจุบันดูแลในชื่อ nfs-ganesha-server-and-external-provisioner) รัน NFS server เป็น Pod ที่มี volume ของตัวเองรองรับ แล้วสร้าง directory ให้ทุก PVC วิธีนี้มีประโยชน์บน platform ที่ block storage รองรับแค่ RWO
StorageClass สำหรับ csi-driver-nfs และ claim ที่ใช้ร่วมกัน:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: nfs-csi
provisioner: nfs.csi.k8s.io
parameters:
server: nfs.internal.example.com
share: /exports/k8s
reclaimPolicy: Delete
volumeBindingMode: Immediate
mountOptions:
- nfsvers=4.1
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: uploads
spec:
accessModes: ["ReadWriteMany"]
storageClassName: nfs-csi
resources:
requests:
storage: 100Giตอนนี้ทุก replica ของ Deployment ก็ mount uploads ได้ ไม่ว่าจะอยู่ node ไหน
แต่ควรรู้ข้อจำกัดก่อนพึ่งพามัน:
- NFS server ในคลัสเตอร์คือ Pod ตัวเดียวบน volume เดียว ถ้ามันล่ม ทุก Pod ที่ใช้งานอยู่จะค้างตาม
- NFS เพิ่ม network latency ให้ทุก operation ของไฟล์ และจัดการ locking ได้คาดเดายากกว่า disk ในเครื่อง อย่าเอา data directory ของ database ไปไว้บน NFS ให้ใช้ block storage แบบ RWO แยกต่อ replica ตามที่อธิบายไว้ใน การรัน database บน production
- สำหรับไฟล์ที่ผู้ใช้อัปโหลด object storage อย่าง S3 มักเหมาะกว่า shared filesystem
แก้ปัญหา persistent volume
| อาการ | สาเหตุที่น่าจะเป็น |
|---|---|
PVC Pending พร้อมข้อความ "waiting for first consumer" | WaitForFirstConsumer: จะ bind เมื่อมี Pod มาใช้ เป็นเรื่องปกติ |
PVC Pending พร้อม "no persistent volumes available" | ไม่มี default StorageClass, สะกดชื่อ class ผิด หรือไม่มี static PV ที่ตรงเงื่อนไข |
Pod Pending พร้อม "unbound immediate PersistentVolumeClaims" | claim สร้างไม่สำเร็จ ให้ดู kubectl describe pvc |
Multi-Attach error | volume แบบ RWO ถูกเรียกใช้จาก node ที่สอง มักเกิดตอน rolling update ของ Deployment |
| "volume node affinity conflict" | disk อยู่คนละ zone กับ node ที่ Pod ถูก schedule ไป |
ค้าง ContainerCreating พร้อม mount timeout | ติดต่อ NFS server ไม่ได้ หรือ node ไม่มีเครื่องมือ NFS client |
กรณี Multi-Attach กับ Deployment ที่มี replica เดียว ให้ใช้ strategy: Recreate หรือเปลี่ยนไปใช้ StatefulSet เพื่อให้ Pod เก่าปล่อย disk ก่อนที่ Pod ใหม่ต้องใช้
คำถามที่พบบ่อย
PV กับ PVC ต่างกันอย่างไร
PV คือ storage จริงที่เป็น resource ระดับคลัสเตอร์ ส่วน PVC คือคำขอใช้ storage ที่อยู่ใน namespace Pod อ้างถึง PVC และ Kubernetes จะ bind แต่ละ PVC เข้ากับ PV หนึ่งตัว
ถ้าลบ PVC ข้อมูลจะหายไหม
ขึ้นกับ reclaim policy ถ้าเป็น Delete ซึ่งเป็นค่าเริ่มต้นของ dynamic provisioning disk จะถูกลบไปด้วย ถ้าเป็น Retain PV และข้อมูลจะยังอยู่จนกว่า admin จะจัดการ
หลาย Pod ใช้ PersistentVolumeClaim เดียวกันได้ไหม
ได้ ถ้าเป็น RWO ต้องเป็น Pod ที่อยู่บน node เดียวกันเท่านั้น ถ้า Pod กระจายหลาย node ต้องใช้ volume แบบ RWX อย่าง NFS, EFS หรือ CephFS
ขยายขนาด PersistentVolumeClaim ได้ไหม
ได้ ถ้า StorageClass ตั้ง allowVolumeExpansion: true และ driver รองรับ ให้แก้ขนาดที่ขอใน PVC แต่ลดขนาดไม่ได้
ควรใช้ static หรือ dynamic provisioning
เกือบทุกกรณีให้ใช้ dynamic provisioning ผ่าน StorageClass ส่วน static PV เหมาะกับ storage ที่มีอยู่แล้วและต้องการนำมาใช้ เช่น NFS share เดิมขององค์กร
Checklist เรื่อง storage
- มี default StorageClass และ block storage แบบ zonal ใช้
WaitForFirstConsumer - ข้อมูลสำคัญใช้
reclaimPolicy: Retainหรือมี snapshot สำรองไว้ - เปิด
allowVolumeExpansionในที่ที่ driver รองรับ - Database ใช้ RWO (หนึ่ง volume ต่อ replica) และใช้ RWX เฉพาะที่ต้องแชร์จริง ๆ
- รู้ว่า PVC จะเป็นอย่างไรเมื่อ scale down, ถูกลบ หรือ node พัง
ถ้าอยากลองตั้ง StorageClass และ PVC บนคลัสเตอร์จริง คอร์ส DevOps ฟรีของ Vectorkub มี lab ให้ลงมือทำ
