强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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)

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

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 流式 socketListenStream=8080
ListenDatagram监听 UDP 数据报 socketListenDatagram=9999
ListenFIFO监听 FIFO(命名管道)ListenFIFO=/run/myapp.sock
ListenSpecial监听特殊文件(如 /dev/logListenSpecial=/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

BindIPv6Only 详解

行为
default使用系统默认设置(net.ipv6.bindv6only
bothIPv4 和 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 refusedSocket 单元未启动systemctl start myapp.socket
Address already in use端口被其他进程占用ss -tlnp | grep :8080
服务启动但不处理连接服务未正确接收 fd检查 LISTEN_FDS 环境变量
LISTEN_PID mismatchfork 后 PID 变化确保 Type=simple 或正确设置

⚠️ 注意:如果服务使用 Type=forkingLISTEN_PID 可能不匹配,建议使用 Type=simpleType=notify


Socket 激活 vs 传统监听

对比项传统监听Socket 激活
socket 创建服务自行创建systemd 预创建
按需启动不支持原生支持
零停机重启需要特殊处理内建支持
启动顺序需要显式依赖自动处理
代码复杂度较高较低

扩展阅读