5 điểm bởi GN⁺ 19 giờ trước | 1 bình luận | Chia sẻ qua WhatsApp
  • systemd timer là lựa chọn thay thế thực dụng cho cron, dùng để chạy các unit như .service theo lịch và giúp quản lý lịch sử, đầu ra, môi trường rõ ràng hơn
  • cron truyền thống có các điểm yếu như $PATH mơ hồ, stdout/stderr dễ bị thất lạc, lịch sử chạy khó theo dõi, và cú pháp lịch khó đọc
  • Timer liên kết .timer.service cùng stem, và dùng OnCalendar, OnBootSec, OnUnitActiveSec để biểu đạt việc chạy theo thời điểm hoặc theo sự kiện
  • Có thể dùng systemd-analyze calendarsystemctl list-timers để kiểm tra biểu thức thời gian và thời điểm chạy tiếp theo; WakeSystem= có thể đánh thức máy để chạy ngay cả khi đang ngủ
  • RandomizedOffsetSecFixedRandomDelay= giúp giảm đỉnh chạy đồng thời, còn Persistent= sẽ bù ngay sau khi máy trực tuyến trở lại cho các lần chạy bị lỡ khi timer không hoạt động

Vì sao dùng systemd timer để thay cron

  • cron job là cách gọi phổ biến để chỉ thành phần cơ bản trong điện toán dùng để chạy tác vụ theo lịch như “chạy việc này mỗi ngày”, “chạy việc kia mỗi tháng”, kể cả khi tác vụ đó không do daemon cron thực sự thực hiện
  • systemd timer là một unit của systemd dùng để chạy một unit khác, thường là .service, theo lịch cụ thể, và có thể trở thành lựa chọn thay thế về mặt chức năng cho daemon cron truyền thống
  • cron truyền thống có một số điểm yếu trong thực tế vận hành
    • Thiết lập $PATH mơ hồ khiến kết quả chạy script khó dự đoán
    • Đầu ra stdoutstderr thường rơi vào hố đen hoặc bị gửi vào hệ thống mail của máy chủ
    • Khó theo dõi và truy vấn lịch sử thực thi
    • Cú pháp lịch như 01,31 04,05 1-15 1,6 * không dễ đọc hay trực quan với con người
  • systemd timer giảm bớt các vấn đề này đồng thời vẫn cung cấp cấu hình lịch kiểu lịch tương tự cron

Cấu trúc cơ bản: service và timer

  • systemd timer cần một đối tượng để chạy, và unit .service về mặt logic có thể xem như một script
  • Ví dụ, nếu đặt unit sau tại /etc/systemd/system/roulette.service thì sẽ cài một service có xác suất 1 trên 10 tắt máy tính
[Unit]
Description=1 in 10 chance to break your chains

[Service]
ExecStart=/usr/bin/env bash -c '[[ $(($RANDOM % 10)) == 0 ]] && systemctl poweroff || echo LIVE ANOTHER DAY'
  • ExecCondition= là cách tích hợp hơn để biểu đạt việc chạy có điều kiện bằng tùy chọn service của systemd, đồng thời thể hiện rõ hơn ở cấp unit rằng “có nên tiếp tục chạy hay không?”
[Unit]
Description=1 in 10 chance to break your chains

[Service]
ExecCondition=/run/current-system/sw/bin/bash -c '[[ $(($RANDOM % 10)) == 0 ]]'
ExecStart=/run/current-system/sw/bin/systemctl poweroff
  • Nếu điều kiện không thỏa, journal sẽ ghi lại thông báo rõ ràng hơn
May 05 11:05:32 diesel systemd[3117]: Condition check resulted in 1 in 10 chance to break your chains being skipped.
  • Nhìn chung, tận dụng các tùy chọn systemd cung cấp thường mang lại trải nghiệm tốt hơn so với tự viết script
    • OnFailure= có thể dùng khi cần phản ứng với việc script service thất bại
    • Restart= có thể dùng để thử phục hồi khỏi lỗi tạm thời

