Systemd Socket Activation:Linuxの起動時間とリソースを最適化するオンデマンドサービス起動

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

クイックスタート:5分でSocket Activationを始める

socket activationは従来のモデルを逆転させます。起動時にサービスを開始してアイドル状態にしておく代わりに、systemdがソケットを保持し、クライアントが実際に接続したときだけサービスを起動します。クライアントの視点からは何も変わりません――直前までサービスが動いていなくても、接続はそのまま通ります。

最小限の動作例を示します。次の2つのファイルを作成してください:

/etc/systemd/system/myapp.socket:

[Unit]
Description=MyApp ソケット

[Socket]
ListenStream=8080

[Install]
WantedBy=sockets.target

/etc/systemd/system/myapp.service:

[Unit]
Description=MyApp サービス

[Service]
ExecStart=/usr/local/bin/myapp
StandardInput=socket

ソケットユニット(サービスではなく)を有効化してテストします:

sudo systemctl daemon-reload
sudo systemctl enable --now myapp.socket
curl http://localhost:8080/

最初のcurlでサービスが起動します。何が起きたか確認しましょう:

systemctl status myapp.socket
systemctl status myapp.service

これが基本パターンです。あとは設定の詳細です。

詳細解説:Socket Activationの仕組み

ファイルディスクリプタの受け渡し

クライアントが接続すると、カーネルは接続を切らず、systemdがバッファに保持します。その後、対応する.serviceユニットを起動し、開いているファイルディスクリプタを環境変数LISTEN_FDSLISTEN_PIDLISTEN_FDNAMES経由で新プロセスに渡します。

アプリケーションはあらかじめバインドされたソケットを受け取るため、自分でbind()listen()を呼ぶ必要はありません。libsystemdsd_listen_fds() APIに対応したライブラリであれば、これを自動で処理します。Pythonのsystemdバインディング、Goのcoreos/go-systemdなど多くのフレームワークがネイティブに対応しています。

実際に使うソケットタイプ

[Socket]セクションでは複数のリスナータイプをサポートしています:

  • ListenStream — TCPまたはUnixストリームソケット(最も一般的)
  • ListenDatagram — UDPまたはUnixデータグラム
  • ListenSequentialPacket — SOCK_SEQPACKET、メッセージ境界を保持
  • ListenFIFO — 名前付きパイプ

ネットワークサービスの場合、ListenStream=8080はポート8080のTCPを意味します。プロセス間通信には絶対パスを使います:ListenStream=/run/myapp/myapp.sock

Acceptモード:シングルインスタンス対inetdスタイル

Accept=ディレクティブは接続のディスパッチ方法を制御します:

  • Accept=no(デフォルト)— systemdはリスニングソケットを1つのサービスインスタンスに渡します。そのインスタンスがすべての受信接続を自ら処理します。モダンなアプリケーションはこれを前提としています。
  • Accept=yes — systemdは受信接続ごとに新しいサービスインスタンスを生成する、クラシックなinetdスタイルです。各インスタンスはリスニングソケットではなく、接続済みソケットを受け取ります。シンプルなリクエスト/レスポンスプロトコルやレガシースクリプトに便利です。

便利なSocket Unitオプション

[Socket]
ListenStream=8080
SocketUser=myapp          # Unixソケットファイルのchown
SocketMode=0660           # chmodのパーミッション
Backlog=128               # listen()のバックログ深度
ReusePort=yes             # マルチプロセスの負荷分散用SO_REUSEPORT
KeepAlive=yes             # TCPキープアライブを有効化
NoDelay=yes               # TCP_NODELAY(Nagleアルゴリズムを無効化)
SocketLinger=0            # クローズ時のlinger時間

高度な使い方

1つのサービスに複数のソケット

サービスは複数のソケットを同時にリッスンできます――ポート80のHTTP、443のHTTPS、ローカル管理コマンド用のUnixソケットなど。すべてのファイルディスクリプタは同じサービス起動時に渡されます:

[Socket]
ListenStream=80
FileDescriptorName=http
ListenStream=443
FileDescriptorName=https
ListenStream=/run/webserver/control.sock
FileDescriptorName=control

[Install]
WantedBy=sockets.target

アプリケーションはLISTEN_FDSをイテレートし、LISTEN_FDNAMESを使ってどのソケットがどれかを特定します――推測は不要です。

既存デーモンへのSocket Activation適用

systemdを考慮せずに作られたデーモンでも恩恵を受けられます。サービス再起動中、systemdは受信接続をバッファリングするため、クライアントは接続拒否されずに待機できます。どんなTCPサービスでもゼロダウンタイム再起動がずっと簡単になります。socket activationに対応させるPython Flaskアプリの例:

import socket
import os
from flask import Flask

app = Flask(__name__)

