跳到主要内容

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 脚本做了以下事情:

  1. 在本地计算机上启动了一个 socket 监听 8080 端口,等待连接。
  2. 当有连接尝试时,它使用 podman run 命令启动了一个 Apache HTTP 服务器的容器实例,并将传入的连接传递给这个容器。
  3. 使用 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 在一段时间不活动后就会停止。当下一个客户端连接到套接字时,服务将再次启动。