Systemdタイマーガイド:信頼性の高いスケジューリングのためのCronジョブの代替

ジャーナルログ、未実行処理、依存関係、リソース制御が必要な場合は、cronの代わりにsystemdタイマーを使用してください。

Systemdタイマーガイド:信頼性の高いスケジューリングのためのCronジョブの代替

cronは多くのジョブで依然として有効です。毎晩シェルスクリプトを実行し、既に動作しているログリダイレクションがある場合、それを置き換えるメリットはありません。多くのチームがスケジュールされたLinuxの作業をsystemdタイマーに移行する理由は、流行ではありません。それは、systemdタイマーがジョブに実際のサービスユニット、予測可能なログ、依存関係の処理、未実行時の動作、リソース制限を提供するからです。

これは、タスクが単に「コマンドを実行する」だけではない場合に重要です。バックアップジョブにはマウントされたディスクが必要な場合があります。キャッシュウォーマーにはネットワークが利用可能である必要がある場合があります。レポートエクスポーターは、ロックダウンされたサービスアカウントとして実行され、次のオンコール担当者が読めるログを残す必要がある場合があります。systemdタイマーを使用すると、これらのニーズをサービスのライフサイクルの他の部分を管理するのと同じ場所で記述できます。

Systemdタイマーの理解

systemdタイマーは、他のsystemdユニット(通常はserviceユニット)がいつアクティブ化されるかを制御するsystemdユニットファイルです。スタンドアロンのデーモンであるcronとは異なり、systemdタイマーはsystemd initシステムの不可欠な部分です。この深い統合により、特に信頼性、ロギング、リソース管理に関して、いくつかの重要な利点がもたらされます。

systemdタイマーは常に別のユニット(最も一般的にはserviceユニット)と連携して動作します。.timerファイルはイベントが発生するタイミングを定義し、対応する.serviceファイルはそのイベントがトリガーされたときに実行されるアクションを定義します。この明確な関心の分離により、systemdタイマーは非常にモジュール化され、柔軟性が高くなります。

Cronに対するSystemdタイマーの主な利点

cronは機能的ですが、systemdタイマーはその制限の多くに対処し、より堅牢で機能豊富なスケジューリングソリューションを提供します。

  • 信頼性と永続性: カレンダータイマーがPersistent=trueを使用していて、スケジュールされた実行中にシステムの電源がオフになった場合、systemdは実行が missed されたことを記録し、次回の起動後に関連するサービスを開始します。通常のcronは、anacronなどの別のツールがない限り、追いつきません。
  • systemdとの統合: タイマーは、systemdの強力なロギング(journalctl経由)、依存関係管理、リソース制御(cgroups)の恩恵を受けます。これにより、監視の向上、エラー報告の明確化、スケジュールされたタスクの複雑な起動シーケンスやリソース制限の定義が可能になります。
  • 再現性とバージョン管理: systemdユニットファイルはプレーンテキストファイルであり、バージョン管理システムに簡単に保存できます。これにより、再現可能なデプロイと、複数のシステムにわたるスケジュールされたタスクの変更の追跡が容易になります。
  • イベントベースのスケジューリング: 単純な時間ベースのスケジューリングを超えて、systemdタイマーはシステム起動時(OnBootSec)またはユニットの最後のアクティブ化後(OnUnitActiveSec)に相対的にトリガーでき、より動的なスケジューリングオプションを提供します。
  • 柔軟な時間表現: systemdは、cronの構文よりも読みやすく多用途であることが多い、豊富なカレンダーイベント式を提供します。これには、毎時、毎日、毎週、特定の日付/時刻が含まれます。
  • リソース管理と依存関係: タイマーによって起動されるsystemdサービスは、cgroup設定を含むsystemd環境を継承し、他のsystemdユニットへの依存関係を宣言できます(例:実行前にネットワークやデータベースが利用可能になるのを待つ)。
  • 標準出力/エラー処理: systemdは、タイマーによって起動されたサービスのstdoutstderrを自動的にキャプチャし、システムジャーナルに送信するため、cronのメールベースの出力や手動リダイレクションよりもデバッグと監査がはるかに簡単になります。

Systemdタイマーの設定

systemdタイマーを設定するには、サービスユニット(.service)とタイマーユニット(.timer)の2つのユニットファイルを作成する必要があります。これらのファイルは通常、システム全体のタイマーの場合は/etc/systemd/system/に、ユーザー固有のタイマーの場合は~/.config/systemd/user/に配置されます。

1. サービスユニット(.serviceファイル)

サービスユニットは、実行する実際のコマンドまたはスクリプトを定義します。これは標準のsystemdサービスファイルですが、多くの場合、非対話的に実行され、特定のタスクを実行するように設計されています。

例: /etc/systemd/system/mytask.service

[Unit]
Description=マイスケジュールタスクサービス

[Service]
Type=oneshot
ExecStart=/usr/local/bin/mytask.sh
User=myuser
Group=mygroup
# オプション: 新しいsystemdリリースでのリソース制限
# CPUWeight=50
# MemoryMax=1G

[Install]
WantedBy=multi-user.target

説明:

  • [Unit]: ユニットに関する一般的な情報が含まれます。
    • Description: 人間が読める説明。
  • [Service]: サービス固有の設定を定義します。
    • Type=oneshot: サービスが単一のコマンドを実行して終了することを示します。これはスケジュールされたタスクで一般的です。
    • ExecStart: 実行するコマンドまたはスクリプト。完全なパスを指定します。
    • User, Group: コマンドを実行するユーザーとグループを定義します。タスクは常に必要最小限の権限で実行してください。
    • CPUWeight, MemoryMax: オプションのcgroup制御。スケジュールされたジョブがホストの他の部分を枯渇させないようにする場合に便利です。
  • [Install]: ユニットを有効にする方法を定義します。
    • WantedBy=multi-user.target: 存在しますが、このセクションはタイマーによってトリガーされるサービスではあまり重要ではありません。タイマーユニット自体が通常アクティブ化を決定するためです。ただし、サービスを手動でアクティブ化できるようにしたり、他のsystemdターゲットに統合したりする場合に役立ちます。

2. タイマーユニット(.timerファイル)

タイマーユニットは、対応するサービスユニットをいつアクティブ化するかを定義します。サービスユニットと同じ名前である必要があります(例:mytask.serviceの場合はmytask.timer)。

例: /etc/systemd/system/mytask.timer

[Unit]
Description=mytask.serviceを毎日実行

[Timer]
OnCalendar=daily
Persistent=true
RandomizedDelaySec=600
AccuracySec=1min

[Install]
WantedBy=timers.target

説明:

  • [Unit]: 一般的な情報。
    • Description: タイマーの説明。
  • [Timer]: タイマー固有の設定を定義します。
    • OnCalendar: 最も一般的な設定で、カレンダーイベントを定義します。次のような式を使用します。
      • daily: 毎日深夜0時。
      • weekly: 毎週月曜日深夜0時。
      • monthly: 毎月1日深夜0時。
      • hourly: 毎時0分。
      • *-*-* 03:00:00: 毎日午前3時。
      • Mon..Fri 08:00..17:00: 平日の午前8時から午後5時まで。
      • Mon *-*-* 03:00:00: 毎週月曜日の午前3時。
    • OnBootSec: システム起動から指定された時間後にサービスをアクティブ化します。例:OnBootSec=10min
    • OnUnitActiveSec: サービスの最後のアクティブ化から指定された時間後にサービスをアクティブ化します。例:前回の実行が完了してから1時間後に実行する場合はOnUnitActiveSec=1h
    • Persistent=true: 信頼性にとって重要です。スケジュールされた実行中にシステムがオフになっている場合、サービスは次回の起動後すぐにトリガーされます。
    • RandomizedDelaySec=600: 最大600秒のランダムな遅延を追加します。これは、多くのマシンが同じタイマーを共有していて、すべてのホストがデータベース、API、またはバックアップサーバーに正確に同じ秒にアクセスするのを防ぎたい場合に便利です。

実際のCronからタイマーへの移行

現在、次のようなroot cronエントリがあるとします。

15 2 * * * /usr/local/sbin/backup-app.sh >> /var/log/backup-app.log 2>&1

これは静かなマシンでは機能しますが、通常の弱点があります。バックアップディスクがマウントされていない場合、スクリプトは途中で失敗する可能性があります。サーバーが午前2時15分にオフの場合、実行はスキップされます。スクリプトが有用なエラーを書き込んだ場合、誰かがどのカスタムログファイルを確認するかを覚えておく必要があります。スクリプトがメモリを使いすぎた場合、cronはそれを抑制するのに役立ちません。

systemdバージョンは、コマンドをスケジュールから分離します。

# /etc/systemd/system/backup-app.service
[Unit]
Description=アプリケーションデータのバックアップ
RequiresMountsFor=/mnt/backups
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
User=backup
Group=backup
WorkingDirectory=/srv/app
ExecStart=/usr/local/sbin/backup-app.sh
MemoryMax=1G
CPUWeight=40
Nice=10
# /etc/systemd/system/backup-app.timer
[Unit]
Description=毎晩アプリケーションバックアップを実行

[Timer]
OnCalendar=*-*-* 02:15:00
Persistent=true
RandomizedDelaySec=15min
AccuracySec=1min
Unit=backup-app.service

[Install]
WantedBy=timers.target

注目すべき詳細がいくつかあります。RequiresMountsFor=/mnt/backupsは、サービスが開始される前にパスがマウントされている必要があることをsystemdに伝えます。After=network-online.targetWants=network-online.targetは、ネットワークマネージャーが実際にwait-onlineサービスを提供している場合にのみ役立ちます。多くのディストリビューションでは、そのサービスはデフォルトで無効になっています。バックアップがローカルディスクにのみ書き込む場合は、ネットワーク依存関係を省略してください。

Type=oneshotは、処理を実行して終了するスクリプトに適しています。実行し続けるデーモンには使用しないでください。WorkingDirectory=は、特定のディレクトリのシェルから起動されることに誤って依存するスクリプトを防ぎます。User=backupは、ジョブをrootとして実行し、スクリプト内のすべてのコマンドが注意深いことを期待するよりも通常は優れています。

