شبكات Kubernetes والتخزين

وحدات التخزين والمجلدات الدائمة

18 دقيقة الدرس 7 من 31

وحدات التخزين والمجلدات الدائمة

كل حاوية في Kubernetes تبدأ بنظام ملفات مؤقت فارغ مبني فوق صورتها. عندما تنتهي الحاوية — بسبب عطل، أو قتل OOM، أو تحديث متدرج — يضيع ذلك النظام الملفات. للخدمات عديمة الحالة هذا ميزة لا عيب. لكن في اللحظة التي تشغّل فيها قاعدة بيانات، أو وسيط رسائل، أو مخزن نقاط تفتيش للتعلم الآلي، أو أي حمل عمل تعيش قيمته في بيانات مكتوبة على القرص — تحتاج تخزينًا يتجاوز عمر الحاوية وكثيرًا ما يتجاوز عمر الـ Pod نفسه. يُصمّم Kubernetes التخزين على ثلاثة مستويات تجريد متمايزة: Volume الخام، وPersistentVolume (PV) على مستوى الكلاستر، والمطالبة به من جانب المستخدم PersistentVolumeClaim (PVC). فهم مكان كل منها، ولماذا صُمّم نموذج الربط بهذه الطريقة، هو معرفة أساسية لتشغيل أحمال العمل ذات الحالة في الإنتاج على نطاق واسع.

وحدات التخزين المؤقتة: عمرها مرتبط بعمر الـ Pod

Volume في Kubernetes ليست PersistentVolume — بل هي دليل يُتاح داخل حاويات الـ Pod، بعمر مقيّد بعمر الـ Pod. عند حذف الـ Pod تُهدم وحدة التخزين. وحدات التخزين المؤقتة مفيدة لثلاثة أنماط بالضبط:

  • emptyDir — دليل فارغ يُنشأ عند بدء الـ Pod، مدعوم بقرص العقدة (أو الذاكرة مع medium: Memory). مثالي للمساحة المؤقتة، والذاكرات المخبأة المشتركة بين الحاويات الجانبية، والتواصل بين الحاويات داخل Pod واحد. يُستخدم على نطاق واسع في ذاكرات التخزين المؤقتة القائمة على Envoy في شبكات الخدمات.
  • configMap / secret — يُوصّل كائنات API كملفات. وحدة تخزين secret مُوصَّلة على /etc/tls تمنح الحاويات الوصول إلى شهادات TLS دون دمجها في الصورة. هذه أيضًا مؤقتة بمعنى أنها تتبع الـ Pod.
  • projected — تجمع مصادر متعددة (configMap، secret، serviceAccountToken، downwardAPI) في نقطة وصل واحدة. الطريقة المعيارية لإتاحة رمز حساب الخدمة المُدار تلقائيًا والقصير العمر للحاوية في الكلاسترات الحديثة.
emptyDir المدعوم بالذاكرة (medium: Memory) يحتسب ضمن حد الذاكرة للحاوية. إذا كتبت حاوية التهيئة 500 MiB من البيانات المُفككة إلى emptyDir مدعوم بالذاكرة وحد حاويتك 512 MiB، سيُقتل الـ Pod بـ OOM قبل أن يبدأ حمل العمل الرئيسي. دائمًا اضبط sizeLimit الصريح على وحدات تخزين emptyDir في بيانات الإنتاج.

PersistentVolumes: موارد التخزين على مستوى الكلاستر

