Podman 套接字激活
套接字激活的概念是通过让 systemd 创建一个套接字(例如 TCP、UDP 或 Unix 套接字)。一旦客户端连接到该套接字,systemd 将启动为套接字配置的 systemd 服务。新启动的程序将继承套接字的文件描述符,然后可以接受传入的连接(换句话说,运行系统调用 accept()
)。这种描述对应于默认的 systemd 套接字配置 Accept=no
,它允许服务接受套接字。
Podman 支持两种套接字激活形式:
- API 服务的套接字激活
- 容器的套接字激活
API 服务的套接字激活
架构看起来像这样:
stateDiagram-v2
[*] --> systemd: 第一个客户端连接
systemd --> podman: 通过 fork/exec 继承套接字
在 Fedora 系统上,文件 /usr/lib/systemd/user/podman.socket 定义了用于无根用户的 Podman API 套接字:
cat /usr/lib/systemd/user/podman.socket
[Unit]
Description=Podman API Socket
Documentation=man:podman-system-service(1)
[Socket]
ListenStream=%t/podman/podman.sock
SocketMode=0660
[Install]
WantedBy=sockets.target
此套接字定义监听在 ~/run/user/$(id -u)/podman/podman.sock
(由 %t
展开为用户的运行时目录)上的流套接字,并且只有套接字所有者和其他组成员(通常是该用户)可以访问它。当第一个客户端尝试连接到这个套接字时,systemd 将启动 Podman 进程,并将套接字文件描述符传递给 Podman。Podman 然后可以开始监听并接受来自客户端的连接。
要使用此套接字激活,请确保已安装并启用 podman.socket
服务。你可以使用以下命令启用和启动它:
systemctl --user enable podman.socket
systemctl --user start podman.socket
启用后,每当用户登录时,systemd 将自动启动 podman.socket
服务。然后,当你运行 podman-remote
命令时,它将自动连接到该套接字,而无需指定任何连接详情。
注意:出于安全原因,通常只有无根 Podman 使用套接字激活。对于 root 用户,通常直接启动 Podman 服务,因为 root 用户不需要通过套接字进行权限隔离。但是,技术上也可以为 root Podman 设置套接字激活,但这通常不是推荐的做法。
该套接字被配置为 Unix 套接字,可以像这样启动:
systemctl --user start podman.socket
ls $XDG_RUNTIME_DIR/podman/podman.sock
/run/user/1000/podman/podman.sock
$
套接字稍后可以由需要 Docker 兼容 API 的工具(例如 docker-compose)使用:
export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/podman/podman.sock
docker-compose up
当 docker-compose 或任何其他客户端连接到 UNIX 套接字 $XDG_RUNTIME_DIR/podman/podman.sock
时,服务 podman.service 将被启动。你可以在文件 /usr/lib/systemd/user/podman.service 中查看它的定义。
容器的套接字激活
自 3.4.0 版本起,Podman 支持容器的套接字激活,即将套接字激活的套接字传递给容器。由于 Podman 的 fork/exec 模型,套接字将首先被 conmon 继承,然后被 OCI 运行时继承,并最终被容器继承,这可以在下面的图中看到:
stateDiagram-v2
[*] --> systemd: 第一个客户端连接
systemd --> podman: 通过 fork/exec 继承套接字
state "OCI 运行时" as s2
podman --> conmon: 通过双重 fork/exec 继承套接字
conmon --> s2: 通过 fork/exec 继承套接字
s2 --> container: 通过 exec 继承套接字
这个流程展示了从 systemd 到 Podman,再到 conmon,最后到 OCI 运行时和容器的套接字传递过程。Podman 作为第一个接收套接字的服务,通过 fork/exec 调用创建子进程(即 conmon),并将套接字文件描述符传递给这个子进程。conmon 随后再次通过 fork/exec 创建 OCI 运行时进程,并将套接字传递给该进程。最终,OCI 运行时在创建容器时,通过 exec 调用将套接字传递给容器内的进程。通过这种方式,容器内的应用程序可以接收来自外部客户端的连接请求,而无需在容器内自行创建监听套接字。
这种套接字激活类型可以用于从容器单元文件(参见 podman-systemd.unit(5))或通过命令 podman generate systemd
(参见 podman-generate-systemd.1.html)生成的 systemd 服务中。需要注意的是,quadlet 需要使用 cgroup v2。
容器本身也必须支持套接字激活。并非所有守护进程都支持套接字激活,但这种支持正在变得越来越普及。例如,Apache HTTP 服务器、MariaDB、DBUS、PipeWire、Gunicorn、CUPS 都支持套接字激活。
示例:在 systemd 服务中运行套接字激活的 echo 服务器容器
这个示例展示了如何运行套接字激活的 echo 服务器 socket-activate-echo 在 systemd 用户服务中。这要求 Podman 版本为 4.4.0 或更高。
首先,为你的常规用户启用 lingering 功能:
loginctl enable-linger $USER
该命令对你的启用的 systemd 用户单元有以下影响:
- 重启后单元会自动启动
- 登出后单元不会自动停止
创建目录:
mkdir -p ~/.config/systemd/user
mkdir -p ~/.config/containers/systemd
在 ~/.config/containers/systemd/echo.container 文件中创建以下内容:
[Unit]
Description=Example echo service
Requires=echo.socket
After=echo.socket
[Container]
Image=ghcr.io/eriksjolund/socket-activate-echo
Network=none
[Install]
WantedBy=default.target
此配置文件定义了一个名为 echo.container
的容器服务,该服务依赖于名为 echo.socket
的套接字单元。容器使用 socket-activate-echo
镜像,并且网络被设置为 none
,这意味着容器不会连接到任何网络。最后,WantedBy=default.target
表示此服务在默认目标(通常是图形用户界面或命令行界面)启动时也会被启动。
现在,你需要创建一个套接字单元文件来激活这个容器。你可以使用 podman generate systemd
命令来生成一个套接字单元文件,然后编辑它以符合你的需求。
一旦你有了套接字单元文件和容器单元文件,你可以使用 systemctl --user
命令来启用和启动它们:
systemctl --user enable echo.socket
systemctl --user start echo.socket
这将启动套接字单元,并当第一个客户端连接到套接字时,容器服务也会被启动。
请注意,这只是一个示例,并且具体步骤可能会根据你的系统和配置有所不同。确保你查阅了 Podman 的文档以获取最新的信息和最佳实践。
该文件遵循了 podman-systemd.unit(5) 中描述的语法。
[Install]
部分是可选的。如果你移除了最后两行,echo.service
就不会在重启后自动启动。相反,当第一个客户端连接到套接字时,echo.service
才会被启动。
Network=none
也是可选的。它通过移除容器的网络连接来增强安全性。不过,由于 Network=none
对已激活的套接字没有影响,容器仍然可以服务于互联网。
套接字激活服务还需要一个 systemd 套接字单元。创建文件 ~/.config/systemd/user/echo.socket,定义容器应该使用的套接字:
[Unit]
Description=Example echo socket
[Socket]
ListenStream=127.0.0.1:3000
ListenDatagram=127.0.0.1:3000
ListenStream=[::1]:3000
ListenDatagram=[::1]:3000
ListenStream=%h/echo_stream_sock
# VMADDR_CID_ANY (-1U) = 2^32 -1 = 4294967295
# 参见 "man vsock"
ListenStream=vsock:4294967295:3000
[Install]
WantedBy=sockets.target
%h
是一个 systemd 的占位符,它会扩展为用户的家目录。
在编辑完单元文件后,systemd 需要重新加载其配置。
systemctl --user daemon-reload
执行这个命令后,systemd 会重新加载所有的用户单元文件,这样它就会注意到新添加的或修改过的文件。一旦重新加载完成,你就可以启用并启动套接字单元,然后等待第一个客户端连接来激活你的容器服务。
当重新加载 systemd 配置时,systemd 会通过执行单元生成器 /usr/lib/systemd/system-generators/podman-system-generator
来从文件 ~/.config/containers/systemd/echo.container 生成 echo.service 单元。
可选:查看生成的 echo.service 以了解将要运行的 podman run
命令。
systemctl --user cat echo.service
配置 systemd 以在重启后自动启动 echo.socket。
systemctl --user enable echo.socket
在启动套接字单元之前,先拉取容器镜像。
podman pull ghcr.io/eriksjolund/socket-activate-echo
启动套接字单元。
systemctl --user start echo.socket
使用 socat 程序测试 echo 服务器。
echo hello | socat -t 30 - tcp4:127.0.0.1:3000
hello
echo hello | socat -t 30 - tcp6:[::1]:3000
hello
echo hello | socat -t 30 - udp4:127.0.0.1:3000
hello
echo hello | socat -t 30 - udp6:[::1]:3000
hello
echo hello | socat -t 30 - unix:$HOME/echo_stream_sock
hello
echo hello | socat -t 30 - VSOCK-CONNECT:1:3000
hello
每个 echo hello | socat
命令都会将 "hello" 字符串发送到不同的套接字地址,并期望从服务器接收到相同的 "hello" 字符串作为响应。这些测试覆盖了 TCP 和 UDP 的 IPv4 和 IPv6 地址,以及 Unix 域套接字和 vsock。每个测试都应该在终端中打印出 "hello",表明 echo 服务器正在正常工作。如果服务器没有响应,那么可能是容器没有正确启动,或者套接字单元配置有误。
-t 30
这个选项配置 socat
在从套接字读取数据时等待文件结束(EOF)的超时时间为 30 秒。由于容器镜像已经被拉取,因此这么长的超时时间并不是必需的。
echo 服务器按预期工作。在接收到文本 "hello" 后,它会回复 "hello"。
示例:使用 systemd-socket-activate 激活 Apache HTTP 服务器
除了设置 systemd 服务来测试 socket 激活外,另一个选择是使用命令行工具 systemd-socket-activate。
下面,我们将为 Apache HTTP 服务器构建一个容器镜像,该镜像配置为在 8080 端口上支持 socket 激活。
创建一个新的目录 ctr 和一个文件 ctr/Containerfile,内容如下:
FROM docker.io/library/fedora
RUN dnf -y update && dnf install -y httpd && dnf clean all
RUN sed -i "s/Listen 80/Listen 127.0.0.1:8080/g" /etc/httpd/conf/httpd.conf
CMD ["/usr/sbin/httpd", "-DFOREGROUND"]
构建容器镜像:
podman build -t socket-activate-httpd ctr
在一个 shell 中,启动 systemd-socket-activate。
systemd-socket-activate -l 8080 podman run --rm --network=none localhost/socket-activate-httpd
TCP 端口号 8080 作为选项传递给 systemd-socket-activate。我们没有使用 podman run
的 --publish
(-p
) 选项。
在另一个 shell 中,从 localhost:8080 获取网页内容:
curl -s localhost:8080 | head -6
<!doctype html>
<html>
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width, initial-scale=1'>
<title>Fedora 上的 HTTP 服务器的测试页面</title>
$
这段 bash 脚本做了以下事情:
- 在本地计算机上启动了一个 socket 监听 8080 端口,等待连接。
- 当有连接尝试时,它使用
podman run
命令启动了一个 Apache HTTP 服务器的容器实例,并将传入的连接传递给这个容器。 - 使用
curl
命令从 Apache 服务器获取了网页的头部内容,并显示了前几行。
请注意,由于容器运行在 --network=none
模式下,它不会连接到外部网络。这确保了 socket 激活仅发生在本地主机上,并且没有外部连接能够访问该服务。这是测试 socket 激活功能的一个安全做法。在生产环境中,您可能需要更复杂的网络配置和安全性措施。
使用 --network=none
禁用网络
如果容器仅需要通过 socket 激活的套接字进行通信,那么可以通过向 podman run
命令传递 --network=none
来禁用网络。这提高了安全性,因为容器以较少的权限运行。
通过 socket 激活的套接字进行本地网络性能
在使用无根 Podman 时,网络流量通常通过 slirp4netns 传递。这会产生性能开销。幸运的是,通过 socket 激活的套接字进行的通信不会经过 slirp4netns,因此它具有与主机上的正常网络相同的性能特性。
启动 socket 激活的服务
在第一次建立连接时,由于需要启动容器,因此会有延迟。为了尽量减少这种延迟,可以考虑向 podman run
命令传递 --pull=never
,并提前拉取容器镜像。此外,服务也可以由管理员明确启动(例如 systemctl --user start echo.service
),而不是等待第一个客户端连接来触发服务的启动。
停止 socket 激活的服务
有些服务运行一个命令(由 systemd 指令 ExecStart
配置),该命令在一段时间不活动后退出。根据服务的重启配置(systemd 指令 Restart
),它可能会被停止。例如,podman.service 在一段时间不活动后就会停止。当下一个客户端连接到套接字时,服务将再次启动。