编程 用 systemd socket activation 让 8080 端口按需拉起服务

2026-09-25 21:31:02

用 systemd socket activation 让 8080 端口按需拉起服务

systemd 先监听 socket,客户端连接进来时才启动对应的 service 进程,并把已经打开的 socket 文件描述符交给它。思路来自传统 inetd,但 systemd 的实现更灵活:单元文件描述 socket 本身,服务不再负责 socket()/bind()/listen()。

启动时序

  1. systemd 读取 .socket 单元文件
  2. systemd 调用 socket()/bind()/listen() 创建并监听 socket
  3. 客户端发起连接,systemd 接受连接
  4. systemd 通过 fork()+exec() 启动对应的 service 进程
  5. service 进程通过继承的文件描述符处理连接

[Socket] 段核心配置

参数说明示例
ListenStream监听 TCP 流式 socketListenStream=8080
ListenDatagram监听 UDP 数据报 socketListenDatagram=9999
ListenFIFO监听 FIFO(命名管道)ListenFIFO=/run/myapp.sock
ListenSpecial监听特殊文件(如 /dev/log)ListenSpecial=/dev/log
ListenNetlink监听 Netlink socketListenNetlink=kobject-uevent 0
BindIPv6OnlyIPv6 独占绑定BindIPv6Only=both
Accept是否为每个连接 fork 新进程Accept=no
MaxConnections最大同时连接数MaxConnections=64
MaxConnectionsPerSource每个源 IP 最大连接数MaxConnectionsPerSource=10
KeepAlive启用 TCP keepaliveKeepAlive=true
NoDelay启用 TCP_NODELAYNoDelay=true
Prioritysocket 优先级Priority=6
ReusePort允许多进程共享端口ReusePort=true
SocketModesocket 文件权限SocketMode=0660
SocketUsersocket 文件所有者SocketUser=www-data
SocketGroupsocket 文件所属组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.socket
  • Address 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。

推荐文章

程序员茄子在线接单