5 Pola Ekspresi Cron yang Digunakan di Produksi Nyata
Ekspresi cron tampak sederhana — lima field, beberapa operator — tetapi tim produksi membuat kesalahan yang sama berulang kali: job yang diam-diam terlewat pada bulan tertentu, eksekusi tumpang tindih yang merusak state bersama, dan ratusan job yang semuanya bangun pada detik yang sama dan membebani basis data. Ekspresinya jarang menjadi masalah; masalahnya adalah tidak memikirkan edge case.
Panduan ini membahas lima pola cron yang benar-benar digunakan tim engineering di produksi, dengan alasan di balik masing-masing dan jebakan yang harus dihindari. Gunakan Toova Cron Parser untuk memverifikasi ekspresi apa pun dan melihat sepuluh waktu eksekusi berikutnya sebelum melakukan deploy.
Anatomi Ekspresi Cron
Ekspresi cron standar memiliki lima field yang dipisahkan spasi:
*/15 * * * *
| | | | |
| | | | +---- hari dalam minggu (0-6, 0=Minggu)
| | | +------- bulan (1-12)
| | +---------- hari dalam bulan (1-31)
| +------------- jam (0-23)
+----------------- menit: setiap menit ke-15 (0, 15, 30, 45)
Setiap field menerima: nilai spesifik (5), wildcard (*), rentang (1-5), daftar (1,3,5), atau nilai langkah (*/15). Beberapa implementasi menambahkan field keenam untuk detik; yang lain (seperti GitHub Actions) mengharapkan tepat lima. Selalu periksa dialek mana yang digunakan scheduler Anda sebelum menulis ekspresi yang kompleks.
Pola 1: Setiap 15 Menit — */15 * * * *
*/15 * * * *
| | | | |
| | | | +---- hari dalam minggu (0-6, 0=Minggu)
| | | +------- bulan (1-12)
| | +---------- hari dalam bulan (1-31)
| +------------- jam (0-23)
+----------------- menit: setiap menit ke-15 (0, 15, 30, 45) Ekspresi andalan untuk pemroses antrean, pemeriksaan heartbeat, dan job polling yang perlu berjalan sering tetapi tidak terus-menerus. Ia berjalan pada :00, :15, :30, dan :45 setiap jam, setiap hari.
Di crontab:
# entri crontab — jalankan batch processor setiap 15 menit
*/15 * * * * /opt/app/bin/process-queue.sh >> /var/log/queue.log 2>&1 Di Node.js:
// Node.js dengan 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);
}); Masalah thundering herd. Saat Anda menjalankan ratusan worker dengan ekspresi */15 yang sama, mereka semua berjalan bersamaan pada :00, :15, :30, dan :45. Jika setiap worker mengakses basis data bersama, Anda mendapat empat lonjakan per jam alih-alih beban yang halus. Perbaikannya adalah menambahkan jitter acak sebelum setiap eksekusi:
// Tambahkan jitter untuk menghindari thundering herd di sistem terdistribusi
cron.schedule('*/15 * * * *', async () => {
const jitterMs = Math.floor(Math.random() * 30_000); // hingga 30 detik
await sleep(jitterMs);
await processBatch();
}); Jitter hingga 30 detik menyebarkan lonjakan ke jendela 30 detik sambil menjaga job tetap dalam siklus 15 menit yang diharapkan. Untuk penyebaran yang lebih canggih, lihat Pola 5 (sintaks H Jenkins).
Gunakan Timestamp Converter untuk menerjemahkan eksekusi terjadwal berikutnya dari epoch Unix ke waktu yang dapat dibaca manusia saat men-debug waktu job.
Pola 2: Jam Kerja Hari Kerja — 0 9 * * 1-5
0 9 * * 1-5
| | | | |
| | | | +-- hari dalam minggu: Senin–Jumat (1-5)
| | | +------ bulan: setiap bulan
| | +---------- hari dalam bulan: setiap hari
| +-------------- jam: 9
+------------------ menit: 0 (tepat pada jam) Ekspresi ini berjalan sekali, tepat pukul 9:00 pagi, Senin hingga Jumat. Ini adalah pola standar untuk: mengirim email rangkuman harian, menjalankan laporan jam kerja, memicu alur kerja yang membutuhkan tinjauan manusia selama hari kerja, dan men-deploy ke staging di awal hari engineering.
Di Kubernetes CronJob:
# Kubernetes CronJob — email rangkuman hari kerja pukul 9 pagi 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 Jebakan zona waktu. Jadwal Kubernetes CronJob berjalan di zona waktu cluster, yang default-nya UTC. "9 pagi UTC" adalah 5 pagi Eastern di musim dingin dan 4 pagi Pacific. Pengguna Anda akan menerima "rangkuman pagi" mereka di tengah malam. Gunakan library cron yang menerima string zona waktu IANA:
// Tangani zona waktu — "9 pagi UTC" jarang sesuai harapan pengguna
// Gunakan library yang memahami zona waktu IANA
import { CronJob } from 'cron';
const job = new CronJob(
'0 9 * * 1-5',
() => sendDailyDigest(),
null,
true,
'America/New_York' // berjalan pukul 9 pagi Eastern, bukan 9 pagi UTC
); Timezone Converter membantu Anda menemukan ekuivalen UTC untuk waktu lokal tertentu di seluruh zona waktu IANA.
Penomoran hari dalam minggu. Definisi cron standar menggunakan 0 atau 7 untuk Minggu. Beberapa implementasi (Quartz, cron4j) menggunakan 1 untuk Senin alih-alih 0. Periksa dokumentasi scheduler Anda dan uji dengan Cron Parser sebelum melakukan deploy.
Pola 3: Batch Malam — 0 22 * * *
0 22 * * *
| | | | |
| | | | +-- hari dalam minggu: apa saja
| | | +------ bulan: apa saja
| | +---------- hari dalam bulan: apa saja
| +-------------- jam: 22 (10 malam)
+------------------- menit: 0 Berjalan sekali per hari pukul 10 malam. Batch malam adalah salah satu pola paling umum dalam data engineering: agregasi event harian, membangun ulang indeks pencarian, menghasilkan laporan, mengarsipkan log, mengirim notifikasi akhir hari.
Mengapa pukul 10 malam dan bukan tengah malam? Dua alasan. Pertama, pukul 10 malam memberi buffer sebelum hari berganti, mengurangi kemungkinan menangkap event yang tiba terlambat. Kedua, menjalankan job berat pada tengah malam berarti ia bersaing dengan tagihan akhir bulan (Pola 4) pada tanggal 1 setiap bulan.
Tiga cara paling umum untuk menulis job harian:
# Tiga cara menulis "tiap malam pukul 10":
0 22 * * * # cron standar
@daily # tengah malam — TIDAK sama dengan 10 malam
0 22 * * * # selalu lebih baik waktu eksplisit daripada @macros untuk non-tengah malam Pencegahan tumpang tindih. Job malam yang biasanya memakan waktu 45 menit kadang-kadang bisa memakan 2 jam — dan jika ia masih berjalan saat pemicu malam berikutnya tiba, kedua job akan memodifikasi data yang sama. Gunakan kunci terdistribusi:
// Pemeriksaan idempotensi — cegah pemrosesan ganda jika job tumpang tindih
async function runNightlyBatch() {
const lock = await redis.set(
'nightly-batch-lock',
process.pid,
'EX', 3600, // TTL: 1 jam
'NX' // hanya set jika belum ada
);
if (!lock) {
console.log('Batch sudah berjalan, dilewati.');
return;
}
try {
await processNightlyData();
} finally {
await redis.del('nightly-batch-lock');
}
}
Flag NX Redis memastikan hanya satu proses yang dapat memegang kunci pada satu waktu. TTL mencegah job yang crash menahan kunci selamanya.
Pola 4: Tagihan Bulanan — 0 0 1 * *
0 0 1 * *
| | | | |
| | | | +-- hari dalam minggu: apa saja
| | | +------ bulan: apa saja
| | +---------- hari dalam bulan: 1 (pertama)
| +-------------- jam: 0 (tengah malam)
+------------------ menit: 0 Berjalan pada tengah malam di hari pertama setiap bulan. Pola klasik untuk: menghasilkan faktur bulanan, mereset kuota pemakaian, menjalankan rekonsiliasi akhir bulan, dan mengarsipkan data bulan sebelumnya.
Jebakan cron untuk job bulanan:
# Tanggal 1 bulan tengah malam UTC
0 0 1 * *
# ---- JEBAKAN YANG HARUS DIHINDARI ----
# Ini TIDAK berjalan "setiap bulan" dengan andal:
0 0 29-31 * * # bulan dengan hari lebih sedikit dilewati diam-diam
# Tagihan kuartalan (Jan, Apr, Jul, Okt):
0 0 1 1,4,7,10 *
# Akhir bulan rumit — tidak ada "hari terakhir" di cron standar
# Gunakan @reboot + logika aplikasi, atau cron yang berjalan harian dan
# memeriksa apakah hari ini adalah hari terakhir bulan ini di dalam kode. Tagihan akhir bulan. Jika Anda perlu menagih pada hari terakhir setiap bulan (alih-alih hari pertama), tidak ada ekspresi cron yang rapi untuk itu. Solusinya:
// Pemeriksaan "hari terakhir bulan" di tingkat aplikasi
cron.schedule('0 23 28-31 * *', async () => {
const today = new Date();
const tomorrow = new Date(today);
tomorrow.setDate(today.getDate() + 1);
// Jika besok tanggal 1, hari ini adalah hari terakhir bulan ini
if (tomorrow.getDate() === 1) {
await runEndOfMonthBilling();
}
}); Ini berjalan pada tanggal 28–31 setiap bulan, kemudian memeriksa di kode apakah hari ini benar-benar hari terakhir. Ia berjalan hingga empat kali yang tidak perlu (pada bulan dengan kurang dari 31 hari), tetapi pemeriksaannya murah dan logikanya jelas.
Untuk tagihan awal kuartal, gunakan operator daftar di field bulan: 0 0 1 1,4,7,10 * (Januari, April, Juli, Oktober).
Pola 5: Jadwal Hash (Sintaks H Jenkins) — H/30 * * * *
H/30 * * * *
|
+--- H berarti Jenkins memilih menit acak per job.
H/30 = sekali setiap 30 menit, dimulai pada H.
Job berbeda mendapat nilai H berbeda, menyebarkan beban.
Token H adalah ekstensi khusus Jenkins untuk sintaks cron. Ia mengganti H dengan angka pseudo-random stabil yang berasal dari nama job. H/30 di field menit berarti "sekali setiap 30 menit, dimulai pada menit yang ditentukan oleh hash dari nama job."
Di Jenkinsfile:
// Jenkinsfile — pipeline dipicu setiap 30 menit, secara terhuyung
pipeline {
triggers {
cron('H/30 * * * *')
}
stages {
stage('Deploy') {
steps {
sh './deploy.sh'
}
}
}
} Mengapa ini penting. Instalasi Jenkins seringkali memiliki ratusan pipeline. Jika setiap pipeline menggunakan */30 * * * *, semuanya berjalan pada :00 dan :30, menciptakan lonjakan beban besar di pool agent dan sistem hilir. Dengan H/30, setiap job mendapat offset berbeda — satu pada :03/:33, yang lain pada :17/:47 — mendistribusikan beban secara merata sepanjang jam.
GitHub Actions tidak mendukung H. Untuk menyebar beberapa workflow Actions, pilih offset menit tetap berbeda secara manual:
// GitHub Actions — H tidak didukung; gunakan schedule dengan offset manual
// Untuk menyebar dua job: pilih menit tetap yang berbeda
name: Job A
on:
schedule:
- cron: '5 * * * *' # berjalan pada :05 setiap jam
---
name: Job B
on:
schedule:
- cron: '35 * * * *' # berjalan pada :35 — 30 menit setelah Job A Ini kurang elegan dibanding H Jenkins tetapi mencapai efek yang sama untuk sejumlah kecil workflow. Untuk workflow dalam jumlah besar, pertimbangkan orkestrator eksternal yang secara native mendukung penjadwalan terdistribusi.
Perhatian Lintas-Bidang
Idempotensi Tidak Bisa Ditawar
Setiap job cron harus aman dijalankan dua kali. Infrastruktur gagal, scheduler restart, dan kunci terdistribusi kadang-kadang timeout. Rancang job sedemikian rupa sehingga menjalankannya untuk kedua kalinya — dengan input yang sama, pada waktu yang sama — menghasilkan hasil yang sama. Gunakan upsert alih-alih insert, pola check-before-act, dan kunci idempotensi untuk panggilan API eksternal.
Peringatan Dead Letter
Job cron yang gagal secara diam-diam lebih buruk daripada yang tidak berjalan sama sekali. Kirim peringatan ke antrean dead letter atau sistem pemantauan saat job gagal atau tidak selesai dalam jendela waktu yang diharapkan. Alat seperti Sentry Crons, Healthchecks.io, dan Cronitor menambahkan pemeriksaan heartbeat: job melakukan ping ke URL saat dimulai dan saat selesai. Jika ping selesai tidak tiba dalam tenggat waktu, peringatan akan berbunyi.
Catat Jadwalnya
Pancarkan baris log terstruktur di awal dan akhir setiap eksekusi cron, termasuk ekspresinya, timestamp dipicu (bukan waktu saat ini, yang mungkin sedikit berbeda karena jitter), dan durasinya. Hal ini memudahkan untuk mengorelasi insiden hilir — "basis data melambat pada pukul 10 malam" — dengan eksekusi job batch tertentu.
Referensi Cepat
Parse dan validasi ekspresi apa pun dengan Cron Parser. Konversikan waktu terjadwal antar zona waktu dengan Timezone Converter. Jika job Anda mencatat timestamp Unix, Timestamp Converter mengubahnya menjadi tanggal yang dapat dibaca seketika.
Untuk referensi komprehensif sintaks cron dan contoh umum, crontab.guru adalah referensi cepat kanonis. Untuk sintaks H Jenkins dan spesifikasi lengkapnya, lihat dokumentasi sintaks cron Jenkins Pipeline.