أساسيات Kubernetes

الـ Pod: الوحدة الأساسية في Kubernetes

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

الـ Pod: الوحدة الأساسية في Kubernetes

في Kubernetes، كل حمل عمل — سواء كان خادم API عديم الحالة، أو قاعدة بيانات، أو مهمة دفعية — يعمل في نهاية المطاف داخل Pod. الـ Pod ليس حاوية (container)؛ بل هو غلاف خفيف يجمع حاوية واحدة أو أكثر في وحدة واحدة قابلة للجدولة ضمن بيئة تشغيل مشتركة. فهم بنية الـ Pod بهذا العمق أمر ضروري: كل تجريد أعلى مستوى (Deployment، StatefulSet، Job) هو في جوهره مصنع يُنشئ الـ Pods ويديرها.

بنية الـ Pod

يصف قسم spec في مانيفيست الـ Pod كل ما يحتاجه المُجدوِل (scheduler) والـ kubelet لتشغيل حمل العمل:

  • containers — مواصفات حاوية واحدة أو أكثر، لكل منها صورة، وأمر، ومنافذ، ومتغيرات بيئة، وطلبات موارد.
  • volumes — وحدات تخزين يمكن لأي حاوية في الـ Pod تثبيتها. نطاق الـ volumes يقتصر على عمر الـ Pod.
  • initContainers — حاويات تعمل حتى الاكتمال قبل بدء أي حاوية عادية. تُستخدم للإعداد المسبق: تشغيل migration قاعدة البيانات، جلب الأسرار، الانتظار حتى تصبح التبعيات جاهزة.
  • restartPolicyAlways (الافتراضي، للخدمات طويلة الأمد)، OnFailure (للمهام)، أو Never.
  • serviceAccountName — هوية RBAC التي يستخدمها الـ Pod لاستدعاء Kubernetes API.
  • securityContext — إعدادات الأمان على مستوى الـ Pod: التشغيل كمستخدم غير جذر، نظام ملفات جذر للقراءة فقط، مرشحات syscall، وملفات تعريف AppArmor.
  • affinity / tolerations / nodeSelector — قيود الجدولة التي تتحكم في أي الـ Nodes يمكن للـ Pod أن يُوضع عليها.

الحقيقة المعمارية الأهم في الـ Pod هي مشاركة شبكة IPC namespace. كل حاوية داخل نفس الـ Pod ترى تماماً واجهة loopback (localhost) نفسها، وعنوان IP نفسه، وـ hostname نفسه. إذا ربطت الحاوية A المنفذ 8080، يمكن للحاوية B الوصول إليه عبر localhost:8080. هذا تصميم مقصود — يتيح للعمليات المساعدة المترابطة (sidecars) التواصل دون الحاجة إلى service mesh لاتصالات داخل الـ Pod.

Pod anatomy: shared network namespace, volumes, init containers, and sidecars Pod (IP: 10.244.1.7) Shared Network Namespace (localhost, same IP, same ports) init-migrate Runs DB migration exits 0 → proceeds initContainer api-server image: my-api:v2 port: 8080 main container log-shipper tails /var/log/app forwards to Loki sidecar container emptyDir Volume: /var/log/app Shared between api-server and log-shipper kubelet on Node Starts initContainers first → then all containers in parallel → monitors liveness/readiness
بنية الـ Pod: يعمل init container أولاً، ثم تبدأ الحاوية الرئيسية والـ sidecar معاً مشاركَين namespace الشبكة والـ volume.

كتابة مانيفيست Pod حقيقي

نادراً ما تُنشئ Pods مجردة في بيئة الإنتاج (تفعل ذلك الـ Deployments نيابةً عنك)، لكن يجب أن تكون قادراً على قراءة المانيفيستات وكتابتها لتشخيص المشكلات وفهم ما تُولِّده الكائنات ذات المستوى الأعلى. فيما يلي مانيفيست Pod بمعايير الإنتاج لحاوية واحدة يتضمن الحقول التي ستصادفها في الـ clusters الحقيقية:

# pod-api.yaml apiVersion: v1 kind: Pod metadata: name: api-server namespace: production labels: app: api version: v2 team: platform annotations: prometheus.io/scrape: "true" prometheus.io/port: "9090" spec: serviceAccountName: api-sa securityContext: runAsNonRoot: true runAsUser: 1000 fsGroup: 2000 initContainers: - name: init-migrate image: my-api:v2 command: ["python", "manage.py", "migrate", "--noinput"] envFrom: - secretRef: name: db-credentials containers: - name: api-server image: my-api:v2 ports: - containerPort: 8080 name: http - containerPort: 9090 name: metrics envFrom: - secretRef: name: db-credentials - configMapRef: name: api-config resources: requests: cpu: "250m" memory: "256Mi" limits: cpu: "1000m" memory: "512Mi" readinessProbe: httpGet: path: /healthz/ready port: 8080 initialDelaySeconds: 5 periodSeconds: 10 livenessProbe: httpGet: path: /healthz/live port: 8080 initialDelaySeconds: 15 periodSeconds: 20 failureThreshold: 3 volumeMounts: - name: app-logs mountPath: /var/log/app volumes: - name: app-logs emptyDir: {} restartPolicy: Always
طلبات الموارد مقابل الحدود: تُستخدم requests من قِبل المُجدوِل لتحديد Node ذات سعة احتياطية كافية. أما limits فهي الحد الأقصى الصارم المُطبَّق بواسطة cgroups أثناء التشغيل. ضبط الـ limits دون requests يجعل الـ requests تساوي الـ limits تلقائياً — وهو سلوك صحيح. لا تضبط الـ limits دون requests في بيئة الإنتاج؛ فذلك يمنع المُجدوِل من تحسين توزيع الأحمال على الـ cluster بكفاءة.

الـ Pods متعددة الحاويات والـ Sidecars

الـ Pod ذو الحاوية الواحدة هو الحالة الشائعة، لكن Kubernetes يدعم صراحةً حاويات متعددة لكل Pod. يُسمى هذا النمط sidecar. الـ sidecar هو حاوية تُعزز الحاوية الرئيسية دون تعديلها. هذا فعّال لأنه يحترم مبدأ المسؤولية الواحدة على مستوى الحاوية: صورة تطبيقك تفعل شيئاً واحداً، وصورة فريق منفصل تُضيف قدرة ما (تسجيل، مقاييس، mTLS) كاهتمام منفصل تماماً.

الأنماط الثلاثة الأساسية لـ sidecar المستخدمة في الشركات الكبرى:

  • ناقل السجلات (Log shipper) — يكتب التطبيق سجلات منظمة إلى volume مشتركة من نوع emptyDir. يقرأ sidecar من Fluentd أو Promtail هذا المجلد ويُعيد توجيهه إلى مجمّع مركزي (Loki, Elasticsearch, Splunk). فريق التطبيق يمتلك صورة التطبيق؛ فريق المنصة يمتلك صورة الناقل. لا أحد منهما يحتاج لمعرفة تفاصيل تنفيذ الآخر.
  • الوكيل / service mesh — يحقن Istio sidecar من Envoy (يُسمى data plane) في كل Pod تلقائياً عبر MutatingAdmissionWebhook. كل حركة المرور الواردة والصادرة تمر عبر Envoy، مما يمنحك mTLS، وإعادة المحاولات، وحماية الدوائر، والتتبع الموزع دون تغيير سطر واحد من كود التطبيق.
  • مزامنة الأسرار (Secret sync) — يُصادق sidecar من Vault Agent على HashiCorp Vault، يسترجع الأسرار، ويكتبها إلى volume مشتركة من نوع tmpfs. يقرأ التطبيق الأسرار من ملفات بدلاً من متغيرات البيئة — وهي أفضل ممارسة أمنية لأن متغيرات البيئة يمكن تسريبها عبر /proc/PID/environ.
# multi-container pod: api + log-shipper sidecar apiVersion: v1 kind: Pod metadata: name: api-with-sidecar labels: app: api spec: containers: - name: api-server image: my-api:v2 volumeMounts: - name: app-logs mountPath: /var/log/app - name: log-shipper image: grafana/promtail:2.9.0 args: - -config.file=/etc/promtail/config.yaml volumeMounts: - name: app-logs mountPath: /var/log/app readOnly: true - name: promtail-config mountPath: /etc/promtail volumes: - name: app-logs emptyDir: {} - name: promtail-config configMap: name: promtail-config
فضّل initContainers على سكريبتات الإقلاع. تشغيل migrations قاعدة البيانات أو عمليات التهيئة داخل نقطة دخول التطبيق يعني أن الفشل هناك يُحطم حاوية التطبيق بطريقة محيّرة. الـ initContainer يجعل الفشل واضحاً: سيُظهر kubectl describe pod <name> بوضوح أي init container فشل ولماذا. الحاوية الرئيسية لا تبدأ أبداً، فلا غموض.

دورة حياة الـ Pod

ينتقل الـ Pod عبر مجموعة محددة من المراحل خلال عمره. تُبلَّغ هذه المراحل في pod.status.phase وهي ما تراه في عمود STATUS عند تشغيل kubectl get pods:

  • Pending — قبل API server الـ Pod لكنه لم يُجدوَل على Node بعد، أو جُدوِل لكن صوره لا تزال تُحمَّل.
  • Running — الـ Pod مرتبط بـ Node، جميع الحاويات أُنشئت، ولا تزال حاوية واحدة على الأقل تعمل أو في طور البدء أو إعادة التشغيل.
  • Succeeded — جميع الحاويات خرجت بكود الحالة 0 ولن تُعاد. هذه الحالة النهائية للـ Jobs.
  • Failed — جميع الحاويات خرجت، وخرجت واحدة على الأقل بكود غير صفري أو قتلها النظام.
  • Unknown — لا يمكن تحديد حالة الـ Pod، عادةً بسبب انقطاع التواصل مع kubelet الـ Node. هذا مؤشر على فشل Node أو انقسام شبكي.

ضمن مرحلة Running، للحاويات الفردية حالتها الخاصة: Waiting، Running، أو Terminated. حقل reason على حالة Waiting أو Terminated هو أول مكان تنظر فيه عند التشخيص — سيُخبرك بـ CrashLoopBackOff، OOMKilled، ImagePullBackOff، ContainerCreating، وغيرها.

CrashLoopBackOff ليست مرحلة — إنها سبب. يبحث المهندسون الجدد على Kubernetes عن "مرحلة CrashLoopBackOff". هذه المرحلة غير موجودة. إنها حقل reason على حالة حاوية من نوع Waiting. تعني أن الحاوية تعطلت مراراً وأن kubelet يطبق تأخيراً تراكمياً أسياً (يبدأ من 10 ثوانٍ، يصل حداً أقصى 5 دقائق) قبل محاولة إعادة التشغيل. شغّل دائماً kubectl logs <pod> --previous للحصول على سجلات نسخة الحاوية السابقة المتعطلة، ليس النسخة المنتظرة الحالية.

البروبات: Liveness و Readiness و Startup

لا يمكن لـ Kubernetes قراءة عقل تطبيقك — يحتاج لإشارات صريحة عن الصحة. ثلاثة أنواع من البروبات متاحة:

  • livenessProbe — "هل هذه الحاوية حية؟" إذا فشلت failureThreshold مرات، يقتل kubelet الحاوية ويعيد تشغيلها. استخدمها للكشف عن الأقفال الميتة: عملية تعمل لكنها عالقة إلى الأبد دون استجابة.
  • readinessProbe — "هل هذه الحاوية جاهزة لخدمة حركة المرور؟" إذا فشلت، يُزال IP الـ Pod من كائن Endpoints لكل Service تحدده. تتوقف حركة المرور نحو ذلك الـ Pod لكن الحاوية لا تُقتل. استخدمها للإشارة خلال الإحماء عند الإقلاع أو عندما تكون تبعية upstream معطلة مؤقتاً.
  • startupProbe — للحاويات بطيئة الإقلاع (تطبيقات JVM، تحميل نماذج ML). بينما تعمل بروبة البدء، تُعطَّل بروبات liveness وreadiness. يمنع هذا إعادات التشغيل المبكرة خلال التهيئة.
التمييز بين readiness وliveness بالغ الأهمية للنشر دون توقف. خلال التحديث المتدرج، يجب أن يجتاز الـ Pod الجديد بروبة readiness قبل إيقاف الـ Pod القديم. إذا كانت بروبة readiness متشددة جداً (مهلة قصيرة، محاولات قليلة)، ستشهد طلبات فاشلة خلال النشر رغم أن تطبيقك سليم تماماً — فقط يحتاج 15 ثانية إضافية لإحماء تجمع الاتصالات.

فحص الـ Pods عملياً

الأوامر التي يشغّلها كل مهندس DevOps عشرات المرات يومياً:

# قائمة الـ pods في جميع الـ namespaces مع Node و IP الخاصة بها kubectl get pods -A -o wide # تفريغ كامل للـ spec والـ status لـ Pod معين (أفيد أمر للتشخيص) kubectl describe pod api-server -n production # تدفق السجلات الحية من الحاوية الرئيسية kubectl logs -f api-server -n production # سجلات من حاوية sidecar محددة kubectl logs -f api-server -c log-shipper -n production # سجلات نسخة الحاوية السابقة المتعطلة kubectl logs api-server --previous -n production # فتح shell داخل حاوية تعمل kubectl exec -it api-server -n production -- /bin/sh # نسخ ملف من Pod للفحص المحلي kubectl cp production/api-server:/app/logs/error.log ./error.log # مراقبة أحداث Pod في الوقت الحقيقي (مفيد خلال النشر) kubectl get events -n production --sort-by='.lastTimestamp' -w