Pods وServices
الموارد الثلاثة التي ستكتبها كل يوم.
Pods وServices وDeployments
لكل Pod عنوان IP. يتغيّر هذا العنوان عند كل إعادة إنشاء للـ Pod، وإعادة إنشاء الـ Pod أمر متكرر: عند كل عملية نشر، وعند كل انهيار، وكلما قرّر المجدوِل (scheduler) نقله. ولذلك لا يمكن لأي تطبيق أن يستدعي تطبيقًا آخر عبر عنوانه. هذه هي المشكلة التي يعالجها هذا الدرس، وهي أعمق مما تبدو عليه.
تشكّل الموارد الثلاثة التالية سلسلة لكل حلقة فيها سبب وجود محدد. يتولى الـ Pod التنفيذ. ويضمن الـ Deployment وجود العدد الصحيح من الـ Pods بالإصدار الصحيح، ويعرف كيف ينتقل من إصدار إلى آخر دون انقطاع. أما الـ Service فيوفّر اسمًا ثابتًا وعنوانًا ثابتًا أمام مجموعة من الـ Pods تتغيّر هويتها باستمرار.
ما يربط هذه الكائنات الثلاثة ليس مرجعًا مباشرًا، وهذه هي النقطة التي تربك في البداية: فهي لا تعرف بعضها باسمها، بل عبر الـ labels. لا يعرف الـ Service أي Pods يعرضها؛ إنه يعلن معيار اختيار، ويربط به النظام باستمرار كل الـ Pods المطابقة له. هذا الاقتران غير المباشر هو ما يسمح لأي Pod بالاختفاء وللـ Pod البديل عنه بأن يُخدَّم فورًا، دون أي إعادة ضبط.
إن فهم آلية الـ labels يعني فهم 80% من Kubernetes. فالمحدد المكتوب بشكل خاطئ ينتج أكثر الأعراض غموضًا لدى المبتدئ: Service يستجيب، لكن بخطأ في الاتصال، لأنه لا يشير إلى شيء.
استخدام Deployment بدلًا من Pod
لا يُنشأ Pod يدويًا إلا في اختبار عابر. فالـ Pod المنفرد لا يملك أي مرونة في التعافي: إن مات بقي ميتًا. نعلن بدلًا من ذلك عن Deployment يتحمل هذه المسؤولية.
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
labels:
app: web
spec:
replicas: 3
selector:
matchLabels:
app: web # doit correspondre aux labels du template ci-dessous
template: # le moule à partir duquel les Pods sont fabriqués
metadata:
labels:
app: web # les labels que porteront les Pods créés
spec:
containers:
- name: nginx
image: nginx:1.27-alpine
ports:
- containerPort: 80
في هذا الملف فخّان، وكلاهما مرتبط بالموضع نفسه.
الأول: يجب أن يتطابق spec.selector.matchLabels مع spec.template.metadata.labels. يقول المحدد (selector) «الـ Pods التي أتولى مسؤوليتها تحمل هذا الـ label»، ويقول القالب (template) «الـ Pods التي أصنعها تحمل هذا الـ label». وإذا اختلفا، يرفض API server الـ manifest، وهذا خبر جيد: فالخطأ يُكتشف لحظة الكتابة.
الثاني: spec.selector غير قابل للتعديل بعد الإنشاء. ولتعديله يجب حذف الـ Deployment وإعادة إنشائه. لذا اختر الـ labels بشيء من العناية.
kubectl apply -f deployment.yaml
kubectl get pods -l app=web -w # observer la montée en charge en direct
kubectl get deployment web # répliques prêtes sur répliques voulues
الوسيط ReplicaSet
لا يدير الـ Deployment الـ Pods مباشرة. إنه ينشئ ReplicaSet، وهذا الـ ReplicaSet هو الذي يحافظ على عدد الـ Pods. تبدو هذه الطبقة بلا فائدة إلى أن نفهم الغرض منها:
kubectl get replicasets
# NAME DESIRED CURRENT READY AGE
# web-7d4b8c9f5 3 3 3 2m
كل إصدار من القالب ينتج ReplicaSet مستقلًا. وعند التحديث، يُنقص الـ Deployment عدد الـ Pods في الـ ReplicaSet القديم ويزيدها في الجديد. ويُحتفظ بالقديم عند صفر نسخة — وهذا بالضبط ما يجعل التراجع ممكنًا وفوريًا: فالتعريف السابق ما زال موجودًا، ويكفي إعادة رفع عدد نسخه.
تحقّق من هذا التسلسل الهرمي: احذف Pod يدويًا، وسيعيد الـ ReplicaSet إنشاءه في ثوانٍ، لأنه لاحظ أن العدد لم يعد مطابقًا.
kubectl delete pod web-7d4b8c9f5-xk2mp
kubectl get pods -l app=web # un nouveau Pod, avec un nouveau suffixe
الـ Labels والمحددات
الـ label زوج مفتاح-قيمة حر، يُوضع على أي كائن. وهو ليس توثيقًا: إنه آلية الربط في النظام.
metadata:
labels:
app: web
tier: frontend
version: v1
environnement: production
يعرف kubectl كيف يستعلم بناءً على هذه الـ labels، وهذا يغني عن قراءة القوائم يدويًا:
kubectl get pods -l app=web # égalité
kubectl get pods -l app=web,tier=frontend # conjonction
kubectl get pods -l 'app!=web' # négation
kubectl get pods -l 'environnement in (staging,production)' # ensemble
kubectl get pods -l version # présence de la clé
kubectl get pods --show-labels # afficher les labels
أكثر النصائح تجنبًا للفوضى: خصّص labels المحدد لهوية التطبيق (app وtier)، وضع كل ما يتغيّر كثيرًا — رقم إصدار أو معرّف commit — في labels خارج المحدد أو في annotations. فالمحدد الذي يحتوي على version: v1 يقطع استمرارية الـ Service عند كل عملية تسليم.
الـ Service: عنوان لا يتغيّر
ينشئ الـ Service عنوان IP افتراضيًا واسم DNS ثابتين، ويوزّع الحركة على كل الـ Pods المطابقة لمحدده.
ClusterIP، الحالة الافتراضية
apiVersion: v1
kind: Service
metadata:
name: web
spec:
type: ClusterIP # valeur par défaut : joignable depuis l'intérieur du cluster
selector:
app: web # tous les Pods portant app=web deviennent des cibles
ports:
- name: http
port: 80 # port du Service
targetPort: 80 # port écouté dans le conteneur
protocol: TCP
التمييز بين port وtargetPort ثابت ويستحق الترسيخ: port هو ما يستدعيه العملاء، وtargetPort هو ما تستمع إليه الحاوية. لا شيء يُلزم بأن يكونا متساويين، وهما نادرًا ما يتساويان عمليًا.
يمكن الوصول إلى الـ Service باسمه من أي Pod في العنقود (cluster)، بفضل DNS الداخلي:
web # même namespace
web.atelier # namespace explicite
web.atelier.svc.cluster.local # nom complet
تحقّق من ذلك من Pod مؤقت، فهذا أسرع اختبار يزيل الشك:
kubectl run debug --image=busybox:1.36 --rm -it --restart=Never -- \
wget -qO- http://web:80
التشخيص الأساسي: الـ endpoints
الـ Service الذي لا يجد أي Pod لا يُرجع خطأ صريحًا. إنه موجود، وله عنوان، ويرفض الاتصالات ببساطة. والسبب في الغالب محدد لا يطابق أي شيء.
kubectl get endpoints web # les Pods réellement derrière le Service
# Liste vide ? Le sélecteur ne correspond à aucun Pod. Comparez les deux :
kubectl get service web -o jsonpath='{.spec.selector}'
kubectl get pods --show-labels
احتفظ بهذا المنعكس: عند رفض الاتصال على Service، انظر إلى الـ endpoints قبل أي شيء آخر. فهذا يحوّل نصف ساعة من التخبّط إلى ثلاثين ثانية.
NodePort
يفتح NodePort المنفذ نفسه على جميع عقد العنقود، ضمن النطاق 30000-32767.
apiVersion: v1
kind: Service
metadata:
name: web-nodeport
spec:
type: NodePort
selector:
app: web
ports:
- port: 80
targetPort: 80
nodePort: 30080 # facultatif ; sinon Kubernetes en attribue un
# minikube donne l'URL directement joignable
minikube service web-nodeport --url
هذا عملي في التطوير ويُفضَّل تجنبه في الإنتاج: فالمنفذ غير قياسي، ولا يوجد TLS ولا اسم نطاق، ويشمل العرض جميع العقد في وقت واحد. أما لحركة HTTP حقيقية، فالجواب الصحيح هو Ingress، الذي نراه في الدرس 6.
LoadBalancer، واعتماده على المزوّد
apiVersion: v1
kind: Service
metadata:
name: web-lb
spec:
type: LoadBalancer
selector:
app: web
ports:
- port: 80
targetPort: 80
لنكن دقيقين، لأن هذه النقطة تضلّل كثيرين: Kubernetes لا يعرف كيف ينشئ موازن حمل (load balancer). إنه يصدر طلبًا وينتظر أن يلبّيه متحكم خارجي. على AWS أو Google Cloud أو Azure يوجد هذا المتحكم ويهيّئ مورًدا مدفوعًا. أما على عنقود محلي أو تثبيت دون تكامل مع السحابة، فلا أحد يستجيب، ويبقى الـ Service إلى ما لا نهاية بعنوان خارجي <pending>.
kubectl get service web-lb
# EXTERNAL-IP reste <pending> sans contrôleur : ce n'est pas un bug
# Avec minikube, une commande simule le comportement (à laisser tourner)
minikube tunnel
| النوع | النطاق | الاستخدام الواقعي |
|---|---|---|
ClusterIP | داخلي للعنقود | التواصل بين الخدمات: الحالة الشائعة |
NodePort | منفذ مفتوح على كل عقدة | التطوير، والوصول لأغراض استكشاف الأعطال |
LoadBalancer | عنوان خارجي توفره السحابة | نقطة دخول عامة، ويعتمد على المزوّد |
التحديثات التدريجية
هذه هي الخدمة التي يؤديها Kubernetes على أفضل وجه، وهي شبه مجانية. غيّر صورة الـ Deployment: تُستبدل الـ Pods على دفعات، مع إبقاء الخدمة متاحة طوال فترة الانتقال.
# Fragment à insérer dans le spec du Deployment, à côté de replicas.
spec:
replicas: 4
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1 # au plus 1 Pod indisponible pendant la bascule
maxSurge: 1 # au plus 1 Pod en plus des 4 voulues
بهذه القيم يوجد دائمًا بين 3 و5 Pods أثناء الانتقال، منها 3 على الأقل متاحة. ويضمن maxUnavailable: 0 عدم فقدان أي قدرة، على حساب نشر أبطأ لأنه يجب الإنشاء أولًا قبل الحذف.
kubectl apply -f deployment.yaml # après modification de l'image du manifeste
# Variante impérative, utile pour un correctif urgent
kubectl set image deployment/web nginx=nginx:1.27.1-alpine
kubectl rollout status deployment/web # se termine quand la bascule est finie
التراجع
بما أن الـ ReplicaSets القديمة محفوظة، فإن العودة إلى الوراء لا تتطلب أي إعادة بناء:
kubectl rollout history deployment/web # historique des révisions
kubectl rollout undo deployment/web # revenir à la précédente
kubectl rollout undo deployment/web --to-revision=2 # à une révision précise
# Suspendre puis reprendre une bascule en cours, si les erreurs augmentent
kubectl rollout pause deployment/web
kubectl rollout resume deployment/web
ملاحظة صريحة حول السجل: افتراضيًا لا يُظهر ما الذي تغيّر بين مراجعتين، بل رقمهما فقط. ولجعل السجل مقروءًا، أضف annotation عند كل تسليم:
metadata:
name: web
annotations:
kubernetes.io/change-cause: "Passage à nginx 1.27.1 — correctif CVE"
تغيير حجم الانتشار
kubectl scale deployment/web --replicas=6 # impératif
kubectl apply -f deployment.yaml # déclaratif, après édition du fichier
فضّل الأسلوب التصريحي. فأمر kubectl scale يُلغى عند أول apply تالٍ للـ manifest، مما ينتج تراجعًا صامتًا يصعب تفسيره.
المجسّات: liveness وreadiness
بدون مجسّات، يعدّ Kubernetes الحاوية سليمة بمجرد أن تعمل عمليتها الرئيسية. وهذا تقريب فجّ: فقد يكون التطبيق قد شغّل خادم HTTP دون أن يكون قادرًا على الرد — الاتصال بقاعدة البيانات لم يُنشأ، أو الذاكرة المؤقتة لم تُحمَّل. عندها سيرسل الـ Service الحركة إلى Pod سيرد بأخطاء.
يجيب مجسّان عن سؤالين مختلفين، والخلط بينهما له عواقب وخيمة.
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 3
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: mon-api:1.0.0
ports:
- containerPort: 8080
# « Ce Pod peut-il recevoir du trafic maintenant ? »
# En échec : retiré des endpoints du Service, mais NON redémarré.
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 3
# « Ce processus est-il définitivement bloqué ? »
# En échec : le conteneur est TUÉ et redémarré.
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 15
periodSeconds: 20
failureThreshold: 3
# « Le démarrage est-il terminé ? »
# Tant qu'elle échoue, la livenessProbe est suspendue.
startupProbe:
httpGet:
path: /healthz
port: 8080
periodSeconds: 5
failureThreshold: 30
الأخطاء الكلاسيكية، التي ينبغي معرفتها قبل ارتكابها:
- توجيه الـ liveness إلى endpoint يفحص قاعدة البيانات. إذا تباطأت القاعدة، تفشل جميع الـ Pods في المجسّ، وتُقتل في الوقت نفسه، فيتحول بطء عابر إلى عطل كامل. يجب ألا تختبر الـ liveness سوى العملية نفسها؛ أما التبعيات الخارجية فمن اختصاص الـ readiness.
- قيمة
initialDelaySecondsقصيرة جدًا في الـ liveness. يُقتل التطبيق قبل أن ينتهي من الإقلاع، في حلقة متكررة: وهذا هوCrashLoopBackOffالغامض. والـstartupProbeهي الحل السليم لحالات الإقلاع البطيء. - عدم تعريف أي readiness. تصل الحركة إلى Pod غير جاهز، وتتسبب كل عملية نشر في موجة من أخطاء 502.
توجد ثلاثة أشكال للمجسّات بحسب ما يعرضه التطبيق:
# HTTP : tout code de 200 à 399 est un succès
readinessProbe:
httpGet: { path: /ready, port: 8080 }
---
# TCP : la connexion s'ouvre-t-elle ?
readinessProbe:
tcpSocket: { port: 5432 }
---
# Commande : code de sortie 0 = succès
livenessProbe:
exec: { command: ["sh", "-c", "test -f /tmp/sain"] }
تشخيص Pod
الأوامر التي ينبغي حفظها عن ظهر قلب، بالترتيب الذي تُستخدم به:
# 1. Vue d'ensemble : STATUS et RESTARTS racontent l'essentiel
kubectl get pods -o wide
# 2. La section Events en fin de sortie donne la cause réelle
kubectl describe pod web-7d4b8c9f5-xk2mp
# 3. Les logs applicatifs
kubectl logs web-7d4b8c9f5-xk2mp
kubectl logs -f deployment/web # suivi en direct
kubectl logs web-7d4b8c9f5-xk2mp --previous # logs du conteneur AVANT son crash
# 4. Entrer dans le conteneur
kubectl exec -it web-7d4b8c9f5-xk2mp -- sh
# 5. Atteindre le Pod depuis le poste, sans l'exposer
kubectl port-forward pod/web-7d4b8c9f5-xk2mp 8080:80
خيار --previous هو الخيار الذي يكتشفه المرء متأخرًا جدًا. ففي Pod بحالة CrashLoopBackOff، يعرض kubectl logs الحاوية الحالية، وغالبًا ما تكون سجلاتها فارغة لأنها أُعيد تشغيلها للتو. أما --previous فيعرض النسخة التي تعطّلت فعلًا، وبالتالي رسالة الخطأ.
أكثر الحالات شيوعًا ومعناها:
| الحالة | السبب المعتاد |
|---|---|
Pending | لا يجد المجدوِل (scheduler) عقدة مناسبة: موارد غير كافية أو وحدة تخزين غير مرتبطة |
ImagePullBackOff | الصورة غير موجودة، أو الوسم (tag) خاطئ، أو سجل خاص دون بيانات اعتماد |
CrashLoopBackOff | تبدأ الحاوية ثم تتوقف في حلقة متكررة: راجع logs --previous |
Running مع 0/1 READY | فشل فحص readinessProbe |
OOMKilled | تجاوز حد الذاكرة (راجع الدرس 6) |
ما يجب تذكّره
- نصرّح عن Deployment ولا ننشئ Pod يدويًا أبدًا. فهو ينشئ ReplicaSet لكل نسخة من القالب، والاحتفاظ بـ ReplicaSets القديمة هو ما يجعل
kubectl rollout undoفوريًا. - ترتبط الكائنات ببعضها عبر التسميات (labels) لا عبر الأسماء. حافظوا على ثبات تسميات المحدِّد (
app،tier)، وأخرجوا من المحدِّد كل ما يتغير كثيرًا — كالإصدارات والـ commits — فهو غير قابل للتعديل على أي حال. - يُشخَّص تعذّر الوصول إلى Service عبر
kubectl get endpoints: القائمة الفارغة تعني أن المحدِّد لا يطابق أي Pod. - يُستخدم
ClusterIPللاتصال الداخلي، وNodePortللتطوير، وLoadBalancerفقط حيث يوجد متحكّم سحابي — وإلا يبقى العنوان الخارجي<pending>إلى أجل غير مسمى. - يسحب readiness الـ Pod من حركة المرور دون إعادة تشغيله، بينما تقتل liveness الحاوية وتعيد تشغيلها. لا تضعوا أبدًا اعتمادًا خارجيًا في liveness: فبطء قاعدة البيانات سيتحول إلى عطل عام.
نصيحة: شغّلوا
kubectl get pods -wفي طرفية، ثم أطلقوا تحديثًا من طرفية أخرى. إن رؤية الـ Pods تظهر وتختفي على دفعات ترسّخ الآلية أفضل من أي شرح.
التعليقات
التعليقات مدعومة عبر GitHub Discussions
سجّل الدخول باستخدام GitHub للمشاركة في النقاش