5 Wzorców Wyrażeń Cron Używanych w Realnej Produkcji
Wyrażenia cron wyglądają prosto - pięć pól, garstka operatorów - ale zespoły produkcyjne wciąż popełniają te same błędy: zadania, które po cichu pomijają niektóre miesiące, nakładające się uruchomienia psujące współdzielony stan oraz setki zadań budzących się w tej samej sekundzie i przeciążających bazę danych. Problemem rzadko jest samo wyrażenie; problemem jest brak przemyślenia przypadków brzegowych.
Ten przewodnik pokrywa pięć wzorców cron, których zespoły inżynierskie faktycznie używają w produkcji, z uzasadnieniem dla każdego i pułapkami do uniknięcia. Użyj Toova Cron Parser, aby zweryfikować dowolne wyrażenie i zobaczyć następne dziesięć czasów wykonania przed wdrożeniem.
Anatomia Wyrażenia Cron
Standardowe wyrażenie cron składa się z pięciu pól oddzielonych spacjami:
*/15 * * * *
| | | | |
| | | | +---- dzień tygodnia (0-6, 0=niedziela)
| | | +------- miesiąc (1-12)
| | +---------- dzień miesiąca (1-31)
| +------------- godzina (0-23)
+----------------- minuta: co 15 (0, 15, 30, 45)
Każde pole akceptuje: konkretną wartość (5), wildcard (*), zakres (1-5), listę (1,3,5) lub wartość kroku (*/15). Niektóre implementacje dodają szóste pole na sekundy; inne (jak GitHub Actions) oczekują dokładnie pięciu. Zawsze sprawdzaj, którego dialektu używa twój scheduler, zanim napiszesz złożone wyrażenia.
Wzorzec 1: Co 15 Minut — */15 * * * *
*/15 * * * *
| | | | |
| | | | +---- dzień tygodnia (0-6, 0=niedziela)
| | | +------- miesiąc (1-12)
| | +---------- dzień miesiąca (1-31)
| +------------- godzina (0-23)
+----------------- minuta: co 15 (0, 15, 30, 45) Domyślne wyrażenie dla procesorów kolejek, sprawdzeń heartbeat i zadań pollingowych, które muszą się uruchamiać często, ale nie ciągle. Wystrzeliwuje o :00, :15, :30 i :45 każdej godziny, każdego dnia.
W crontabie:
# wpis crontab - uruchom przetwarzanie wsadowe co 15 minut
*/15 * * * * /opt/app/bin/process-queue.sh >> /var/log/queue.log 2>&1 W Node.js:
// Node.js z 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);
}); Problem efektu stada. Gdy uruchamiasz setki workerów z tym samym wyrażeniem */15, wszystkie wystrzeliwują równocześnie o :00, :15, :30 i :45. Jeśli każdy worker uderza we współdzieloną bazę, dostajesz cztery skoki na godzinę zamiast płynnego obciążenia. Rozwiązaniem jest dodanie losowego jittera przed każdym uruchomieniem:
// Dodaj jitter, aby uniknąć efektu stada w systemach rozproszonych
cron.schedule('*/15 * * * *', async () => {
const jitterMs = Math.floor(Math.random() * 30_000); // do 30s
await sleep(jitterMs);
await processBatch();
}); Do 30 sekund jittera rozkłada skok w 30-sekundowym oknie, utrzymując zadanie w oczekiwanym 15-minutowym cyklu. Dla bardziej zaawansowanego rozłożenia, spójrz na Wzorzec 5 (składnia H Jenkins).
Użyj Timestamp Converter, aby przetłumaczyć następne zaplanowane uruchomienie z Unix epoch na czas czytelny dla człowieka podczas debugowania harmonogramu zadań.
Wzorzec 2: Godziny Pracy w Dni Robocze — 0 9 * * 1-5
0 9 * * 1-5
| | | | |
| | | | +-- dzień tygodnia: poniedziałek–piątek (1-5)
| | | +------ miesiąc: każdy miesiąc
| | +---------- dzień miesiąca: każdy dzień
| +-------------- godzina: 9
+------------------ minuta: 0 (dokładnie o pełnej godzinie) To wyrażenie wystrzeliwuje raz, dokładnie o 9:00 rano, od poniedziałku do piątku. To standardowy wzorzec dla: wysyłania codziennych e-maili z podsumowaniem, uruchamiania raportów godzin pracy, wyzwalania workflow wymagających przeglądu w ciągu dnia roboczego oraz wdrażania na staging na początku dnia inżynierskiego.
W Kubernetes CronJob:
# Kubernetes CronJob - codzienny e-mail w dni robocze o 9 rano 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 Pułapka strefy czasowej. Harmonogramy Kubernetes CronJob działają w strefie czasowej klastra, która domyślnie jest UTC. "9 rano UTC" to 5 rano czasu wschodniego zimą i 4 rano czasu pacyficznego. Twoi użytkownicy otrzymają swój "poranny digest" w środku nocy. Użyj biblioteki cron przyjmującej ciągi stref IANA:
// Obsługa strefy czasowej - "9 rano UTC" rzadko jest tym, czego oczekują użytkownicy
// Użyj biblioteki rozumiejącej strefy IANA
import { CronJob } from 'cron';
const job = new CronJob(
'0 9 * * 1-5',
() => sendDailyDigest(),
null,
true,
'America/New_York' // uruchamia się o 9 rano czasu wschodniego, nie UTC
); Timezone Converter pomoże ci znaleźć ekwiwalent UTC dla danego czasu lokalnego w dowolnej strefie IANA.
Numerowanie dnia tygodnia. Standardowa definicja cron używa 0 lub 7 dla niedzieli. Niektóre implementacje (Quartz, cron4j) używają 1 dla poniedziałku zamiast 0. Sprawdź dokumentację swojego schedulera i przetestuj z Cron Parser przed wdrożeniem.
Wzorzec 3: Nocny Batch — 0 22 * * *
0 22 * * *
| | | | |
| | | | +-- dzień tygodnia: dowolny
| | | +------ miesiąc: dowolny
| | +---------- dzień miesiąca: dowolny
| +-------------- godzina: 22 (10 wieczorem)
+------------------- minuta: 0 Uruchamia się raz dziennie o 22. Nocny batch to jeden z najczęstszych wzorców w inżynierii danych: agregowanie zdarzeń dnia, przebudowywanie indeksów wyszukiwania, generowanie raportów, archiwizowanie logów, wysyłanie powiadomień końca dnia.
Dlaczego 22 zamiast północy? Dwa powody. Po pierwsze, 22 daje bufor zanim dzień się przewróci, zmniejszając szansę na podłapanie zdarzeń, które przychodzą późno. Po drugie, uruchomienie ciężkiego zadania o północy oznacza, że konkuruje ono z rozliczeniem końca miesiąca (Wzorzec 4) pierwszego każdego miesiąca.
Trzy najczęstsze sposoby zapisu dziennego zadania:
# Trzy sposoby zapisu "co noc o 22":
0 22 * * * # standardowy cron
@daily # północ - NIE to samo co 22
0 22 * * * # zawsze preferuj jawny czas zamiast @makr dla godzin innych niż północ Zapobieganie nakładaniu się. Nocne zadanie, które normalnie zajmuje 45 minut, może czasami zająć 2 godziny - a jeśli nadal trwa, gdy następnej nocy wystrzeliwuje trigger, oba zadania zmodyfikują te same dane. Użyj rozproszonej blokady:
// Sprawdzenie idempotentności - zapobiegaj podwójnemu przetwarzaniu, gdy zadanie się zazębia
async function runNightlyBatch() {
const lock = await redis.set(
'nightly-batch-lock',
process.pid,
'EX', 3600, // TTL: 1 godzina
'NX' // ustaw tylko jeśli nie istnieje
);
if (!lock) {
console.log('Batch already running, skipping.');
return;
}
try {
await processNightlyData();
} finally {
await redis.del('nightly-batch-lock');
}
}
Flaga Redis NX zapewnia, że tylko jeden proces może w danym momencie trzymać blokadę. TTL zapobiega trzymaniu blokady na zawsze przez zawieszony proces.
Wzorzec 4: Comiesięczne Rozliczenie — 0 0 1 * *
0 0 1 * *
| | | | |
| | | | +-- dzień tygodnia: dowolny
| | | +------ miesiąc: dowolny
| | +---------- dzień miesiąca: 1 (pierwszy)
| +-------------- godzina: 0 (północ)
+------------------ minuta: 0 Wystrzeliwuje o północy pierwszego dnia każdego miesiąca. Klasyczny wzorzec dla: generowania miesięcznych faktur, resetowania limitów wykorzystania, uruchamiania uzgodnień końca miesiąca oraz archiwizowania danych poprzedniego miesiąca.
Pułapki cron dla zadań miesięcznych:
# Pierwszego dnia miesiąca o północy UTC
0 0 1 * *
# ---- PUŁAPKI DO UNIKNIĘCIA ----
# To NIE uruchamia się "co miesiąc" niezawodnie:
0 0 29-31 * * # miesiące z mniejszą liczbą dni po cichu pomijają
# Rozliczenia kwartalne (sty, kwi, lip, paź):
0 0 1 1,4,7,10 *
# Koniec miesiąca jest podchwytliwy - w standardowym cronie nie ma "ostatniego dnia"
# Użyj @reboot + logika aplikacji, lub crona, który uruchamia się codziennie i
# sprawdza w kodzie, czy dziś jest ostatni dzień miesiąca. Rozliczanie na koniec miesiąca. Jeśli musisz rozliczać ostatniego dnia każdego miesiąca (zamiast pierwszego), nie ma czystego wyrażenia cron dla tego. Obejście:
// Sprawdzenie "ostatniego dnia miesiąca" na poziomie aplikacji
cron.schedule('0 23 28-31 * *', async () => {
const today = new Date();
const tomorrow = new Date(today);
tomorrow.setDate(today.getDate() + 1);
// Jeśli jutro jest 1., dziś jest ostatni dzień miesiąca
if (tomorrow.getDate() === 1) {
await runEndOfMonthBilling();
}
}); To wystrzeliwuje w dni 28-31 każdego miesiąca, a następnie sprawdza w kodzie, czy dziś rzeczywiście jest ostatni dzień. Uruchamia się do czterech razy niepotrzebnie (w miesiącach krótszych niż 31 dni), ale sprawdzenie jest tanie, a logika jasna.
Dla rozliczania na początku kwartału, użyj operatora listy w polu miesiąca: 0 0 1 1,4,7,10 * (styczeń, kwiecień, lipiec, październik).
Wzorzec 5: Hashowany Harmonogram (Składnia H Jenkins) — H/30 * * * *
H/30 * * * *
|
+--- H oznacza, że Jenkins wybiera losową minutę dla każdego zadania.
H/30 = raz na 30 minut, zaczynając od H.
Różne zadania dostają różne wartości H, rozkładając obciążenie.
Token H to rozszerzenie składni cron specyficzne dla Jenkins. Zastępuje H stabilną pseudolosową liczbą wyprowadzoną z nazwy zadania. H/30 w polu minut oznacza "raz na 30 minut, zaczynając od minuty określonej przez skrót nazwy zadania".
W Jenkinsfile:
// Jenkinsfile - pipeline uruchamiany co 30 minut, rozłożony w czasie
pipeline {
triggers {
cron('H/30 * * * *')
}
stages {
stage('Deploy') {
steps {
sh './deploy.sh'
}
}
}
} Dlaczego to ma znaczenie. Instalacje Jenkins często mają setki pipeline'ów. Jeśli każdy pipeline używa */30 * * * *, wszystkie wyzwalają się o :00 i :30, tworząc masywne skoki obciążenia w puli agentów i wszystkich systemach downstream. Z H/30 każde zadanie dostaje inny offset - jedno o :03/:33, inne o :17/:47 - rozkładając obciążenie równomiernie przez godzinę.
GitHub Actions nie obsługuje H. Aby rozłożyć wiele workflow Actions, wybierz różne stałe offsety minutowe ręcznie:
// GitHub Actions - H nie jest obsługiwane; użyj schedule z ręcznym offsetem
// Aby rozłożyć dwa zadania: wybierz różne stałe minuty
name: Job A
on:
schedule:
- cron: '5 * * * *' # uruchamia się o :05 każdej godziny
---
name: Job B
on:
schedule:
- cron: '35 * * * *' # uruchamia się o :35 - 30 minut po Job A Jest to mniej eleganckie niż Jenkins H, ale daje ten sam efekt dla małej liczby workflow. Dla dużej liczby workflow rozważ zewnętrzny orchestrator natywnie obsługujący rozproszone harmonogramowanie.
Sprawy Przekrojowe
Idempotentność Nie Podlega Negocjacji
Każde zadanie cron powinno być bezpieczne do dwukrotnego uruchomienia. Infrastruktura zawodzi, schedulery restartują się, a rozproszone blokady czasami wygasają. Projektuj zadania tak, aby uruchomienie ich po raz drugi - z tymi samymi wejściami, w tym samym czasie - dawało ten sam wynik. Używaj upsertów zamiast wstawek, wzorców check-before-act i kluczy idempotentności dla wywołań zewnętrznych API.
Alerty Dead Letter
Zadanie cron, które zawodzi po cichu, jest gorsze niż takie, które wcale się nie uruchamia. Wysyłaj alert do kolejki dead letter lub systemu monitorowania, gdy zadanie zawiedzie lub nie zakończy się w spodziewanym oknie czasowym. Narzędzia jak Sentry Crons, Healthchecks.io i Cronitor dodają sprawdzenie heartbeat: zadanie pinguje URL, gdy się zaczyna i kończy. Jeśli ping zakończenia nie dotrze w deadline, wystrzela alert.
Loguj Harmonogram
Emituj ustrukturyzowaną linię logu na początku i końcu każdego uruchomienia cron, włącznie z wyrażeniem, znacznikiem czasu wyzwolenia (nie aktualnym czasem, który może różnić się nieznacznie ze względu na jitter) oraz czasem trwania. To ułatwia skorelowanie incydentu downstream - "baza danych zwolniła o 22" - z konkretnym uruchomieniem zadania batch.
Szybka Referencja
Parsuj i waliduj dowolne wyrażenie za pomocą Cron Parser. Konwertuj zaplanowane czasy między strefami czasowymi z Timezone Converter. Jeśli twoje zadanie loguje znaczniki czasu Unix, Timestamp Converter zamienia je w czytelne daty natychmiast.
Aby uzyskać kompleksową referencję składni cron i typowych przykładów, crontab.guru to kanoniczna szybka referencja. Dla składni H Jenkins i jej pełnej specyfikacji, zobacz dokumentację składni cron Jenkins Pipeline.