PersistentVolume (PV) هو مورد على نطاق الكلاستر يمثل قطعة من التخزين — مجلد EBS في AWS، قرص GCP دائم، مشاركة NFS، صورة Ceph RBD — تم إعدادها وتسجيلها مع Kubernetes. فكّر في PV كما تفكر في العقدة: هو مورد في مخزون الكلاستر، مستقل عن أي حمل عمل بعينه. يُشفّر PV أربع خصائص حاسمة:

  • السعة (Capacity) — حجم التخزين (storage: 50Gi).
  • أوضاع الوصول (Access modes) — كم عقدة وبأي طريقة يمكنها وصل وحدة التخزين (انظر أدناه).
  • سياسة الاسترداد (Reclaim policy) — ما يحدث للتخزين الأساسي عند حذف PVC (Retain، Delete، أو Recycle المهجورة).
  • وضع الحجم (VolumeMode)Filesystem (الافتراضي، يُوصَّل كدليل) أو Block (جهاز block خام، تستخدمه قواعد البيانات التي تدير I/O بنفسها كـ Cassandra وبعض تكوينات PostgreSQL).

أوضاع الوصول — الحقل الأكثر سوء فهمًا

أوضاع الوصول تحدد العقد بين خلفية التخزين ومجدول الكلاستر. هناك أربعة أوضاع محددة في الـ API:

  • ReadWriteOnce (RWO) — يمكن وصل وحدة التخزين للقراءة والكتابة من عقدة واحدة في آن. هذا الوضع مدعوم من جميع خلفيات تخزين block (EBS، GCP PD، Azure Disk). RWO لا يعني Pod واحد — يمكن لعدة Pods على نفس العقدة وصلها.
  • ReadOnlyMany (ROX) — يمكن وصل وحدة التخزين للقراءة فقط من عقد متعددة في آن. مفيد لتوزيع البيانات المرجعية للقراءة فقط (أوزان نماذج ML، حزم الأصول الثابتة) على أسطول من العقد.
  • ReadWriteMany (RWX) — يمكن وصل وحدة التخزين للقراءة والكتابة من عقد متعددة في آن. أنظمة الملفات المشتركة فقط تدعم هذا: NFS، AWS EFS، Azure Files، GCP Filestore، CephFS. خلفيات تخزين Block لا تدعم RWX.
  • ReadWriteOncePod (RWOP) — أُدخل في Kubernetes 1.22، نسخة أكثر صرامة من RWO تفرض دلالات Pod واحد على مستوى الـ API لا مجرد عقدة واحدة. RWOP هو الخيار الصحيح لحجم قاعدة بيانات أساسي حيث يمكن لـ Podين يتسابقان على وصل نفس وحدة التخزين أن يتسببا في split-brain أو إفساد البيانات.
RWO لا يحمي من split-brain. إذا انعزلت عقدة شبكيًا لكن لم تُحذف، قد يستمر kubelet عليها في تشغيل الـ Pod مع وحدة التخزين RWO مُوصَّلة. عند حذف Kubernetes للـ Pod قسرًا وإعادة جدولته على عقدة سليمة، قد يمتلك الـ Pod القديم (المعلق) والجديد وحدة التخزين في الوقت ذاته. لقواعد بيانات كـ PostgreSQL أو etcd هذا سيناريو إفساد بيانات. دائمًا اقرن وحدات التخزين RWO لقواعد البيانات بـ ReadWriteOncePod (مستقر في Kubernetes 1.29+) واضبط pod disruption budgets بعناية.
PV/PVC Binding Lifecycle PV / PVC Binding Lifecycle Storage Backend EBS / NFS / Ceph provision PersistentVolume cluster-scoped | 50Gi | RWO Status: Available Control Plane PV Controller — matches PVC PersistentVolumeClaim namespace-scoped | 50Gi | RWO Status: Pending Status: Bound Status: Bound Pod mounts PVC as /data 1-to-1 binding: one PV is exclusively bound to one PVC. PV cannot be reused until released.
دورة حياة ربط PV/PVC: يتطابق PV (مورد الكلاستر) و PVC (طلب النطاق) بواسطة PV Controller؛ ثم يوصّل الـ Pod الـ PVC المرتبط.

PersistentVolumeClaims: طلبات تخزين قابلة للنقل