@app.route("/")
def index():
    return "socket-activated Flaskからこんにちは!\n"

if __name__ == "__main__":
    listen_fds = int(os.environ.get("LISTEN_FDS", 0))
    if listen_fds == 1:
        # Systemdは事前バインド済みソケットをfd 3として渡す(fdは3から始まる。stdin/stdout/stderrの後)
        sock = socket.fromfd(3, socket.AF_INET, socket.SOCK_STREAM)
        sock.setblocking(True)  # Flaskの開発サーバーはブロッキングモードが必要
        app.run(debug=False, fd=sock.fileno())
    else:
        app.run(host="0.0.0.0", port=8080)

マルチテナント構成向けインスタンス化ソケットサービス

socket activationはsystemdテンプレートユニット(@構文)と自然に組み合わせて使え、ユーザー・テナント・名前空間ごとに独立したインスタンスを立ち上げられます:

# /etc/systemd/system/[email protected]
[Unit]
Description=%i 用 MyApp ソケット

[Socket]
ListenStream=/run/myapp/%i.sock

[Install]
WantedBy=sockets.target
sudo systemctl enable --now [email protected]
sudo systemctl enable --now [email protected]

各ユーザーは専用のUnixソケットと独立したサービスインスタンスを持ち、オンデマンドで起動します。インスタンスは必要な間だけ存在します。

Systemdセキュリティ強化との組み合わせ

socket-activatedなサービスは新鮮な独立した環境で起動するため、前回の実行からの状態が残りません。これによりサンドボックスの設計が格段に考えやすくなります。[Service]セクションにこれらのディレクティブを追加しましょう:

[Service]
DynamicUser=yes          # 一時的なUID、残留ファイルなし
PrivateTmp=yes           # 呼び出しごとに隔離された/tmp
ProtectSystem=strict     # システムパスは読み取り専用
ProtectHome=yes          # /homeへのアクセス不可
NoNewPrivileges=yes      # 特権昇格をブロック

オンデマンド起動と厳格なサンドボックスの組み合わせにより、サービスは必要な時だけ存在し、動作中でも最小限のフットプリントを保ちます。

3年間のVPS管理から得た実践的なヒント

本番環境に触れる前に必ずテストする

3年間で10台以上のLinux VPSを管理してきた経験から、本番環境に変更を適用する前には必ず徹底的にテストすることを学びました。socket activationの障害モードは微妙です――Accept=モードの設定ミス、StandardInput=socketの記述漏れ、起動時のレースコンディションなどが、実負荷下では再現困難なサイレントな接続断を引き起こす可能性があります。私の標準チェックリスト:

  1. 接続前にソケットがアクティブであることを確認:systemctl status myapp.socket
  2. サービスがまだ起動していないことを確認:systemctl status myapp.service
  3. 最初の接続を行い、サービスが起動するのを確認:curl localhost:8080/ && systemctl status myapp.service
  4. 最初の接続時にジャーナルをtailして起動エラーを捕捉:journalctl -u myapp.service -f
  5. クラッシュをシミュレートしてソケットが生き残ることを確認:systemctl kill myapp.serviceを実行し、再接続する

ローカルテストにはsystemd-socket-activateを使う

アプリがfdの受け渡しを正しく処理するか確認するだけなら、/etc/systemd/system/に触れる必要はありません。systemd-socket-activateユーティリティがターミナルからプロセス全体をシミュレートします:

# Accept=no(デフォルトモード)をシミュレート
systemd-socket-activate -l 8080 -- /usr/local/bin/myapp

# Accept=yes(inetdモード)をシミュレート
systemd-socket-activate -l 8080 --inetd -- /usr/local/bin/myapp

この方法でアプリを起動してcurlでリクエストを送れば、ユニットファイルを1行も書く前に、fdハンドリングのコードが正しく動くかどうかすぐに分かります。

使わないべきケースを知る

socket activationはタダではありません――バッファ、プロセス生成、fdの受け渡しのオーバーヘッドがあります。常時トラフィックがあるサービス(データベース、リバースプロキシ)では常時起動が勝ります。以下の場合に使いましょう:

  • 使用頻度の低いサービス:管理API、メンテナンスエンドポイント、デバッグインターフェース
  • 起動が遅く、そのまま起動プロセスをブロックしてしまうサービス
  • 使用間隔が数時間に及ぶテナント分離インスタンス
  • アクティブに必要な時だけサービスを起動したい開発環境

システム上のSocket-Activatedサービスを確認する

systemctl list-sockets --all

マシン上のすべてのソケットユニットがここに表示されます――リスンアドレスと、対応するサービスが現在動いているかどうか。見慣れないサーバーを調査する際、このコマンドを最初に実行すれば、オンデマンドサービスの全体像が数秒で把握できます。

Share: