أحمال عمل Kubernetes وإعدادها

StatefulSets

18 دقيقة الدرس 5 من 32

StatefulSets

معظم أعباء العمل في Kubernetes عديمة الحالة: أي Pod مطابق لأي Pod آخر، ويمكن إيقاف أو استبدال الـ Pods بأي ترتيب. قواعد البيانات وسماسرة الرسائل وذاكرات التخزين المؤقت الموزعة لا تستطيع العمل بهذه الطريقة. يمتلك وسيط Kafka هوية ثابتة يستخدمها الوسطاء الآخرون والعملاء في إعداداتهم. تمتلك عقدة Cassandra جزءاً من البيانات يجب أن يتبعها عبر عمليات إعادة التشغيل. تشكل عقد Elasticsearch مجموعة بالاسم ويجب أن تعيد الانضمام بنفس معرف العقدة بعد الترقية التدريجية. وُجدت StatefulSets لمنح الـ Pods هوية ثابتة ودائمة — اسم مضيف يمكن التنبؤ به، وتسلسل بدء وإيقاف منظم، ومطالبة PersistentVolumeClaim مخصصة تبقى على قيد الحياة عبر إعادة جدولة Pod.

ما الذي يجعل StatefulSet مختلفاً

يتعامل Deployment مع جميع Pods الخاصة به على أنها قطعان قابلة للاستبدال. يتعامل StatefulSet مع كل Pod باعتباره فرداً محدداً ومرتباً. الضمانات التي يوفرها هي:

  • هوية شبكة ثابتة: يحصل كل Pod على اسم DNS بصيغة <pod-name>.<headless-service>.<namespace>.svc.cluster.local — على سبيل المثال postgres-0.postgres-headless.production.svc.cluster.local. يظل اسم المضيف هذا ثابتاً عبر عمليات إعادة التشغيل وإعادة الجدولة إلى عقد مختلفة.
  • تخزين ثابت: لكل Pod مطالبة PersistentVolumeClaim خاصة به تُنشأ من كتلة volumeClaimTemplates. يُسمى المطالبة <volume-name>-<pod-name> (مثل data-postgres-0). عند إعادة جدولة Pod postgres-0، يعيد الاتصال بنفس PVC — وبالتالي بنفس البيانات الأساسية.
  • طرح منظم ومتحكم فيه: يتم إنشاء وحذف الـ Pods بترتيب حتمي (0، 1، 2 … للإنشاء؛ عكسي للحذف). يجب أن يكون Pod في حالة Running وReady قبل بدء الترتيب التالي.
الـ Headless Service ليس اختيارياً. يتطلب StatefulSet إنشاء Headless Service (clusterIP: None) بشكل منفصل. هذه الخدمة هي ما يسجل سجلات DNS لكل Pod في DNS الخاصة بالمجموعة. بدونها، لا تعمل ضمانة اسم المضيف الثابت.

تشريح مانيفست StatefulSet

فيما يلي StatefulSet لـ PostgreSQL واقعي للإنتاج. لاحظ ترابط serviceName وتعريف Headless Service وvolumeClaimTemplates:

# 1. Headless Service — يجب أن يوجد قبل StatefulSet apiVersion: v1 kind: Service metadata: name: postgres-headless namespace: production labels: app: postgres spec: clusterIP: None # headless — لا VIP، فقط سجلات DNS A لكل pod selector: app: postgres ports: - name: postgres port: 5432 --- # 2. StatefulSet apiVersion: apps/v1 kind: StatefulSet metadata: name: postgres namespace: production spec: serviceName: postgres-headless # يرتبط بالـ headless service أعلاه replicas: 3 selector: matchLabels: app: postgres updateStrategy: type: RollingUpdate rollingUpdate: partition: 0 # اضبط على N للكاناري: يُحدَّث فقط pods >= N podManagementPolicy: OrderedReady # افتراضي؛ استخدم Parallel للتوسع الأسرع (لكن أقل أماناً) template: metadata: labels: app: postgres spec: terminationGracePeriodSeconds: 60 containers: - name: postgres image: postgres:16.2 ports: - containerPort: 5432 name: postgres env: - name: PGDATA value: /var/lib/postgresql/data/pgdata - name: POSTGRES_PASSWORD valueFrom: secretKeyRef: name: postgres-secret key: password resources: requests: cpu: "500m" memory: "1Gi" limits: cpu: "2000m" memory: "4Gi" volumeMounts: - name: data mountPath: /var/lib/postgresql/data readinessProbe: exec: command: ["pg_isready", "-U", "postgres"] initialDelaySeconds: 10 periodSeconds: 5 failureThreshold: 3 livenessProbe: exec: command: ["pg_isready", "-U", "postgres"] initialDelaySeconds: 30 periodSeconds: 15 failureThreshold: 5 volumeClaimTemplates: - metadata: name: data spec: accessModes: ["ReadWriteOnce"] storageClassName: gp3-encrypted # للإنتاج: استخدم فئة مشفرة عالية IOPS resources: requests: storage: 100Gi

