起因
我有一台小 VPS 跑十几个偶尔被用的服务(小工具 API、自建图床、内部
Wiki 之类)。每个 24x7 常驻进程都吃 50-200 MB 内存。
"按需启动 + 闲置自动停"能省一大半内存。
xinetd 是老古董做法;systemd 的 socket activation 是现代等价物 +
完全集成系统单元 + 更灵活。
工作原理
- systemd 启动时只创建 listening socket(端口),不启服务进程
- 第一个请求到达端口时,systemd 启动服务进程并把 socket fd 传给它
- 服务正常工作,处理后续请求
- 闲置一段时间后服务可以 exit,systemd 重新只监听端口
- 下次请求重复 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 服务 | 微服务集群 |
踩过的坑
-
Type=notify但代码不发 READY:systemd 默认 90 秒后认为启动
失败,把服务 kill 掉。改Type=simple或确认代码发 sd_notify。 -
第一次冷启动慢得离谱:venv 加载 / 模型加载 / DB 连接初始化
都在第一次请求时发生。考虑WatchdogSec=+ 预热请求。 -
socket 端口冲突:先确保对应端口没被别的进程占用。
Type=simple的进程自己 bind 会冲突,必须用 socket activation 提供
的 fd。 -
service 退出后端口短暂关闭:闲置退出 → systemd 检测到 → 重新
开 socket。这之间几十 ms 可能拒绝连接。KeepAlive=true+ 服务退出
前发 stop 信号给 systemd 让它先准备好。 -
不适合高并发:socket activation 设计给"偶尔用"。如果服务持续
有流量,常驻反而更高效(避免冷启动)。
登录后参与评论。