Đến phần nội dung
Toova
Tất cả công cụ

5 Mẫu Biểu Thức Cron Dùng Trong Production Thực Tế

Toova

Biểu thức cron trông đơn giản — năm trường, một vài toán tử — nhưng các đội ngũ production lặp đi lặp lại cùng những sai lầm: các job âm thầm bỏ qua trong một số tháng, các lần chạy chồng chéo làm hỏng trạng thái chia sẻ, và hàng trăm job cùng thức dậy ở cùng một giây làm quá tải cơ sở dữ liệu. Biểu thức hiếm khi là vấn đề; vấn đề là không suy nghĩ thấu đáo các trường hợp biên.

Hướng dẫn này bao quát năm mẫu cron mà các đội ngũ kỹ thuật thực sự dùng trong production, kèm lý do cho mỗi mẫu và các bẫy cần tránh. Sử dụng Toova Cron Parser để xác minh bất kỳ biểu thức nào và xem mười lần thực thi tiếp theo trước khi triển khai.

Cấu Trúc Biểu Thức Cron

Một biểu thức cron tiêu chuẩn có năm trường được phân tách bằng dấu cách:

*/15  *  *  *  *
  |   |  |  |  |
  |   |  |  |  +---- thứ trong tuần (0-6, 0=Chủ Nhật)
  |   |  |  +------- tháng (1-12)
  |   |  +---------- ngày trong tháng (1-31)
  |   +------------- giờ (0-23)
  +----------------- phút: mỗi 15 phút (0, 15, 30, 45)

Mỗi trường chấp nhận: một giá trị cụ thể (5), ký tự đại diện (*), phạm vi (1-5), danh sách (1,3,5) hoặc giá trị bước (*/15). Một số triển khai thêm trường thứ sáu cho giây; một số khác (như GitHub Actions) yêu cầu chính xác năm trường. Luôn kiểm tra biến thể mà scheduler của bạn dùng trước khi viết các biểu thức phức tạp.

Mẫu 1: Mỗi 15 Phút — */15 * * * *

*/15  *  *  *  *
  |   |  |  |  |
  |   |  |  |  +---- thứ trong tuần (0-6, 0=Chủ Nhật)
  |   |  |  +------- tháng (1-12)
  |   |  +---------- ngày trong tháng (1-31)
  |   +------------- giờ (0-23)
  +----------------- phút: mỗi 15 phút (0, 15, 30, 45)

Biểu thức hàng đầu cho các bộ xử lý hàng đợi, kiểm tra heartbeat và các job polling cần chạy thường xuyên nhưng không liên tục. Nó kích hoạt lúc :00, :15, :30, và :45 mỗi giờ, mỗi ngày.

Trong crontab:

# entry crontab — chạy bộ xử lý batch mỗi 15 phút
*/15 * * * * /opt/app/bin/process-queue.sh >> /var/log/queue.log 2>&1

Trong Node.js:

// Node.js với 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);
});

Vấn đề thundering herd. Khi bạn chạy hàng trăm worker với cùng biểu thức */15, chúng đều kích hoạt đồng thời lúc :00, :15, :30, và :45. Nếu mỗi worker truy cập một cơ sở dữ liệu chung, bạn nhận bốn lần tăng đột biến mỗi giờ thay vì tải mượt mà. Cách khắc phục là thêm jitter ngẫu nhiên trước mỗi lần chạy:

// Thêm jitter để tránh thundering herd trên hệ thống phân tán
cron.schedule('*/15 * * * *', async () => {
  const jitterMs = Math.floor(Math.random() * 30_000); // tối đa 30s
  await sleep(jitterMs);
  await processBatch();
});

Tối đa 30 giây jitter trải đột biến trên cửa sổ 30 giây trong khi giữ job trong chu kỳ 15 phút như mong đợi. Để phân tải tinh vi hơn, xem Mẫu 5 (cú pháp H của Jenkins).

Sử dụng Timestamp Converter để dịch lần chạy tiếp theo từ Unix epoch sang thời gian dễ đọc khi gỡ lỗi thời gian job.