الهوية الثابتة من الداخل

عند تطبيق هذا المانيفست، ينشئ Kubernetes Pods باسم postgres-0 وpostgres-1 وpostgres-2 — بهذا الترتيب، منتظراً أن يكون كل منها Ready قبل المتابعة. كما ينشئ تلقائياً PVCs باسم data-postgres-0 وdata-postgres-1 وdata-postgres-2. يسجل DNS الخاص بالمجموعة بعد ذلك سجلات A بحيث يتحلل postgres-0.postgres-headless.production.svc.cluster.local دائماً إلى IP الخاص بـ Pod المسمى postgres-0 حالياً، بصرف النظر عن العقدة الفيزيائية التي يعمل عليها.

StatefulSet stable identity: headless Service, ordered Pods, and dedicated PVCs Service: postgres-headless clusterIP: None Pod: postgres-0 postgres-0.postgres-headless Pod: postgres-1 postgres-1.postgres-headless Pod: postgres-2 postgres-2.postgres-headless PVC: data-postgres-0 100 Gi · gp3-encrypted PVC: data-postgres-1 100 Gi · gp3-encrypted PVC: data-postgres-2 100 Gi · gp3-encrypted الإنشاء بالترتيب 0 → 1 → 2 ؛ الحذف بالترتيب 2 → 1 → 0
StatefulSet بثلاث نسخ — لكل Pod اسم DNS ثابت ومطالبة PersistentVolumeClaim مخصصة.

التحديثات التدريجية وحقل Partition

تتقدم تحديثات StatefulSet التدريجية بترتيب رقمي عكسي (من الفهرس الأعلى أولاً). حقل partition في updateStrategy.rollingUpdate هو أحد أكثر عناصر التحكم في الإنتاج فائدة — وأقلها استخداماً. يعني ضبط partition: 2 على مجموعة مكونة من ثلاث نسخ أن Pod postgres-2 فقط يُحدَّث عند تغيير قالب Pod. تستمر Pods postgres-0 وpostgres-1 في تشغيل المواصفات القديمة. هذه آلية كاناري مدمجة لأعباء العمل ذات الحالة: تحقق من الإصدار الجديد على النسخة الأقل تأثيراً قبل تطبيقه على الـ Primary.

# طرح كاناري: تحديث postgres-2 أولاً فقط kubectl patch statefulset postgres -n production \ --type='json' \ -p='[{"op":"replace","path":"/spec/updateStrategy/rollingUpdate/partition","value":2}]' # تأكيد أن postgres-2 على الصورة الجديدة، ثم تحديث postgres-1 kubectl patch statefulset postgres -n production \ --type='json' \ -p='[{"op":"replace","path":"/spec/updateStrategy/rollingUpdate/partition","value":1}]' # أخيراً، تحديث postgres-0 (غالباً الـ primary — آخر من يُحدَّث) kubectl patch statefulset postgres -n production \ --type='json' \ -p='[{"op":"replace","path":"/spec/updateStrategy/rollingUpdate/partition","value":0}]' # التحقق من أن جميع الـ pods على الإصدار الجديد kubectl rollout status statefulset/postgres -n production kubectl get pods -n production -l app=postgres \ -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.containers[0].image}{"\n"}{end}'

دورة حياة PVC وسياسة الاحتفاظ

