systemd-analyzeでLinuxサーバーの起動時間を半分に短縮した話

Linux tutorial - IT technology blog
Linux tutorial - IT technology blog

終わらないブートの正体

半年前、ずっと見て見ぬふりをしていた問題がありました。本番環境のUbuntu 22.04サーバー(RAM 4GB、Dockerコンテナ数個とNginxリバースプロキシを稼働中)のブートに40秒以上かかっていたのです。計画的なメンテナンス作業なら「まあ仕方ない」で済ませられますが、カーネルアップデート後の予期せぬ再起動が続くと、本当にリスクを感じるようになりました。

「古いハードウェアだとこういうもの」と思い込んでいました。ところがある日、同僚に「実際に何が遅いか調べたことある?」と聞かれたのです。調べたことはありませんでした。その一言が、サーバーメンテナンスに対する考え方を根本から変えました。

何を発見したか、原因は何だったか、そして40秒のブートを18秒に縮めるために使った具体的な手順を紹介します。重要なサービスは一切無効化していません。

ブートタイムラインを読む

systemd-analyzeから始めましょう。systemdに同梱されているので、インストール不要です。まずサマリーを確認します。

systemd-analyze

私のサーバーではこのような結果が返ってきました。

Startup finished in 3.102s (kernel) + 37.841s (userspace) = 40.943s
multi-user.target reached after 37.729s in userspace

カーネル部分はほぼ常に5秒以内です。興味深いことはすべてユーザースペースで起きています。カーネルからの引き継ぎ後にsystemdユニットが連鎖起動する部分です。37秒という数字を見て、何かがひどくおかしいと確信しました。

サービスごとの起動時間を詳しく調べるには次のコマンドを使います。

systemd-analyze blame

起動時間の長い順にすべてのサービスが一覧表示されます。上位の結果はこうなっていました。

34.201s apt-daily-upgrade.service
 8.320s snapd.service
 4.118s NetworkManager-wait-online.service
 2.903s mysql.service
 1.440s cloud-init.service
 0.891s docker.service
 0.703s ssh.service

一行を見るだけですべてがわかりました。apt-daily-upgrade.serviceだけで34秒を消費していたのです。

依存関係チェーンを可視化する

数値だけでは依存関係の全体像はわかりません。あるサービスが遅く見えても、実際には別の何かを待ってブロックされているだけというケースがあります。SVGプロットがその隙間を埋めてくれます。

systemd-analyze plot > boot-analysis.svg

ブラウザで開くと、すべてのユニットのガントチャートが表示されます。開始時刻、所要時間、各ユニットが何を待っていたかが一目でわかります。私のサーバーでは、NetworkManager-wait-online.serviceが本当に遅いのではなく、apt-daily-upgrade.serviceがロックを解放するのをただ待っていただけだとわかりました。

特定のユニットについてテキストベースで依存関係を素早くトレースするには次のコマンドを使います。

systemd-analyze critical-chain multi-user.target

デフォルトターゲットに至る最長の依存チェーンを見つけてくれます。私の環境ではこのような結果でした。

multi-user.target @37.729s
└─apt-daily-upgrade.service @3.528s +34.201s
  └─apt-daily.service @3.411s +0.089s
    └─network-online.target @3.380s +0.031s

チェーンは明確でした。apt-daily-upgrade.serviceを修正すれば、下流のすべてが速くなります。

根本原因:遅いユニットの3つのカテゴリ

過去半年間で十数台のサーバーを調査した結果、原因は一貫して3つのバケツに分類されることがわかりました。

1. ブート時に実行すべきでないサービス

自動パッケージアップグレード(apt-daily-upgrade.service)、snapの自動更新(snapd.service)、ベアメタルサーバー上のcloud-initはすべてここに該当します。これらには本来の役割があります。ただし、サーバーがオンラインになろうとしているブート時に実行すべきではありません。

2. ネットワークを待っているサービス

NetworkManager-wait-online.serviceは典型的な落とし穴です。ネットワークが完全に接続されるまでブート全体をブロックしますが、ほとんどのサービスが必要としているのはネットワークスタックが起動していることだけであり、インターネットへのルーティングが確立していることではありません。私のサーバーでは、これが4秒の無駄な待機を生んでいました。

3. 設定ミスまたは孤立したサービス

以前の開発者が監視エージェントをインストールし、その後削除したものの、systemdのユニットファイルを残したままにしていました。毎回のブートで起動を試み、失敗し、リトライし、最終的にタイムアウトしていたのです。毎回、静かに6秒を食い潰していました。

各カテゴリの修正方法

自動アップデートを遅延させる

apt-daily-upgrade.serviceに対する正しい対処は、停止ではなく遅延です。セキュリティアップデートは依然として重要です。ただし、ユーザーがサーバーの復帰を待っている間に実行させる必要はありません。

sudo systemctl edit apt-daily-upgrade.service

ブートから10分後に遅らせるオーバーライドを追加します。

[Unit]
After=multi-user.target

[Service]
ExecStartPre=/bin/sleep 600

または、タイマーを使って固定時刻にスケジュールする方法もあります。

