systemd Socket Activationを活用してLinuxサーバーのRAMを節約する方法

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

なぜ誰も使っていないのにサービスを動かし続けるのか?

アクセスが全くないのに、起動したばかりのVPSが500MBものRAMを消費していることに疑問を感じたことはありませんか?通常、サービスを常に利用可能な状態にするために systemctl enable --now を使用します。しかし、20〜30個の内部マイクロサービスや、たまにしかメッセージが来ないTelegramボットなどを動かしている場合、それらにリソースを24時間365日占有させるのは非常に大きな無駄です。

かつて RAM がわずか 2GB しかない古いサーバー群を管理していた頃、再起動のたびに数十のサービスがCPUを奪い合って初期化されるため、サーバーが5分間もフリーズし、頭を抱えていました。そこで systemd Socket Activation を導入したところ、待機時のRAM使用量を1.2GBから400MB未満に削減できました。重いサービスの大部分が「休止状態」にあるため、仮想マシンの起動もほぼ瞬時になりました。

基本的には、アプリケーションが自らポートを開く代わりに、systemdが「受付係」としてそのポートを監視します。パケットが届くと、systemdがアプリケーションを起動して接続を引き渡します。誰もアクセスしてこなければ、アプリケーションはRAM上に存在すらしないのです。

動作メカニズム:Systemdが「門番」になる時

この仕組みは伝説的な inetd を継承したものですが、より強力になっています。プロセスは非常にスマートです:

  • Systemdがソケット(TCP/UDP/Unix)を作成し、アプリケーションの代わりにリスニングします。
  • 接続があると、systemdはそのリクエストをキュー(バッファ)に保持します。
  • 対応するサービスが即座に起動されます。
  • Systemdはソケットの File Descriptor (FD) をアプリケーションに渡します。
  • アプリケーションが処理を引き継ぎます。クライアントは、自分が休止中のサービスを叩き起こしたことにすら気づきません。

実践:PythonアプリケーションでのSocket Activation設定

シンプルなPythonスクリプトでデモを行います。このアプリは、ポート9999に接続があった時だけ「目覚める」ようにします。

ステップ1:Socket Activation対応のコードを書く

アプリケーション側でポートを bind するのではなく、systemdから特定のファイルディスクリプタ経由でソケットを受け取るように少し修正する必要があります。

# 必要なライブラリのインストール
sudo apt update && sudo apt install python3-systemd -y

# /opt/my_app.py にファイルを作成
sudo nano /opt/my_app.py

/opt/my_app.py の内容:

import socket
from systemd import daemon

def main():
    # systemdからソケットを取得(通常はFD 3)
    fds = daemon.listen_fds()
    if not fds:
        print("エラー: systemdからソケットを受け取れませんでした!")
        return

    sock = socket.fromfd(fds[0], socket.AF_INET, socket.SOCK_STREAM)
    sock.setblocking(True)

    while True:
        conn, addr = sock.accept()
        with conn:
            print(f"接続元: {addr}")
            conn.sendall(b"こんにちは!サービスがリクエストを処理するために起動しました。\n")
            break # 処理終了後にサービスが終了することを示すデモのため、ループを抜ける

if __name__ == "__main__":
    main()

ステップ2:.socketファイルを作成する

ここでポートを定義します。/etc/systemd/system/my_app.socket に作成します:

[Unit]
Description=Pythonアプリ用のゲートウェイソケット

[Socket]
ListenStream=9999
Accept=no

[Install]
WantedBy=sockets.target

注意:Accept=no

Share: