التخطي إلى المحتوى
Toova
جميع الأدوات

5 أنماط تعبيرات Cron المستخدمة في بيئات الإنتاج الحقيقية

Toova

تبدو تعبيرات cron بسيطة — خمسة حقول وعدد من العوامل — لكن فرق الإنتاج تقع في الأخطاء ذاتها مراراً: مهام تُتخطى بصمت في أشهر معينة، وتشغيلات متداخلة تُفسد الحالة المشتركة، ومئات المهام التي تستيقظ في الثانية نفسها فتُثقل قاعدة البيانات. نادراً ما يكون التعبير هو المشكلة؛ المشكلة هي عدم التفكير في الحالات الطرفية.

يغطي هذا الدليل خمسة أنماط cron تستخدمها فرق الهندسة فعلياً في الإنتاج، مع الأسباب وراء كل نمط والمزالق التي ينبغي تجنبها. استخدم محلل Cron من Toova للتحقق من أي تعبير ورؤية أوقات التنفيذ العشرة التالية قبل النشر.

تشريح تعبير Cron

يتكون تعبير cron القياسي من خمسة حقول مفصولة بمسافات:

*/15  *  *  *  *
  |   |  |  |  |
  |   |  |  |  +---- day of week (0-6, 0=Sunday)
  |   |  |  +------- month (1-12)
  |   |  +---------- day of month (1-31)
  |   +------------- hour (0-23)
  +----------------- minute: every 15th (0, 15, 30, 45)

يقبل كل حقل: قيمة محددة (5)، أو حرف بديل (*)، أو نطاقاً (1-5)، أو قائمة (1,3,5)، أو قيمة خطوة (*/15). تضيف بعض التطبيقات حقلاً سادساً للثواني؛ وتتوقع أخرى (كـ GitHub Actions) خمسة حقول بالضبط. تحقق دائماً من اللهجة التي يستخدمها جدولك قبل كتابة تعبيرات معقدة.

النمط 1: كل 15 دقيقة — */15 * * * *

*/15  *  *  *  *
  |   |  |  |  |
  |   |  |  |  +---- day of week (0-6, 0=Sunday)
  |   |  |  +------- month (1-12)
  |   |  +---------- day of month (1-31)
  |   +------------- hour (0-23)
  +----------------- minute: every 15th (0, 15, 30, 45)

التعبير المفضل لمعالجات قوائم الانتظار وفحوصات النبض ومهام الاستطلاع التي تحتاج إلى تشغيل متكرر دون أن يكون مستمراً. يُطلق عند :00 و:15 و:30 و:45 من كل ساعة، كل يوم.

في crontab:

# crontab entry — run batch processor every 15 minutes
*/15 * * * * /opt/app/bin/process-queue.sh >> /var/log/queue.log 2>&1

في Node.js:

// Node.js with node-cron
import cron from 'node-cron';

cron.schedule('*/15 * * * *', async () => {
  const batch = await db.query(
    'SELECT * FROM jobs WHERE status = $1 LIMIT 100',
    ['pending']
  );
  await processBatch(batch);
});

مشكلة القطيع المتراكض. عندما تشغّل مئات العمال بالتعبير ذاته */15، يُطلقون جميعاً في آنٍ واحد عند :00 و:15 و:30 و:45. إذا أصاب كل عامل قاعدة بيانات مشتركة، تحصل على أربعة ارتفاعات في الحمل في الساعة بدلاً من حمل ناعم. الحل هو إضافة تأخير عشوائي قبل كل تشغيل:

// Add jitter to avoid thundering herd on distributed systems
cron.schedule('*/15 * * * *', async () => {
  const jitterMs = Math.floor(Math.random() * 30_000); // up to 30s
  await sleep(jitterMs);
  await processBatch();
});

يوزع تأخير يصل إلى 30 ثانية الارتفاع عبر نافذة زمنية مع إبقاء المهمة ضمن دورة الـ 15 دقيقة المتوقعة. للتدرج الأكثر تطوراً، انظر النمط 5 (صيغة H في Jenkins).

استخدم محوّل الطوابع الزمنية لتحويل وقت التشغيل التالي من Unix epoch إلى وقت مقروء عند تشخيص توقيت المهام.

النمط 2: ساعات العمل في أيام الأسبوع — 0 9 * * 1-5

0   9   *   *   1-5
|   |   |   |   |
|   |   |   |   +-- day of week: Monday–Friday (1-5)
|   |   |   +------ month: every month
|   |   +---------- day of month: every day
|   +-------------- hour: 9
+------------------ minute: 0 (exactly on the hour)

يُطلق هذا التعبير مرة واحدة، في تمام الساعة التاسعة صباحاً، من الاثنين إلى الجمعة. إنه النمط المعياري لـ: إرسال رسائل ملخص يومية، وتشغيل تقارير ساعات العمل، وتشغيل سير عمل تحتاج مراجعة بشرية خلال يوم العمل، ونشر التطوير في بيئة staging في بداية يوم الهندسة.

في Kubernetes CronJob:

# Kubernetes CronJob — weekday digest email at 9 AM UTC
apiVersion: batch/v1
kind: CronJob
metadata:
  name: digest-email
spec:
  schedule: "0 9 * * 1-5"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: mailer
            image: myapp/mailer:latest
            command: ["/app/send-digest"]
          restartPolicy: OnFailure

فخ المنطقة الزمنية. تعمل جداول Kubernetes CronJob بمنطقة الكتلة الزمنية، وهي UTC افتراضياً. "9 صباحاً UTC" تعادل 5 صباحاً بتوقيت الشرق الأمريكي في الشتاء و4 صباحاً بتوقيت المحيط الهادئ. سيستقبل مستخدموك "ملخص الصباح" في منتصف الليل. استخدم مكتبة cron تقبل سلاسل المنطقة الزمنية IANA:

// Handle timezone — "9 AM UTC" is rarely what users expect
// Use a library that understands IANA timezones
import { CronJob } from 'cron';

const job = new CronJob(
  '0 9 * * 1-5',
  () => sendDailyDigest(),
  null,
  true,
  'America/New_York' // runs at 9 AM Eastern, not 9 AM UTC
);

يساعدك محوّل المناطق الزمنية في إيجاد التوقيت المكافئ بتوقيت UTC لأي توقيت محلي عبر أي منطقة زمنية IANA.

ترقيم أيام الأسبوع. يستخدم تعريف cron القياسي 0 أو 7 للأحد. تستخدم بعض التطبيقات (Quartz وcron4j) 1 للاثنين بدلاً من 0. تحقق من توثيق جدولك واختبر باستخدام محلل Cron قبل النشر.

النمط 3: دُفعة ليلية — 0 22 * * *

0   22   *   *   *
|    |   |   |   |
|    |   |   |   +-- day of week: any
|    |   |   +------ month: any
|    |   +---------- day of month: any
|    +-------------- hour: 22 (10 PM)
+------------------- minute: 0

يعمل مرة واحدة يومياً عند الساعة العاشرة مساءً. تُعدّ الدُفعة الليلية أحد أكثر الأنماط شيوعاً في هندسة البيانات: تجميع أحداث اليوم، وإعادة بناء فهارس البحث، وإنشاء التقارير، وأرشفة السجلات، وإرسال إشعارات نهاية اليوم.

لماذا العاشرة مساءً بدلاً من منتصف الليل؟ لسببين. أولاً، تمنح الساعة العاشرة مساءً هامشاً قبل انقضاء اليوم، مما يقلل من احتمال التقاط أحداث وصلت متأخرة. ثانياً، تشغيل مهمة ثقيلة عند منتصف الليل يعني منافستها لفوترة نهاية الشهر (النمط 4) في أول كل شهر.

ثلاث طرق شائعة لكتابة مهمة يومية:

# Three ways to write "nightly at 10 PM":
0 22 * * *    # standard cron
@daily        # midnight — NOT the same as 10 PM
0 22 * * *    # always prefer explicit time over @macros for non-midnight

منع التداخل. مهمة ليلية تستغرق عادةً 45 دقيقة يمكن أن تأخذ ساعتين أحياناً — وإذا كانت لا تزال تعمل عند إطلاق ليلة الغد، فكلتا المهمتين ستعدّلان البيانات نفسها. استخدم قفلاً موزعاً:

// Idempotency check — prevent double-processing if job overlaps
async function runNightlyBatch() {
  const lock = await redis.set(
    'nightly-batch-lock',
    process.pid,
    'EX', 3600,   // TTL: 1 hour
    'NX'          // only set if not exists
  );

  if (!lock) {
    console.log('Batch already running, skipping.');
    return;
  }

  try {
    await processNightlyData();
  } finally {
    await redis.del('nightly-batch-lock');
  }
}

يضمن الخيار NX في Redis أن عملية واحدة فقط يمكنها الاحتفاظ بالقفل في وقت واحد. يمنع TTL مهمة متوقفة من الاحتفاظ بالقفل إلى الأبد.

النمط 4: فوترة شهرية — 0 0 1 * *

0   0   1   *   *
|   |   |   |   |
|   |   |   |   +-- day of week: any
|   |   |   +------ month: any
|   |   +---------- day of month: 1 (first)
|   +-------------- hour: 0 (midnight)
+------------------ minute: 0

يُطلق عند منتصف ليلة أول يوم من كل شهر. النمط الكلاسيكي لـ: إنشاء الفواتير الشهرية، وإعادة تعيين حصص الاستخدام، وتشغيل مطابقة نهاية الشهر، وأرشفة بيانات الشهر السابق.

مزالق cron للمهام الشهرية:

# First of month at midnight UTC
0 0 1 * *

# ---- TRAPS TO AVOID ----

# This does NOT run "every month" reliably:
0 0 29-31 * *   # months with fewer days silently skip

# Quarter billing (Jan, Apr, Jul, Oct):
0 0 1 1,4,7,10 *

# End of month is tricky — there is no "last day" in standard cron
# Use @reboot + application logic, or a cron that runs daily and
# checks whether today is the last day of the month in code.

فوترة نهاية الشهر. إذا أردت الفوترة في آخر يوم من كل شهر (وليس الأول)، لا يوجد تعبير cron نظيف لذلك. الحل:

// Application-level "last day of month" check
cron.schedule('0 23 28-31 * *', async () => {
  const today = new Date();
  const tomorrow = new Date(today);
  tomorrow.setDate(today.getDate() + 1);

  // If tomorrow is the 1st, today is the last day of the month
  if (tomorrow.getDate() === 1) {
    await runEndOfMonthBilling();
  }
});

يُطلق هذا في الأيام 28-31 من كل شهر، ثم يفحص في الكود ما إذا كان اليوم هو آخر يوم فعلاً. يعمل حتى أربع مرات دون داعٍ (في الأشهر التي أقل من 31 يوماً)، لكن الفحص رخيص والمنطق واضح.

لفوترة بداية الربع، استخدم عامل القائمة في حقل الشهر: 0 0 1 1,4,7,10 * (يناير وأبريل ويوليو وأكتوبر).

النمط 5: جدول التجزئة (صيغة H في Jenkins) — H/30 * * * *

H/30 * * * *
|
+--- H means Jenkins picks a random minute per job.
     H/30 = once every 30 minutes, starting at H.
     Different jobs get different H values, staggering load.

رمز H هو امتداد خاص بـ Jenkins لصيغة cron. يستبدل H برقم شبه عشوائي مستقر مشتق من اسم المهمة. H/30 في حقل الدقائق تعني "مرة كل 30 دقيقة، بدءاً من دقيقة تحددها دالة التجزئة (hash) من اسم المهمة."