Liên kết unit timer và thực thi

  • Đặt /etc/systemd/system/roulette.timer với cùng stem tên file sẽ giúp liên kết timer với roulette.service
[Unit]
Description=impending destruction

[Timer]
OnCalendar=10:00

[Install]
WantedBy=timers.target
  • Mặc định, thiết lập Unit= của timer sẽ chọn unit service cùng stem với hậu tố .service
    • Trong ví dụ này, roulette.service sẽ được chọn
    • Nếu muốn chạy một unit service tên khác, có thể đổi Unit=
  • Đối tượng trong ExecStart= mặc định không được chạy như lệnh shell
    • Đích có đường dẫn tuyệt đối cần được xử lý như script hoặc như trình thông dịch nhận script dưới dạng đối số chuỗi
    • ExecStart=/usr/bin/echo Hello | /usr/bin/awk sẽ không hoạt động trong ngữ cảnh này vì pipe không có ý nghĩa
  • Các đối số của ExecStart= mặc định không kế thừa biến môi trường ngoài một số giá trị mặc định của system manager
    • $PATH mặc định gần như trống
    • Chạy /usr/bin/env là một biện pháp bảo vệ đơn giản để các mục như systemctl vẫn dùng được
    • Nếu chỉ dùng ExecStart=/usr/bin/bash thì $PATH sẽ có các mục mặc định, nhưng dùng env là lớp an toàn bổ sung
  • Có thể chạy service trực tiếp mà không cần timer
systemctl start roulette
  • Service không có phần [Install] thì không thể enable; trong cấu trúc này, timer là cách chuẩn để chạy service một cách nhất quán
  • systemctl mặc định vẫn thao tác với roulette.service ngay cả khi không ghi hậu tố rõ ràng
  • Áp dụng systemctl start cho unit .timer sẽ đưa timer vào trạng thái hoạt động, nhưng không chạy ngay service đích trong Unit=
systemctl start roulette.timer
  • status sẽ hiển thị lần tiếp theo timer sẽ chạy
systemctl status roulette.timer
Trigger: Sat 2026-04-18 10:00:00 MDT; 35min left
  • Luồng đơn giản nhất là tạo service đích, đặt timer có lịch ở cùng vị trí, rồi khởi động timer thay vì khởi động target
  • Nếu [Install] của unit timer có WantedBy= thì cũng có thể để timer tự lên cùng lúc khởi động máy
systemctl enable roulette.timer

Biểu đạt thời gian: sự kiện lịch và khoảng thời gian

  • Trong timer, cách biểu đạt lịch là điều quan trọng, và cần phân biệt giữa khoảng thời gian lặp lại với sự kiện lịch hoặc timestamp
  • Trang manual systemd.time(7) có nhiều ví dụ hữu ích và đáng là tài liệu đầu tiên để tham khảo khi viết timer
  • systemd-analyze có thể kiểm tra và diễn giải biểu thức thời gian
systemd-analyze calendar '*-*-* *:*:*'
Normalized form: *-*-* *:*:*
    Next elapse: Sat 2026-04-18 16:44:26 MDT
       (in UTC): Sat 2026-04-18 22:44:26 UTC
       From now: 431ms left
  • systemd timer không chỉ hỗ trợ thời điểm lặp lại theo đồng hồ thực, mà khác với cron truyền thống, còn cho phép định nghĩa khoảng thời gian lặp lại dựa trên một sự kiện trước đó
  • Dạng đầy đủ của daily có nghĩa là chạy lúc 00:00:00 mỗi năm, mỗi tháng, mỗi ngày
*-*-* 00:00:00
│ │ │ │  │  ╰── at second 00
│ │ │ │  ╰───── at minute 00
│ │ │ ╰──────── at hour 00
│ │ ╰────────── every day
│ ╰──────────── every month
╰────────────── every year
  • Có thể dùng các dạng viết tắt như daily, dạng đầy đủ, và các giá trị khác được systemd.time(7) hỗ trợ; systemd-analyze giúp kiểm chứng các giả định đó

