Nếu bạn đã dùng Linux một thời gian, chắc bạn biết đến cron jobs. Bạn dùng nó để chạy script lúc 2 giờ sáng mỗi đêm, hoặc mỗi 5 phút. Hầu hết các hệ thống Linux hiện đại (Debian, Ubuntu, Fedora, Arch, RHEL) dùng systemd làm init system, và systemd có cách riêng để lập lịch công việc. Chúng ta gọi đó là timer units.
Systemd Timers làm cùng công việc như cron, nhưng chúng gắn liền với phần còn lại của systemd. Điều đó có nghĩa bạn có logging đúng chuẩn, xử lý dependency, và kiểm soát tài nguyên miễn phí.
Một systemd timer luôn gồm hai phần:
- Một unit
.timer. Unit này quyết định khi nào chạy. - Một unit
.service. Unit này quyết định cái gì chạy.
Công việc duy nhất của timer là khởi động service vào đúng thời điểm. Khi bạn hiểu sự tách biệt này, mọi thứ khác sẽ dễ dàng.
Hãy thử ngay bây giờ, trước khi viết bất cứ gì: chạy systemctl list-timers trên máy bạn. Gần như chắc chắn bạn sẽ thấy các timer đang chạy, như apt-daily.timer, logrotate.timer, hoặc man-db.timer.
$ systemctl list-timers
NEXT LEFT LAST PASSED UNIT ACTIVATES
Tue 2026-08-25 11:30:00 UTC 3min 40s Tue 2026-08-25 11:20:00 UTC 6min ago sysstat-collect.timer sysstat-collect.service
Tue 2026-08-25 12:21:48 UTC 55min - - fwupd-refresh.timer fwupd-refresh.service
Tue 2026-08-25 19:37:38 UTC 8h Tue 2026-08-25 11:06:29 UTC - motd-news.timer motd-news.service
Tue 2026-08-25 21:35:55 UTC 10h - - apt-daily.timer apt-daily.service
Wed 2026-08-26 00:00:00 UTC 12h - - dpkg-db-backup.timer dpkg-db-backup.service
Wed 2026-08-26 00:00:00 UTC 12h - - sysstat-rotate.timer sysstat-rotate.service
Wed 2026-08-26 00:07:00 UTC 12h - - sysstat-summary.timer sysstat-summary.service
Wed 2026-08-26 00:45:21 UTC 13h - - logrotate.timer logrotate.service
Wed 2026-08-26 06:29:14 UTC 19h - - apt-daily-upgrade.timer apt-daily-upgrade.service
Wed 2026-08-26 11:06:17 UTC 23h - - man-db.timer man-db.service
Wed 2026-08-26 11:12:00 UTC 23h Tue 2026-08-25 11:12:00 UTC 14min ago update-notifier-download.timer update-notifier-download.service
Wed 2026-08-26 11:22:00 UTC 23h Tue 2026-08-25 11:22:00 UTC 4min 19s ago systemd-tmpfiles-clean.timer systemd-tmpfiles-clean.service
Sun 2026-08-30 03:10:01 UTC 4 days - - xfs_scrub_all.timer xfs_scrub_all.service
Sun 2026-08-30 03:10:34 UTC 4 days - - e2scrub_all.timer e2scrub_all.service
Mon 2026-08-31 00:55:31 UTC 5 days - - fstrim.timer fstrim.service
Sun 2026-09-06 01:25:59 UTC 1 week 4 days - - update-notifier-motd.timer update-notifier-motd.service
16 timers listed.
Pass --all to see loaded but inactive timers, too.
Những cái này không đặc biệt. Chúng là các timer unit bình thường, được xây dựng giống cách bạn sắp xây dựng cái của mình. Bạn có thể xem bất kỳ cái nào bằng systemctl cat man-db.timer để thấy chính xác cách một timer production thực sự được viết.
Xin lưu ý rằng timers phụ thuộc vào systemd chạy với PID 1.
$ ps -p 1
PID TTY TIME CMD
1 ? 00:00:06 systemd
Bên trong hầu hết các Docker container, một số chroot tối thiểu, và một số cấu hình WSL, systemd không chạy, vì vậy timers (và cả systemctl) sẽ không hoạt động ở đó nếu không thiết lập thêm. Nếu một timer bạn tạo có vẻ im lặng không làm gì, hãy kiểm tra ps -p 1 trước, trước khi bắt đầu debug các unit file.
1. Thiết lập cơ bản
Giả sử bạn muốn sao lưu một thư mục mỗi ngày lúc 2:30 sáng. Bạn cần hai file.
- Một service unit
- Một timer unit
Đầu tiên, tạo service unit:
sudo nano /etc/systemd/system/backup.service
Thêm các dòng sau vào đó:
[Unit]
Description=Back up home directory
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
Nice=19
IOSchedulingClass=idle
ProtectSystem=strict
ReadWritePaths=/var/backups
Tiếp theo, tạo timer unit:
sudo nano /etc/systemd/system/backup.timer
Thêm các dòng này:
[Unit]
Description=Run backup.service daily at 3:30 AM
[Timer]
OnCalendar=*-*-* 03:30:00
Persistent=true
[Install]
WantedBy=timers.target
Một vài điều cần lưu ý:
- Timer và service dùng chung tên (
backup). systemd dùng sự khớp tên này để liên kết chúng. Bạn chỉ cần trỏ timer tới một service khác nếu bạn tự đặtUnit=. Type=oneshotphù hợp với hầu hết các tác vụ lập lịch chạy đến khi hoàn thành. Nếu script của bạn fork một tiến trình nền hoặc tự daemon hóa,Type=oneshotsẽ không theo dõi đúng, và bạn cầnType=forkinghoặcRemainAfterExit=truethay thế. Hầu hết script bạn tự viết sẽ không cần điều này.Nice=19vàIOSchedulingClass=idlebáo cho kernel rằng tác vụ này ưu tiên thấp, để nó không cạnh tranh với việc khẩn cấp hơn.ProtectSystem=strictlàm cho toàn bộ filesystem chỉ đọc với service này, trừ các đường dẫn bạn cho phép rõ ràng bằngReadWritePaths=. Không cái nào bắt buộc để timer hoạt động, nhưng đó là loại hardening nhỏ, rẻ, đáng thêm mặc định khi bạn đã qua giai đoạn nó có chạy không.- Bạn enable và start timer, không phải service. Bạn chỉ start service trực tiếp khi muốn test.
sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer
2. Hai cách lập lịch: thời gian lịch và thời gian tương đối
Đây là nơi nhiều người mới bắt đầu bối rối, nên hãy chậm lại ở đây.
2.1 Timer theo lịch (OnCalendar=)
Loại này kích hoạt tại một thời điểm cố định trên đồng hồ, giống cron. Cú pháp đến từ systemd.time(7):
OnCalendar=*-*-* 03:30:00 # every day at 3:30:00 AM
OnCalendar=Mon,Fri *-*-* 09:00:00 # every Monday and Friday at 9 AM
OnCalendar=*-*-01 00:00:00 # the first day of every month, at midnight
OnCalendar=weekly # shortcut for Monday at 00:00
OnCalendar=daily # shortcut for every day at midnight
OnCalendar=hourly # shortcut for the top of every hour
OnCalendar=*:0/15 # every 15 minutes
Đây là một mẹo nhỏ giúp cuộc sống dễ dàng hơn. Bạn có thể test bất kỳ biểu thức nào mà không cần chờ nó kích hoạt:
$ systemd-analyze calendar "Mon,Fri *-*-* 09:00:00"
Original form: Mon,Fri *-*-* 09:00:00
Normalized form: Mon,Fri *-*-* 09:00:00
Next elapse: Mon 2026-08-17 09:00:00 IST
(in UTC): Mon 2026-08-17 03:30:00 UTC
From now: 1 day 18h left
Chạy lệnh này trước khi bạn enable một timer mới. Nó cứu bạn khỏi lỗi kinh điển: viết dòng OnCalendar= trông có vẻ đúng nhưng kích hoạt sai thời điểm.
Có một kiểm tra bổ sung thứ hai cũng đáng chạy:
systemd-analyze verify
Lệnh này nạp unit file giống cách systemd làm và in ra bất kỳ lỗi cú pháp nào, mà không thực sự khởi động gì.
sudo systemd-analyze verify /etc/systemd/system/backup.timer
Điều này quan trọng vì systemd thường dễ dãi với lỗi chính tả trong unit file. Một directive viết sai có thể bị bỏ qua im lặng thay vì báo lỗi to, và timer của bạn cứ lặng lẽ không làm điều bạn mong đợi. Hãy tạo thói quen chạy systemd-analyze verify sau mỗi lần sửa, ngay cạnh daemon-reload.
Xin lưu ý rằng systemd timers không chính xác tuyệt đối theo mặc định. Có thiết lập ẩn AccuracySec= với giá trị mặc định là 1 phút, vì vậy systemd có thể dịch chuyển thời điểm bắt đầu công việc của bạn lên đến một phút theo cả hai hướng. Nó làm điều này có chủ đích, để gộp các lần thức dậy gần nhau (wake-ups) và tiết kiệm điện — điều này rất quan trọng trên laptop.
Nếu bạn thực sự cần độ chính xác second-level, hãy đặt AccuracySec=1us trong phần [Timer]. Với gần như mọi thứ bạn xây dựng khi mới bắt đầu, mặc định là đủ và bạn có thể bỏ qua điều này.
2.2 Timer tương đối (OnBootSec=, OnUnitActiveSec=, và những cái khác)
Loại này không quan tâm đến đồng hồ treo tường. Thay vào đó, chúng kích hoạt sau một khoảng thời gian nhất định kể từ khi một sự kiện xảy ra. cron không có tương đương, nên đáng để học kỹ.
Đây là một cái bẫy khiến nhiều người mắc. OnUnitActiveSec= và OnUnitInactiveSec= nghe gần giống nhau, nhưng chúng không giống. Với một công việc oneshot ngắn, sự khác biệt gần như không quan trọng, vì công việc bắt đầu và dừng gần như tức thì. Nhưng với một service chạy lâu, hai cái này hành xử rất khác. OnUnitActiveSec=1h đếm từ khi service bắt đầu. OnUnitInactiveSec=1h đếm từ khi nó dừng. Trộn lẫn chúng và lịch trình của bạn sẽ lặng lẽ trôi dạt khỏi quỹ đạo.
Một mẫu phổ biến: chạy script dọn dẹp 10 phút sau khi khởi động, rồi mỗi 6 hours.
[Timer]
OnBootSec=10min
OnUnitActiveSec=6h
3. Bắt kịp các lần chạy bị bỏ lỡ với Persistent=
Giả sử laptop của bạn đang ngủ lúc 2:30 sáng khi backup.timer đáng lẽ kích hoạt. Không có gì xảy ra. Công việc bị bỏ qua, trừ khi bạn thêm dòng này:
[Timer]
Persistent=true
Với Persistent=true, systemd nhớ lần cuối timer thực sự kích hoạt (nó lưu trong /var/lib/systemd/timers/). Nếu máy bạn bỏ lỡ một lần chạy theo lịch vì tắt hoặc ngủ, công việc sẽ chạy một lần, ngay sau lần khởi động tiếp theo, để bắt kịp. Điều này hoạt động giống công cụ cũ anacron. Đó là lý do bạn sẽ muốn Persistent=true trên laptop và desktop gần như mặc định, trong khi server luôn bật thường không cần.
4. Dàn trải tải với RandomizedDelaySec=
Hãy tưởng tượng: bạn quản lý năm mươi server, và tất cả chúng chạy OnCalendar=daily lúc nửa đêm. Cả năm mươi cùng tấn công server backup của bạn vào cùng một giây. Đó là một đàn bò rống (thundering herd), và nó có thể làm sập hệ thống.
Sửa như thế này:
[Timer]
OnCalendar=daily
RandomizedDelaySec=1800
Mỗi máy giờ đây tự chọn độ trễ ngẫu nhiên riêng, lên đến 30 phút, trước khi thực sự chạy. Tải sẽ tự nhiên dàn trải. Nếu bạn muốn mỗi máy chọn cùng một độ trễ mỗi lần (thay vì ngẫu nhiên mới mỗi lần chạy), hãy thêm FixedRandomDelay=true (có từ systemd 247 trở đi).
5. Các lệnh systemd timers bạn sẽ dùng mỗi ngày
# List all active timers and when each one fires next
systemctl list-timers
# Include timers that are disabled or haven't triggered yet
systemctl list-timers --all
# Check a specific timer's status
systemctl status backup.timer
# Check how the last run of the service went
systemctl status backup.service
# Read the logs from the service the timer triggered
journalctl -u backup.service
# Trigger the service right now, without waiting for the timer
sudo systemctl start backup.service
# Enable at boot and start immediately
sudo systemctl enable --now backup.timer
# Stop and disable it
sudo systemctl disable --now backup.timer
# Run this after you edit any unit file
sudo systemctl daemon-reload
systemctl list-timers cho bạn một bản tóm tắt rõ ràng trong một dòng:
NEXT LEFT LAST PASSED UNIT ACTIVATES
Mon 2026-08-17 03:30:00 IST 12h left Sun 2026-08-16 03:30:00 IST 9h ago backup.timer backup.service
Bạn thấy ngay khi nào nó chạy tiếp theo, còn bao lâu, lần cuối chạy khi nào, và nó kích hoạt service nào. Với cron, bạn thường phải tin vào crontab -l và hy vọng điều tốt đẹp. Đây là một bước tiến thực sự.
Một lưu ý nhỏ về log: journalctl -u backup.service giả định journal thực sự lưu lịch sử. Một số bản phân phối tối giản hoặc nhúng đi kèm với Storage=volatile trong /etc/systemd/journald.conf, nghĩa là log chỉ sống trong bộ nhớ và biến mất khi khởi động lại. Nếu log của bạn có vẻ biến mất, thiết lập đó là thứ đầu tiên cần kiểm tra.
6. Điều gì xảy ra nếu một công việc vẫn đang chạy khi timer kích hoạt lần nữa
Giả sử backup.service thường hoàn thành trong 5 phút, nhưng tối nay, vì lý do nào đó, nó vẫn chạy 6 hours sau khi timer đến hạn kích hoạt lần nữa. Điều gì xảy ra?
Không có gì xấu, nhưng có thể không như bạn mong đợi: systemd không khởi động một instance thứ hai chồng lấn. Kích hoạt một unit đã active thực chất là no-op. Lần chạy theo lịch tiếp theo của timer trôi qua lặng lẽ, và service cứ tiếp tục chạy cho đến khi hoàn thành.
Với hầu hết tác vụ, đây chính là lưới an toàn bạn muốn, vì nó ngăn chặn sự chồng chất mất kiểm soát (pile-ups). Nhưng nó cũng có nghĩa nếu một công việc treo, bạn có thể mất cả ngày chạy mà không có lỗi nào xuất hiện ở bất kỳ đâu.
Hai điều rút ra thực tế:
- Nếu một tác vụ thực sự không bao giờ được phép chồng lấn và không bao giờ được treo im lặng, hãy kết hợp với
TimeoutStartSec=trong[Service](xem danh sách lỗi bên dưới) để một lần chạy kẹt bị giết thay vì chặn mọi lần chạy tương lai. - Nếu bạn nghi ngờ một công việc bị kẹt,
systemctl status backup.servicesẽ hiển thị nó ở trạng tháiactive (running)lâu hơn nhiều so với dự kiến. Đó là tín hiệu của bạn.
7. Timer một lần nhanh mà không cần viết unit file
Đôi khi bạn chỉ muốn chạy một thứ gì đó một lần, sớm, mà không cần tạo cặp .timer và .service. Lệnh systemd-run làm điều này ngay lập tức:
# Run a command 10 minutes from now
sudo systemd-run --on-active="10min" /path/to/script.sh
# Run at a specific date and time
sudo systemd-run --on-calendar="2026-08-20 22:00:00" /path/to/script.sh
# Run 5 minutes after every boot, as your own user
systemd-run --user --on-boot="5min" /path/to/script.sh
systemd-run tạo một timer và service tạm thời trong nền (bạn có thể thấy chúng bằng systemctl list-timers), và nó tự động xóa chúng khi khởi động lại.
Điều này rất hữu ích để test một ý tưởng lịch trình, hoặc cho một tác vụ bạn chỉ cần một lần. Với bất cứ thứ gì bạn muốn tồn tại qua reboot và chạy mãi, hãy quay lại viết unit file đúng chuẩn như đã trình bày ở trên.
8. Chạy timers không cần root
Mọi thứ cho đến nay giả định unit toàn hệ thống trong /etc/systemd/system/. Bạn cũng có thể thiết lập timer như một người dùng thường bằng cờ --user. Không cần root!
mkdir -p ~/.config/systemd/user
# put backup.service and backup.timer in that folder
systemctl --user daemon-reload
systemctl --user enable --now backup.timer
Xin lưu ý rằng user timer thường chỉ chạy khi phiên của bạn đang hoạt động. Nếu bạn muốn chúng tiếp tục chạy ngay cả sau khi đăng xuất, hãy bật lingering (cái này cần root, ngay cả cho tài khoản của bạn):
sudo loginctl enable-linger $USER
9. systemd Timers so với cron: Khi nào dùng cái nào
Lý do dùng timers:
Chúng cung cấp logging thực sự qua journalctl, nên bạn không còn cần các mẹo MAILTO= hoặc chuyển hướng output thủ công. Bạn cũng có thể khai báo dependency đúng chuẩn, ví dụ After=network-online.target hoặc Requires=postgresql.service, điều cron không thể làm.
Bạn có thể sandbox service với các tùy chọn như ProtectSystem=strict hoặc PrivateTmp=true. Và bạn có kiểm tra tích hợp (systemd-analyze calendar) cùng tổng quan sạch sẽ (list-timers), cùng với hành vi bắt kịp (catch-up) và jitter được xử lý tự nhiên thay vì gắn thêm bằng script.
Lý do dùng cron:
Nó chạy gần như ở mọi nơi, bao gồm các hệ thống không có systemd, như Alpine với OpenRC, BSD, hoặc một container rút gọn. Một dòng crontab đơn giản hơn nhiều so với viết hai unit file, đặc biệt cho tác vụ không có dependency nào.
Và nếu đội của bạn đã có nhiều năm công cụ dựa trên cron, chuyển đổi mọi thứ có thể không đáng công sức.
Vậy, bạn nên chọn cái nào?
Trên hệ thống dùng systemd, timers thường là lựa chọn mặc định tốt hơn, đặc biệt khi logging, phục hồi lần chạy bị bỏ lỡ, hoặc thứ tự dependency quan trọng với bạn.
Với một script nhanh, di động, không dependency, cron vẫn làm tốt công việc. Hãy nghĩ điều này ít như một quy tắc mà nhiều như một phán đoán dựa trên môi trường của bạn.
9.1 Chuyển đổi dòng crontab sang OnCalendar
Nếu bạn đang di chuyển các cron job hiện có, đây là ánh xạ bạn sẽ dùng lặp đi lặp lại. Các trường cron đi theo thứ tự minute hour day month weekday; OnCalendar= sắp xếp lại thành weekday year-month-day hour:minute:second.
Lưu ý hàng cuối: @reboot không có tương đương calendar vì nó không gắn với thời gian đồng hồ. Đó chính là thứ OnBootSec= tồn tại để làm, và là ví dụ tốt về việc systemd timers làm điều cron về cấu trúc không thể.
10. Nhận cảnh báo khi một công việc thất bại
Đây là câu hỏi gần như ai cũng hỏi ngay sau khi timer đầu tiên hoạt động: làm sao tôi biết nó ngừng hoạt động?
Với cron, câu trả lời trung thực thường là bạn không biết, cho đến khi ai đó nhận ra các bản sao lưu backup biến mất.
systemd có câu trả lời thực sự: OnFailure=.
Thêm nó vào service của bạn, trỏ tới một service nhỏ thứ hai gửi thông báo:
# /etc/systemd/system/backup.service
[Unit]
Description=Back up home directory
OnFailure=alert-on-failure@%n.service
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
# /etc/systemd/system/[email protected]
[Unit]
Description=Send a failure alert for %i
[Service]
Type=oneshot
ExecStart=/usr/local/bin/notify-failure.sh "%i"
%n mở rộng thành tên của unit đã failed, và %i trong service cảnh báo nhận được nó. notify-failure.sh có thể đơn giản như một lệnh gọi curl tới webhook, hoặc gọi mail. Điểm mấu chốt không phải cơ chế thông báo, mà là OnFailure= kích hoạt tự động, chỉ khi thất bại, không cần polling và không cần timer thêm để kiểm tra cái đầu tiên.
11. Ví dụ đầy đủ: Dọn dẹp log hàng tuần
# /etc/systemd/system/logcleanup.service
[Unit]
Description=Purge logs older than 30 days
After=network.target
[Service]
Type=oneshot
ExecStart=/usr/bin/find /var/log/myapp -name "*.log" -mtime +30 -delete
# /etc/systemd/system/logcleanup.timer
[Unit]
Description=Weekly log cleanup
[Timer]
OnCalendar=Sun *-*-* 04:00:00
Persistent=true
RandomizedDelaySec=600
[Install]
WantedBy=timers.target
Bây giờ đưa nó vào hoạt động và kiểm tra nó thực sự được lên lịch:
sudo systemctl daemon-reload
sudo systemctl enable --now logcleanup.timer
systemd-analyze calendar "Sun *-*-* 04:00:00" # confirm the schedule is right
systemctl list-timers logcleanup.timer # confirm systemd knows about it
sudo systemctl start logcleanup.service # test it right away
journalctl -u logcleanup.service -n 20 # check that it worked
12. Những lỗi người mới hay mắc (và cách tránh)
- Bỏ qua
daemon-reload. Sau khi bạn sửa unit file, systemd vẫn giữ bản cũ trong bộ nhớ. Luôn reload. - Enable service thay vì timer. Điều này chạy công việc một lần, ngay lập tức, và sau đó không bao giờ chạy theo lịch nữa.
- Trộn lẫn
OnUnitActiveSec=vàOnUnitInactiveSec=. Điều này chủ yếu gây hại với service chạy lâu, như đã đề cập ở phần 3.2. - Bỏ sót
Persistent=true. Trên laptop, điều này có nghĩa công việc hàng ngày của bạn lặng lẽ bỏ qua nhiều ngày mỗi khi máy ngủ vào thời điểm đã lên lịch. - Quên phần
[Install]. Không cóWantedBy=timers.target, không có gì đểsystemctl enablegắn vào, nên timer sẽ không khởi động khi boot. - Không kiểm tra trạng thái sau khi có sự cố. Một công việc
oneshotbịfailedsẽ hiển thị làfailed, và timer vẫn sẽ kích hoạt lại vào chu kỳ tiếp theo. Nhưng bạn sẽ không nhận ra trừ khi bạn kiểm tra log hoặc thiết lậpOnFailure=để cảnh báo. - Mong đợi
OnCalendar=kích hoạt đúng giây. Như đã đề cập ở phần 3.1,AccuracySec=1minmặc định có nghĩa công việc của bạn có thể trễ lên đến một phút. Điều này bình thường, không phải lỗi. - Tin tưởng unit file chỉ vì
daemon-reloadkhông phàn nàn. systemd có thể im lặng bỏ qua một directive viết sai thay vì báo lỗi. Chạysystemd-analyze verifysau khi sửa để bắt điều này. - Giả định một công việc treo cuối cùng sẽ tự bị giết. Mặc định thì không. Nếu một script có thể kẹt (một network mount cũ, chờ input không bao giờ đến), hãy thêm
TimeoutStartSec=trong[Service]để systemd giết nó thay vì để nó chặn mọi lần chạy tương lai, như đã đề cập ở phần 7.
13. Tham khảo nhanh cho Systemd Timers
Để biết thêm chi tiết về systemd Timers, đọc các trang manual.
man systemd.timercho danh sách đầy đủ các directiveman systemd.timecho cú pháp calendar và time-spanman systemd.servicecho các tùy chọn service nhưType=và các directive sandboxing



