ليست كل أحمال العمل خوادم تعمل باستمرار. فخطوط معالجة البيانات، وترحيل قواعد البيانات، وتدريب نماذج التعلم الآلي، وتوليد التقارير، وتدفئة ذاكرة التخزين المؤقت، والنسخ الاحتياطية الليلية — كلها تشترك في شكل جوهري واحد: تبدأ، وتُنجز عملًا محدودًا، ثم تتوقف. Jobs وCronJobs في Kubernetes هي الأدوات المُصمَّمة خصيصًا لهذا النوع من الأحمال، وعلى نطاق الشركات التقنية الكبرى تُشغِّل ملايين المهام الدُفعية يوميًا بضمانات اكتمال دقيقة لا تستطيع Deployments العادية تقديمها.
يُنشئ Job واحدًا أو أكثر من الـ Pods، ويضمن إنهاء عدد محدد منها بنجاح، ويتتبع حالة الاكتمال هذه في etcd — مما يمنحك سجلًا دائمًا وقابلًا للاستعلام بأن العمل قد اكتمل. أما CronJob فهو متحكم يُنشئ كائن Job جديدًا وفق جدول cron. إن فهم كليهما، وفهم الدلالات المتعلقة بالفشل فيما بينهما، أمرٌ ضروري لموثوقية الإنتاج.
تشريح الـ Job
الحقول الحرجة في مواصفة Job هي: completions وparallelism وbackoffLimit وactiveDeadlineSeconds. كل قرار تتخذه بشأن هذه الحقول الأربعة يحدد مباشرةً كيف يتصرف حمل العمل الدُفعي لديك تحت الفشل.
# job-basic.yaml — مهمة باكتمال واحد
apiVersion: batch/v1
kind: Job
metadata:
name: db-migration-v3-12
namespace: production
labels:
app: payments
version: v3.12
spec:
# عدد الـ Pods التي يجب أن تكتمل بنجاح (الافتراضي: 1)
completions: 1
# عدد الـ Pods التي يمكن تشغيلها بالتوازي (الافتراضي: 1)
parallelism: 1
# الحد الأقصى لفشل الـ Pod قبل فشل الـ Job ذاته (الافتراضي: 6)
backoffLimit: 3
# الحد الزمني الصارم لكامل المهمة (بالثواني)
# تُعلَّم المهمة بالفشل إذا لم تنته في هذا الوقت
activeDeadlineSeconds: 300
# مدة الاحتفاظ بكائنات Job المكتملة (وPods الخاصة بها) للوصول إلى السجلات
# بدون هذا، تتراكم Jobs المكتملة وتملأ etcd
ttlSecondsAfterFinished: 3600
template:
metadata:
labels:
job-name: db-migration-v3-12
spec:
# حرج: لا تستخدم Always (الافتراضي للـ Deployments)
# OnFailure: يُعيد تشغيل الحاوية في مكانها
# Never: يُنشئ Pod جديدًا عند الفشل (يتيح فحص سجلات الأعطال)
restartPolicy: Never
containers:
- name: migrator
image: payments-migrator:v3.12
command: ["python", "manage.py", "migrate", "--no-input"]
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: db-credentials
key: url
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "2"
memory: "2Gi"
يجب أن يكون restartPolicy إما Never أو OnFailure — وليس Always أبدًا: تعتمد Deployments افتراضيًا على restartPolicy: Always، مما يُعيد تشغيل الحاوية إلى أجل غير مسمى. بالنسبة للـ Job، هذا يعني أن حاوية ترحيل فاشلة ستدور في حلقة لا نهاية لها داخل نفس الـ Pod — فلا تفشل المهمة أبدًا، ولا يُفعَّل backoffLimit. Never هو التفضيل الإنتاجي: كل فشل يُنشئ Pod جديدًا، مما يمنحك سجل أعطال محفوظًا (قابلًا للقراءة بـkubectl logs <failed-pod>) ويُشغِّل تراجعًا صحيحًا. OnFailure يُعيد التشغيل في المكان، مما يوفر نفقات إنشاء الـ Pod لكنه يستبدل سجلات الحاوية السابقة.
المهام المتوازية: completions و parallelism
تتضح القوة الحقيقية للـ Jobs عندما تحتاج إلى معالجة حمل عمل كبير بشكل متواز. يوفر Kubernetes ثلاثة أنماط للـ Jobs، تُختار بناءً على مجموعة completions وparallelism:
مهمة باكتمال واحد (completions: 1، parallelism: 1): يعمل Pod واحد؛ عند خروجه بالرمز 0، تكتمل المهمة. تُستخدم للترحيل والسكريبتات ذات التشغيل الفردي.
مهمة بعدد اكتمالات محدد (completions: N، parallelism: K): يُشغِّل Kubernetes ما يصل إلى K من الـ Pods بالتوازي ويستمر في إنشاء جدد حتى يتراكم N اكتمال ناجح. تُستخدم لمعالجة N عنصر عمل عندما يتعامل كل Pod مع عنصر واحد بالضبط. منذ Kubernetes 1.24، يمنح حقل completionMode: Indexed كل Pod مؤشرًا فريدًا يبدأ من الصفر في JOB_COMPLETION_INDEX، مما يُلغي الحاجة إلى قائمة انتظار عمل منفصلة.
مهمة قائمة الانتظار (completions غير محدد، parallelism: K): تسحب الـ Pods عناصر من قائمة انتظار خارجية (SQS أو RabbitMQ أو قائمة Redis). تكتمل المهمة عندما يخرج أي Pod بنجاح وتنتهي جميع الأخرى أو تُنهى. تُستخدم عندما لا يُعرف عدد العناصر مسبقًا.
# job-indexed.yaml — مهمة متوازية مفهرسة (Kubernetes 1.24+)
# معالجة 500 شريحة من تقارير العملاء، 20 في كل مرة
apiVersion: batch/v1
kind: Job
metadata:
name: reports-q4-2024
namespace: data-platform
spec:
completions: 500
parallelism: 20
completionMode: Indexed # كل Pod يحصل على متغير JOB_COMPLETION_INDEX (0..499)
backoffLimit: 5
activeDeadlineSeconds: 7200 # حد زمني صارم ساعتين للدُفعة بأكملها
ttlSecondsAfterFinished: 86400
template:
spec:
restartPolicy: Never
containers:
- name: reporter
image: data-platform/report-generator:1.8.3
command:
- /bin/sh
- -c
- |
SEGMENT=${JOB_COMPLETION_INDEX}
echo "Processing segment ${SEGMENT}"
python generate_report.py --segment "${SEGMENT}" --output s3://reports/q4-2024/
env:
- name: AWS_REGION
value: us-east-1
resources:
requests:
cpu: "1"
memory: "1Gi"
limits:
cpu: "2"
memory: "2Gi"
مهمة بعدد اكتمالات محدد (completions=4, parallelism=2) تُشغِّل موجتين من الـ Pods، مع إعادة محاولة واحدة مستهلَكة من backoffLimit.
دلالات إعادة المحاولة وأوضاع الفشل
إن فهم متى وكيف تفشل المهمة بالضبط هو الفرق بين نظام دُفعي موثوق وآخر يفشل صامتًا دون أن ينتهي. ثمة حالتا فشل متمايزتان:
فشل الـ Pod: يخرج Pod برمز غير صفري، أو يُقتل بسبب نفاد الذاكرة، أو يُطرد. هذا يزيد عداد فشل المهمة. متى تجاوز العداد backoffLimit، تنتقل المهمة إلى حالة Failed ولا تُنشأ Pods إضافية.
تجاوز الموعد النهائي: يُصل إلى activeDeadlineSeconds بغض النظر عن عداد الفشل. هذا له الأولوية المطلقة — مهمة بـbackoffLimit: 1000 لكن activeDeadlineSeconds: 60 ستُقتل بعد 60 ثانية حتى لو حدث فشل واحد فقط.
قدَّم Kubernetes 1.26 سياسة فشل الـ Pod (podFailurePolicy)، التي توفر تحكمًا دقيقًا: يمكنك إخبار المهمة بتجاهل رموز خروج معينة (مثلًا، معاملة رمز الخروج 137 — قتل نفاد الذاكرة — كفشل عابر يستحق إعادة المحاولة) أو فشل المهمة فورًا عند رموز خروج محددة (مثلًا، رمز الخروج 42 = "تم اكتشاف تلف البيانات، أوقف فورًا"). هذا هو النهج الإنتاجي المستخدم في Google وما شابهها.
# job-with-failure-policy.yaml (Kubernetes 1.26+)
apiVersion: batch/v1
kind: Job
metadata:
name: data-processor
namespace: data-platform
spec:
completions: 100
parallelism: 10
backoffLimit: 4
activeDeadlineSeconds: 3600
# تحكم دقيق في الفشل (1.26+)
podFailurePolicy:
rules:
# رمز الخروج 42 = "خطأ لا يمكن التعافي منه": يُفشل المهمة بالكامل فورًا
- action: FailJob
onExitCodes:
containerName: processor
operator: In
values: [42]
# قتل نفاد الذاكرة (137) أو SIGTERM (143): لا يُحسب كفشل backoffLimit،
# مجرد إعادة تشغيل — هذه أعطال بنية تحتية وليست أخطاء تطبيق
- action: Ignore
onExitCodes:
containerName: processor
operator: In
values: [137, 143]
# كل شيء آخر: يُحسب في backoffLimit (السلوك الافتراضي)
template:
spec:
restartPolicy: Never
containers:
- name: processor
image: data-platform/processor:2.1.0
command: ["python", "process.py"]
احرص دائمًا على ضبط activeDeadlineSeconds في مهام الإنتاج: بدونه، قد تعمل مهمة معلقة (بسبب انقطاع الشبكة، أو جمود، أو إعادة محاولة لا نهاية لها) إلى الأبد وتحجز موارد الكلاستر. يجعل ضبط موعد نهائي أسوأ حالات السلوك صريحًا ومحدودًا. قاعدة عملية جيدة: 3 أضعاف وقت التشغيل الفعلي المتوقع — هامش كافٍ لإعادة المحاولات والبنية التحتية البطيئة، لكن غير غير محدود.
المهام المجدولة (CronJobs)
CronJob هو مصنع Jobs مُدار بجدول cron. كل نبضة في الجدول تُنشئ كائن Job جديدًا، والذي بدوره يُنشئ Pods. يسجِّل المتحكم آخر مرات الجدولة، ويحسب التشغيلات الفائتة عند إعادة التشغيل، ويُطبِّق حدودًا على عدد كائنات Job المتزامنة والتاريخية التي يجب الاحتفاظ بها.
# cronjob-nightly-report.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
name: nightly-revenue-report
namespace: analytics
spec:
# cron ذو 5 حقول القياسي: دقيقة ساعة يوم-الشهر شهر يوم-الأسبوع
# جميع الأوقات بتوقيت UTC إلا إذا كان الكلاستر يحتوي على إعداد منطقة زمنية
schedule: "0 2 * * *" # 02:00 UTC كل يوم
# ماذا تفعل إذا حان موعد تشغيل جديد لكن السابق لا يزال يعمل:
# Allow: شغِّل على أي حال (يمكن أن يسبب تشغيلات متوازية — خطير للمهام غير المثلى)
# Forbid: تخطَّ هذا التشغيل كليًا
# Replace: اقتل المهمة الجارية وابدأ من جديد
concurrencyPolicy: Forbid
# Kubernetes 1.27+: منطقة زمنية IANA — لا مزيد من حساب إزاحة UTC يدويًا
timeZone: "America/New_York"
# إذا فات CronJob موعد بدئه بأكثر من هذا العدد من الثواني، تخطَّه
# يمنع التشغيلات القديمة من اللحاق بعد انقطاع طويل في الكلاستر
startingDeadlineSeconds: 3600
# عدد سجلات Jobs المكتملة/الفاشلة التي يجب الاحتفاظ بها (للوصول إلى السجلات)
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 5
jobTemplate:
spec:
backoffLimit: 2
activeDeadlineSeconds: 1800 # حد صارم 30 دقيقة لكل تشغيل
ttlSecondsAfterFinished: 86400
template:
spec:
restartPolicy: Never
serviceAccountName: analytics-job-sa
containers:
- name: reporter
image: analytics/revenue-reporter:3.4.1
command: ["python", "generate_revenue_report.py", "--date", "yesterday"]
envFrom:
- secretRef:
name: analytics-db-credentials
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "2"
memory: "4Gi"
concurrencyPolicy: Allow خاطئ تقريبًا دائمًا للمهام التي تحتوي على حالة: إذا كان تقريرك الليلي يكتب في جدول قاعدة بيانات والتشغيل السابق لا يزال ينفَّذ عند بدء التشغيل الجديد، ستحصل على كاتبَين يتنافسان على نفس المخرج. النتيجة إما صفوف مكررة، أو تلف في البيانات، أو حالة جمود. سياسة التزامن الآمن الوحيدة للمهام التي تُعدِّل حالة مشتركة هي Forbid. احتفظ بـAllow للعمليات عديمة الحالة والمثلى تمامًا كإرسال نبضة رسالة تأكيد.
أوامر التشغيل
تتطلب العمليات اليومية على Jobs وCronJobs مجموعة صغيرة من الأوامر الأساسية:
# --- فحص الـ Jobs ---
kubectl get jobs -n data-platform
kubectl get jobs -n data-platform -w # مشاهدة مباشرة
kubectl describe job reports-q4-2024 -n data-platform
# التحقق من عدد الاكتمالات المتراكمة
kubectl get job reports-q4-2024 -n data-platform \
-o jsonpath='{.status.succeeded}/{.spec.completions}'
# عرض سجلات جميع Pods لمهمة معينة (يتطلب تسمية job-name)
kubectl logs -l job-name=reports-q4-2024 -n data-platform --tail=50
# --- تشغيل CronJob يدويًا (للتصحيح) ---
kubectl create job --from=cronjob/nightly-revenue-report manual-run-$(date +%s) \
-n analytics
# --- تعليق CronJob (إيقاف مؤقت بدون حذف) ---
kubectl patch cronjob nightly-revenue-report -n analytics \
-p '{"spec":{"suspend":true}}'
# استئناف:
kubectl patch cronjob nightly-revenue-report -n analytics \
-p '{"spec":{"suspend":false}}'
# --- تنظيف مهمة فاشلة وPods الخاصة بها ---
kubectl delete job db-migration-v3-12 -n production # يحذف Pods تبعيًا
# أو الاعتماد على ttlSecondsAfterFinished للتنظيف التلقائي
# --- التحقق من تشغيلات CronJob الفائتة ---
kubectl describe cronjob nightly-revenue-report -n analytics | grep -A10 "Events:"
ttlSecondsAfterFinished إلزامي على النطاق الواسع: بدونه، تتراكم كائنات Job المكتملة والفاشلة — مع Pods المرتبطة بها — في etcd إلى أجل غير مسمى. على نطاق آلاف CronJobs التي تعمل كل ساعة، يمكن أن يُستنفد تخزين etcd، ويُبطئ خادم API، ويُدهور أداء kubectl get pods عبر الكلاستر. احرص دائمًا على ضبط هذا الحقل. قيمة افتراضية معقولة هي 86400 (24 ساعة) حتى تكون السجلات متاحة ليوم كامل للتحقيق في ما بعد الحوادث قبل التنظيف التلقائي.
أنماط الإنتاج
توصَّلت فرق الشركات التقنية الكبرى إلى ممارسات عديدة لأحمال العمل الدُفعية الموثوقة في Kubernetes:
المثلية (Idempotency) أولًا: يجب أن تكون كل مهمة آمنة لإعادة التشغيل. إذا عُولج عنصر مرتين، يجب أن تكون النتيجة مطابقة لمعالجته مرة واحدة. استخدم upserts بدلًا من inserts، اكتب في مواقع مؤقتة ثم أعِد التسمية الذرية إلى المسار النهائي، وخزِّن معرِّفات العناصر المعالجة في جدول اكتمال.
أصدر مقياسًا عند الاكتمال: اجعل كل حاوية مهمة تُرسل مقياس Prometheus (عبر Pushgateway أو حاوية جانبية) يسجِّل وقت الاكتمال والنجاح أو الفشل. اضبط تنبيهًا على "لا اكتمال ناجح في آخر N نوافذ مجدولة." حالة الـ Job في etcd لكنها لا تُنبِّهك — يجب أن تفعل ذلك منظومة الملاحظة الخاصة بك.
استخدم النطاقات وRBAC لعزل أحمال العمل الدُفعية: مهمة خط بيانات تتصرف بشكل سيئ يجب ألا تصل إلى خوادم API الإنتاجية. النطاقات المخصصة مع ServiceAccounts محدودة النطاق لفقط الموارد التي تحتاجها المهمة هو النمط القياسي.
قم بنسخ أسماء مهامك بالإصدار: سمِّ Jobs باستخدام إصدار الكود وتاريخ التشغيل (مثلًا migration-v3-12-20240115). هذا يجعل ناتج kubectl get jobs يشرح نفسه بنفسه ويضمن إنشاء كائن Job مميز لكل نشر ترحيل.
نستخدم ملفات تعريف الارتباط لتشغيل هذا الموقع وتحليل الزيارات وعرض إعلانات مخصّصة. يمكنك قبول كل ملفات تعريف الارتباط أو رفض غير الأساسية منها.
سياسة الخصوصية