Podman 与 Conmon v3 重构设计文档解析:从 C 语言单容器监控到 Rust 事件循环与插件化日志架构
2026/9/19 5:12:52 网站建设 项目流程

Podman 与 Conmon v3 重构设计文档解析:从 C 语言单容器监控到 Rust 事件循环与插件化日志架构

【免费下载链接】podmanPodman: A tool for managing OCI containers and pods.项目地址: https://gitcode.com/gh_mirrors/po/podman

导读

本文基于 Podman 仓库contrib/design-docs/Conmonv3.md设计文档,完整解析 Conmon v3 重构的来龙去脉:为什么放弃维护 8 年的 C 语言 Conmon、为什么否决 Rust 重写的 Conmon-rs、如何用 Rust + 极简事件循环实现内存仅 1.2-1.5MB 的新监控进程,以及插件化日志系统如何满足用户积压近 5 年的日志功能诉求。文中结合libpod/oci_conmon_common.golibpod/define/config.golibpod/container_log.go等当前仓库源码,深入剖析 Conmon 与 Podman 的现有接口契约,帮助读者理解这一关键组件的未来演进方向。


1. 背景:为什么 Conmon 需要一次根本性重构

Conmon(Container Monitor)是 Podman 与 CRI-O 生态中的核心监控进程:它由 OCI runtime 启动容器之后接管容器主进程,负责将容器 STDOUT/STDERR 转发到日志驱动、维护podman attach会话、以及向 Podman 传递容器退出状态。在 libpod/oci_conmon_common.go 中,newConmonOCIRuntime明确将conmonPath与 OCI runtime 绑定,Podman 通过它启动每个容器对应的 Conmon 进程。

原版 Conmon 在近 8 年的服役中暴露出六类严重问题:

问题具体表现
架构局限为近十年前 CRI-O 早期开发快速编写,从未将长期可维护性纳入设计
语言门槛纯 C 语言实现,Podman 维护者中具备 C 经验且愿意投入的人极少
维护停滞因前两点,Conmon 长达约 4 年无人维护,日志相关用户诉求有的积压近 5 年
测试缺失仅有少量单元测试、无集成测试、E2E 测试依赖 CRI-O 测试套件的一部分,曾导致回归问题漏出
错误处理薄弱架构上难以正确处理错误,多数情况下 Conmon 直接退出而不向 Podman 传播错误,容器启动失败时完全没有调试信息
维护者池小重写为维护者熟悉的语言可显著扩大贡献者来源,加快功能开发速度

补充说明:设计文档特别反驳了"Conmon 久经考验、安全稳定"的观感——这种稳定性并非来自代码库质量,而是长期无人维护带来的"静态稳定"。一旦恢复维护并持续合入变更,缺乏测试的问题立刻暴露。文档明确给出底线:若放弃重构、继续沿用旧 Conmon,则补全测试是继续迭代的硬性前置条件,而这项工作量本身已相当可观。

2. 方案对比:为什么不直接增强旧 Conmon、不采用 Conmon-rs

2.1 增强旧 Conmon 仅作备份方案

理论上,现有维护者中 C 语言经验足以支撑一次包含插件化日志的局部重构。设计文档将其定位为备份计划:它无法解决架构、测试、错误处理等根本问题,一旦重新开始加功能,补齐测试的工作量会拖累整个项目。

2.2 Conmon-rs 的三大否决理由

Conmon-rs 是 CRI-O 维护者主导的 Rust 重写版,曾承诺"单 Conmon 管理多容器 + gRPC 远程访问 API"。但其设计存在与 Podman 需求根本冲突的缺陷:

  1. 过于复杂:完整 gRPC API 带来显著功能增量,但 Podman 并不需要其中大部分能力。
  2. 资源占用过高:Conmon-rs 原始设计面向 CRI-O "每 Pod 一个"模型,允许较高资源消耗。但 Podman 必须保持conmon:container = 1:1——这是 systemd 集成与 Quadlet 等核心特性的硬性前提;同时 1:1 隔离确保处理容器内不可信数据的 Conmon 即便被攻破,也只影响单个容器而不扩散。低资源边缘环境是 Podman 的重要使用场景,文档认为不彻底重构 Conmon-rs(并放弃 gRPC 等特性)无法解决资源问题。
  3. 与现有 Conmon 差异过大:接入 Podman 所需改动量成为事实上的采纳障碍。

结论清晰:与其改造 Conmon-rs,不如继续使用原 Conmon——Conmon-rs 的高资源开销是此结论的最大权重。这也解释了文档的最终选择:第二个 Conmon 重构尝试,即 Conmon v3。

3. Conmon v3 详细设计

3.1 语言选型:Rust,但"无 GC、少依赖、低内存"

Conmon v3 的核心约束是不使用垃圾回收语言,以维持与原 Conmon 相近的内存占用(原版估算约600KB/容器)。这一约束排除了 Go 与 Python,现实选项只剩 C 与 Rust;C 的维护者池问题依旧,因此选定Rust