Mẫu 2: Giờ Làm Việc Ngày Trong Tuần — 0 9 * * 1-5

0   9   *   *   1-5
|   |   |   |   |
|   |   |   |   +-- thứ trong tuần: Thứ Hai–Thứ Sáu (1-5)
|   |   |   +------ tháng: mỗi tháng
|   |   +---------- ngày trong tháng: mỗi ngày
|   +-------------- giờ: 9
+------------------ phút: 0 (chính xác đầu giờ)

Biểu thức này kích hoạt một lần, chính xác lúc 9:00 sáng, từ Thứ Hai đến Thứ Sáu. Đây là mẫu tiêu chuẩn cho: gửi email tóm tắt hàng ngày, chạy báo cáo giờ làm việc, kích hoạt các workflow cần xem xét thủ công trong ngày làm việc, và triển khai lên staging vào đầu ngày làm việc kỹ thuật.

Trong Kubernetes CronJob:

# Kubernetes CronJob — email tóm tắt ngày làm việc lúc 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

Bẫy múi giờ. Lịch trình Kubernetes CronJob chạy theo múi giờ cluster, mặc định là UTC. "9 AM UTC" là 5 giờ sáng Eastern vào mùa đông và 4 giờ sáng Pacific. Người dùng của bạn sẽ nhận "tóm tắt buổi sáng" vào giữa đêm. Dùng thư viện cron chấp nhận chuỗi múi giờ IANA:

// Xử lý múi giờ — "9 AM UTC" hiếm khi là điều người dùng mong đợi
// Dùng thư viện hiểu múi giờ IANA
import { CronJob } from 'cron';

const job = new CronJob(
  '0 9 * * 1-5',
  () => sendDailyDigest(),
  null,
  true,
  'America/New_York' // chạy lúc 9 AM Eastern, không phải 9 AM UTC
);

Timezone Converter giúp bạn tìm giá trị UTC tương đương cho thời gian địa phương đã cho trên bất kỳ múi giờ IANA nào.

Đánh số thứ trong tuần. Định nghĩa cron tiêu chuẩn dùng 0 hoặc 7 cho Chủ Nhật. Một số triển khai (Quartz, cron4j) dùng 1 cho Thứ Hai thay vì 0. Kiểm tra tài liệu của scheduler và thử với Cron Parser trước khi triển khai.

Mẫu 3: Batch Hàng Đêm — 0 22 * * *

0   22   *   *   *
|    |   |   |   |
|    |   |   |   +-- thứ trong tuần: bất kỳ
|    |   |   +------ tháng: bất kỳ
|    |   +---------- ngày trong tháng: bất kỳ
|    +-------------- giờ: 22 (10 giờ tối)
+------------------- phút: 0

Chạy một lần mỗi ngày lúc 10 giờ tối. Batch hàng đêm là một trong những mẫu phổ biến nhất trong kỹ thuật dữ liệu: tổng hợp các sự kiện trong ngày, xây dựng lại chỉ mục tìm kiếm, tạo báo cáo, lưu trữ log, gửi thông báo cuối ngày.

Tại sao 10 giờ tối thay vì nửa đêm? Hai lý do. Đầu tiên, 10 giờ tối cung cấp đệm trước khi ngày chuyển sang ngày mới, giảm khả năng lấy phải các sự kiện đến muộn. Thứ hai, chạy job nặng vào nửa đêm có nghĩa nó cạnh tranh với thanh toán cuối tháng (Mẫu 4) vào mùng 1 mỗi tháng.

Ba cách phổ biến nhất để viết một job hàng ngày:

# Ba cách viết "hàng đêm lúc 10 PM":
0 22 * * *    # cron tiêu chuẩn
@daily        # nửa đêm — KHÔNG giống 10 PM
0 22 * * *    # luôn ưu tiên thời gian rõ ràng hơn @macros cho thời gian khác nửa đêm

