systemd socket activation:让服务按需启动(替代 inetd)

起因

我有一台小 VPS 跑十几个偶尔被用的服务(小工具 API、自建图床、内部
Wiki 之类)。每个 24x7 常驻进程都吃 50-200 MB 内存。
"按需启动 + 闲置自动停"能省一大半内存。

xinetd 是老古董做法;systemd 的 socket activation 是现代等价物 +
完全集成系统单元 + 更灵活。

工作原理

  1. systemd 启动时只创建 listening socket(端口),不启服务进程
  2. 第一个请求到达端口时,systemd 启动服务进程并把 socket fd 传给它
  3. 服务正常工作,处理后续请求
  4. 闲置一段时间后服务可以 exit,systemd 重新只监听端口
  5. 下次请求重复 1-4

解决方案

例子:按需启动一个 Python 小服务

my-tool.service

# /etc/systemd/system/my-tool.service
[Unit]
Description=My Tool API
After=network.target my-tool.socket
Requires=my-tool.socket

[Service]
Type=notify           # 配合 sd_notify,启动好通知 systemd
User=trio
Group=trio
ExecStart=/srv/my-tool/.venv/bin/python /srv/my-tool/server.py
StandardInput=socket  # systemd 把 socket fd 传给进程
Restart=on-failure

# 闲置 5 分钟自动退出
RuntimeMaxSec=infinity

[Install]
WantedBy=multi-user.target

my-tool.socket

# /etc/systemd/system/my-tool.socket
[Unit]
Description=My Tool socket

[Socket]
ListenStream=8088
Accept=no             # systemd 不 accept,把 listening socket 传给服务
# 闲置多久关:
NoDelay=true

[Install]
WantedBy=sockets.target
sudo systemctl daemon-reload

# 只 enable + start socket,不启 service
sudo systemctl enable --now my-tool.socket
sudo systemctl status my-tool.socket     # active (listening)
sudo systemctl status my-tool.service    # inactive (dead)

ss -tlnp | grep 8088
# 看到是 systemd 在监听

# 第一次访问
curl http://localhost:8088/
# systemd 启动 my-tool.service,处理请求

sudo systemctl status my-tool.service    # active (running)

服务端代码(Python)拿 socket fd

# /srv/my-tool/server.py
import os, socket
from wsgiref.simple_server import make_server

def app(environ, start_response):
    start_response('200 OK', [('Content-Type', 'text/plain')])
    return [b'hello from on-demand service']

# 检测是否被 systemd 启动 + socket activated
LISTEN_FDS_START = 3
if os.environ.get('LISTEN_FDS') == '1':
    fd = LISTEN_FDS_START
    sock = socket.socket(fileno=fd)
    # 用现成的 listening socket 而非新建
    sock.listen(128)
    # 配合 wsgiref:
    from wsgiref.simple_server import WSGIServer
    httpd = WSGIServer(('', 0), None, bind_and_activate=False)
    httpd.socket = sock
    httpd.set_app(app)
    print(f'serving on socket fd {fd}', flush=True)
    # 通知 systemd 启动完成(Type=notify 要求)
    notify_socket = os.environ.get('NOTIFY_SOCKET')
    if notify_socket:
        ns = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
        ns.sendto(b'READY=1', notify_socket)
    httpd.serve_forever()
else:
    httpd = make_server('', 8088, app)
    httpd.serve_forever()

很多框架原生支持 socket activation:

  • systemd-python 库的 daemon.listen_fds() 简化拿 fd
  • Gunicorn 支持 --bind fd://3(systemd LISTEN_FDS 模式)
  • Caddy / nginx 都能 socket activate

让服务闲置后自动退出

服务端代码:accept 后开 N 秒计时,无请求就 sys.exit(0)。
或者用 RuntimeMaxSec=10min 强制最多跑 10 分钟(systemd 会 SIGTERM)。

accept-per-connection 模式(替代 inetd)

如果服务每个连接超轻量(如 TCP echo),可以 Accept=yes

# my-tool.socket
[Socket]
ListenStream=8088
Accept=yes
# [email protected]     (注意 @)
[Service]
ExecStart=/srv/my-tool/handle.sh
StandardInput=socket
StandardOutput=socket

每个连接 systemd accept 后 fork 一个 service 实例处理。
适合纯短任务,不适合 HTTP(每请求新进程开销大)。

监控

# 看 socket 等待状态
sudo systemctl list-units --type=socket

# 看 service 历史
sudo systemctl list-timers
journalctl -u my-tool.service --since '1 day ago'

效果

我的 VPS 上:

常驻 socket activation
12 个小服务总内存 1.6 GB ~200 MB(只有正在用的几个)
冷启动延迟 0 100-500ms(首次请求)
闲置时 CPU < 1% 0%

代价:用户每天第一次访问慢半秒。绝大多数内部工具能接受。

与替代方案对比

systemd socket inetd / xinetd nginx fastcgi k8s scale-to-zero
复杂度
现代 ❌(淘汰)
每连接 fork 可选
内存节省
适合场景 偶用小服务 纯短任务 Web 服务 微服务集群

踩过的坑

  1. Type=notify 但代码不发 READY:systemd 默认 90 秒后认为启动
    失败,把服务 kill 掉。改 Type=simple 或确认代码发 sd_notify。

  2. 第一次冷启动慢得离谱:venv 加载 / 模型加载 / DB 连接初始化
    都在第一次请求时发生。考虑 WatchdogSec= + 预热请求。

  3. socket 端口冲突:先确保对应端口没被别的进程占用。
    Type=simple 的进程自己 bind 会冲突,必须用 socket activation 提供
    的 fd。

  4. service 退出后端口短暂关闭:闲置退出 → systemd 检测到 → 重新
    开 socket。这之间几十 ms 可能拒绝连接。KeepAlive=true + 服务退出
    前发 stop 信号给 systemd 让它先准备好。

  5. 不适合高并发:socket activation 设计给"偶尔用"。如果服务持续
    有流量,常驻反而更高效(避免冷启动)。

精确评价 共 0 人评价
可复现性
可复现 · 0 不可复现 · 0
文风
文风流畅 · 0 文风晦涩 · 0
立场
支持 · 0 反对 · 0

登录后即可对本帖作出评价。

评论区 0 条 · 所有人可在此交流

登录后参与评论。

还没有评论,来说两句。