التطبيقات المعقّدة
تجميع اللبنات: نقطة دخول واحدة، وحصص للموارد، ومرونة في التوسّع.
نشر التطبيقات المعقّدة
أصبحتَ الآن قادرًا على كتابة Deployment، وكشفه، وحقن إعداداته فيه، ومنحه وحدة تخزين. أما التطبيق الحقيقي فيتألف من عدة خدمات يجب أن تتخاطب فيما بينها، وأن يمكن الوصول إليها من الإنترنت تحت اسم نطاق واحد، وألّا تستنزف إحداها موارد الأخرى، وأن تتحمل ارتفاع حركة المرور دون تدخل بشري.
تظهر ثلاث مشكلات جديدة على هذا النطاق. الأولى هي نقطة الدخول: فإنشاء Service من النوع LoadBalancer لكل خدمة مصغّرة يعني عناوين عامة بعدد الخدمات، وكلٌّ منها مدفوع، دون توجيه حسب المسار ودون شهادة مشتركة. والثانية هي تقاسم الموارد: فمن دون قيود معلنة، قد تتسبب خدمة تسرّب الذاكرة في إسقاط Pods مجاورة على العقدة نفسها. والثالثة هي المرونة: فتحجيم النظام وفق ذروة الحمل يعني دفع كلفة الذروة بصورة دائمة.
أما المشكلة الرابعة فليست تقنية بل تنظيمية، وكثيرًا ما تكون الأسرع إيلامًا. فتطبيق من خمس خدمات يعني نحو ثلاثين manifest، يجب نسخها لكل بيئة مع تغيير ثلاث قيم. وهنا يصبح النسخ واللصق مصدرًا رئيسيًا للأعطال: يعدّل أحدهم ملف بيئة ما قبل الإنتاج وينسى ملف الإنتاج.
يجمع هذا الدرس اللبنات في مثال متكامل، ثم يعالج هذه النقطة الأخيرة بنظرة أولى على Helm. الهدف ليس إضافة مفاهيم جديدة، بل إظهار كيف تترابط المفاهيم التي تعرفها.
التطبيق المرجعي
ثلاثة مكوّنات في namespace مخصص: قاعدة بيانات PostgreSQL، وواجهة API، وواجهة ويب. والـ namespace هنا ليس للزينة، فهو يحمل الحصص وقواعد الوصول، ويتيح حذف كل شيء بخطوة واحدة.
apiVersion: v1
kind: Namespace
metadata:
name: boutique
labels:
environnement: production
قاعدة البيانات
StatefulSet مع Service من النوع headless، تمامًا وفق نموذج الدرس السابق: serviceName: postgres، وclusterIP: None، وvolumeClaimTemplates بسعة 10 جيبيبايت. وهناك إضافتان خاصتان بالنشر الكامل، تُدرجان داخل الحاوية:
# حجز سخي: قاعدة بيانات تُجدوَل على عقدة مثقلة أصلًا
# ستكون بطيئة دون أن يُطلَق أي تنبيه.
resources:
requests:
cpu: "250m"
memory: "512Mi"
limits:
memory: "1Gi"
# يمنع pg_isready وصول حركة المرور إلى الـ API قبل اكتمال التهيئة.
readinessProbe:
exec:
command: ["pg_isready", "-U", "boutique", "-d", "boutique"]
initialDelaySeconds: 10
periodSeconds: 5
واجهة API
هذا هو الـ manifest الأكثر إفادة: فهو يجمع الإعدادات والسر والفحوصات والموارد.
apiVersion: v1
kind: ConfigMap
metadata:
name: api-config
namespace: boutique
data:
APP_ENV: "production"
LOG_LEVEL: "info"
# اسم الـ Service يكفي: يحلّه DNS الداخلي داخل الـ namespace.
DB_HOST: "postgres"
DB_PORT: "5432"
DB_NAME: "boutique"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
namespace: boutique
labels:
app: api
tier: backend
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0 # لا تنخفض السعة أبدًا أثناء التحويل
maxSurge: 1
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
tier: backend
spec:
containers:
- name: api
image: exemple/boutique-api:1.4.0
ports:
- containerPort: 8080
name: http
envFrom:
- configMapRef:
name: api-config
- secretRef:
name: db-credentials
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
memory: "256Mi"
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 20
periodSeconds: 20
---
apiVersion: v1
kind: Service
metadata:
name: api
namespace: boutique
spec:
type: ClusterIP
selector:
app: api
ports:
- port: 8080
targetPort: 8080
name: http
واجهة الويب
البنية نفسها: Deployment بنسختين (replicas) وService من النوع ClusterIP على المنفذ 80. وثمة نقطة واحدة تستحق الانتباه، وكثيرًا ما يقع فيها المبتدئون:
env:
# المتصفح ليس داخل الـ cluster: فهو لا يحلّ
# http://api:8080. يجب أن يكون العنوان مسارًا عامًا في الـ Ingress.
- name: API_BASE_URL
value: "/api"
وحدها الشيفرة المنفّذة داخل الـ cluster تستفيد من DNS الداخلي. أما JavaScript في المتصفح فيمرّ حتمًا عبر نقطة الدخول العامة، ومن هنا جاء القسم التالي.
الـ Ingress
الـ Ingress قاعدة توجيه HTTP: اسم نطاق، ومسارات، وServices مستهدفة. ونقطة دخول واحدة تخدم التطبيق بأكمله.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: boutique
namespace: boutique
annotations:
# يعيد كتابة /api/produits إلى /produits قبل التمرير.
# هذا الـ annotation خاص بـ ingress-nginx.
nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
ingressClassName: nginx
tls:
- hosts:
- boutique.example.com
secretName: boutique-tls # Secret من النوع kubernetes.io/tls
rules:
- host: boutique.example.com
http:
paths:
- path: /api(/|$)(.*)
pathType: ImplementationSpecific
backend:
service:
name: api
port:
number: 8080
- path: /
pathType: Prefix
backend:
service:
name: web
port:
number: 80
والنقطة التي ينبغي فهمها قبل أن تقضي ساعة في تتبّع الأخطاء: كائن Ingress لا يفعل شيئًا بمفرده. إنه مجرد تصريح خامل ما لم يقرأه متحكّم الدخول (ingress controller). فإذا لم يكن أي متحكّم مثبّتًا، ينجح apply ولا يُوجَّه أي شيء.
minikube addons enable ingress
kubectl get pods -n ingress-nginx # هل المتحكّم قيد التشغيل؟
kubectl get ingress -n boutique # ADDRESS فارغ = القاعدة غير مأخوذة بعين الاعتبار
minikube ip # العنوان الذي يوضع في /etc/hosts
التحفظ الثاني أكثر بنيوية: فالـ annotations خاصة بكل متحكّم. إذ لا معنى لـ nginx.ingress.kubernetes.io/rewrite-target عند Traefik أو HAProxy أو AWS Load Balancer Controller، فلكلٍّ منها بادئته وصياغته. وحدها بنية rules موحَّدة؛ أما إعادة الكتابة والمهل وتحديد معدل الطلبات وإعادة التوجيه إلى HTTPS فهي لهجات خاصة. وهذا التباين هو ما دفع إلى ابتكار Gateway API، المرشّحة لتحلّ محل Ingress مستقبلًا.
لـ pathType ثلاث قيم: Exact (مطابقة صارمة)، وPrefix (حسب مقاطع المسار، وهي الحالة الشائعة)، وImplementationSpecific، وتُترك للمتحكّم وهي مطلوبة للأنماط ذات الالتقاط مثل النمط أعلاه.
طلبات الموارد وحدودها
قيمتان بدورين مختلفين. والخلط بينهما يُنتج أعطالًا يصعب تفسيرها.
resources:
requests: # حجز: يُستخدم في الجدولة
cpu: "100m" # 100 millicores = 0.1 نواة
memory: "128Mi"
limits: # سقف: يُستخدم لاحتواء الانحرافات
memory: "256Mi"
requests هي ما يستخدمه المُجدوِل (scheduler) لاختيار العقدة: فهو يجمع طلبات الـ Pods الموجودة أصلًا، ولا يضع Pod جديدًا إلا إذا بقي المجموع دون السعة. أما الـ Pod الذي لا يملك requests فيوضع في أي مكان، بما في ذلك عقدة مشبعة أصلًا.
limits هي السقف المطبَّق أثناء التشغيل، ولا يتصرف المعالج والذاكرة عنده بالطريقة نفسها إطلاقًا:
- تجاوز حد المعالج (CPU) يُسبب throttling، أي إبطاء الحاوية. يصبح التطبيق بطيئًا دون خطأ ودون إعادة تشغيل، وهو عَرَض ماكر كثيرًا ما يُنسب خطأً إلى الشبكة أو قاعدة البيانات.
- تجاوز حد الذاكرة يُسبب
OOMKilled: تُقتل الحاوية فورًا. فالذاكرة غير قابلة للضغط.
kubectl describe pod api-7d4b8c9f5-xk2mp | grep -A5 "Last State"
kubectl top pods -n boutique # الاستهلاك الفعلي، إذا كان metrics-server موجودًا
kubectl top nodes
ومن هنا الاستخدام الموصى به، وهو مخالف للحدس في البداية: حد للذاكرة، دون حد للمعالج، وهذا هو الخيار المعتمد في الـ manifests أعلاه. يحمي حد الذاكرة العقدة من جار يسرّب الذاكرة، لأن تشبّع الذاكرة يسبب عمليات إخلاء (eviction) واسعة. أما حد المعالج فيترك قدرة معالجة غير مستغلة بينما يمكن للتطبيق الاستفادة منها، والطلب (request) يضمن أصلًا حصته عند التنافس.
ويمكن للـ namespace أن يفرض ضوابط وقائية، بدلًا من الاعتماد على انضباط كل كاتب manifest:
apiVersion: v1
kind: LimitRange
metadata:
name: valeurs-par-defaut
namespace: boutique
spec:
limits:
- type: Container
default: # حدود تُطبَّق إذا غابت عن الـ Pod
memory: "256Mi"
defaultRequest: # طلبات تُطبَّق إذا غابت عن الـ Pod
cpu: "100m"
memory: "128Mi"
---
apiVersion: v1
kind: ResourceQuota
metadata:
name: quota-boutique
namespace: boutique
spec:
hard:
requests.cpu: "4"
requests.memory: "8Gi"
limits.memory: "16Gi"
persistentvolumeclaims: "10"
نتيجة ينبغي توقّعها: بمجرد أن يشمل ResourceQuota موردًا حسابيًا، يجب على كل Pod في الـ namespace أن يصرّح بالقيمة المقابلة، وإلا رُفض إنشاؤه. وLimitRange هو بالضبط ما يمنع هذه القاعدة من كسر عمليات النشر القائمة.
التوسّع الأفقي التلقائي
يضبط HorizontalPodAutoscaler عدد النسخ (replicas) وفق مقياس مرصود.
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: api
namespace: boutique
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: api
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70 # 70 % من الطلب (request)، وليس من الحد (limit)
behavior:
scaleUp: # التوسّع بسرعة لاستيعاب الذروة
stabilizationWindowSeconds: 0
policies:
- type: Percent
value: 100
periodSeconds: 30
scaleDown: # التقلّص ببطء، وإلا حدث تذبذب
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 50
periodSeconds: 60
التفصيل الحاسم موجود في التعليق: فـ averageUtilization: 70 يتعلق بـالطلب (request) لا بالحد (limit). ومع requests.cpu: 100m يبدأ التوسّع عند متوسط استهلاك قدره 70 millicores. لذلك فإن طلبًا غير مضبوط جيدًا يجعل التوسّع التلقائي غير منتظم: إن كان منخفضًا جدًا أضاف نسخًا باستمرار، وإن كان مرتفعًا جدًا لم يستجب أبدًا.
شرطان مسبقان صارمان، يفسّران جلّ حالات HPA التي «لا تعمل»: يجب تثبيت metrics-server، ويجب أن تصرّح الحاويات المستهدفة بـ requests للمقياس المختار، فمن دون طلب CPU يستحيل حساب النسبة المئوية.
minikube addons enable metrics-server
kubectl get hpa -n boutique # TARGETS عند <unknown> = المقاييس غائبة
kubectl describe hpa api -n boutique
وأخيرًا، لاحظ أن HPA يضيف Pods لكنه لا ينشئ عقدًا: فإذا امتلأ الـ cluster بقيت الـ Pods الجديدة في حالة Pending. أما إضافة سعة من الأجهزة فمن اختصاص Cluster Autoscaler أو Karpenter، وكلاهما خاص بمزوّد الخدمة السحابية.
نشر المجموعة والتحقق منها
لا حاجة إلى تنسيق ترتيب الإنشاء: فحلقات المصالحة (reconciliation) تتقارب نحو الحالة المطلوبة، وأي Pod لا يجد اعتماديته بعد تُعاد محاولة تشغيله.
kubectl apply -f k8s/ --recursive
kubectl rollout status deployment/api -n boutique
kubectl rollout status statefulset/postgres -n boutique
kubectl get all -n boutique
kubectl get events -n boutique --sort-by=.metadata.creationTimestamp
يتبع تشخيص تطبيق متعدد الخدمات المسار نفسه دائمًا، من الخارج إلى الداخل:
kubectl describe ingress boutique -n boutique # 1. هل القاعدة مأخوذة بعين الاعتبار؟
kubectl get endpoints -n boutique # 2. هل تشير الـ Services إلى Pods؟
kubectl get pods -n boutique # 3. هل READY هي 3/3، وليس Running فقط؟
kubectl port-forward -n boutique service/api 8080:8080 # 4. تجاوز الـ Ingress
يعزل هذا التدرّج العطل في أربعة أوامر. فإذا اشتغل port-forward ولم يشتغل الـ Ingress، فالمشكلة في التوجيه لا في التطبيق، وبذلك توفّر ساعة من قراءة سجلات التطبيق.
نظرة أولى على Helm
هذه الـ manifests تعمل. لكن المشكلة تظهر عند البيئة الثانية: الشيء نفسه بنسختين، واسم نطاق آخر، وصورة لبيئة ما قبل الإنتاج. فالنسخ واللصق يخلق مرجعيتين ستتباعدان مع الوقت.
Helm مدير حزم لـ Kubernetes. يجمع الـ chart manifests مُعلمَمة (parameterized) تُستخرج قيمها في ملف منفصل: يصف Chart.yaml الحزمة، ويحمل values.yaml القيم الافتراضية، ويحتوي templates/ على الـ manifests.
# values.yaml
api:
image:
repository: exemple/boutique-api
tag: "1.4.0"
replicas: 3
ingress:
host: boutique.example.com
# templates/deployment-api.yaml — مقتطف
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}-api
namespace: {{ .Release.Namespace }}
spec:
replicas: {{ .Values.api.replicas }}
template:
spec:
containers:
- name: api
image: "{{ .Values.api.image.repository }}:{{ .Values.api.image.tag }}"
# التحقق من الناتج دون إرسال أي شيء إلى الـ cluster: افعل ذلك دائمًا
helm template boutique ./boutique -f values-production.yaml
helm install boutique ./boutique -n boutique --create-namespace
helm upgrade boutique ./boutique -n boutique -f values-production.yaml
helm history boutique -n boutique
helm rollback boutique 1 -n boutique
ما يقدّمه Helm: مجموعة واحدة من الـ manifests لعدة بيئات، وترقيم إصدارات لعمليات التسليم، وتراجعًا على مستوى التطبيق بأكمله، في حين أن kubectl rollout undo يعالج كائنًا واحدًا في كل مرة.
ما يترتب على ذلك، ولا بد من قوله: إن القوالب (templating) تُضعف قابلية القراءة. فالـ chart الكثير المعاملات يتحول إلى تمرين في منطق القوالب، ولا يعود بالإمكان رؤية ملف YAML الناتج. ولهذا وجدت بدائل مثل Kustomize، التي تُطبّق التعديلات فوق ملفات YAML تبقى مقروءة، جمهورها. اعتد تشغيل helm template قبل أي helm upgrade: سترى ما سيستقبله العنقود (cluster) فعلاً.
الخلاصة
- يوفّر Ingress نقطة دخول HTTP مشتركة لجميع الـ Services لديك. وهو لا يعمل من دون وجود متحكم دخول (ingress controller) مثبّت، كما أن تعليقاته التوضيحية (annotations) خاصة بكل متحكم: بنية
rulesوحدها هي القابلة للنقل بين المتحكمات. - تُستخدم
requestsفي الجدولة، وتُستخدمlimitsفي التنفيذ. فتجاوز حد المعالج (CPU) يؤدي إلى إبطاء صامت (throttling)، أما تجاوز حد الذاكرة فيؤدي إلى إنهاء الحاوية (OOMKilled). ومن هنا جاء الاستخدام الشائع: وضع حد للذاكرة دائماً، وحد للمعالج نادراً. - يحتسب HPA نسبته المئوية من الطلب (request) وليس من الحد (limit). ويتطلب وجود
metrics-serverوrequestsمعرَّفة، وهو يضيف Pods دون أن ينشئ عُقداً (nodes). - شخّص المشكلة من الخارج إلى الداخل: Ingress، ثم endpoints، ثم حالة الـ Pods، ثم
port-forwardلتجاوز التوجيه وعزل الطبقة المعطوبة. - يجنّبك Helm تكرار ملفات manifests بين البيئات، ويوفّر تراجعاً شاملاً إلى إصدار سابق. أما الثمن فهو قابلية القراءة: تحقق دائماً من الناتج باستخدام
helm templateقبل التطبيق.
للمزيد: المواضيع التي تكمّل هذه الدورة بصورة طبيعية هي RBAC وNetworkPolicy والمراقبة (Prometheus وGrafana) والنشر المستمر عبر GitOps. وتبقى الوثائق الرسمية kubernetes.io/fr/docs/concepts/ المرجع الذي ينبغي الرجوع إليه أولاً.
التعليقات
التعليقات مدعومة عبر GitHub Discussions
سجّل الدخول باستخدام GitHub للمشاركة في النقاش