ファイルを保存した後:

sudo systemctl daemon-reload
sudo systemctl enable --now backup-app.timer
systemctl list-timers backup-app.timer

ジョブをすぐにテストするには、タイマーではなくサービスを開始します。

sudo systemctl start backup-app.service
journalctl -u backup-app.service -n 100 --no-pager

この1つの区別により、多くの混乱を防ぐことができます。backup-app.timerを開始すると、スケジュールが設定されます。backup-app.serviceを開始すると、実際のバックアップが実行されます。

適切なタイマー式の選択

OnCalendar=はcron構文に最も近い代替ですが、読み方が異なります。配布する前に、systemdが式をどのように解釈するかを確認できます。

systemd-analyze calendar 'Mon..Fri 03:30'
systemd-analyze calendar '*-*-01 04:00:00'
systemd-analyze calendar 'Sun *-*-* 23:00:00'

壁時計の作業にはカレンダータイマーを使用します。毎晩のバックアップ、毎週のレポート、毎月のクリーンアップ、証明書のチェック、および人間のカレンダーが重要なその他のタスクです。「何かが起こった後に実行する」動作には単調タイマーを使用します。

[Timer]
OnBootSec=10min
OnUnitActiveSec=1h

このパターンは、起動後10分でサービスを開始し、その後最後のアクティブ化から1時間後に再度開始します。これは、ポーリング、ローカルクリーンアップ、軽量メンテナンスループに適しています。これは「毎時0分に」と同じではありません。ジョブに12分かかる場合、次の実行は壁時計の期待ではなく、アクティブ化のタイミングからカウントされます。

また、オーバーラップについても考慮してください。通常のサービスユニットの場合、systemdは次のタイマーイベントが到着したという理由だけで、同じアクティブなユニットの2番目のコピーを開始しません。ジョブが間隔より長く実行される可能性がある場合は、それが許容できるかどうかを判断してください。場合によっては、スクリプト内のロック(flockなど)が正しい答えです。これは、「前回の実行がまだアクティブです」という明確なメッセージを生成できるためです。場合によっては、間隔を長くすることが正しい答えです。

時間を節約する運用習慣

タイマービューが最初のダッシュボードです。

systemctl list-timers --all

最後の実行、次の実行、各タイマーがアクティブ化するユニットが表示されます。タイマーがリストされているがサービスが実行されない場合は、カレンダー式とタイマーが有効になっているかどうかを確認してください。サービスが実行されて失敗した場合は、タイマーをしばらく無視してサービスを調査します。

systemctl status backup-app.service
journalctl -u backup-app.service --since today

いずれかのユニットファイルを編集したら、次を実行します。

sudo systemctl daemon-reload
sudo systemctl restart backup-app.timer

スケジュール変更後にタイマーを再起動することは、次のアクティブ化時間をすぐに更新するため、良い習慣です。スクリプト自体のみを変更した場合は、通常daemon-reloadは必要ありません。

ユーザータイマーの場合は、systemctl --userを使用し、ユニットを~/.config/systemd/user/に配置します。これらは開発者ワークステーションやユーザーごとの自動化に役立ちますが、1つの重要な注意点があります。デフォルトでは、ユーザーサービスはユーザーのログインセッションに結び付けられています。ログアウト後もユーザータイマーを実行し続ける必要がある場合は、loginctl enable-linger usernameで lingering を有効にします。これは意図的な管理上の選択であり、魔法の修正として記事内に隠すべきものではありません。

Cronが依然として優れたツールである場合

すべてを盲目的に移行しないでください。Cronは、特にsystemdがPID 1ではない古いサーバーや最小限のコンテナでは、小さなユーザーローカルタスクに対して読みやすいです。唯一の要件が「この無害なコマンドを5分ごとに実行する」ことである場合、cronが最も明確な答えかもしれません。

Systemdタイマーは、ジョブにサービスのようなニーズ(制御されたID、ジャーナル内のログ、リソース制限、依存関係、キャッチアップ動作、またはユニットファイルによる標準デプロイ)がある場合に効果を発揮します。実際には、スケジュールされたタスクが失敗した場合に誰かを起こすような場合にタイマーを使用します。追加のユニットファイルは、次のオペレーターに「何が実行されたか?」から「何が失敗したか?」から「何が変更されたか?」への直接的なパスを提供する場合に価値があります。

移行中に採用する価値のある最後の習慣は、タイマーが数回正常に実行されるまで古いcronエントリをコメントアウトして近くに保持し、その後削除することです。重複したスケジュールは、静かな損害の原因です。2つのバックアップジョブが同じロックを競合し、2つのクリーンアップジョブが予想よりも早くファイルを削除し、2つのレポートジョブが重複したメールを送信する可能性があります。タイマーを有効にした後、systemctl list-timers --allを確認し、サービスのジャーナルを確認し、古いcronパスがアクティブでなくなったことを確認してください。