PersistentVolumeClaim (PVC) هو طلب تخزين مقيّد بنطاق namespace. يكتب المطوّر PVC يحدد الحجم الأدنى، ووضع الوصول، واختياريًا storageClassName. يفحص PV controller في مستوى التحكم الـ PVs المتاحة ويربط أول واحد يستوفي المعايير الثلاثة. الربط حصري وواحد لواحد: بمجرد ربط PV بـ PVC، لا يمكن لأي PVC آخر ربطه. هذه خاصية تصميمية حاسمة — تعني أن PV بسعة 100Gi سيستهلك بالكامل من قِبَل PVC بسعة 10Gi إذا كان هذا التطابق الوحيد المتاح، مما يهدر 90Gi. StorageClasses والتوفير الديناميكي (الدرس التالي) يحلان هذا بإنشاء PVs بالحجم المناسب عند الطلب.

# PV ثابت (المسؤول يوفر مسبقًا مجلد EBS) apiVersion: v1 kind: PersistentVolume metadata: name: postgres-pv-01 spec: capacity: storage: 100Gi volumeMode: Filesystem accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Retain # احتفظ بمجلد EBS عند حذف PVC storageClassName: gp3-retain # يجب أن يطابق storageClassName الـ PVC awsElasticBlockStore: volumeID: vol-0abc123def456789a # معرف مجلد EBS موجود مسبقًا fsType: ext4 --- # بيان PVC (فريق المطورين / التطبيق) apiVersion: v1 kind: PersistentVolumeClaim metadata: name: postgres-data namespace: production spec: accessModes: - ReadWriteOnce resources: requests: storage: 100Gi storageClassName: gp3-retain --- # Pod يستهلك الـ PVC apiVersion: v1 kind: Pod metadata: name: postgres namespace: production spec: containers: - name: postgres image: postgres:16 env: - name: PGDATA value: /var/lib/postgresql/data/pgdata volumeMounts: - name: data mountPath: /var/lib/postgresql/data volumes: - name: data persistentVolumeClaim: claimName: postgres-data # يشير إلى الـ PVC بالاسم

سياسات الاسترداد وما يحدث فعلًا لبياناتك

يحدد حقل persistentVolumeReclaimPolicy ما يفعله الكلاستر بمورد التخزين الأساسي عند حذف الـ PVC:

  • Retain — لا يُحذف الـ PV ولا يُتاح لإعادة الربط. يدخل في حالة Released. يجب على المسؤول فحص البيانات يدويًا، والتقاط snapshot إذا لزم، ثم حذف كائن الـ PV لتحرير التخزين الأساسي. هذه السياسة الصحيحة لقواعد بيانات الإنتاج.
  • Delete — يُحذف كائن الـ PV وأصل التخزين الأساسي (مجلد EBS، GCP PD، إلخ) تلقائيًا عند حذف الـ PVC. هذا الافتراضي للـ PVs المُوفَّرة ديناميكيًا. آمن للتخزين المؤقت عديم الحالة؛ خطير لقواعد البيانات.
  • Recycle — مهجور منذ Kubernetes 1.11 وأُزيل في 1.25. لا تستخدمه.
# فحص حالة PV وسياسة الاسترداد kubectl get pv -o wide # NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM # postgres-pv-01 100Gi RWO Retain Bound production/postgres-data # بعد حذف PVC، تحقق من حالة PV: kubectl delete pvc postgres-data -n production kubectl get pv postgres-pv-01 # STATUS ستكون "Released" — البيانات آمنة، لكن PV غير قابل للاستخدام حتى التنظيف اليدوي # لاسترداد PV يدويًا في حالة Released (بعد التأكد من سلامة البيانات/snapshot): kubectl patch pv postgres-pv-01 -p '{"spec":{"claimRef": null}}' # يعود PV إلى Available؛ يمكنه الآن الارتباط بـ PVC جديد # (افعل هذا فقط بعد التحقق من أن البيانات القديمة انتهت أو غير ضرورية) # التحقق من حالة ربط PVC وبأي PV ارتبط: kubectl get pvc -n production # NAME STATUS VOLUME CAPACITY ACCESS MODES # postgres-data Bound postgres-pv-01 100Gi RWO
توسيع وحدة التخزين بدون توقف: إذا كان PVC يقترب من امتلاء المساحة، عدّل spec.resources.requests.storage الخاص به إلى قيمة أكبر — لا يمكنك تصغير PVC. سيوسّع درايفر CSI نظام الملفات أثناء التشغيل (لمعظم درايفرات block-storage على Kubernetes 1.24+) دون إعادة تشغيل الـ Pod. دائمًا فعّل allowVolumeExpansion: true في StorageClass (مُغطى في الدرس التالي). راقب استخدام القرص الفعلي عبر kubectl exec + df -h أو أظهره عبر مقاييس kubelet_volume_stats_* في Prometheus، وأنشئ تنبيهًا عند 80% سعة لمنحك وقتًا للتوسع قبل بلوغ الحد.

