5 ポイント 投稿者 GN⁺ 19 시간 전 | 1件のコメント | WhatsAppで共有
  • systemd タイマーcron の実務的な代替であり、スケジュールに応じて .service のようなユニットを実行し、履歴・出力・環境管理をより明確にしてくれる
  • 伝統的な cron は、あいまいな $PATH、失われやすい stdout/stderr、追跡しづらい実行履歴、読みにくいスケジュール構文が弱点
  • タイマーは同じステムの .timer.service を結び付け、OnCalendarOnBootSecOnUnitActiveSec で時刻・イベント基準の実行を表現する
  • systemd-analyze calendarsystemctl list-timers で時間表現と次回実行時刻を確認でき、WakeSystem= はスリープ状態からでも実行のために復帰させられる
  • RandomizedOffsetSecFixedRandomDelay= は同時実行のピークを減らし、Persistent= は非アクティブ中に取り逃した実行をオンライン直後に補完する

systemd タイマーを cron の代替として使う理由

  • cron job は実際の cron デーモンでなくても、「毎日これを実行」「毎月あれを実行」のように、スケジュールに従って処理を実行するコンピューティングの基本要素を指す言葉として広く使われている
  • systemd timer は特定のスケジュールに従って別のユニット、通常は .service を実行する systemd ユニットであり、伝統的な cron デーモンの機能的な代替になりうる
  • 伝統的な cron にはいくつか実務上の弱点がある
    • あいまいな $PATH 設定 のため、スクリプト実行結果の予測が難しい
    • stdoutstderr の出力がブラックホールに消えたり、ホストのメールシステムに送られたりしがち
    • 実行履歴を追跡・問い合わせしづらい
    • 01,31 04,05 1-15 1,6 * のようなスケジュール構文は人間にとって読みやすくも直感的でもない
  • systemd タイマーはこうした問題を減らしつつ、cron スタイルの表現に近いカレンダー設定も提供する

基本構造: サービスとタイマー

  • systemd タイマーには実行対象が必要で、.service ユニットは論理的にはスクリプトのように見なせる
  • 例として /etc/systemd/system/roulette.service に次のユニットを置くと、10分の1の確率でコンピューターをシャットダウンするサービスをインストールできる
