- systemd timer เป็นตัวแทนเชิงปฏิบัติของ
cron สำหรับงานจริง โดยใช้รันยูนิตอย่าง .service ตามกำหนดเวลา และช่วยให้การจัดการประวัติ เอาต์พุต และสภาพแวดล้อมชัดเจนยิ่งขึ้น
cron แบบดั้งเดิมมีจุดอ่อนคือ $PATH ที่กำกวม, stdout/stderr ที่สูญหายได้ง่าย, ประวัติการรันที่ติดตามได้ยาก และไวยากรณ์ตารางเวลาที่อ่านยาก
- timer จะเชื่อม
.timer และ .service ที่มี stem เดียวกัน และใช้ OnCalendar, OnBootSec, OnUnitActiveSec เพื่อบอกการรันตามเวลาและตามเหตุการณ์
- สามารถใช้
systemd-analyze calendar และ systemctl list-timers เพื่อตรวจสอบรูปแบบเวลาและเวลารันถัดไปได้ และ WakeSystem= สามารถปลุกระบบขึ้นมารันได้แม้อยู่ในโหมดสลีป
RandomizedOffsetSec และ FixedRandomDelay= ช่วยลดพีคจากการรันพร้อมกัน ส่วน Persistent= จะชดเชยงานที่พลาดไประหว่างช่วงที่ timer ไม่ทำงานทันทีหลังกลับมาออนไลน์
ทำไมจึงใช้ systemd timer แทน cron
- คำว่า
cron job ถูกใช้กันอย่างกว้างขวางเพื่อหมายถึงองค์ประกอบพื้นฐานของการประมวลผลที่รันงานตามตาราง เช่น “รันสิ่งนี้ทุกวัน”, “รันสิ่งนั้นทุกเดือน” แม้เบื้องหลังจะไม่ใช่ cron daemon จริงก็ตาม
- systemd timer คือ systemd unit ที่ใช้รันยูนิตอื่นตามกำหนดเวลา โดยมากมักเป็น
.service และสามารถทำหน้าที่แทน cron daemon แบบดั้งเดิมได้ในเชิงฟังก์ชัน
cron แบบดั้งเดิมมีจุดอ่อนในทางปฏิบัติอยู่หลายอย่าง
- การตั้งค่า
$PATH ที่กำกวมทำให้คาดเดาผลการรันสคริปต์ได้ยาก
- เอาต์พุต
stdout และ stderr มักหายเข้าแบล็กโฮลหรือถูกส่งไปยังระบบเมลของโฮสต์
- ติดตามและค้นดูประวัติการรันได้ยาก
- ไวยากรณ์ตารางเวลาอย่าง
01,31 04,05 1-15 1,6 * ไม่ได้อ่านง่ายหรือเข้าใจได้ตรงไปตรงมาสำหรับมนุษย์
- systemd timer ช่วยลดปัญหาเหล่านี้ พร้อมทั้งยังมีการตั้งค่าแบบปฏิทินที่คล้ายกับการเขียนสไตล์
cron
โครงสร้างพื้นฐาน: service และ timer
- systemd timer ต้องมีเป้าหมายสำหรับการรัน และยูนิต
.service สามารถมองได้ในเชิงตรรกะว่าเป็นเหมือนสคริปต์
- ตัวอย่างเช่น ถ้าวางยูนิตต่อไปนี้ไว้ที่
/etc/systemd/system/roulette.service ก็จะเป็นการติดตั้ง service ที่มีโอกาส 1 ใน 10 ที่จะปิดเครื่องคอมพิวเตอร์
[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= เป็นวิธีที่บูรณาการกับ systemd มากกว่าสำหรับการแสดงการรันแบบมีเงื่อนไข และทำให้เห็นชัดเจนขึ้นในระดับยูนิตว่า “ควรรันต่อหรือไม่?”
[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
- หากเงื่อนไขไม่ผ่าน จะมีข้อความที่ชัดเจนกว่าเดิมถูกบันทึกไว้ใน journal
May 05 11:05:32 diesel systemd[3117]: Condition check resulted in 1 in 10 chance to break your chains being skipped.
- โดยทั่วไปแล้ว การใช้ตัวเลือกที่ systemd มีให้มักให้ประสบการณ์ที่ดีกว่าการเขียนสคริปต์เอง
OnFailure= ใช้ได้เมื่อต้องการตอบสนองต่อความล้มเหลวของ service script
Restart= ใช้ได้เมื่อต้องการพยายามกู้คืนจากความล้มเหลวชั่วคราว
การเชื่อม timer unit และการรัน
- หากมี
/etc/systemd/system/roulette.timer ที่ใช้ stem ชื่อไฟล์เดียวกัน ก็จะเชื่อม timer เข้ากับ roulette.service ได้
[Unit]
Description=impending destruction
[Timer]
OnCalendar=10:00
[Install]
WantedBy=timers.target
- โดยปกติแล้ว การตั้งค่า
Unit= ของ timer จะเลือก service unit ที่มี .service ต่อท้ายจาก stem เดียวกัน
- ในตัวอย่างนี้จะเลือก
roulette.service
- หากต้องการรัน service unit ที่ชื่ออื่น ก็เปลี่ยน
Unit= ได้
- เป้าหมายของ
ExecStart= จะไม่ถูกรันเป็น shell command โดยอัตโนมัติ
- เป้าหมายแบบ absolute path ต้องถูกมองเป็นสคริปต์ หรือเป็น interpreter ที่คาดหวังสคริปต์เป็น string argument
ExecStart=/usr/bin/echo Hello | /usr/bin/awk จะไม่ทำงานในบริบทนี้ เพราะ pipe ไม่มีความหมาย
- argument ของ
ExecStart= โดยปกติจะไม่สืบทอด environment variable นอกเหนือจากค่าเริ่มต้นบางส่วนของ system manager
- ค่า
$PATH ปริยายแทบจะเรียกได้ว่าเกือบว่าง
- การรัน
/usr/bin/env เป็นตัวป้องกันแบบง่ายที่ช่วยให้ใช้รายการอย่าง systemctl ได้
- ถ้าใช้แค่
ExecStart=/usr/bin/bash ก็จะมีรายการพื้นฐานใน $PATH อยู่แล้ว แต่การใช้ env ก็เป็นชั้นป้องกันเพิ่มอีกระดับ
- สามารถรัน service ได้โดยตรงโดยไม่ต้องมี timer
systemctl start roulette
- service ที่ไม่มีส่วน
[Install] จะ enable ไม่ได้ และในโครงสร้างนี้ timer คือวิธีมาตรฐานสำหรับรัน service อย่างสม่ำเสมอ
systemctl จะทำงานกับ roulette.service โดยปริยายแม้จะไม่ได้ระบุ suffix ชัดเจน
- หากใช้
systemctl start กับยูนิต .timer จะเป็นการทำให้ timer เข้าสู่สถานะทำงาน แต่จะยังไม่รัน service เป้าหมายตาม Unit= ทันที
systemctl start roulette.timer
status จะแสดงว่า timer จะรันครั้งถัดไปเมื่อใด
systemctl status roulette.timer
Trigger: Sat 2026-04-18 10:00:00 MDT; 35min left
- โฟลว์ที่ง่ายที่สุดคือสร้าง service เป้าหมายสำหรับการรัน วาง timer ที่มีตารางเวลาไว้ตำแหน่งเดียวกัน แล้วเริ่มต้น timer แทนที่จะเริ่มเป้าหมายโดยตรง
- ถ้าใน
[Install] ของ timer unit มี WantedBy= ก็สามารถทำให้ timer ถูกยกขึ้นมาพร้อมตอนบูตได้
systemctl enable roulette.timer
การแสดงเวลา: calendar event และช่วงเวลา
- ใน timer วิธีแสดงกำหนดเวลาถือว่าสำคัญ และควรแยกความต่างระหว่างช่วงเวลาที่เกิดซ้ำ กับ calendar event หรือ timestamp
- คู่มือ
systemd.time(7) มีตัวอย่างเพียงพอและเหมาะจะเป็นแหล่งอ้างอิงอันดับแรกเวลาเขียน timer
systemd-analyze สามารถตรวจสอบและอธิบายนิพจน์เวลาได้
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 ไม่ได้จำกัดแค่เวลาบน wall clock ที่เกิดซ้ำ แต่ยังสามารถนิยามช่วงเวลาที่เกิดซ้ำโดยอิงจากเหตุการณ์ก่อนหน้าได้ด้วย ซึ่งต่างจาก
cron แบบดั้งเดิม
- รูปแบบเต็มของ
daily หมายถึงให้รันเวลา 00:00:00 ของทุกปี ทุกเดือน และทุกวัน
*-*-* 00:00:00
│ │ │ │ │ ╰── at second 00
│ │ │ │ ╰───── at minute 00
│ │ │ ╰──────── at hour 00
│ │ ╰────────── every day
│ ╰──────────── every month
╰────────────── every year
- สามารถใช้รูปแบบย่ออย่าง
daily, รูปแบบเต็ม หรือค่าอื่นที่ systemd.time(7) รองรับได้ และตรวจสอบสมมติฐานด้วย systemd-analyze
เมื่อการรันตามเหตุการณ์เหมาะกว่า
- สำหรับงานจริง หลายครั้งการบอกว่า “รันหลังจากมีเหตุการณ์อื่น” เหมาะกว่าการบอกว่า “รันทุกวันเวลาเดียวกัน”
- งานล้างไดเรกทอรีชั่วคราวอาจแทบไม่มีอะไรให้เก็บกวาดใน
/tmp เลย หากเครื่องเพิ่งบูตขึ้นมาหลังเวลาที่นิพจน์ cron ควรจะทำงานไปแล้ว
- การบอกว่า “รันหนึ่งชั่วโมงหลังคอมพิวเตอร์เริ่มทำงาน แล้วหลังจากนั้นรันทุกชั่วโมง” จะสอดคล้องกับพฤติกรรมจริงของ service และตรรกะของตารางเวลามากกว่า
[Timer]
OnBootSec=1h
OnUnitActiveSec=1h
OnBootSec=1h หมายถึงให้รันหนึ่งครั้งหลังจากเครื่องเริ่มทำงานไปแล้ว 1 ชั่วโมง
OnUnitActiveSec=1h หมายถึงให้รันอีกครั้งหลังจาก Unit= ทำงานไปแล้ว 1 ชั่วโมง และทำให้ timer วนซ้ำต่อเนื่องโดยปริยาย
- การแสดงคาบเวลาแบบนี้มักเหมาะกับงานประเภท “รันเป็นระยะ ๆ” มากกว่าการเขียนว่า “รันทุกชั่วโมงที่นาทีนี้”
- ในตัวอย่าง Slack bot ที่คอย poll API ของ Advent of Code นิพจน์
*/15 ของ cron ปฏิบัติตามนโยบาย “ทุก 15 นาที” ของ API ได้ แต่ถ้าทุกคน poll แบบเดียวกันก็อาจทำให้ทราฟฟิกกระจุกตัว
- หากเริ่ม timer หลังแก้โค้ด แล้วปล่อยให้รันทุก 15 นาทีหลังจากนั้น ก็อาจยังตอบโจทย์พฤติกรรมที่ต้องการ พร้อมทั้งช่วยลดปัญหา thundering herd ได้
ดูสถานะ timer ทั้งระบบได้ในครั้งเดียว
systemctl list-timers เป็นคำสั่งระดับสูงที่สรุปภาพรวมของ timer บนเครื่องหนึ่งเครื่อง
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.
- เพียงคำสั่งเดียวก็เห็นภาพรวมทั้งหมดของสิ่งที่ถูกตั้งให้รันตามตารางของ timer ได้
list-timers เป็นส่วนหนึ่งของชุดคำสั่งย่อยของ systemd ที่ใช้บ่อย
list-units ก็มีประโยชน์
list-paths เป็นคำสั่งย่อยที่เพิ่งถูกเพิ่มใน systemctl เมื่อไม่นานมานี้
ปลุกเครื่องจากสลีปเพื่อรันงาน
WakeSystem= สามารถทำให้ timer ที่ถึงเวลารันปลุกระบบขึ้นมาจากโหมดสลีปได้
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.
...
- ฟีเจอร์นี้มีประโยชน์เมื่อจำเป็นต้องรันสคริปต์สำคัญโดยไม่ต้องรอให้คนมาเปิดฝาโน้ตบุ๊ก
- บนดิสโทรที่รองรับการดาวน์โหลดแพ็กเกจอัปเดตก่อนใช้งาน เช่น Arch หรือ NixOS สามารถดึงแพ็กเกจอัปเดตไว้ล่วงหน้าตอนดึก แล้วมาติดตั้งตอนเช้าหน้าคีย์บอร์ดได้
- คู่มือระบุว่า หากต้องการให้เครื่องกลับเข้าสู่โหมดสลีปอีกครั้งหลัง
.service จบงาน จะต้องสั่งให้สลีปใหม่ด้วยตนเอง
กระจายเวลารันและลดปัญหา thundering herd
- ปัญหา thundering herd คือปัญหาระบบที่เกิดขึ้นเมื่อหลายโปรเซสตื่นขึ้นมาพร้อมกัน
- หากระบบ Debian ทั่วโลกถูกฮาร์ดโค้ดให้รัน
apt update ตอน 00:00:00 ทั้งหมด เที่ยงคืนก็จะกลายเป็นช่วงพีคทราฟฟิกที่แย่สำหรับทุกคน
FixedRandomDelay= และ RandomizedOffsetSec= ช่วยกระจายเวลารันได้
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. ...
- สามารถใช้การตั้งค่าเหล่านี้กับระบบจริงที่ต้องคอยตรวจสอบซอฟต์แวร์อัปเดตได้
- การกระจายเวลารันให้เป็นการแจกแจงแบบสม่ำเสมอช่วยลดปัญหา thundering herd ทำให้พฤติกรรมสม่ำเสมอขึ้น และช่วยเลี่ยงกิจกรรมรบกวนอย่างการรีสตาร์ต daemon ระหว่างกำลังประสานงานบริการแบบกระจาย
- โดยรวมแล้วตัวเลือกด้านเวลาเหล่านี้ปรับแต่งได้สูงมากและให้การควบคุมที่ละเอียด
ชดเชยงานที่พลาดไปทันที
Persistent= เหมาะอย่างยิ่งกับสคริปต์ตามกำหนดเวลาที่ไม่ควรถูกข้ามเพียงเพราะโน้ตบุ๊กอยู่ในโหมดสลีป แต่ก็ยังไม่จำเป็นต้องถึงขั้นใช้ 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. ...
- หากระบบที่ตั้งให้เช็กอินกับระบบจัดการคอนฟิกเกิด downtime การใส่
Persistent= ไว้ใน .timer ก็เพียงพอที่จะช่วยให้ระบบกลับเข้าสู่สถานะที่ถูกต้องทันทีหลังออนไลน์อีกครั้ง
- หากไม่มี
Persistent= ก็อาจต้องรอจนถึงเวลารันปกติครั้งถัดไปของ timer ซึ่งอาจนานมาก
- งานอื่นที่ไม่ควรรอเมื่อพบว่าพลาด activation ไปแล้ว ได้แก่ การอัปเดตระบบ การตรวจสอบงานแบตช์ เป็นต้น
ข้อควรระวังเวลาเขียน timer
- timer ในบริบท user manager ที่จัดการผ่าน
systemctl --user ก็ใช้งานได้เช่นกัน แต่ต้องระวัง target ที่ใส่ไว้ใน [Install]
- สำหรับบางดิสโทร target ที่เหมาะสมของ user timer อาจเป็น
default.target
- เช่นเดียวกับ
cron ข้อควรระวังทั่วไปเรื่องการรักษาเวลาในระบบให้แม่นยำยังคงใช้เหมือนเดิม
- ผู้ใช้ systemd สามารถตรวจสอบสถานะการซิงก์ได้ด้วย
timedatectl timesync-status
- โปรแกรมแก้ไขข้อความหลายตัวรองรับรูปแบบไฟล์ systemd unit มาให้โดยตรง ซึ่งช่วยได้มากเมื่อไฟล์ unit เริ่มใหญ่ขึ้น
- บน Emacs สามารถใช้แพ็กเกจ emacs systemd ได้
1 ความคิดเห็น
ความคิดเห็นจาก Lobste.rs
systemd ไม่ได้สมบูรณ์แบบ แต่รู้สึกว่าการตัดสินใจด้านการออกแบบหลายอย่างตั้งอยู่บนบทเรียนที่ได้จากวิธีแบบดั้งเดิมในอดีต
ไม่นานมานี้ฉันกลับไปฟังตอนของ CRE ปี 2015 ที่ Lennart Poettering อธิบายที่มาที่ไปเหล่านั้นอีกครั้ง และยังคงเป็นสิ่งที่แนะนำได้อยู่
โดยพื้นฐานแล้วฉันอยู่ฝั่งที่ไม่ชอบ systemd แบบเข้าเส้น แต่ก็ยังมองว่า systemd.timers เป็นหนึ่งในแนวคิดที่ “พอใช้ได้กว่า” ของผลิตภัณฑ์นี้
เพราะงั้นเลยค่อนข้างแปลกใจที่ผู้เขียนออกมาปกป้องมันด้วยท่าทีคล้ายดูแคลนคนที่มีข้อบ่นอย่างมีเหตุผล
ถึงอย่างนั้น การใช้ร่วมกับคำสั่ง
atก็ยังดีอยู่ สำหรับคำสั่งที่ต้องรันครั้งเดียวในเวลาที่กำหนดก็ใช้atนอกนั้นก็ใช้ systemd timer กับ unit file แบบเรียบง่ายสิ่งที่อยากให้ปรับปรุงที่สุดคือทำให้รู้ได้ว่า ผู้ใช้คนไหน กำลังรัน timer อยู่ ถึงฉันจะเป็นหนึ่งในไม่กี่คนที่ยังดูแล shell box อยู่ในปี 2026 แต่ถ้ารู้ได้ว่า timer ที่คอยกระแทกดิสก์ทุกวินาทีถูกสร้างโดยผู้ใช้คนไหนก็คงมีประโยชน์มาก
เท่าที่เข้าใจ ถ้าใช้
loginctl enable-lingerก็จะรันได้แม้ไม่มี active user session แน่นอนว่าก็คงมีกรณีใช้งานที่แค่นั้นยังไม่พอ แต่ฉันไม่รู้บริบทเฉพาะอยากให้ systemd timer มีภาระเริ่มต้นที่ต่ำกว่านี้ โดยเฉพาะฝั่ง การจัดการผู้ใช้
พอดูปริมาณการตั้งค่าที่ต้องมีแล้ว ก็ยากมากที่จะสู้
crontab -eได้จริง ๆฉันคิดอยู่นานว่าจะรวบรวมล็อกของ cron script อย่างเป็นระบบได้ยังไง ก่อนจะมานึกได้ว่าแค่ใช้ systemd timer ก็พอ
ปัญหาเรื่องล็อกก็หมดไป ตอนนี้เลยไม่มีเหตุผลจะกลับไปใช้ cron อีก และเสียดายที่ไม่น่ารู้ช้าขนาดนี้
loggerหรือ append ลงไฟล์ล็อกด้วย>>หรือจะปล่อยค่าเริ่มต้นไว้แล้วรับอีเมลก็ไม่ได้หรือ?จะเรียกว่าล้าสมัยก็ได้ แต่ฉันยังตั้งค่า อีเมล ที่ส่งมาถึงฉันจากเซิร์ฟเวอร์ไว้อยู่
ถ้าทำให้เป็นอัตโนมัติ มันก็ติดมาฟรีกับทุกโฮสต์ใหม่ และในชีวิตประจำวันก็สะดวกใช้ได้ทีเดียว
เช่น เปิด multiplexer ทิ้งไว้หนึ่งตัวแล้วรัน
long_running_process | mail root@localhost -s "done $?"จากนั้นก็ลืมมันไปได้เลยเป็นบทความที่ดี และฉันเองก็มีร่างบทความคล้าย ๆ กันอยู่ แต่ช่วงหลังต้องกลับมาอ้างอิงอีก
ถ้าคุณดำดิ่งเข้าไปในโพรงกระต่าย systemd แบบฉัน ขอแนะนำให้ทำ symbolic link ของ unit file และ timer ในโฟลเดอร์โปรเจ็กต์ที่เกี่ยวข้องไปไว้ที่
/etc/systemd/system/ข้อบ่นอย่างหนึ่งที่ฉันมีต่อ systemd คือมันไม่ช่วยแยก unit ที่ดิสโทรติดตั้งมาออกจาก unit ที่เราเขียนเอง แต่การใช้ symbolic link ทำให้เรารักษาการแยกนั้นไว้เองได้
unit ของระบบ/แพ็กเกจ/ดิสโทรจะอยู่ใน
/usr/lib/systemd/systemส่วน local override หรือ local unit จะอยู่ใน/etc/systemd/system