用 systemd socket activation 让 8080 端口按需拉起服务
systemd 先监听 socket,客户端连接进来时才启动对应的 service 进程,并把已经打开的 socket 文件描述符交给它。思路来自传统 inetd,但 systemd 的实现更灵活:单元文件描述 socket 本身,服务不再负责 socket()/bind()/listen()。
启动时序
- systemd 读取
.socket单元文件 - systemd 调用
socket()/bind()/listen()创建并监听 socket - 客户端发起连接,systemd 接受连接
- systemd 通过
fork()+exec()启动对应的 service 进程 - service 进程通过继承的文件描述符处理连接
[Socket] 段核心配置
| 参数 | 说明 | 示例 |
|---|---|---|
| ListenStream | 监听 TCP 流式 socket | ListenStream=8080 |
| ListenDatagram | 监听 UDP 数据报 socket | ListenDatagram=9999 |
| ListenFIFO | 监听 FIFO(命名管道) | ListenFIFO=/run/myapp.sock |
| ListenSpecial | 监听特殊文件(如 /dev/log) | ListenSpecial=/dev/log |
| ListenNetlink | 监听 Netlink socket | ListenNetlink=kobject-uevent 0 |
| BindIPv6Only | IPv6 独占绑定 | BindIPv6Only=both |
| Accept | 是否为每个连接 fork 新进程 | Accept=no |
| MaxConnections | 最大同时连接数 | MaxConnections=64 |
| MaxConnectionsPerSource | 每个源 IP 最大连接数 | MaxConnectionsPerSource=10 |
| KeepAlive | 启用 TCP keepalive | KeepAlive=true |
| NoDelay | 启用 TCP_NODELAY | NoDelay=true |
| Priority | socket 优先级 | Priority=6 |
| ReusePort | 允许多进程共享端口 | ReusePort=true |
| SocketMode | socket 文件权限 | SocketMode=0660 |
| SocketUser | socket 文件所有者 | SocketUser=www-data |
| SocketGroup | socket 文件所属组 | SocketGroup=www-data |
Accept=no 与 Accept=yes
Accept=no:systemd 把 socket fd 传给服务进程,服务自行处理所有连接,长驻服务用这个。
Accept=yes:每个新连接 fork 一个独立服务实例,类似传统 inetd。此时服务单元名必须用模板(如 myapp@.service),%i 或 %I 会被替换为连接标识。
示例:8080 端口按需启动(RHEL)
第 1 步:创建 socket 单元
/etc/systemd/system/myapp.socket:
[Unit]
Description=MyApp Socket
[Socket]
ListenStream=0.0.0.0:8080
Accept=false
[Install]
WantedBy=sockets.target
对应的 /etc/systemd/system/myapp.service:
[Unit]
Description=MyApp Service
Requires=myapp.socket
[Service]
ExecStart=/usr/local/bin/myapp
Restart=on-failure
[Install]
WantedBy=multi-user.target
第 2 步:启用并启动 socket
注意启动的是 socket,不是 service。
sudo systemctl daemon-reload
sudo systemctl enable --now myapp.socket
验证:
systemctl status myapp.socket
ss -tlnp | grep 8080
此时 systemctl status myapp.service 应显示未运行(inactive/dead)。
第 3 步:测试触发
nc 127.0.0.1 8080 # 连接会触发 service 启动
再次 systemctl status myapp.service 应变为 active。
第 4 步:Accept 模式(每连接一个进程)
myapp-accept.socket:Accept=true,ListenStream=8081。
服务必须是模板单元 myapp-accept@.service:
[Service]
ExecStart=/usr/local/bin/myapp-handler
StandardInput=socket
StandardOutput=socket
systemctl daemon-reload && systemctl enable --now myapp-accept.socket
第 5 步:空闲超时
sudo mkdir -p /etc/systemd/system/myapp.service.d
/etc/systemd/system/myapp.service.d/timeout.conf:
[Service]
Environment=MYAPP_IDLE_TIMEOUT=60
systemctl daemon-reload && systemctl stop myapp.service
观测与排查
systemctl list-sockets
systemctl show myapp.socket --property=NAccepted
systemctl show myapp.socket --property=NConnections
journalctl -u myapp.socket -u myapp.service --follow
常见问题
Connection refused→ Socket 单元未启动,systemctl start myapp.socketAddress already in use→ 端口被其他进程占用,ss -tlnp | grep :8080- 服务启动但不处理连接 → 服务未正确接收 fd,检查
LISTEN_FDS环境变量 LISTEN_PID mismatch→ fork 后 PID 变化,确保Type=simple或Type=notify- 如果服务用
Type=forking,LISTEN_PID可能不匹配,建议Type=simple或Type=notify
这样做的收益
- 只在收到连接时才启动服务,省内存和 CPU;适合不常访问的服务、开发环境、系统里一堆可选服务
- socket 由 systemd 管理,服务重启期间不会丢连接
- systemd 可以先创建所有 socket 再并行启动服务,客户端不会遇到「连接被拒绝」
- 服务不必关心 socket 创建、绑定、权限,只需从 systemd 接收 fd
systemd-socket-proxyd:给不支持 socket activation 的程序做包装
不是所有程序都能原生支持 socket activation(需要能接收一个已初始化好的 socket)。systemd 提供 systemd-socket-proxyd 做代理,支持 socket activation 并把连接转发到另一个 unix 或网络 socket,双向转发。它从 systemd 209 起提供。
典型用法(用 Podman 跑容器 + 按需激活):systemd 用 .socket 单元在 172.16.18.70:8080 建 TCP socket,连接打进来后激活对应 .service,该 service 执行 systemd-socket-proxyd,并 Requires= 一个启动容器的 service。容器把端口绑到 127.0.0.1:8080,避免和 systemd 监听同端口但不同地址冲突。
[Unit]
Requires=container-httpd.service
After=container-httpd.service
Requires=container-httpd-proxy.socket
After=container-httpd-proxy.socket
[Service]
ExecStart=/usr/lib/systemd/systemd-socket-proxyd 127.0.0.1:8080
只启用 socket 单元:
systemctl --user enable --now container-httpd-proxy.socket
检查:
netstat -ltpn | grep 8080
systemd-socket-proxyd 关键开关:
--exit-idle-time:无连接多久后退出(systemd 246 起)- 在容器 service 上加
StopWhenUnneeded=yes,代理单元结束时停止容器 - 自动缩容(idle 退出)需要 systemd 246,RHEL 8 不支持
用它给 Nginx 做零停机升级也常见:systemd 管 Nginx 监听 socket,Nginx 原生 socket activation 支持有限,所以用 systemd-socket-proxyd 作中间代理,代理到 /run/nginx.sock。