设计决策说明
静态链接代价控制Rust 静态链接意味着每个依赖包都增大二进制体积(虽不直接等同内存,但会拖慢exec调用、增加容器启动时间);策略是尽量少用库,用到时优先体积小、依赖少的
兜底方案若体积仍超标,可移除 Rust 标准库、直接基于 C 标准库构建
内存目标每 Conmon 约1.2–1.5MB,是原版的 2–2.5 倍,但绝对值依然极低
动态分配限制评估使用no_alloc进一步约束动态内存分配,确保运行期内存不增长

仓库佐证:Podman 侧对内存极度敏感——libpod/define/config.go 中默认RLimitDefaultValue为 1048576(nofile/nproc 默认上限),以及边缘场景的长期支持,都印证了"低资源占用"是贯穿性工程约束。

设计文档同时预告了一个独立的后续方案:通过 systemd 直接管理容器、彻底消灭 Conmon,进一步压低 Podman 的常驻内存与 CPU 占用;代价是 attach、日志等部分功能弱化,对边缘用例大体可接受。该方案不在本文档范围,未来数年无论 Conmon v3 是否落地都会推进。

3.2 架构核心:极简事件循环,拒绝异步运行时

为守住资源占用底线,架构上选择直接使用 C 的epollAPI 编写极简事件循环,明确规避 async/await 与 Tokio 运行时,从而做到极少甚至零外部依赖。这一取舍部分抵消了 Rust 在开发体验上的优势,但鉴于 Conmon v3 目标有限、事件循环本身足够简单,文档认为不成问题。

事件循环初期聚焦三类 I/O 转发:

  • 容器STDOUT / STDERR→ 日志驱动 + 所有活跃 attach 会话
  • exec 会话STDIN→ 容器

进程回收(reaping)是否并入事件循环、还是单独线程,留待评估;文档特别建议调研PIDFD替代传统 SIGCHLD 等待,为将来把更多进程(如 exec 会话)纳入同一 Conmon 铺路。

attach 会话上限:原版 Conmon 允许对单容器发起任意数量的podman attach(此特性鲜为人知)。若将上限收敛到约128 个并发 attach 会话,可用静态数组简化代码、利于启用 no_alloc,同时不损失任何已知合理用例。

3.3 安全模型:处理不可信数据的守护进程

Conmon 的职责是处理来自容器内部的不可信数据并转发,因此必须严防恶意容器数据导致崩溃甚至容器逃逸(container escape)。Rust 天然的内存安全在此有用武之地,但仍需谨慎。初版完成后可评估追加安全加固:如降权(dropping capabilities)或引入 Seccomp profile进一步限制 Conmon。

3.4 插件化日志系统(首要目标)

插件化日志是 Conmon v3 的第一大目标,用于兑现 Podman 用户多年积压的日志增强诉求。设计要点如下:

  • 首选动态链接插件,但方向未最终敲定;备选为独立进程 + RPC 通信——后者实现更简单但会增加资源占用,需对比评估(若动态链接收益过小,可能不值得为此投入)。
  • Rust 不原生支持动态链接,需通过FFI 调用 C API。插件 API 可保持极简:只需"初始化(接收任意长度的参数数组)"与"写日志"两类操作。
  • 日志驱动背压(backpressure)虽需考虑,但在事件循环架构下实现难度不大。
  • 初版仅允许同时激活一个日志插件,以兼容现有 Conmon 的 CLI;是否支持多驱动并发留待后续按需重估。
  • 插件不包含 Podman 集成:类似现有none驱动,插件产生的日志无法通过podman logs读取,这与 Docker 的行为一致。

直接内建(非插件)的优选驱动:需要与 Podman 代码协同才能正确展示日志的驱动将直接集成,并享受更充分的测试:

驱动当前仓库中的定义说明
k8s-filelibpod/define/config.go 中KubernetesLogging = "k8s-file"Kubernetes 日志格式,默认驱动
journald同文件JournaldLogging = "journald"写入 systemd journal
passthrough同文件PassthroughLogging = "passthrough"(含passthrough-tty直通容器输出,不落盘
json-file(拟新增)同文件已有JSONLogging = "json-file"常量Docker 兼容 JSON 格式,考虑作为全功能直接支持驱动

仓库印证:libpod/container_log.go 的init()当前注册了k8s-filenonepassthrough三个可读日志驱动;ReadLogjson-file分支标注了 "TODO provide a separate implementation of this when Conmon has support"——正是 Conmon v3 需要补齐的能力。设计文档还提出按用户诉求增强优选驱动,典型如日志轮转(log rotation)

3.5 CLI/API:1:1 复刻,先易后难

Conmon v3 初版 CLI 将逐参数复刻现有 Conmon,使 Podman 现有交互代码零改动即可使用。文档明确这种"完全兼容"不会持续太久:Podman 一旦依赖 Conmon v3,即可逐步提升所需版本,用新功能替换令人生厌的旧接口(如过度冗长的 CLI 参数、每个 exec 会话都要新起 Conmon 的现状)。旧接口先标记弃用,随 Podman 7.0 移除。版本节奏预计与 Podman锁步发布(6.0/3.0、6.1/3.1……)。

仓库印证——Podman 侧现有 Conmon 调用契约:libpod/oci_conmon_common.go 的sharedConmonArgs展示了 Podman 目前传给 Conmon 的核心参数:--api-version 1-c(容器 ID)、-u(CUUID)、-r(runtime 路径)、-b(bundle 路径)、-p(PID 文件)、-n(容器名)、--exit-dir--persist-dir--full-attach-s(systemd cgroup)、-l(日志驱动,k8s-file:<path>journald等)、--log-level--syslog--log-size-max--log-tag--log-label等。日志标签仅允许大写字母、数字、下划线(validJournaldFieldName校验),且仅journald驱动可用。这些接口正是 Conmon v3 初版必须 1:1 兼容的对象。exec 路径在 libpod/oci_conmon_exec_common.go 中则使用-e--exec-attach--exec-process-spec--exit-command等独立参数。

此外,Conmon 通过_OCI_SYNCPIPE_OCI_STARTPIPE_OCI_ATTACHPIPE三个环境变量指定的 Unix socket 对与 Podman 通信(见oci_conmon_exec_common.goExtraFiles组装),readConmonPipeData解析syncInfoJSON(data+message字段)获得容器 PID 与错误信息——这正是文档批评的"错误传播不畅"接口,也是 v3 架构要重点改进之处。Podman 侧还要求 Conmon 满足最低版本校验(libpod/define/errors.go 中ErrConmonOutdatedErrConmonDead等错误专门刻画了 Conmon 异常场景)。

4. 使用场景与发布计划

核心使用场景:改善所有 Podman 容器(并潜在惠及 CRI-O 容器)的日志能力与错误处理。

目标发布版本:Podman 6.0(2026 年春季),届时 Podman 将独占使用新 Conmon,原 Conmon 在 Podman 中正式弃用。

影响面评估

  • CLI:无需任何改动。
  • Libpod:复用现有 Conmon 接口代码,改动最小;仅在扩展日志驱动列表时可能涉及日志选项代码调整。
  • 其他:原 Conmon 在 Podman 中弃用。

Stakeholders:勾选确认的包括 Podman 用户、Podman 开发者、CRI-O;Buildah、Skopeo、存储库、镜像库、Netavark/aardvark-dns 等未受影响。指定 Assignees:@mheon、@ashley-cui、@jankaluza。

5. 未来工作展望(明确不在初版范围)

  • exec 会话合并:让podman exec复用目标容器既有的 Conmon 会话。收益显著——清理带 exec 会话的已停止容器可从"每个 exec 一次podman container cleanup"简化为"每容器一次";代价是需要为 Conmon 暴露创建 exec 会话的 API,增加 v3 复杂度。列为后续研究项。
  • 内建健康检查:将(含 startup 类型的)健康检查并入 Conmon v3,可让无 systemd 环境(如容器内运行 Podman)也能执行健康检查,并省去每容器一个 systemd timer 服务的管理负担。初版非必需,资源增量小,是强候选特性。

6. 测试策略:先补齐旧 Conmon,再验证新 Conmon

设计文档将测试视为 Conmon v3 的必要前置条件(对旧 Conmon 本身也是高价值投资):

  1. 先为原 Conmon 编写 BATS 测试套件,覆盖全部核心功能:日志、attach、exit 文件、cleanup 进程、exec 会话;
  2. 测试必须是E2E,且对旧代码全部通过——成为新旧 Conmon 兼容性的回归基线;
  3. 套件就绪后,同时用于验证 legacy Conmon 与 Conmon v3;
  4. 从 Conmon 仓库移除对 CRI-O、Podman 的直接反向依赖测试,上游专注 BATS 核心行为测试,下游集成验证交由各项目自己负责;
  5. 新增 Rust 代码的关键功能辅以单元测试

选择 BATS 而非 Go 接口,是为了降低外部依赖、兼容多语言,让测试套件本身更易维护、更可移植。

7. 结语:一份"先兼容、再演进"的重构蓝图

Conmon v3 设计文档的精髓可以概括为三句话:

  1. 兼容优先:初版 CLI/API 逐参数复刻现有 Conmon,让 Podman 零改动采纳,将采纳成本压到最低——这正是 Conmon-rs 失败的核心教训;
  2. 资源优先:无 GC、无 async 运行时、极简 epoll 事件循环、控制依赖体积,守住 1.2–1.5MB/实例的内存目标,兼顾边缘场景与 1:1 安全隔离;
  3. 演进留白:exec 会话合并、内建健康检查、插件化多驱动等重磅能力全部推迟到 Podman 6 之后,确保初版尽量简单、可落地。

对于想深入源码的读者,建议按以下路径继续探索当前仓库:Conmon 启动参数契约见 libpod/oci_conmon_common.go,exec 路径见 libpod/oci_conmon_exec_common.go,日志驱动常量与读取逻辑见 libpod/define/config.go 与 libpod/container_log.go,Conmon 异常与版本错误定义见 libpod/define/errors.go。

【免费下载链接】podmanPodman: A tool for managing OCI containers and pods.项目地址: https://gitcode.com/gh_mirrors/po/podman

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询