بشكل افتراضي، لا يتم حذف PVCs التي أنشأها StatefulSet عند تقليص StatefulSet أو حذفه. هذا مقصود: لا تريد أن يمسح kubectl delete statefulset بيانات قاعدة البيانات. منذ Kubernetes 1.27 يتيح حقل persistentVolumeClaimRetentionPolicy التحكم في هذا بشكل صريح:

spec: persistentVolumeClaimRetentionPolicy: whenDeleted: Retain # تبقى PVCs عند حذف StatefulSet (آمن للإنتاج) whenScaled: Delete # تُحذف PVCs عند تقليص النسخ (لاسترداد التخزين)
لا تضبط whenDeleted: Delete أبداً على قواعد البيانات في الإنتاج إلا إذا كانت لديك نسخ احتياطية مُتحققة ومختبرة. سيدمر kubectl delete statefulset --cascade=foreground مع تلك السياسة كل PVC. الافتراضي الآمن هو Retain على كلا المحورين — تعيش PVCs بعد StatefulSet ويجب تنظيفها يدوياً.

أنماط الفشل في الإنتاج

فهم أوجه فشل StatefulSets لا يقل أهمية عن معرفة كيفية تكوينها:

  • انقسام الدماغ بعد فشل العقدة: إذا أصبحت عقدة غير قابلة للوصول (تقسيم شبكة وليس عطلاً)، يُعلَّم خادم API على Pods الخاصة بها بـ Unknown لكنه لا يُنهيها قسراً. لن ينشئ متحكم StatefulSet Pod بديلاً للترتيب N طالما قد يكون Pod بالاسم نفسه لا يزال يعمل — يُفضل خطأ عدم وجود نسخة مكررة على خطر وجود podين يكتبان في نفس PVC. الإصلاح في الإنتاج: ضبط تسامح قصير لـ node.kubernetes.io/unreachable على مواصفة Pod، أو استخدام Pod disruption budget مع سير عمل استنزاف عقدة من cluster autoscaler.
  • استنزاف سعة PVC: على عكس Deployments، لا يستطيع Pod في StatefulSet الانتقال إلى PVC مختلف إذا امتلأ تخزينه. ضع دائماً تنبيهات التخزين عند 75% من الطاقة واستخدم allowVolumeExpansion: true على StorageClass حتى تتمكن من تغيير الحجم دون توقف.
  • ترتيب Init Container لتشغيل المجموعة لأول مرة: تحتاج التطبيقات ذات الحالة مثل Cassandra أو Kafka إلى معرفة ما إذا كانت العقدة الأولى في مجموعة جديدة أم عقدة استبدال تنضم إلى مجموعة موجودة. استخدم init container يستعلم عن الـ headless service — إذا أعاد تحليل DNS صفر سجلات، فهي مجموعة جديدة؛ وإذا أعاد IPs موجودة، فهي عملية انضمام.
استخدم Service مستقلة للقراءة لتطبيقك: الـ Headless Service للاكتشاف بين الأقران، وليس لحركة مرور التطبيق. أنشئ Service عادية بـ ClusterIP (أو LoadBalancer للوصول الخارجي) تختار بنفس التصنيفات وتوجه إلى النسخة الصحيحة (مثل الـ primary في إعداد primary/replica، أو جميع النسخ لأعباء عمل القراءة المتوازنة). تستخدم كثير من الفرق تصنيفات مثل role: primary تُضاف إلى Pod الرئيسي عند البدء لجعل اختيار Service أمراً بسيطاً.

التوسع والحذف

قم بالتوسع بـ kubectl scale statefulset postgres --replicas=5 -n production. تُضاف Pods الجديدة بترتيب تصاعدي (3، 4) ويجب أن يكون كل منها Ready قبل إنشاء التالي. للتقليص، شغّل نفس الأمر بعدد أقل — تُنهى Pods بترتيب تنازلي (4، 3). تبقى PVCs الخاصة بها ما لم تقل سياسة الاحتفاظ خلاف ذلك. احرص دائماً على استنزاف نسخة ذات حالة بأمان (تسليم العمليات الجارية، مسح WAL، إطلاق عضوية المجموعة) بالتأكد من وجود خطاف preStop مناسب أو بتكوين التطبيق للتعامل مع SIGTERM بتسلسل إيقاف نظيف ضمن terminationGracePeriodSeconds.