Khi chạy theo sự kiện phù hợp hơn

  • Trong công việc thực tế, nhiều khi cách diễn đạt “chạy sau một sự kiện khác” phù hợp hơn là “chạy mỗi ngày vào cùng một giờ”
  • Với tác vụ dọn thư mục tạm, nếu biểu thức cron ngay sau lúc khởi động đã trôi qua thì /tmp có thể hầu như chẳng có gì để dọn
  • Diễn đạt là “chạy sau một giờ kể từ khi máy khởi động, rồi sau đó chạy mỗi giờ” sẽ khớp hơn với hành vi thực của service và logic lịch
[Timer]
OnBootSec=1h
OnUnitActiveSec=1h
  • OnBootSec=1h nghĩa là chạy một lần sau 1 giờ kể từ lúc máy khởi động
  • OnUnitActiveSec=1h nghĩa là chạy lại sau 1 giờ kể từ khi Unit= đã được thực thi, khiến timer ngầm tiếp tục lặp mãi
  • Kiểu biểu đạt chu kỳ như vậy thường phù hợp hơn cho các tác vụ “thỉnh thoảng chạy một lần” so với kiểu “chạy vào phút này của mỗi giờ”
  • Trong ví dụ bot Slack polling API của Advent of Code, biểu thức cron */15 đúng với chính sách “mỗi 15 phút” của API, nhưng nếu ai cũng polling như nhau thì có thể gây dồn lưu lượng
  • Nếu khởi động timer sau khi sửa mã và để nó chạy mỗi 15 phút kể từ đó, vẫn đáp ứng yêu cầu mà còn có khả năng giảm vấn đề thundering herd

Xem trạng thái timer trong nháy mắt

  • systemctl list-timers là lệnh cấp cao giúp tóm tắt tình trạng timer trên một máy
systemctl list-timers
NEXT                                 LEFT LAST                                  PASSED UNIT                         ACTIVATES
Mon 2026-04-20 15:15:00 MDT      1min 40s Mon 2026-04-20 15:00:05 MDT        13min ago zfs-snapshot-frequent.timer  zfs-snapshot-frequent.service
Mon 2026-04-20 15:32:16 MDT         18min Mon 2026-04-20 14:22:15 MDT        51min ago fwupd-refresh.timer          fwupd-refresh.service
Mon 2026-04-20 16:00:00 MDT         46min Mon 2026-04-20 15:00:05 MDT        13min ago logrotate.timer              logrotate.service
Mon 2026-04-20 16:00:00 MDT         46min Mon 2026-04-20 15:00:05 MDT        13min ago zfs-snapshot-hourly.timer    zfs-snapshot-hourly.service
Tue 2026-04-21 00:00:00 MDT            8h Mon 2026-04-20 09:43:22 MDT     5h 29min ago zfs-snapshot-daily.timer     zfs-snapshot-daily.service
Tue 2026-04-21 07:31:28 MDT           16h Sun 2026-04-19 20:15:47 MDT           7h ago systemd-tmpfiles-clean.timer systemd-tmpfiles-clean.service
Mon 2026-04-27 00:00:00 MDT        6 days Mon 2026-04-20 09:43:22 MDT     5h 29min ago zfs-snapshot-weekly.timer    zfs-snapshot-weekly.service
Mon 2026-04-27 01:09:27 MDT        6 days Mon 2026-04-20 09:43:22 MDT     5h 29min ago fstrim.timer                 fstrim.service
Mon 2026-04-27 04:28:38 MDT        6 days Mon 2026-04-20 09:43:22 MDT     5h 29min ago zpool-trim.timer             zpool-trim.service
Fri 2026-05-01 00:00:00 MDT 1 week 3 days Wed 2026-04-01 10:07:51 MDT 1 week 1 day ago zfs-snapshot-monthly.timer   zfs-snapshot-monthly.service
Fri 2026-05-01 03:17:17 MDT 1 week 3 days Wed 2026-04-01 10:07:51 MDT 1 week 1 day ago zfs-scrub.timer              zfs-scrub.service