[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
  • 条件が満たされない場合、ジャーナルにはより明確な文言が残る
May 05 11:05:32 diesel systemd[3117]: Condition check resulted in 1 in 10 chance to break your chains being skipped.
  • 一般に、systemd が提供するオプションを活用したほうが、直接スクリプトを書くより良い体験になる
    • OnFailure= はサービススクリプトの失敗に反応したいときに使える
    • Restart= は一時的な失敗からの復旧を試みたいときに使える

タイマーユニットの接続と実行

  • 同じファイルステムを持つ /etc/systemd/system/roulette.timer を置けば、roulette.service とタイマーを接続できる
[Unit]
Description=impending destruction

[Timer]
OnCalendar=10:00

[Install]
WantedBy=timers.target
  • デフォルトでは、タイマーの Unit= 設定 は同じステムに .service が付いたサービスユニットを選ぶ
    • この例では roulette.service が選ばれる
    • 別名のサービスユニットを実行したいなら Unit= を変更できる
  • ExecStart= の対象はデフォルトではシェルコマンドとして実行されない
    • 絶対パスの対象は、スクリプトそのものか、文字列引数としてスクリプトを受け取るインタープリターのように扱う必要がある
    • ExecStart=/usr/bin/echo Hello | /usr/bin/awk はこの文脈ではパイプに意味がないため動作しない
  • ExecStart= の引数はデフォルトでは、一部のシステムマネージャー既定値を除いて環境変数を継承しない
    • デフォルトの $PATH はほとんど空に近い
    • /usr/bin/env を実行すると、systemctl のような項目を使えるようにする簡単な安全策になる
    • ExecStart=/usr/bin/bash だけを書いた場合でも $PATH には既定の項目が入るが、env の使用は追加の安全策になる
  • タイマーなしでサービスを直接実行することもできる
systemctl start roulette
  • [Install] セクションのないサービスは enable できず、この構成ではタイマーがサービスを一貫して実行する標準的な方法になる
  • systemctl は明示的な接尾辞がなくても、デフォルトで roulette.service に対して動作する
  • .timer ユニットに systemctl start を適用するとタイマーを稼働状態にはするが、実際の Unit= 対象サービスを即座に実行はしない
systemctl start roulette.timer
  • status はタイマーが次にいつ実行されるかを表示する
systemctl status roulette.timer
Trigger: Sat 2026-04-18 10:00:00 MDT; 35min left
  • 最も単純な流れは、実行対象サービスを作成し、スケジュール付きのタイマーを同じ場所に置き、対象ではなくタイマーを起動するというもの
  • タイマーユニットの [Install]WantedBy= があれば、起動時にもタイマーを立ち上げられる
systemctl enable roulette.timer

時間表現: カレンダーイベントと期間

  • タイマーではスケジュール表現の方式が重要で、繰り返しの時間区間と、カレンダーイベントまたはタイムスタンプを区別する必要がある
  • systemd.time(7) マニュアルは例が十分にあり、タイマー作成時の最初の参考資料として使える
  • 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 タイマーは、繰り返しの壁時計ベースの時刻だけでなく、伝統的な 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 で前提を検証できる

イベント基準の実行がより適している場合

  • 実際の作業では、「毎日同じ時刻に実行する」よりも「別のイベントの後に実行する」ほうが適していることが多い
  • 一時ディレクトリを空にする作業は、起動直後に cron 式の時刻が過ぎていたなら /tmp に片付けるものがほとんどないかもしれない
  • 「コンピューターが起動してから1時間後に実行し、その後は毎時間実行する」と表現したほうが、サービスの実際の動作とスケジュールの論理によく合う
[Timer]
OnBootSec=1h
OnUnitActiveSec=1h
  • OnBootSec=1h は、マシン起動の1時間後に一度実行することを意味する
  • OnUnitActiveSec=1h は、Unit= が実行されてから1時間後に再度実行することを意味し、タイマーを暗黙的に繰り返し動作させる
  • このような周期的な期間表現は、「毎時間この分に実行する」といった表現よりも、「ときどき1回ずつ実行する」用途に合うことが多い
  • Advent of Code API をポーリングする Slack ボットの例では、*/15 の cron 式は API の「15分ごと」というポリシーを守るが、全員が同じようにポーリングするとトラフィックが集中しうる
  • コード修正後にタイマーを起動し、以後15分経つたびに実行されるようにすれば、必要な動作を満たしつつ thundering herd 問題を減らせる可能性がある

タイマーの状態をひと目で確認する

  • systemctl list-timers は、1台のマシンにおけるタイマーの状況を要約して表示する高レベルコマンド
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.
  • 1つのコマンドだけで、タイマースケジュールに従って実行される項目の全体像を把握できる
  • list-timers は、よく使われる systemd のサブコマンド群の一部
    • list-units も有用
    • list-pathssystemctl に比較的新しく追加されたサブコマンド

スリープ状態から復帰して実行する

  • WakeSystem= は、時間が来たタイマーによってシステムをスリープ状態から復帰させられる
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.
...
  • この機能は、人がノートPCのふたを開ける物理的な操作をしなくても重要なスクリプトを実行する必要がある場合に便利
  • Arch や NixOS のように、使用前にパッケージ更新のダウンロードをサポートするディストリビューションでは、深夜に更新パッケージを先に取得しておき、朝にキーボードの前で更新できる
  • .service が終了した後に再びスリープ状態へ戻すには、手動で再度スリープ処理を行う必要があるとマニュアルにある

実行時刻の分散と thundering herd の緩和

  • thundering herd 問題は、多数のプロセスが同時に目覚めるときに発生するシステム上の問題
  • もし世界中の Debian システムがすべて 00:00:00apt update するようハードコードされていたら、深夜0時は誰にとっても好ましくないトラフィックピークになる
  • 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 問題を減らし、動作を一貫させ、分散サービスを調整中のデーモン再起動のような妨害要因を避ける助けになる
  • タイミングオプションは全体として非常に設定可能で、細かな制御を提供する

取り逃した実行を即座に補完する

  • Persistent= は、スリープ中のノートPCのために飛ばしてはいけないが、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. ...
  • 構成管理チェックインを予定していたシステムがダウンタイムを経験したなら、.timerPersistent= を置くだけで、オンライン直後に正しい状態へ収束できる
  • Persistent= がなければ、タイマーの通常実行時刻まで待つ必要があり、その時間が長くなることもある
  • 取り逃したアクティベーションを検知したときに待つべきでない他の作業として、システム更新、バッチ処理の確認などがある

タイマー作成時の注意点

  • systemctl --user で扱う ユーザーマネージャー 文脈のタイマーも有効だが、[Install] に書くターゲットには注意が必要
  • ディストリビューションによっては、ユーザータイマーに適したターゲットは default.target の場合がある
  • cron と同様、正確なシステムクロックを維持する必要があるという一般的な注意点はそのまま当てはまる
  • systemd ユーザーは timedatectl timesync-status で同期状態を確認できる
  • 多くのエディターは systemd ユニットファイル形式を標準サポートしており、ユニットファイルが大きくなってくると助けになる
  • Emacs では emacs systemd パッケージを利用できる

1件のコメント

 
Lobste.rsの意見
  • systemdは完璧ではないが、多くの設計判断は、より伝統的な過去のやり方から得た学びに基づいているように感じる
    最近、Lennart Poetteringがその背景を説明したCREの2015年のエピソードを聴き直したが、今でもおすすめできる

  • 根っからのsystemd嫌いではあるが、systemd.timersはこの製品の中では「まだましな」概念の一つだと思う
    だからこそ、筆者がもっともな不満を持つ人たちを見下すような形で擁護していたのは少し意外だった
    それでもatコマンドと併用するのは良い。特定時刻に一度だけ実行するコマンドはat、それ以外はsystemdタイマーと単純なユニットファイルを使う、という運用だ
    いちばん改善してほしいのは、どのユーザーがタイマーを実行しているのか分かるようにすることだ。2026年にシェルボックスを運営している数少ない人間の一人ではあるが、毎秒ディスクを叩くタイマーをどのユーザーが作ったのか分かると便利だ

    • この目的なら、全員にシステムタイマーをインストールさせる代わりにユーザーユニットを使えるのではないかと思う
      loginctl enable-lingerを使えば、アクティブなユーザーセッションがなくても実行できるはずだ。もちろん、それでは足りないユースケースもあるだろうし、具体的な状況は分からない
  • systemdタイマーは、特にユーザー管理の面で初期負担がもっと低いとよいと思う
    必要な設定量を見ると、crontab -eに勝つのは本当に難しい

    • タイマー一つ設定するのに複数の設定ファイルやサービスを要求するsystemd方式は… APIの選択としては話にならないレベルだ
  • cronスクリプトのログを体系的に集める方法を長いこと考えていたが、単にsystemdタイマーを使えばいいのだと気づいた
    ロギングの問題が解決する。もうcronを使い直す理由はなく、もっと早く知っていればよかった

    • loggerにパイプするか、ログファイルに>>で追記するか、デフォルトのままメールを受け取ればいいのでは?
  • 古臭いと言われてもいいが、サーバーから自分に届くメールは今でも設定してある
    自動化しておけば新しいホストごとにおまけで付いてくるし、普段からかなり便利だ
    例えば、マルチプレクサを一つ開いてlong_running_process | mail root@localhost -s "done $?"を実行して、そのまま忘れておく、といった使い方だ

  • 良い記事で、私も似たような記事の下書きを持っていたが、最近また参照する必要があった
    私のようにsystemdのウサギ穴に入り込むなら、関連プロジェクトのフォルダーにあるユニットファイルやタイマーを/etc/systemd/system/シンボリックリンクしておくのをおすすめする
    systemdへの不満の一つは、ディストリビューションがインストールしたユニットと自分で書いたユニットを区別してくれないことだが、シンボリックリンクを使えばその分離を自分で維持できる

    • 実際には、その区別はパスが担っている
      システム/パッケージ/ディストリビューションのユニットは/usr/lib/systemd/systemに入り、ローカルオーバーライドやローカルユニットは/etc/systemd/systemに入る