资讯动态

Podman无守护进程架构解析:从源码到RHEL实操的容器运行时深度指南

发布时间:2026/9/20 2:18:07 来源:尧图企业网站定制
如果你在一台装好 Podman 的 RHEL 8/9 机器上敲下podman run -it --rm ubi9/ubi bash然后盯着进程列表看几秒钟会看到一幕和 Docker 完全不同的“接力赛”podman这个命令本身很快就退出了真正留在现场的是conmon以及容器里被直接拉起的 bash。没有常驻内存的 dockerd也没有 containerd 替你管理状态。Red Hat 这套被称为“无守护进程容器引擎”的方案不是宣传口号而是从进程模型到源码组织都贯穿始终的工程选择。这篇复盘我想从源码走向和实际运行两个角度把 Podman 的架构拆开讲清楚无守护进程到底是怎么实现的、代价在哪、和 Docker 的兼容边界到哪里以及团队选型时哪些场景适合上 Podman、哪些场景别硬凑。适合所有正在评估容器运行时、或者已经从 Docker 迁移到 Podman 但心里还有疑问的人。1. 从源码看进程接力无守护进程的“假象”与真相很多没有读过代码的人会把“无守护进程”理解成“没有任何常驻后台服务”。这个说法接近事实但不准确。更精确的描述是没有一个集中式的容器守护进程来接收所有客户端请求、维护全局状态、替你管理一切。Podman 把原来守护进程的工作拆成了若干子进程和系统组件各自干一段干完就退。1.1 敲下 podman run 后发生了什么从调用链上看podman二进制本身是入口但它背后会调用libpod库。libpod 负责解析命令行参数合成容器配置读取镜像层然后协调 storage、runtime 和 conmon 一起来完成一次容器启动。大致流程是这样podmanCLI 解析参数调用 libpod 中对应的容器创建接口。libpod 通过containers/storage确认本地镜像是否存在不存在就走containers/image拉取。libpod 调用pkg/specgen生成一份 OCI 运行时规范。根据配置找到 OCI runtimeRHEL 上默认是crun。创建一个conmon进程作为容器进程的监护者。crun创建命名空间、cgroup真正拉起容器内进程。如果没加-dCLI 会留在前台保持 stdin/stdout 连接加了-dCLI 等容器起来后直接退出。这个链路里没有哪个进程是“永远活着”的。每次podman命令都是一个新的客户端进程用完即走。这跟 Docker 的 client-server 模型有本质差异Docker CLI 不管执行什么最后都是往/var/run/docker.sock发请求真正干活的永远是那个醒着的 dockerd。1.2 conmon不是主守护进程却是每个容器背后的守护员conmon是 “container monitor” 的缩写用 C 写的各发行版里通常是一个独立的小二进包。它的作用很具体每个容器启动时都会陪着一个 conmon 进程专门负责管理这个容器的 stdin/stdout/stderr、PTY、日志转发以及转发信号给容器主进程。为什么要做这么一层因为容器进程本身在退出后需要有人收尸需要有人把退出码记下来需要有人把日志写到 journald。如果没有 conmon进程会直接被 initsystemd接管那容器生命周期和系统 init 的职责就搅在一起了。所以准确的说法是Podman 没有“全局守护进程”但每个容器都有一个轻量级的“看门人”。这本质上是把守护粒度从全机缩小到单容器换来的是进程边界更清晰、单个容器的生命周期更可控。注意如果你看到有人说“Podman 完全没有常驻进程”那是在特定语境下的简化说法。开了podman.socket之后会有 socket 激活的服务常驻跑 rootless 容器时systemd user 实例也会存在。真正要对比的是“没有集中式容器守护进程”这个事实。1.3 systemd 才是全局意义上真正的常驻层Podman 的设计中systemd 不是配角。RHEL 8/9 上你甚至可以不用手动启动任何 Podman 服务容器也能跑。但当你需要开机自启容器、需要和宿主机 service 体系协作时systemd 就会出场。Podman 有两种和 systemd 协作的方式老一些的方式podman generate systemd生成一个 service 文件再由 systemd 管理。新方式quadlet.container文件直接写单元配置Podman 会帮你解析并生成 systemd unit。这是“无守护进程”架构里非常顺的一环容器生命周期可以交给 systemd 管而不是交给一个额外的容器守护进程。你在宿主机上看到的服务管理方式和容器内的服务管理方式能保持统一。反过来这也说明 Podman 的“无守护进程”是建立在 systemd 这个系统级监听者之上的。你要是在一个没有 systemd 的精简环境里跑 Podman体验会差很多甚至某些功能用不了。这是选型时需要考虑的环境前提。2. 源码地图libpod、containers/common、containers/storage 各管什么Red Hat 的容器工具链和 Docker 最大的区别之一是它把项目拆成了一组职责非常单一的库和工具而不是一个大而全的二进制。理解这些模块的分工对排查问题很有帮助。2.1 模块粒度对照模块主要职责对应场景containers/libpod容器、Pod 的生命周期管理核心库podman run、podman podcontainers/common配置解析、认证、通用工具containers.conf、auth.jsoncontainers/image镜像拉取、推送、签名校验、转换podman pull、skopeo copycontainers/storage层存储、镜像分层、挂载graphdriver、overlay 挂载containers/buildah无守护进程方式构建镜像podman build底层调用crun/runcOCI runtime真正启动容器进程容器执行这张表里有个很关键的现象几乎所有模块都以“containers”前缀存在而且这些库本身也能被其他项目复用。skopeo就是containers/image的独立命令行封装buildah是镜像构建层的独立工具。Podman 把buildah、skopeo的能力合进来做成子命令但底层用的还是同一套库。这带来的好处是你在用podman build时遇到镜像层的问题可以直接用skopeo inspect去查远程镜像你在podman pull遇到认证失败错误信息里往往能看到containers/image那层抛出来的原因。排查问题时思路可以很直接不需要在一大坨闭源代码里捞线索。2.2 运行时切换crun 与 runc 为什么可以共存OCI runtime 是真正干“创建容器进程”这个脏活的部分。Podman 作为一个上层管理工具并不关心你用的是 crun 还是 runc只要实现 OCI 规范就行。RHEL 上默认用crun原因是它在 C 里实现启动速度快、内存占用低而且在 cgroup v2 的适配上有不少优化。但实际工作中我见过不少团队还在用 runc或者自己编译了一个别的 runtime。这完全可行方式就是改/etc/containers/containers.conf里的runtime字段或者直接在命令行指定--runtime。源码里这层抽象也很清楚libpod 把 runtime 封装成接口调用方不关心具体二进制是谁只关心容器能不能起、能不能停、退出码能不能拿到。这里有一个容易混淆的点很多人以为 Podman 用 crun 就比 Docker 的 runc 快很多其实在真实业务里启动差异通常只有几十到几百毫秒。真正影响体感的往往是 Podman 不需要等待守护进程、不需要经过 dockerd 那一层转发所以命令行操作响应更快。2.3 一装 Podman 为什么会多出一堆命令你如果看 RHEL 上安装podman之后装进来的文件会发现它的依赖里有skopeo、buildah、crun、conmon、fuse-overlayfs等。第一次见可能会觉得“怎么装个 Podman 搞了这么多东西”但这也是架构一致性带来的结果。每个工具只负责一件事podman统一用户入口管理容器和 Pod。buildah提供构建镜像的能力podman build就是转调它。skopeo镜像仓库层工具podman pull/push背后依赖同一套镜像操作库。crun/runc进程级容器启动器。conmon单个容器的日志和信号监护。fuse-overlayfsrootless 模式下 overlay 挂载的用户态方案。对比 Docker 一个 daemon 包打天下的做法这套设计更接近 Unix 哲学小工具、单职责、可组合。缺点也很明显——迁移方要想在现有自动化脚本里复刻 Docker 的命令行体验需要额外适配的东西不少。3. 收益的另一面状态、网络、卷在无守护进程模式下的代价无守护进程当然不是免费的。它换来的是多用户隔离更干净、系统集成更顺滑但代价是原来守护进程集中管理的状态、网络、卷现在得换一种方式实现。如果你没提前了解这些边界迁移过程中容易踩到 “为什么我在 Docker 里能跑、Podman 里就报错” 的坑。3.1 容器状态存在哪没有集中式数据库状态怎么管理Docker 的守护进程会把所有容器状态统一存在/var/lib/docker下由 dockerd 单点维护。Podman 没有这个单点它的状态管理是把每个容器、镜像、卷的状态分散在不同目录配合文件锁和可插拔的状态后端。具体到源码层面libpod 对容器状态管理做了抽象历史上有过不同实现。你可以简单理解成它不再需要一个永远在线的进程来维护状态而是把状态写到本地持久化位置每次 CLI 启动时读取。这也解释了为什么你在一台机器上直接跑多个podman命令不会像有些基于 socket 的架构那样容易出现状态竞争——它用文件锁来保证并发安全。这带来的一个实际收益是多用户场景下每个用户有自己的~/.local/share/containers/storage用户 A 删容器不会影响用户 B 的容器。Docker 要实现这种隔离通常得起多个 dockerd 实例或者做复杂的权限隔离复杂度完全不同。3.2 rootless 网络与端口转发的真实性能账无守护进程模式下普通用户跑 rootless 容器是一件很自然的事。但自然不等于免费。rootless 意味着你没有权限创建 veth 网卡、没有权限操作 iptables所以网络方案只能走用户态协议栈。默认情况下rootless 容器使用slirp4netns或更新的pasta来提供一个用户态网络栈。你从容器访问外部网络没问题但从宿主机访问容器端口时需要一个额外的端口转发进程。这个过程比 Docker 的 bridge 模式多了几次用户态和内核态的切换端口转发吞吐有损耗。如果有人告诉你“rootless Podman 和 root 模式网络性能完全一致”那是睁眼说瞎话。实测场景下高频小包转发的性能差距尤其明显。适合 rootless 的是开发环境、CI 跑任务、边缘节点低并发服务不适合的是生产环境里依赖毫秒级端口响应、长连接密集的中转服务。提示RHEL 9 里的 Podman 版本较新网络默认可能已经换用 pasta。但无论底层怎么优化用户态协议栈的上限就在那里。真要追求性能和稳定要么用 root 模式要么干脆把负载交给 systemd 管理的直接 socket 转发绕过容器网络栈。3.3 卷映射和 UID 移交的边界keep-id、unshare 和 :Z 标签rootless 容器还有一个高频故障点宿主机的文件挂进容器后容器里看到的属主完全不是你以为的那个人。这是因为 rootless 容器里 UID 0 其实映射的是宿主机的普通用户和宿主机文件的真实 UID 不匹配。GitHub 上不少 issue 都指向这个场景用户在宿主机把/home/me/data挂进容器容器里写文件发现宿主机上文件属主变成了别的 ID 或者直接 Permission denied。解决思路有两种启动容器时加--usernskeep-id让容器内 UID 和宿主机当前用户 UID 保持一致。重新映射子 UID也就是接受 rootless 模式的默认逻辑再用podman unshare chown去修改文件属主。在 RHEL 这种默认开 SELinux 的系统上还有一个隐藏规则挂载宿主机目录时必须在卷参数里带:Z或:z否则 SELinux 会拒绝容器访问。:Z是私有标签:z是共享标签。多容器共享同一个目录时如果你在 Docker 里用的是-v /data:/data在 Podman 上就得写成-v /data:/data:Z这个差异在迁移时最容易漏掉。4. Docker 兼容 API 的迁移边界能平替多少不能平替多少很多人把 Podman 当作 Docker 的直接替代品确实可以替换但代价在细节里。源码层面 Podman 特意实现了 Docker 兼容 API就是为了让现有基于 Docker 的客户端和脚本能少改一些。可这个兼容层不是无限覆盖是否适合迁移取决于你的工作流里到底用了多少 Docker 的隐藏语义。4.1 compat API 是“可选开启”开启后就有了事实守护Podman 默认不会监听任何 socket。要让 Docker 客户端连上来需要手动启用它对应的 socket 单元systemctl --user enable podman.socket --now export DOCKER_HOSTunix:///run/user/$(id -u)/docker.sock之后docker ps、docker exec这类命令就能直接操作 Podman 管理的容器。实现原理是 libpod 在公共 API 层做了一层兼容适配把 Docker REST API 的请求映射到 Podman 自己的控制面。要注意的是一旦开启了podman.socket就存在一个常驻服务进程它和传统守护进程的区别已经非常模糊。如果你用 Podman 的核心诉求是“绝对干净、没有任何常驻容器端进程”那你别开这个 socket用命令行就好。如果你需要的是兼容 Docker 生态那开 socket 是合理的只是要清楚代价。4.2 和 docker-compose、K8s 打交道的边界docker-compose 是目前 Docker 生态里最常用的编排入口。到了 Podman 这边podman-compose可以做一部分兼容但它不是逐行复刻 Compose 的完整语义。项目名称处理、网络命名、depends_on的启动顺序判定、健康检查等待这些细节都可能出现偏差。podman play k8s又是一个不同的方向它接受 Kubernetes YAML把 Deployment、Service、Pod 等资源映射成本地 Podman 对象。但注意这只是把 YAML 导入成本地 Pod不等于你在本地跑了一个 Kubernetes。副本扩展、滚动更新、故障重新调度这些特性统统没有。所以我的建议是如果你的团队已经在用 docker-compose 维护一套复杂得多文件编排直接切到 Podman 前一定要把现有的 compose 文件列表逐项拿出来跑一遍先看网络依赖和卷依赖再看健康检查逻辑。4.3 真正不能平替的场景集中管理、跨节点、容器节点运行时有些场景选型一开始就不该考虑 Podman。需要跨多台机器用一个统一 Docker context 管理远端主机Podman 默认的podman context能力远不如 Docker context 生态那么顺滑。需要 Docker Swarm 这种集群模式Podman 不支持Red Hat 也没打算支持。要给 Kubernetes 节点装容器运行时节点级选择应该是 CRI-O 或 containerd而不是 Podman。Podman 的定位是单机管理和开发运维人员不是 K8s 的 CRI 实现。虽然podman play k8s看起来像 K8s但它在 K8s 集群里没有位置。认清这个边界能避免很多“为什么我的 Podman 不能像 Docker 那样做…”的纠结。5. RHEL 实操里容易踩的坑代理、离线导入、rootless 权限接下来这部分是日常支持里最常见的问题。不一定都是源码层面的东西但都是和 Podman 落地强相关的实操细节。5.1 代理配置registries、certs.d 和环境变量拉镜像连不上外部仓库是换到 Podman 后最常见的问题之一。第一步先确认环境变量export HTTP_PROXYhttp://proxy.internal:8080 export HTTPS_PROXYhttp://proxy.internal:8080 export NO_PROXYlocalhost,127.0.0.1,.internalPodman 本质上是一个普通命令行程序它走的是系统标准网络栈所以HTTP_PROXY对它是有效的。但在 systemd user 服务里跑容器时问题会变复杂user unit 默认不继承你.bashrc里的代理变量需要在 unit 文件里加Environment或者先执行systemctl --user import-environment导入。企业内部 registry 的自签证书也是一大坑。Docker 会把这类证书放到/etc/docker/certs.d/registry/Podman 对应的是/etc/containers/certs.d/registry/或者~/.config/containers/certs.d/。格式相同路径不同迁移时很多人会忽略。5.2 离线环境下的镜像导入别再折腾 tar 包了离线环境装镜像不少人还停留在docker save和docker load的老思路其实在 Podman 生态里有个更顺手的办法# 在能联网的机器上直接拉镜像到本地目录 skopeo copy docker://registry.example.com/app:v1.0 dir:/data/offline/app-v1.0 # 拷贝目录到内网机器后直接从目录导入本地存储 skopeo copy dir:/data/offline/app-v1.0 containers-storage:app:v1.0skopeo copy这种dir:格式的好处是不需要先转成 tar 再导入它就是一个普通目录拷到内网机器上就能直接用。这也反映了containers/image库在设计上的好处镜像的格式转换和仓库交互被抽得很干净不同场景可以走不同转换路径。RHEL 上离线安装容器引擎本身也建议把 RPM 包提前同步到本地仓库然后通过dnf离线安装。这样做的好处是依赖关系能正确处理不会出现手动 rpm 安装时缺什么 lib 才临时找包的尴尬。5.3 rootless 权限子 UID、user namespace 和 SELinux 的联动RHEL 用户跑 rootless 容器前系统管理员得先确认用户名映射cat /etc/subuid cat /etc/subgid这两组配置为普通用户提供了可用的子 UID/子 GID 段范围。如果文件里没有你的用户名rootless 容器会启动失败或者提示找不到映射。这是很多 rootless 问题的起点排查前先看它。诊断容器内外的 UID 映射先用这个命令podman unshare cat /proc/self/uid_map它会显示当前用户映射到容器内的 UID 范围。如果发现映射不对再考虑--usernskeep-id重新指定。再加上 SELinux 这一层问题又会叠加。RHEL 默认开启 SELinuxpodman run -v /host_dir:/container_dir:Z这个:Z标签不是可选项而是必须项。忘加了容器启动会报 “Permission denied” 或 “Operation not permitted”但很多人第一反应是去关 SELinux那是错误的方向。正确做法是给挂载目录打好容器用的 SELinux 标签而不是全局放宽安全策略。6. 选型判断表到底什么场景该用 Podman什么场景别硬用到这一步原理、源码、边界基本都清楚了。最后说点实在的我实际做选型时是怎么判断的。6.1 我的选择习惯场景推荐方案原因个人开发机、单机容器测试Podman无守护进程、rootless 友好、systemd 集成自然多用户共享 Linux 服务器Podman用户级存储隔离、不需要 root、权限边界清晰团队已有大量 docker-compose 工作流先评估再迁移compose 兼容不完整网络和健康检查易踩坑Kubernetes 集群节点CRI-O / containerdPodman 不是 CRI 实现也不该硬塞进去依赖 Docker daemon 缓存的 CI 构建场景不建议直接换构建缓存机制、并行 task 调度和 Docker 差异较大生产环境 root 模式单机容器Podman 可考虑前提是网络方案、存储驱动、SELinux 策略全部验证过这张表不是标准答案但它能帮你把讨论焦点从“哪个工具更好”拉到“我的场景到底需要什么”。6.2 迁移前先跑一组“压力测试”假设你决定把一个已有服务迁到 Podman别一上来就全量切换。先拿最小服务跑一遍这五件事容器启动后宿主机能不能正常访问业务端口容器挂载宿主机目录读写权限是否符合预期业务进程崩溃后systemd 能不能按预期拉起日志能不能正常进 journald镜像拉取、离线导入、代理切换这条路径是否通畅这五件事看着基础但凡是迁移出问题基本都藏在这里面。最后再分享一个我长期用下来的判断习惯遇到容器运行时选型问题时先别急着比功能列表先看“这台机器上到底有几个用户、需不需要 root、有没有 SELinux、将来要不要进 K8s”。这四个问题问完答案往往已经很清楚了。Podman 确实是 Red Hat 这些年做得最有代表性的基础设施项目之一但它的核心价值是“无守护进程架构下的多用户隔离和系统集成”不是“在所有场景下替代 Docker”。把这个定位记清楚选型时就不容易跑偏。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价