11 timers listed.
Pass --all to see loaded but inactive timers, too.
  • Chỉ với một lệnh là có thể nhìn toàn cảnh các mục đang chạy theo lịch timer
  • list-timers là một phần của nhóm lệnh con systemd thường dùng
    • list-units cũng hữu ích
    • list-paths là lệnh con mới được thêm vào systemctl gần đây hơn

Đánh thức máy đang ngủ để chạy

  • WakeSystem= có thể cho phép timer khi đến hạn sẽ đánh thức hệ thống khỏi trạng thái ngủ
WakeSystem=
    Takes a boolean argument. If true, an elapsing timer will
    cause the system to resume from suspend, should it be
    suspended and if the system supports this.
...
  • Tính năng này hữu ích khi cần chạy một script quan trọng mà không cần hành động vật lý như người dùng mở nắp laptop
  • Trên các bản phân phối như Arch hay NixOS vốn hỗ trợ tải trước gói cập nhật trước khi dùng, có thể tải gói cập nhật vào đêm khuya rồi sáng hôm sau cập nhật khi đã ngồi trước bàn phím
  • Manual có nói rằng nếu muốn hệ thống ngủ lại sau khi .service kết thúc thì cần tự xử lý việc đưa máy về chế độ ngủ lần nữa

Phân tán thời điểm chạy và giảm thundering herd

  • Vấn đề thundering herd là vấn đề hệ thống xảy ra khi nhiều tiến trình cùng thức dậy một lúc
  • Nếu toàn bộ hệ thống Debian trên thế giới đều bị hardcode chạy apt update lúc 00:00:00, thì nửa đêm sẽ trở thành thời điểm đỉnh lưu lượng tệ hại cho tất cả
  • FixedRandomDelay=RandomizedOffsetSec= giúp phân tán thời điểm thực thi
FixedRandomDelay=
    Takes a boolean argument. When enabled, the randomized delay
    specified by RandomizedDelaySec= is chosen deterministically,
    and remains stable between all firings of the same timer,
    even if the manager is restarted. ...

RandomizedOffsetSec=
    Offsets the timer by a stable, randomly-selected, and evenly
    distributed amount of time between 0 and the specified time
    value. ...
  • Có thể dùng các thiết lập này trên hệ thống thực tế để kiểm tra cập nhật phần mềm
  • Việc phân bố thời điểm chạy theo phân phối đều giúp giảm vấn đề thundering herd, làm hành vi nhất quán hơn, và tránh các yếu tố gây nhiễu như daemon khởi động lại trong lúc đang điều phối dịch vụ phân tán
  • Nhìn chung, các tùy chọn về thời gian rất giàu khả năng cấu hình và cho phép kiểm soát chi tiết

Bù ngay các lần chạy bị lỡ

  • Persistent= đặc biệt phù hợp với các script theo lịch không nên bị bỏ qua do laptop đang ngủ, nhưng cũng chưa cần đến WakeSystem=
Persistent=
    Takes a boolean argument. If true, the time when the service
    unit was last triggered is stored on disk. When the timer is
    activated, the service unit is triggered immediately if it
    would have been triggered at least once during the time when
    the timer was inactive. ...
  • Nếu một hệ thống có lịch check-in quản lý cấu hình gặp downtime, chỉ cần bật Persistent= trong .timer là có thể hội tụ lại trạng thái đúng ngay sau khi máy trực tuyến trở lại
  • Nếu không có Persistent=, có thể phải chờ đến thời điểm chạy bình thường tiếp theo của timer, và khoảng chờ đó có thể rất dài
  • Những tác vụ khác cũng không nên chờ sau khi phát hiện lỡ kích hoạt gồm cập nhật hệ thống, kiểm tra batch job, v.v.

Những điểm cần chú ý khi viết timer

  • Timer trong ngữ cảnh user manager được quản lý bằng systemctl --user vẫn hoàn toàn hợp lệ, nhưng cần chú ý target được ghi trong [Install]
  • Tùy bản phân phối, target phù hợp cho user timer có thể là default.target
  • Cũng như cron, lưu ý chung về việc phải giữ đồng hồ hệ thống chính xác vẫn hoàn toàn áp dụng
  • Người dùng systemd có thể kiểm tra trạng thái đồng bộ bằng timedatectl timesync-status
  • Nhiều trình soạn thảo hỗ trợ sẵn định dạng file unit của systemd, rất hữu ích khi file unit bắt đầu dài ra
  • Trên Emacs có thể dùng gói emacs systemd