sudo systemctl edit apt-daily-upgrade.timer
[Timer]
OnBootSec=15min
OnCalendar=
OnCalendar=03:00

私のサーバーでは、タイマーを午前3時に変更するだけで32秒のブート時間を取り戻せました。アップデートは今も実行されています。誰も見ていない時間帯に。

NetworkManager-wait-onlineを修正する

ブート時に完全にオンラインなネットワークを本当に必要としているサービスがあるか確認します。

systemd-analyze dot --require | grep network-online

出力に重要そうなものが見当たらなければ、待機を無効化しても安全です。

sudo systemctl disable NetworkManager-wait-online.service

起動時にインターネット接続が本当に必要なサービス(ConsulやリモートのSecretsストアから設定を取得するなど)がある場合はそのままにしてください。ただし、その依存関係が本当に必要なのか、それとも単に確認されていないデフォルトなのかを先に問い直してください。

孤立したユニットを削除する

静かに失敗しているものを見つけます。journalctlによるモダンなLinuxログ解析と組み合わせると、失敗の原因を素早く特定できます。

systemctl list-units --state=failed

もう使っていないソフトウェアに紐づいた失敗ユニットごとに以下を実行します。

sudo systemctl disable --now old-monitoring-agent.service
sudo rm /etc/systemd/system/old-monitoring-agent.service
sudo systemctl daemon-reload

snapdの対処

snapパッケージを使っていないなら、無効化よりも完全削除のほうがすっきりします。

# インストール済みのsnapを一覧表示する
snap list

# 不要なsnapを削除する
sudo snap remove --purge snap-store
sudo snap remove --purge core20

# snapd本体を削除する
sudo apt remove --purge snapd
sudo apt-mark hold snapd

apt-mark holdは他のパッケージアップグレード時にsnapdが再インストールされるのを防ぎます。snapを使っていないサーバーでは、これだけで6〜10秒を取り戻せます。

マスク・無効化・遅延の違い

この3つはよく混同されますが、間違った選択をすると深刻な問題を引き起こします。

  • 無効化(Disable) — 自動起動からサービスを除外しますが、手動起動や別のユニットからの依存関係による起動は引き続き可能です。
  • マスク(Mask) — 他のサービスが引き込もうとしても含めて、ユニットを完全にブロックします。そのマシンでは絶対に実行すべきでないと確信している場合にのみ使用してください。
  • 遅延(Delay・オーバーライド) — サービスは動かしつつ、実行タイミングをずらします。本来の価値はあるが実行タイミングが問題なメンテナンスタスクに最適な選択肢です。
# ユニットを無効化した場合に何が壊れるか確認する
systemctl list-dependencies --reverse apt-daily-upgrade.service

# 絶対に実行させたくないユニットをマスクする
sudo systemctl mask iscsid.service

# マスクを解除する
sudo systemctl unmask iscsid.service

何かを無効化する前には必ず逆依存チェックを実行してください。一度これをサボって、他の3つのサービスが依存しているサービスを無効化してしまい、次回ブート後にDockerネットワークが壊れたデバッグに1時間費やしました。

変更を確認する

変更のたびに再起動して計測します。

systemd-analyze
systemd-analyze blame | head -20

ベースラインと比較してください。この「計測→変更→計測」のループが、40秒から18秒へと縮めた方法そのものです。推測ではなく実測に基づいてサーバーを最適化する習慣が、一度の大がかりな変更ではなく、3ラウンドの的を絞った修正によって達成できた理由です。

すべての変更後の最終結果:

Startup finished in 3.091s (kernel) + 14.802s (userspace) = 17.893s
multi-user.target reached after 14.690s in userspace

systemd-analyze blameの上位は今や正当なものばかりです。mysql、docker、nginx。初期化に時間がかかるのは当然のサービスです。無駄はゼロです。

本当に大切なこと

ブート時間は単なる指標に過ぎません。systemd-analyzeで本当にやっていることは、ほとんどのシステム管理者が決して見ていないものを可視化することです。サーバーが最初の40秒間に何をしているか、そしてなぜそうなっているかを。

ブートが遅いのはほぼ常に積み重なった負債です。インストールされたまま忘れられたサービス、見直されることのないデフォルト設定、ヘッドレスサーバーで動き続けるデスクトップ向けの依存関係。数ヶ月に一度30分かけてブート監査を行うと大きな効果があります。リアルタイム監視ツールを導入すると、インシデント後の復旧が速くなり、謎の起動失敗が減り、マシンで実際に何が動いているかが明確になります。

覚えておくべき4つのコマンド:

  • systemd-analyze — 合計時間
  • systemd-analyze blame — ユニットごとの内訳
  • systemd-analyze critical-chain — 最長の依存パス
  • systemd-analyze plot — ビジュアルタイムライン

blameから始めましょう。最大の問題を見つけてください。そのサービス自体が遅いのか、それとも何かを待っているだけなのかを理解してください。そして選択します。遅延・無効化・マスク。数値が妥当に見えるまで繰り返す。複雑なことは何もありません。ただ、実際に見てみることが必要なだけです。

Share: