Znaczniki czasu Uniksa w zaplanowanych zadaniach i zadaniach cron
Zadania cron wydają się proste na pierwszy rzut oka: uruchom tę komendę o tej godzinie. Ale szeregowanie w produkcji staje się szybko skomplikowane. Musisz śledzić, kiedy zadanie ostatnio się uruchomiło, wykryć, czy jest opóźnione, obsłużyć nakładające się uruchomienia i logować wszystko z wystarczającą precyzją, aby debugować błędy później. Znaczniki czasu Uniksa to praktyczne narzędzie do wszystkiego tego.
Użyj konwertera znaczników czasu Uniksa, aby przekonwertować dowolny znacznik czasu na czytelną dla człowieka datę lub uzyskać bieżący czas Uniksa. Ten artykuł omawia, jak znaczniki czasu są używane w systemach szeregowania — od podstawowego cron do bardziej złożonych kolejek zadań.
Dlaczego zaplanowane zadania używają znaczników czasu Uniksa
Wyrażenie cron takie jak 0 2 mówi planistowi, kiedy* uruchomić zadanie. Ale nie mówi ci nic o tym, co się stało podczas jego uruchomienia. Do tego potrzebujesz znaczników czasu zanotowanych w czasie uruchomienia: kiedy zadanie się zaczęło, kiedy się skończyło, czy się powiodło.
Znaczniki czasu Uniksa to naturalny format na to, ponieważ:
- Są jednoznaczne — brak zamieszania ze strefami czasowymi, brak formatowania zależnego od ustawień regionalnych
- Można je porównywać — możesz odjąć dwa znaczniki czasu, aby uzyskać upłynięte sekundy
- Zajmują mało miejsca — pojedyncza liczba całkowita przechowuje się w dowolnej kolumnie bazy danych lub linii dziennika
- Można je sortować — porządkowanie liczb całkowitych równa się porządkowaniu chronologicznemu
Zaplanowane zadanie, które zapisuje started_at: 1712534400 do bazy danych, rejestruje coś precyzyjnego i możliwego do zapytania. Zadanie, które zapisuje started_at: "8 kwietnia 2026, godz. 2:00" rejestruje coś, co wymaga rozparsowania zanim będzie użyteczne.
Przechowywanie znaczników czasu ostatniego uruchomienia
Najczęstsze użycie znaczników czasu w zaplanowanych zadaniach to śledzenie ostatniego pomyślnego uruchomienia. Prosty wzorzec:
1. Zadanie się uruchamia, czyta znacznik czasu last_run z magazynu 2. Zadanie wykonuje swoją pracę 3. Po sukcesie zapisuje bieżący znacznik czasu Uniksa do last_run 4. Po niepowodzeniu pozostawia last_run bez zmian (lub zapisuje do osobnego pola last_failed_at)
# Przykład w powłoce
LAST_RUN=$(cat /var/run/myjob.timestamp 2>/dev/null || echo 0)
NOW=$(date +%s)
# Wykonaj pracę tutaj...
if [ $? -eq 0 ]; then
echo $NOW > /var/run/myjob.timestamp
fi
Ten wzorzec sprawia, że łatwo odpowiedzieć na pytanie „kiedy to zadanie ostatnio się powiodło?" — wystarczy przeczytać plik i przekonwertować znacznik czasu. Ułatwia to również wykrycie zastarzałości: jeśli now - last_run > expected_interval, zadanie jest opóźnione.
Wykrywanie opóźnionych lub utraconych zadań
Cron nie wykrywa swoich własnych niepowodzeń. Jeśli serwer jest wyłączony podczas zaplanowanego uruchomienia, cron nie podejmuje ponownie próby i nie wysyła alertu. Jeśli zadanie się uruchamia, ale kończy się z błędem, cron nie oznacza go jako nieudanego. Wykrycie tych sytuacji wymaga monitorowania zewnętrznego, które sprawdza znaczniki czasu.
Prosty check opóźnienia: jeśli bieżący czas minus last_run przekracza próg, coś poszło nie tak.
import time
EXPECTED_INTERVAL_SECONDS = 3600 # zadanie powinno uruchamiać się co godzinę
TOLERANCE_SECONDS = 300 # dopuszczamy 5 minut odchylenia
last_run = get_last_run_timestamp() # z bazy danych lub pliku
now = int(time.time())
if now - last_run > EXPECTED_INTERVAL_SECONDS + TOLERANCE_SECONDS:
alert("Zadanie opóźnione — ostatnio uruchomione o {}".format(last_run))
Tolerancja ma znaczenie. Zadania cron nie zawsze uruchamiają się dokładnie zgodnie z harmonogramem — obciążenie systemu, dryft zegara i czas uruchamiania oznaczają, że zadanie zaplanowane na 02:00:00 może faktycznie uruchomić się o 02:00:04. Bez tolerancji, sprawdzenie monitorowania uruchamiane o 02:59:56 mogłoby zobaczyć zadanie jako opóźnione, chociaż uruchomiło się dobrze o 02:00:04 i jest zaplanowane ponownie za 4 sekundy.
W przypadku zadań co godzinę, 5 minut to uzasadniona tolerancja. W przypadku zadań codziennych, 30–60 minut jest typowe.
Zapobieganie nakładającym się uruchomieniom za pomocą znaczników czasu
Długotrwałe zadania mogą się nakładać, jeśli następne zaplanowane uruchomienie zacznie się zanim bieżące się zakończy. Codzienna kopia zapasowa, która trwa 2 godziny, jest w porządku. Ta, która trwa 26 godzin, zaczyna się nakładać i ostatecznie powoduje kaskadowe błędy.
Znaczniki czasu rozwiązują to prostym wzorcem blokady:
1. Zadanie się uruchamia, czyta started_at z rekordu blokady 2. Jeśli started_at istnieje i now - started_at < timeout, inne uruchomienie jest w toku — wyjdź 3. Jeśli brak blokady lub blokada wygasła, zapisz bieżący znacznik czasu jako started_at 4. Wykonaj pracę 5. Wyczyść blokadę po zakończeniu
LOCK_TIMEOUT = 7200 # 2 godziny — jeśli zadanie trwa dłużej, załóż, że jest zablokowane
lock_time = get_lock_timestamp()
now = int(time.time())
if lock_time and (now - lock_time) < LOCK_TIMEOUT:
print("Zadanie już uruchomione, uruchomione o {}".format(lock_time))
exit(0)
set_lock_timestamp(now)
# ... wykonaj pracę ...
clear_lock()
Timeout obsługuje przypadek, w którym zadanie się zawala bez czyszczenia blokady. Bez timeout, zawalone zadanie blokowałoby wszystkie przyszłe uruchomienia na zawsze. Z blokadą opartą na znaczniku czasu, blokada zawalonego uruchomienia wygasa po timeout, a następne zaplanowane uruchomienie może przejść dalej.
Planowanie przyszłych zadań za pomocą znaczników czasu
Kolejki zadań (Sidekiq, Celery, BullMQ, RQ) często planują przyszłe zadania, przechowując znacznik czasu Uniksa na moment, w którym zadanie powinno się wykonać. Pracownik kolejki ankietuje zadania, gdzie run_at <= current_timestamp.
-- Znajdź zadania gotowe do uruchomienia
SELECT * FROM scheduled_jobs
WHERE run_at <= EXTRACT(EPOCH FROM NOW())::int
AND status = 'pending'
ORDER BY run_at ASC;
To jest bardziej elastyczne niż cron do dynamicznego szeregowania. Zamiast „uruchamiaj co godzinę", możesz powiedzieć „uruchom 24 godziny po rejestracji użytkownika" lub „uruchom 15 minut po niepowodzeniu tej płatności". Zadanie jest wstawiane z run_at = NOW_UNIX + delay_seconds, a pracownik je odbiera, kiedy nadejdzie czas.
Logika ponawiania również działa w ten sposób. Po niepowodzeniu, zaplanuj ponownie z wykładniczym backoffem:
attempt = job.attempt_count
delay = min(2 ** attempt * 60, 3600) # 1min, 2min, 4min... aż do 1h
job.run_at = int(time.time()) + delay
job.save()
Po 1 próbie: ponów za 60 sekund. Po 2: 120 sekund. Po 3: 240 sekund. Limit na 3600 zapobiega nieograniczonym opóźnieniom.
Logowanie uruchomień zadań ze znacznikami czasu Uniksa
Dzienniki uruchomień zadań są najbardziej przydatne, gdy zawierają precyzyjne pomiary czasu. Linia dziennika taka jak:
[1712534400] backup-job uruchomione
[1712534447] backup-job ukończone w 47s, 3.2GB zapisane
jest natychmiast przydatna do debugowania. Możesz przekonwertować 1712534400 na czytelną dla człowieka datę za pomocą konwertera znaczników czasu Uniksa, skorelować to z innymi dziennikami systemowymi i obliczyć dokładny czas trwania bez rozparsowywania ciągów dat.
Alternatywa — logowanie sformatowanych ciągów takich jak "2026-04-08 02:00:00 UTC" — jest czytelna dla ludzi, ale niestabilna dla maszyn. Różne nośniki dziennika formatują daty inaczej, konwersje stref czasowych wprowadzają błędy, a porównywanie ciągów jest wolniejsze niż porównywanie liczb całkowitych dla zapytań o zakresy czasu.
Wspólny wzorzec to logowanie obu: surowy znacznik czasu dla czytelności maszyny i sformatowana data dla czytelności człowieka.
started_at=1712534400 started_at_human="2026-04-08T02:00:00Z" duration_s=47
Pomiar czasu trwania zadania i wydajności w czasie
Znaczniki czasu umożliwiają śledzenie wydajności między uruchomieniami. Zanotuj started_at i completed_at dla każdego uruchomienia i możesz zapytać:
- Średni czas trwania w ciągu ostatnich 30 uruchomień
- Najdłuższe uruchomienie w ciągu ostatniego tygodnia
- Czy czas trwania rośnie (możliwa regresja wydajności)
SELECT
AVG(completed_at - started_at) AS avg_duration_seconds,
MAX(completed_at - started_at) AS max_duration_seconds,
COUNT(*) AS run_count
FROM job_runs
WHERE job_name = 'nightly-report'
AND started_at > EXTRACT(EPOCH FROM NOW())::int - (30 * 86400);
Ten rodzaj zapytania jest możliwy tylko dlatego, że czasy trwania są przechowywane jako proste odejmowanie liczb całkowitych. Gdybyś przechowywał sformatowane daty, musiałbyś rozparsować ciągi przed wykonaniem operacji arytmetycznych.
Precyzja znacznika czasu: sekundy vs milisekundy w szeregowaniu
W przypadku większości zaplanowanych zadań, precyzja na poziomie sekund jest wystarczająca. Zadanie zaplanowane na uruchomienie o 1712534400 nie potrzebuje dokładności poniżej sekundy.
Ale w przypadku kolejek zadań o wysokiej częstotliwości — zadań, które mogą uruchamiać się setki razy na sekundę — znaczniki czasu w milisekundach mają znaczenie. Jeśli dwa zadania są wstawiane w tej samej sekundzie i sortujesz po znaczniku czasu, aby określić kolejność przetwarzania, znaczniki czasu na poziomie sekund tworzą remisy. Znaczniki czasu w milisekundach zachowują kolejność wstawiania w ramach każdej sekundy.
JavaScript Date.now() zwraca milisekundy. Python time.time() zwraca float z precyzją na poziomie mikrosekund. Większość baz danych obsługuje znaczniki czasu z precyzją mikrosekund. Wybór precyzji powinien odpowiadać częstotliwości szeregowania — milisekundy dla kolejek dużych wolumenów, sekundy dla standardowych zadań podobnych do cron.
Obsługa czasu letniego w zaplanowanych zadaniach
Tutaj ukrywają się wiele błędów szeregowania. Zadanie cron skonfigurowane jako 0 2 * uruchamia się o 2:00 czasu lokalnego. Kiedy zegary przesuwają się do przodu, 2:00 nie istnieje — zegar przechodzi z 1:59 do 3:00. Zadanie albo się pomija, albo uruchamia o 3:00 w zależności od implementacji cron.
Kiedy zegary coją się wstecz, 2:00 występuje dwa razy. Zadanie może uruchomić się dwa razy.
Rozwiązanie: uruchom cron w UTC. 0 2 * w UTC to zawsze 2:00 UTC — brak niejasności, brak niespodzianek z czasu letniego. Czas zadania zmienia się względem czasu lokalnego dwa razy w roku, ale zawsze uruchamia się dokładnie raz.
Znaczniki czasu Uniksa to z definicji UTC, dlatego są odporne na ten problem. Zadanie zaplanowane na uruchomienie o znaczniku czasu 1712534400 uruchamia się w tym dokładnym momencie niezależnie od tego, w jakiej strefie czasowej znajduje się serwer lub jaki offset czasu letniego dotyczy tego momentu. To jest główny argument za używaniem harmonogramów opartych na znacznikach czasu zamiast cron dla wszystkiego, gdzie dokładne pomiary mają znaczenie.
W przypadku zadań, gdzie semantyka czasu lokalnego ma znaczenie — „uruchom o 9:00 godzin biznesowych" — potrzebujesz jawnej obsługi stref czasowych, nie tylko UTC. Przechowuj docelową strefę czasową obok harmonogramu, konwertuj na UTC w momencie tworzenia harmonogramu i przelicz, jeśli reguły czasu letniego strefy czasowej ulegną zmianie.