في Jenkinsfile:

// Jenkinsfile — pipeline triggered every 30 minutes, staggered
pipeline {
  triggers {
    cron('H/30 * * * *')
  }
  stages {
    stage('Deploy') {
      steps {
        sh './deploy.sh'
      }
    }
  }
}

لماذا هذا مهم. كثيراً ما تحتوي تثبيتات Jenkins على مئات من pipelines. إذا استخدمت كل pipeline */30 * * * *، تُطلق جميعها عند :00 و:30، مما يُنشئ ارتفاعات هائلة في الحمل على مجموعة العوامل وأي أنظمة لاحقة. مع H/30، تحصل كل مهمة على إزاحة مختلفة — واحدة عند :03/:33، وأخرى عند :17/:47 — موزعةً الحمل بالتساوي عبر الساعة.

GitHub Actions لا تدعم H. لتدريج عدة سير عمل Actions، اختر إزاحات دقائق ثابتة مختلفة يدوياً:

// GitHub Actions — H is not supported; use schedule with offset manually
// To stagger two jobs: pick different fixed minutes
name: Job A
on:
  schedule:
    - cron: '5 * * * *'   # runs at :05 of every hour

---
name: Job B
on:
  schedule:
    - cron: '35 * * * *'  # runs at :35 — 30 minutes after Job A

هذا أقل أناقة من H في Jenkins لكنه يحقق التأثير ذاته لعدد صغير من سير العمل. لعدد كبير من سير العمل، فكّر في منظّم خارجي يدعم الجدولة الموزعة أصلاً.

اعتبارات شاملة

الأمثلية (Idempotency) ليست خياراً

يجب أن تكون كل مهمة cron آمنة للتشغيل مرتين. تفشل البنية التحتية وتُعاد تشغيل الجدولات وتنتهي مهل الأقفال الموزعة أحياناً. صمّم المهام بحيث يُنتج تشغيلها مرة ثانية — بنفس المدخلات وفي نفس الوقت — النتيجة ذاتها. استخدم upserts بدلاً من inserts، وأنماط الفحص قبل التنفيذ، ومفاتيح الأمثلية لاستدعاءات API الخارجية.

التنبيه على الفشل

مهمة cron تفشل بصمت أسوأ من مهمة لا تعمل أصلاً. أرسل تنبيهاً إلى قائمة انتظار الخطأ أو نظام المراقبة عندما تفشل مهمة أو لا تكتمل ضمن نافذتها الزمنية المتوقعة. تضيف أدوات مثل Sentry Crons وHealthchecks.io وCronitor فحص نبض: تُرسل المهمة إشارة إلى رابط عند البدء وعند الانتهاء. إذا لم تصل إشارة الانتهاء ضمن المهلة، يُطلق تنبيه.

سجّل الجدول

أصدر سطر سجل منظم في بداية ونهاية كل تشغيل cron، يتضمن التعبير والطابع الزمني للإطلاق (ليس الوقت الحالي الذي قد يختلف قليلاً بسبب التأخير العشوائي)، والمدة. يجعل هذا ربط حادث لاحق — "تباطأت قاعدة البيانات عند العاشرة مساءً" — بتشغيل مهمة دُفعة محددة أمراً سهلاً.

مرجع سريع

حلّل وتحقق من أي تعبير باستخدام محلل Cron. حوّل الأوقات المجدولة بين المناطق الزمنية مع محوّل المناطق الزمنية. إذا سجّلت مهامك طوابع Unix الزمنية، يحوّلها محوّل الطوابع الزمنية إلى تواريخ مقروءة فوراً.

للاطلاع على مرجع شامل لصيغة cron والأمثلة الشائعة، يُعدّ crontab.guru المرجع السريع المعياري. لصيغة H في Jenkins ومواصفاتها الكاملة، راجع توثيق صيغة cron في Jenkins Pipeline.