المسارات الفرعية للحجم: مشاركة حجم واحد لاستخدامات متعددة

نمط شائع في الإنتاج للأحمال الصغيرة هو وصل PVC واحد في مسارات متعددة داخل الحاوية باستخدام subPath. على سبيل المثال، قد يوفر PV واحد عبر NFS أدلة منفصلة لسجلات التطبيق، والملفات المرفوعة، ونسخ احتياطية للإعدادات. استخدم subPath بحذر: التغييرات على ConfigMap أو Secret مُوصَّل لا تنتشر إلى الحاويات التي تستخدم subPath — هذا قيد معروف في Kubernetes حيث يُثبَّت inode عند وقت الوصل. لتدوير ConfigMap/Secret، فضّل الوصل الكامل وقراءة مسار الملف.

التوفير الثابت مقابل التوفير الديناميكي: كل ما في هذا الدرس هو توفير ثابت — مسؤول يُنشئ كائنات PV يدويًا. التوفير الثابت لا يزال يُستخدم عندما تحتاج تحكمًا دقيقًا في التخزين المادي الذي يدعم حمل عمل حرجًا (مجلد EBS محدد في AZ معروفة، صورة Ceph RBD محددة محمّلة مسبقًا بالبيانات). التوفير الديناميكي عبر StorageClasses — الذي يلغي كائن PV الذي ينشئه المسؤول تمامًا — هو المعيار لمعظم أحمال العمل ومُغطى في الدرس التالي.

أنماط فشل الإنتاج التي يجب استيعابها

أعطال التخزين من أكثر الأعطال ضررًا في Kubernetes لأنها كثيرًا ما تظهر بصمت. الأنماط الأكثر شيوعًا على نطاق واسع:

  • PVC عالق في Pending — لا يوجد PV يستوفي المطالبة. تحقق من تطابق السعة، ووضع الوصول، وstorageClassName مع PV متاح. kubectl describe pvc <name> سيُظهر سبب فشل الربط.
  • مهلة وصل وحدة التخزين على مستوى العقدة — خاصة مع EBS: عند إعادة جدولة Pod بعد فشل عقدة، قد تستغرق دورة فصل/إعادة وصل EBS من 60 إلى 90 ثانية. خلال هذه الفترة يبقى الـ Pod الجديد عالقًا في ContainerCreating. تخفيف: معالجات إنهاء العقد (AWS Node Termination Handler) تفصل وحدات التخزين بشكل استباقي قبل إنهاء المثيل.
  • الوحدة الممتلئة تقتل العملية — كتابة إلى نظام ملفات ext4 ممتلئ تعيد ENOSPC، ومعظم قواعد البيانات (PostgreSQL، MySQL) تتعطل بدلًا من التدهور بأمان. راقب عند 80%؛ توسّع قبل الوصول للحد. فكّر في ضبط الكتل المحجوزة على 0 (tune2fs -m 0) على وحدات تخزين قواعد البيانات لأن 5% المحجوزة ليس لها قيمة لـ DB يملكها عملية واحدة.