Ngăn chặn chồng chéo. Một job hàng đêm thường mất 45 phút đôi khi có thể mất 2 giờ — và nếu nó vẫn đang chạy khi trigger đêm tiếp theo kích hoạt, cả hai job sẽ sửa đổi cùng dữ liệu. Sử dụng khóa phân tán:

// Kiểm tra idempotent — ngăn xử lý kép nếu job bị chồng chéo
async function runNightlyBatch() {
  const lock = await redis.set(
    'nightly-batch-lock',
    process.pid,
    'EX', 3600,   // TTL: 1 giờ
    'NX'          // chỉ set nếu chưa tồn tại
  );

  if (!lock) {
    console.log('Batch đang chạy, bỏ qua.');
    return;
  }

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

Cờ NX của Redis đảm bảo chỉ một tiến trình có thể giữ khóa tại một thời điểm. TTL ngăn job bị crash giữ khóa mãi mãi.

Mẫu 4: Thanh Toán Hàng Tháng — 0 0 1 * *

0   0   1   *   *
|   |   |   |   |
|   |   |   |   +-- thứ trong tuần: bất kỳ
|   |   |   +------ tháng: bất kỳ
|   |   +---------- ngày trong tháng: 1 (đầu tháng)
|   +-------------- giờ: 0 (nửa đêm)
+------------------ phút: 0

Kích hoạt lúc nửa đêm vào ngày đầu tiên của mỗi tháng. Mẫu kinh điển cho: tạo hóa đơn hàng tháng, đặt lại hạn ngạch sử dụng, chạy đối chiếu cuối tháng, và lưu trữ dữ liệu tháng trước.

Các bẫy cron cho job hàng tháng:

# Đầu tháng lúc nửa đêm UTC
0 0 1 * *

# ---- BẪY CẦN TRÁNH ----

# Cái này KHÔNG chạy "mỗi tháng" một cách đáng tin cậy:
0 0 29-31 * *   # các tháng có ít ngày hơn sẽ bỏ qua âm thầm

# Thanh toán hàng quý (Tháng 1, 4, 7, 10):
0 0 1 1,4,7,10 *

# Cuối tháng phức tạp — không có "ngày cuối" trong cron tiêu chuẩn
# Dùng @reboot + logic ứng dụng, hoặc một cron chạy hàng ngày và
# kiểm tra trong code xem hôm nay có phải là ngày cuối của tháng không.

Thanh toán cuối tháng. Nếu bạn cần thanh toán vào ngày cuối của mỗi tháng (thay vì ngày đầu), không có biểu thức cron sạch cho việc đó. Giải pháp:

// Kiểm tra "ngày cuối tháng" ở mức ứng dụng
cron.schedule('0 23 28-31 * *', async () => {
  const today = new Date();
  const tomorrow = new Date(today);
  tomorrow.setDate(today.getDate() + 1);

  // Nếu ngày mai là mùng 1, hôm nay là ngày cuối tháng
  if (tomorrow.getDate() === 1) {
    await runEndOfMonthBilling();
  }
});

Cái này kích hoạt vào các ngày 28–31 mỗi tháng, sau đó kiểm tra trong code xem hôm nay có thực sự là ngày cuối không. Nó chạy tối đa bốn lần không cần thiết (trong các tháng có ít hơn 31 ngày), nhưng phép kiểm tra rẻ và logic rõ ràng.

Đối với thanh toán đầu quý, dùng toán tử danh sách trong trường tháng: 0 0 1 1,4,7,10 * (Tháng 1, 4, 7, 10).

Mẫu 5: Lịch Được Hash (Cú Pháp H của Jenkins) — H/30 * * * *

H/30 * * * *
|
+--- H nghĩa là Jenkins chọn một phút ngẫu nhiên cho mỗi job.
     H/30 = mỗi 30 phút một lần, bắt đầu tại H.
     Các job khác nhau nhận giá trị H khác nhau, phân tải đều.

Token H là một mở rộng đặc thù của Jenkins cho cú pháp cron. Nó thay H bằng một số giả ngẫu nhiên ổn định bắt nguồn từ tên job. H/30 trong trường phút nghĩa là "mỗi 30 phút một lần, bắt đầu từ một phút được xác định bởi hash của tên job."

Trong Jenkinsfile:

// Jenkinsfile — pipeline được kích hoạt mỗi 30 phút, được phân tải
pipeline {
  triggers {
    cron('H/30 * * * *')
  }
  stages {
    stage('Deploy') {
      steps {
        sh './deploy.sh'
      }
    }
  }
}

Tại sao điều này quan trọng. Các cài đặt Jenkins thường có hàng trăm pipeline. Nếu mọi pipeline dùng */30 * * * *, chúng đều kích hoạt lúc :00 và :30, tạo các đỉnh tải khổng lồ trên pool agent và bất kỳ hệ thống hạ nguồn nào. Với H/30, mỗi job nhận offset khác nhau — một ở :03/:33, một khác ở :17/:47 — phân phối tải đồng đều trên một giờ.

GitHub Actions không hỗ trợ H. Để phân tải nhiều workflow Actions, chọn các offset phút cố định khác nhau thủ công:

// GitHub Actions — H không được hỗ trợ; dùng schedule với offset thủ công
// Để phân tải hai job: chọn các phút cố định khác nhau
name: Job A
on:
  schedule:
    - cron: '5 * * * *'   # chạy lúc :05 mỗi giờ

---
name: Job B
on:
  schedule:
    - cron: '35 * * * *'  # chạy lúc :35 — 30 phút sau Job A

Đây kém thanh lịch hơn H của Jenkins nhưng đạt được hiệu ứng tương tự cho một số ít workflow. Đối với số lượng workflow lớn, hãy cân nhắc một bộ điều phối bên ngoài hỗ trợ nguyên bản lập lịch phân tán.

Mối Quan Tâm Xuyên Suốt

Tính Idempotent Là Không Thể Thương Lượng

Mọi cron job nên an toàn để chạy hai lần. Hạ tầng thất bại, scheduler khởi động lại, và khóa phân tán đôi khi hết hạn. Thiết kế các job sao cho chạy lại — với cùng đầu vào, cùng thời điểm — tạo cùng kết quả. Dùng upsert thay vì insert, các mẫu kiểm tra-trước-khi-hành-động, và khóa idempotent cho các cuộc gọi API bên ngoài.

Cảnh Báo Dead Letter

Một cron job thất bại âm thầm còn tệ hơn một cái không chạy gì cả. Gửi cảnh báo đến hàng đợi dead letter hoặc hệ thống giám sát khi job thất bại hoặc không hoàn thành trong cửa sổ thời gian mong đợi. Các công cụ như Sentry Crons, Healthchecks.io, và Cronitor thêm một kiểm tra heartbeat: job ping một URL khi bắt đầu và khi hoàn thành. Nếu ping kết thúc không đến trong thời hạn, cảnh báo kích hoạt.

Ghi Log Lịch Trình

Phát ra một dòng log có cấu trúc ở đầu và cuối mỗi lần chạy cron, bao gồm biểu thức, thời điểm kích hoạt (không phải thời gian hiện tại, có thể khác đôi chút do jitter), và thời lượng. Điều này giúp đơn giản hóa việc tương quan một sự cố hạ nguồn — "cơ sở dữ liệu chậm lại lúc 10 giờ tối" — với một lần chạy batch job cụ thể.

Tham Khảo Nhanh

Phân tích và xác thực bất kỳ biểu thức nào với Cron Parser. Chuyển đổi thời gian đã lên lịch giữa các múi giờ với Timezone Converter. Nếu job của bạn ghi log Unix timestamp, Timestamp Converter chuyển chúng thành ngày tháng dễ đọc ngay lập tức.

Để có tham khảo toàn diện về cú pháp cron và các ví dụ phổ biến, crontab.guru là tham khảo nhanh chuẩn. Để biết cú pháp H của Jenkins và đặc tả đầy đủ, xem tài liệu cú pháp cron Jenkins Pipeline.