1 bình luận

 
Ý kiến trên Lobste.rs
  • systemd không hoàn hảo, nhưng tôi có cảm giác nhiều quyết định thiết kế của nó dựa trên những bài học rút ra từ cách làm truyền thống trước đây
    Gần đây tôi nghe lại tập năm 2015 của CRE, nơi Lennart Poettering giải thích bối cảnh đó, và đến giờ vẫn đáng để giới thiệu

  • Tôi thuộc phe không thích systemd từ tận xương tủy, nhưng systemd.timers là một trong số ít khái niệm “còn đỡ hơn” của sản phẩm này
    Vì vậy tôi khá bất ngờ khi tác giả lại bảo vệ nó theo kiểu hạ thấp những người có phàn nàn chính đáng
    Dù vậy, dùng cùng lệnh at thì khá ổn. Với lệnh chỉ cần chạy một lần vào thời điểm cụ thể thì dùng at, còn lại thì dùng systemd timer và file unit đơn giản
    Điều tôi muốn thấy được cải thiện nhất là có thể biết người dùng nào đang chạy timer. Tôi có lẽ là một trong số ít người vẫn vận hành shellbox vào năm 2026, nhưng sẽ rất hữu ích nếu biết timer nào đang gõ đĩa mỗi giây là do người dùng nào tạo ra

    • Với mục đích này, có lẽ có thể dùng user unit thay vì bắt mọi người cài system timer
      Theo tôi biết thì với loginctl enable-linger, nó có thể chạy ngay cả khi không có phiên người dùng đang hoạt động. Tất nhiên sẽ có những trường hợp cách đó vẫn chưa đủ, và tôi không biết tình huống cụ thể
  • Tôi đặc biệt mong systemd timer có gánh nặng ban đầu thấp hơn ở phía quản lý người dùng
    Xét về lượng cấu hình cần thiết thì thực sự rất khó thắng được crontab -e

    • Cách làm của systemd, nơi để cấu hình một timer phải cần nhiều file cấu hình và service, thì… xét như một lựa chọn API là khó chấp nhận nổi
  • Tôi đã suy nghĩ khá lâu về cách thu thập log cron script một cách có hệ thống, rồi nhận ra là chỉ cần dùng systemd timer
    Thế là vấn đề logging được giải quyết. Giờ tôi không còn lý do gì để quay lại cron nữa, giá mà biết điều này sớm hơn

    • Chẳng phải chỉ cần pipe sang logger, hoặc nối vào file log bằng >>, hoặc cứ để mặc định rồi nhận email là được sao?
  • Cứ bảo là lỗi thời cũng được, nhưng tôi vẫn cấu hình email từ server gửi đến mình
    Nếu tự động hóa sẵn thì mỗi host mới đều có kèm theo miễn phí, và bình thường cũng khá tiện
    Ví dụ như mở một multiplexer, chạy long_running_process | mail root@localhost -s "done $?" rồi quên luôn

  • Bài viết hay, và tôi cũng từng có bản nháp một bài tương tự, đến gần đây lại phải tham khảo lại
    Nếu bạn giống tôi và đã chui vào hang thỏ systemd, thì tôi khuyên nên liên kết tượng trưng các file unit và timer trong thư mục project liên quan vào /etc/systemd/system/
    Một điều tôi không thích ở systemd là nó không phân biệt unit do distro cài với unit do mình tự viết, và dùng symbolic link thì có thể tự duy trì sự tách biệt đó

    • Thực ra sự phân biệt đó nằm ở đường dẫn
      Unit hệ thống/gói/phân phối nằm trong /usr/lib/systemd/system, còn local override hoặc local unit thì nằm trong /etc/systemd/system