systemd 教程 / Socket 激活(Socket Activation)
Socket 激活(Socket Activation)
概述
Socket 激活是 systemd 提供的一种按需启动服务的机制。systemd 先监听指定的 socket,当有客户端连接时,systemd 才启动对应的服务进程,并将已打开的 socket 文件描述符传递给它。这种方式来源于传统的 inetd 超级服务器,但 systemd 的实现更加灵活和强大。
核心概念
什么是 Socket 激活?
传统方式中,服务进程需要自行创建 socket、绑定端口、监听连接。而 Socket 激活将这个职责交给 systemd:
┌──────────┐ ┌──────────┐ ┌──────────────┐
│ Client │────▶│ systemd │────▶│ Service │
│ │ │ (监听端口)│ │ (接收 fd) │
└──────────┘ └──────────┘ └──────────────┘
工作流程(fork + exec)
- systemd 读取
.socket单元文件 - systemd 调用
socket()、bind()、listen()创建并监听 socket - 客户端发起连接,systemd 接受连接
- systemd 通过
fork()+exec()启动对应的服务进程 - 服务进程通过继承的文件描述符处理连接
Socket 单元文件
基本结构
Socket 单元文件以 .socket 为后缀,核心配置在 [Socket] 段中:
# /etc/systemd/system/myapp.socket
[Unit]
Description=MyApp Socket
[Socket]
ListenStream=8080
Accept=no
[Install]
WantedBy=sockets.target
[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 |
BindIPv6Only 详解
| 值 | 行为 |
|---|---|
default | 使用系统默认设置(net.ipv6.bindv6only) |
both | IPv4 和 IPv6 都监听(推荐) |
ipv6-only | 仅监听 IPv6 |
[Socket]
ListenStream=8080
BindIPv6Only=both
⚠️ 注意:如果不设置 BindIPv6Only=both,在某些系统上 ListenStream=8080 可能只监听 IPv6。
Accept 模式 vs 非 Accept 模式
Accept=no(推荐)
systemd 将 socket fd 传递给服务进程,服务自行处理所有连接:
# myapp.socket
[Socket]
ListenStream=8080
Accept=no
# myapp.service
[Unit]
Description=MyApp
[Service]
ExecStart=/usr/bin/myapp
Accept=yes
每个新连接 fork 一个独立的服务实例,类似传统 inetd:
# echo.socket
[Socket]
ListenStream=7
Accept=yes
# [email protected](模板单元)
[Unit]
Description=Echo Service
[Service]
ExecStart=/usr/bin/cat
StandardInput=socket
StandardOutput=socket
StandardError=journal
⚠️ 注意:Accept=yes 模式下,服务单元名称必须使用模板(如 [email protected]),%i 或 %I 会被替换为连接标识。
Socket 激活的优势
1. 按需启动
服务只在收到连接时才启动,节省内存和 CPU 资源。
💡 提示:对于不常使用的服务(如 SSH),Socket 激活可以显著减少开机后的内存占用。
2. 零停机重启
由于 socket 由 systemd 管理,服务重启期间不会丢失连接:
旧进程退出 ──▶ systemd 继续监听 ──▶ 新进程启动 ──▶ 接收积压的连接
3. 并行启动
systemd 可以先创建所有 socket,再并行启动所有服务,因为 socket 已经就绪,客户端不会遇到"连接被拒绝"。
4. 简化服务代码
服务无需关心 socket 创建、绑定、权限等细节,只需从 systemd 接收 fd。
实际案例
案例 1:Nginx Socket 激活
让 systemd 管理 Nginx 的监听 socket,实现零停机升级。
Socket 单元:
# /etc/systemd/system/nginx.socket
[Unit]
Description=Nginx Socket
[Socket]
ListenStream=80
ListenStream=443
BindIPv6Only=both
[Install]
WantedBy=sockets.target
服务单元:
# /etc/systemd/system/nginx.service
[Unit]
Description=Nginx HTTP Server
After=network.target
[Service]
Type=notify
ExecStart=/usr/sbin/nginx -g 'daemon off;'
ExecReload=/bin/kill -HUP $MAINPID
ExecStop=/bin/kill -QUIT $MAINPID
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target
Nginx 配置(需要接收 systemd 传递的 fd):
# /etc/nginx/nginx.conf
# 注意:使用 systemd 的 socket 激活时,
# listen 指令需要配合 listen ... reuseport 或使用
# systemd-socket-proxyd
由于 Nginx 原生 socket 激活支持有限,通常使用
systemd-socket-proxyd作为中间代理。
使用 systemd-socket-proxyd:
# /etc/systemd/system/nginx-proxy.socket
[Unit]
Description=Nginx Proxy Socket
[Socket]
ListenStream=80
BindIPv6Only=both
[Install]
WantedBy=sockets.target
# /etc/systemd/system/nginx-proxy.service
[Unit]
Description=Nginx Proxy
Requires=nginx.service
After=nginx.service
[Service]
ExecStart=/usr/lib/systemd/systemd-socket-proxyd /run/nginx.sock
Restart=on-failure
案例 2:自定义 TCP 服务
Socket 单元:
# /etc/systemd/system/myapp.socket
[Unit]
Description=My Custom App Socket
[Socket]
ListenStream=9090
BindIPv6Only=both
NoDelay=true
MaxConnections=128
[Install]
WantedBy=sockets.target
服务单元:
# /etc/systemd/system/myapp.service
[Unit]
Description=My Custom App
[Service]
Type=simple
ExecStart=/opt/myapp/bin/server
Restart=on-failure
RestartSec=3s
应用代码接收 socket(Python 示例):
import socket
import os
# systemd 传递的 fd 从 3 开始
LISTEN_FDS = int(os.environ.get('LISTEN_FDS', 0))
LISTEN_PID = int(os.environ.get('LISTEN_PID', 0))
if LISTEN_PID != os.getpid():
raise RuntimeError('LISTEN_PID mismatch')
# 获取第一个传递的 fd(fd=3)
sock = socket.fromfd(3, socket.AF_INET, socket.SOCK_STREAM)
print('Listening on systemd-activated socket')
while True:
conn, addr = sock.accept()
data = conn.recv(1024)
conn.sendall(b'Echo: ' + data)
conn.close()
💡 提示:使用 sd_listen_fds() 函数(libsystemd)比手动读取环境变量更安全可靠。
案例 3:D-Bus Socket 激活
systemd 本身就是 D-Bus socket 激活的典型用户:
# /usr/lib/systemd/system/dbus.socket
[Unit]
Description=D-Bus System Message Bus Socket
[Socket]
ListenStream=/run/dbus/system_bus_socket
[Install]
WantedBy=sockets.target
多实例 Socket 服务
使用模板单元实现多实例 socket 激活:
# /etc/systemd/system/[email protected]
[Unit]
Description=WebApp Instance Socket
[Socket]
ListenStream=%i
BindIPv6Only=both
[Install]
WantedBy=sockets.target
# /etc/systemd/system/[email protected]
[Unit]
Description=WebApp Instance
[Service]
Type=simple
ExecStart=/opt/webapp/bin/server --port=%i
Restart=on-failure
启动多个实例:
# 启动 8080 端口的实例
sudo systemctl enable --now [email protected]
# 启动 9090 端口的实例
sudo systemctl enable --now [email protected]
调试 Socket 激活
检查 Socket 状态
# 查看 socket 单元状态
systemctl status myapp.socket
# 查看所有活跃的 socket
systemctl list-sockets
# 查看 socket 详细信息
systemctl cat myapp.socket
测试 Socket 连接
# 使用 socat 测试 TCP socket
echo "hello" | socat - TCP:localhost:8080
# 使用 netcat 测试
echo "hello" | nc localhost 8080
# 检查端口是否被监听
ss -tlnp | grep 8080
查看文件描述符
# 查看 systemd 监听的 fd
systemctl show myapp.socket -p FileDescriptorName
# 查看服务进程的 fd
ls -la /proc/$(pidof myapp)/fd/
常见问题排查
| 问题 | 原因 | 解决方案 |
|---|---|---|
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=forking,LISTEN_PID 可能不匹配,建议使用 Type=simple 或 Type=notify。
Socket 激活 vs 传统监听
| 对比项 | 传统监听 | Socket 激活 |
|---|---|---|
| socket 创建 | 服务自行创建 | systemd 预创建 |
| 按需启动 | 不支持 | 原生支持 |
| 零停机重启 | 需要特殊处理 | 内建支持 |
| 启动顺序 | 需要显式依赖 | 自动处理 |
| 代码复杂度 | 较高 | 较低 |