Podman 被推到台前,理由几乎都是同一句话——daemonless 加 rootless,容器不用 root 跑,更安全。这话没错,但它只讲了收益那一半。真正决定你能不能把业务稳稳跑在 rootless 上的,是另外两件不太有人愿意讲的事:容器网络要走用户态转发,容器存储多数要走 FUSE。两者都在 IO 路径上,小规模试跑几乎看不出差别,一旦连接数、镜像层数、文件数量上来,CPU 和延迟的账单就开始显形。这篇把用户命名空间的 UID 分配、rootless 网络的几条可选路径、存储驱动与文件系统的硬门槛、daemonless 的运维连锁反应拆开讲清楚,顺带给出资源规划画像和避坑清单。
先把结论摆在前面。rootless 解决的是"容器里的 root 就是宿主机上的 root"这个问题,它没有解决"容器 IO 要走更绕的路"这个问题,甚至后者恰恰是它带来的。你不用 root 启动容器,就拿不到在宿主机网络命名空间里建 veth pair、把一端插到网桥上的权限,也拿不到直接挂载 overlayfs 的权限。网络和存储这两条最热的路径,都得绕道用户态。
绕道的方式很具体。网络这一头,rootless 默认由 slirp4netns 在用户态做 NAT(Podman 5 之后,pasta/passt 在不少发行版上成为新的 rootless 默认),容器的包要先交给这个用户态进程,再由它用宿主机的普通 socket 发出去,每包多一次上下文切换和一次数据拷贝。存储这一头,rootful 直接让内核挂 overlay,rootless 在内核不支持非特权 overlay 之前只能挂 fuse-overlayfs,一个用户态文件系统,所有元数据操作都要穿过 FUSE 到守护进程再回到内核。
这两笔开销有个共同特点:它们在低负载时几乎看不出来。跑个静态页面、跑个内部管理后台、跑几个定时任务,你只会觉得 Podman 挺好用,和 Docker 没什么区别。等连接数上到几千、镜像层数堆到十几层、容器里开始大量读写小文件,CPU 的用户态占用曲线就跟 Docker 明显分岔了。所以选型阶段真正要问的不是"要不要上 rootless",而是"我的 IO 模型吃不吃得住这两层绕路"。
下面按这个顺序展开:先讲用户命名空间和 subuid 号段分配(这是 rootless 的地基,分配错了容器根本起不来),再讲网络的几条路径和端口映射的限制,接着讲存储驱动和文件系统的硬门槛,然后讲 daemonless 带来的运维连锁反应,最后给资源规划画像和避坑清单。顺序是有意为之的——地基没打好,后面几节的调优都无从谈起。
rootless 的核心机制是用户命名空间(user namespace)。容器里的 UID 0 并不是宿主机的 UID 0,而是被映射到宿主机上一个普普通通的高位 UID。你在容器里执行 id 看到的是 root,在宿主机上 ps 看同一个进程,属主可能是 100000 之类的某个普通用户。容器逃逸时拿到的是这个普通 UID,动不了宿主机的系统文件,这就是 rootless 安全收益的来源。
映射规则写在两个文件里:/etc/subuid 和 /etc/subgid,格式是"用户名:起始ID:数量"。比如 deploy:100000:65536,表示给 deploy 这个用户分配了 100000 到 165535 这一段、共 65536 个从属 ID。容器里要映射多少个 UID/GID,就要从这个池子里划多少出来。真正执行映射的是 newuidmap 和 newgidmap 这两个 setuid 辅助程序(RHEL 系里通常由 shadow-utils 或 uidmap 包提供,Debian 系是 uidmap 包),没装这两个东西,rootless 容器会在启动阶段直接失败,报错信息还常常指不到根因。
数量不够是最常见的报错来源。一个基础镜像如果里面有多个系统用户、文件属主散布在几百个 UID 上,或者镜像层里带了需要保留属主信息的 setuid 文件,Podman 就需要足够的 ID 才能把这些属主一一映射过去。池子只有 65536 而镜像需要更多,你会看到类似 no subuid ranges found 或映射数量不足的报错。这类报错的迷惑性在于:小镜像一点问题没有,某天拉了个大镜像就起不来,很容易被误判成镜像损坏或者仓库故障。
规划上给个经验值:单个交互用户给 65536 通常够用,但如果这台机器要跑多层构建、要跑带大量系统用户的镜像、或者一个用户要同时跑很多容器,建议直接给到 131072 甚至更大。给大了没有浪费,这些是 ID 号段不是内存;给小了才会卡脖子。改完 subuid/subgid 之后别忘记一件事:已有用户的容器存储需要按新范围改写属主,Podman 提供的 podman system migrate 就是干这个的,扩容 ID 范围之后必须跑一遍,否则老容器会因为属主对不上而出各种奇怪的权限问题。
一台机器上跑多个非 root 用户各自的 Podman,是 rootless 最典型的落地形态——研发 A 和研发 B 各用自己的账号起容器,互相看不到对方的东西。前提是每个用户的 subuid/subgid 号段互不重叠。号段重叠会导致两个用户映射到同一段宿主机 UID,跨用户的文件权限判断立刻乱套,而且这种乱套是间歇性的,排查起来非常费时间。
切法很简单但要提前做:按用户顺序递增分配,比如 deploy01 拿 100000:65536,deploy02 拿 165536:65536,deploy03 拿 231072:65536,以此类推。用 useradd 创建用户时,系统通常会自动分配一段并写进 subuid/subgid,但如果你批量建用户、或者用配置管理工具统一下发,一定要显式检查这两个文件,别指望它一定帮你排好。uidmap 包提供的 usermod --add-subuids 也可以事后补。
还有两件事要一起想清楚。一是容器存储目录,rootless 默认落在 ~/.local/share/containers,每个用户一份,镜像缓存不共享——十个人拉同一个两 GB 的基础镜像就是二十 GB。要么接受这个冗余换隔离,要么用一块共享存储配合 podman --root 指向自己的子卷,代价是失去一部分隔离性。二是磁盘配额,rootless 容器写在用户家目录下,家目录所在的分区如果没有配额或者子卷限额,某个用户把镜像缓存堆满就能把整台机器拖死。用 xfs 的 project quota,或者 btrfs/LVM 子卷给每个用户设上限,比事后清理省事得多。
Docker 那套大家熟悉的模型是:daemon 有 root,为每个容器建一对 veth,一端塞进容器的网络命名空间,另一端挂到 docker0 这个网桥上,容器之间的转发和出网 NAT 全在内核里完成。rootless 没有这个权限——创建 veth pair 并挂到网桥上需要 CAP_NET_ADMIN,而且要在宿主机的网络命名空间里操作,非特权用户拿不到。
于是 rootless 换了个思路:先创建一个用户命名空间兼网络命名空间,在里面用 TAP 设备跟一个用户态进程对话,由这个用户态进程代替内核做 NAT 和转发。这个进程就是 slirp4netns。Podman 5 之后,pasta(passt 的用户态实现)在不少发行版上成为新的 rootless 默认,思路类似但实现更轻:pasta 默认不做地址转换,直接把容器的地址和宿主机地址放在同一网段上透传出去,省掉了 NAT 这一步的开销。两者本质上都还是用户态转发,只是 pasta 少绕一道。
代价是明摆着的。包从容器网卡出来,先到 TAP,被用户态进程读走,处理完再用宿主机的普通 socket 发出去——两次用户态与内核态之间的切换,还有至少一次数据拷贝。内核态的 bridge 加 veth 路径是在软中断里把包从一端挪到另一端,几乎不占进程 CPU。这两条路的吞吐和 PPS 差距不小,尤其是小包、高并发短连接的场景,先把用户态进程打满的往往是 PPS,而不是先把网卡带宽打满。
换句话说:如果你的服务是少量长连接、吞吐为主——比如内部的文件同步、批处理任务、定时导出——slirp4netns 的损耗通常可以吃下;如果是大量短连接、请求响应很小很密——比如 API 网关、反向代理、高并发的 Web 前端——这条路径会成为瓶颈,而且它消耗的 CPU 会算在你自己的容器账上,不是白给的。
rootless 容器发布端口,表面上和 Docker 一样都是 -p 8080:80,底下干的事情完全不同。Docker 是在内核的 iptables 或 nftables 里加一条 DNAT 规则,非特权用户是在宿主机上起一个用户态监听进程占住 8080,收到连接再转发进容器。这就带出两个限制,一个是权限上的,一个是性能上的。
权限上的限制是低端口。Linux 有个 sysctl 参数 net.ipv4.ip_unprivileged_port_start,默认值是 1024,意思是非特权进程不允许 bind 1024 以下的端口。所以 rootless 容器想直接发布 80 或 443,默认是被拒绝的。改法有几种:把这个 sysctl 调到 0(sysctl -w net.ipv4.ip_unprivileged_port_start=0,再写进 /etc/sysctl.d 持久化),这是最直接的一种;或者不让 Podman 直接占低端口,前面挂一层有 root 的 nginx 或者 systemd socket activation 做反代,容器只监听高位端口。我更推荐后者——改动小,也不需要为了一个容器去放宽整机的默认安全基线。
性能上的限制是转发本身。既然是用户态进程在转发,那么每一个连接的建立、每一段数据的搬运都要过这个进程。连接数高的时候,这个进程自己就是热点。这也是为什么前面说高并发短连接要谨慎:端口发布这条路径是叠加在网络出口那条路径之上的,两处用户态转发会一起吃掉 CPU,而且你很难从容器的 CPU 统计里把这部分剥离出来,看上去像是应用本身变慢了。
先把能吃的列出来。跑批处理、定时任务、数据清洗这类不追求网络极限的负载,rootless 的默认网络完全够用。开发机、CI runner、内部工具、需要多用户隔离的单机环境,也基本无感。还有一类是"出网为主、入网很少"的负载,比如爬虫、数据同步、构建任务,损耗集中在出口,量不大就无所谓。这类业务用 rootless 换安全,性价比很高。
必须换方案的是这几类。第一类是网关型和代理型服务,连接数上万、小包密集,用户态转发的 PPS 天花板会先到。第二类是要跑满千兆以上带宽的服务,用户态转发在吞吐上同样有上限,而且上限随 CPU 主频和核数波动,不容易预测。第三类是容器间需要大量二层通信、或者依赖组播与广播的服务,用户态 NAT 网络对这些的支持不完整。第四类是要求容器拿到宿主机同网段真实 IP 的服务,比如某些按 IP 授权的商业中间件。
换的路径有三条。一是干脆放弃 rootless,用 rootful Podman 或 Docker,拿回 bridge 加 veth 的内核路径,这是最简单也最有效的一种。二是保留 rootless,但改由 rootful 预先建好网络(用 podman network create 配合 netavark 建好,再让 rootless 用户加入),实际部署里这类混合方案少见,配置复杂度和收益不太成正比。三是用 macvlan 或 IPVLAN 让容器直接拿到二层或三层地址,绕开 NAT——不过 macvlan 在 rootless 下同样受权限约束,通常还是得由 root 来建,等于把特权收回一部分。别指望存在既不 rootful 又要内核级网络性能的免费午餐。
容器的分层镜像靠的是叠加文件系统。rootful 场景下 Podman 直接用内核的 overlayfs,lowerdir、upperdir、workdir 几个目录一挂,内核在 VFS 层把多层合并成一个视图,读写全部在内核里完成。rootless 场景在很长一段时间里没这个待遇,因为挂载 overlayfs 需要特权,非特权用户挂不了,只能找替代。
替代方案是 fuse-overlayfs,一个用 FUSE 实现的用户态叠加文件系统。它的目录结构和 overlayfs 一样,但所有元数据操作——lookup、getattr、open、mkdir——都要从内核 VFS 穿到 FUSE 内核模块,再唤醒 fuse-overlayfs 进程,处理完原路返回。一次 stat 在内核 overlay 里是一次 VFS 查找,在 fuse-overlayfs 里是一次完整的用户态往返。单次开销是微秒级,架不住次数多:一个 Java 应用启动要访问几万个 class 文件,一次依赖安装要创建几十万个小文件,乘起来就是实打实的秒级差距。
好消息是内核已经在补这块。Linux 5.11 起,overlayfs 允许在用户命名空间内挂载,Podman 对应的做法是给 overlay 加上 userxattr 选项,把原本存在 trusted.overlay.* 里的扩展属性改存到 user.overlay.*,绕开非特权用户写不了 trusted xattr 的限制。满足内核版本、文件系统支持、以及发行版打包的 Podman 版本这几个条件之后,rootless 也能用上原生的内核 overlayfs,Podman 会自动选择,通常不需要手动指定驱动。
但这件事有几个前提要逐一核对。一是内核版本,5.11 是门槛,生产上我更建议 5.15 LTS 或更高,因为某些文件系统上的支持铺得更晚;二是底层文件系统,不同文件系统对"在用户命名空间里挂 overlay"的支持进度不一样,xfs 上的要求比 ext4 更苛刻,社区里常见的说法是要更晚的内核才铺开;三是别照抄文档,直接在你的目标镜像和目标内核上验证一遍——podman info 的输出里能看到当前实际生效的存储驱动是 overlay 还是 fuse-overlayfs,看这一行比看任何教程都准。
不管 rootful 还是 rootless,overlay 都依赖扩展属性来记录文件的层信息。xfs 早期版本的文件格式里,inode 上没有记录文件类型(file type)的字段,overlay 在部分操作上要靠额外查找来补,正确性和性能都受影响。这就是 ftype=1 的由来:格式化时开启这个选项,xfs 会把文件类型写进 inode,overlay 才能正常工作。
这个坑狠在隐蔽。一块 xfs 盘如果是在没开 ftype 的系统上格式化的(一些老镜像、老安装器默认没开),你在上面建容器存储,轻则 Podman 直接报 overlay requires ftype=1 拒绝启动,重则在旧版本上退化成别的行为,跑着跑着出现莫名其妙的属主和权限问题。检查方法很简单:xfs_info 挂载点,看输出里有没有 ftype=1。没有的话,没有平滑补救办法,只能备份、重新格式化、恢复。
所以选型阶段就把这件事定死。用 xfs 就要在 mkfs 时显式加上 -n ftype=1,并在交付验收时核对一遍;用 ext4 则没有这个烦恼,小文件性能和稳定性也够,代价是在超大目录、超大文件、并行写入密集的场景下不如 xfs。选哪个看负载:镜像多、层多、大量小文件,ext4 用起来省心;单盘容量大、并发写多、需要 project quota 做用户级配额,xfs 更合适,但一定记得开 ftype=1。
还有一层是容器存储的落盘位置。rootless 默认在用户家目录下,家目录如果在一块小系统盘上,而数据盘是另一块没开 ftype 的 xfs,就会出现"系统盘上跑得好好的,迁到数据盘就报错"的情况。把容器存储单独挂到一个已经核对过参数的文件系统上,比事后排查便宜得多。这一条在做磁盘分区规划的时候顺手就做了,别等到扩容才想起来。
如果你的环境最终还是落在 fuse-overlayfs 上(老内核、或者文件系统不满足原生 overlay 的条件),就得把它的 CPU 开销当成一笔预算来规划,而不是当成一个可以忽略的常数。这笔开销有三个放大器:文件数量、镜像层数、以及元数据操作的密度。
文件数量是第一放大器。FUSE 的开销是按操作次数收的,不是按字节收的。顺序写一个 10GB 的大文件,走 FUSE 和走内核 overlay 差别不大;创建十万个几 KB 的小文件,差距就非常明显——每个创建都要来一次用户态往返,还要叠加 lookup 和 getattr。前端项目的依赖目录、Java 的依赖树、Python 的包目录,都是典型的小文件密集型负载,也是最容易在这条路径上翻车的负载。
镜像层数是第二放大器。每次文件查找,overlay 都要从最上层往下逐层找,层数越多查找链越长,而每一层的查找在 FUSE 下都是一次用户态往返。一个二十几层的镜像和一个五层的镜像,在 fuse-overlayfs 下的元数据开销不在一个量级。这也是为什么构建镜像时要认真合并 RUN 指令——在 rootful 下这只是"镜像瘦一点"的优化,在 rootless 加 FUSE 下它直接换算成 CPU。
缓解手段有几个值得一试。一是开启 metacopy,让元数据操作走 FUSE、数据读写尽量直通底层文件,减少大文件的拷贝开销;二是把构建过程和运行时分开,构建放在 rootful 机器或者 CI 机器上做,交付到生产时镜像层数已经压到最少;三是如果负载确实重,评估一下能不能升内核换到原生 rootless overlay,这通常比任何调参都有效。至于 CPU 该留多少,没有统一答案,取决于你的小文件密度,规划时按"FUSE 至少要吃掉一到两个核的量级"来预留比较稳妥,上线后看容器的 CPU 曲线再收敛。
下面这张表把 rootful 和 rootless 在这几个关键技术点上的路径差异摆在一起,方便你对着自己的负载逐行核对。
| 技术点 | rootful 走的路径 | rootless 走的路径 | 性能/运维影响 | 什么时候必须注意 |
|---|---|---|---|---|
| 容器出网与转发 | veth pair 挂 bridge,内核软中断转发 + NAT | slirp4netns 或 pasta 在用户态做 NAT / 透传 | 每包多一次用户态往返与拷贝,PPS 和吞吐上限低于内核路径 | 网关、代理、高并发短连接、需要跑满千兆以上带宽 |
| 端口发布 | 内核 iptables / nftables 加 DNAT 规则 | 用户态进程占用宿主机端口再转发进容器 | 连接建立也要过用户态,高连接数时该进程成为热点 | 要直接监听 80/443 等 1024 以下端口 |
| 镜像层叠文件系统 | 内核 overlayfs 直接 mount,VFS 层合并 | fuse-overlayfs 用户态;新内核可用原生 rootless overlay | 元数据操作多一次用户态往返,小文件密集时 CPU 明显上升 | 大量小文件、多层镜像、构建型任务与 CI |
| 底层文件系统门槛 | ext4 或 xfs(ftype=1)均可正常挂 overlay | 用户命名空间内挂 overlay,对内核与文件系统更挑剔 | 条件不满足会退回 fuse-overlayfs,或直接拒绝启动 | 格式化阶段未开 ftype=1、内核版本过旧 |
| 容器守护与开机自启 | dockerd / containerd 常驻,restart policy 立即生效 | 无守护进程,靠 systemd user unit + linger 或 Quadlet | 未开 linger 时用户登出或重启后容器不会起来 | 生产容器要求随机器启动、要求崩溃自动拉起 |
| 镜像与容器存储位置 | /var/lib/containers 统一存放并共享缓存 | ~/.local/share/containers,每用户一份,subuid 分段 | 镜像缓存不共享,磁盘与页缓存出现多份副本 | 单机多用户多租户、磁盘容量与配额规划 |
| 特权与能力边界 | 可 cap-add、可挂设备与内核文件系统 | 受用户命名空间限制,部分能力与挂载拿不到 | 个别负载跑不起来,或需要额外 sysctl 放行 | 需要挂载块设备、NFS、特殊内核文件系统的场景 |
Podman 没有 dockerd 这样的常驻守护进程,这是它的设计选择。好处是少一个需要提权的攻击面,也少了一个"一旦挂掉全体容器失联"的单点。代价是:所有原本由后台服务统一负责的事情,都要你自己找替代方案。最典型的一件就是开机自启。
Docker 里这件事的答案是 restart: always,写进 compose 文件就完事了,因为总有个 daemon 在盯着容器状态。Podman 里没有这个角色,容器是你手动或者有脚本拉起来的,机器重启之后没人记得把它拉回来。替代方案是 systemd:给容器生成一个 systemd unit,交给 systemd 去管。但 rootless 下用的是 systemd 的 user instance(systemd --user),不是系统级那个,这就引出下一节要说的一串坑。
除了自启,还有几件事也变了。日志没有统一的 daemon 收集,要看容器的标准输出得配合 journald 或者自己接日志驱动;没有 daemon 就没有统一的 API 端点,远程管理要走 podman system service 暴露的 socket,或者干脆走 ssh;健康检查、资源统计这些原本 daemon 顺手做了的事,都要靠 systemd 或外部监控补上。这些不是不能做,是都要有人做——评估 Podman 的时候把这部分运维工作量算进去,别只看它"命令和 Docker 几乎一样"这一面。
systemd 的 user instance 有个默认行为:用户登出之后,这个实例就会被清理掉,它管的服务跟着停。对桌面会话来说这很合理,对跑容器来说就是灾难——你 ssh 上去起了容器,断开连接,容器没了。解决办法是 loginctl enable-linger 用户名,让这个用户的 systemd user instance 在开机时就启动并且长期驻留,不依赖任何登录会话。没做这一步,后面所有自启配置都是白搭。
第二步是生成 unit。podman generate systemd --new --name 容器名 可以把一个已有容器导出成 systemd unit 文件,放到 ~/.config/systemd/user/ 下,再用 systemctl --user enable --now 跑起来。这里有几个细节会咬人:不加 --new 生成的是依赖容器 ID 的 unit,容器重建之后 ID 变了,unit 就失效了,所以要加 --new,让它每次都用 podman run 新建一个容器;unit 文件的命名和容器名要保持一致,否则时间一长自己都分不清;systemctl --user 必须在正确的用户上下文里执行,用 sudo -u 切换用户经常会丢掉 XDG_RUNTIME_DIR 和 DBUS_SESSION_BUS_ADDRESS 这两个环境变量,导致命令报找不到用户总线。用 machinectl shell 或者 su - 登录式切换会更稳。
再说一个版本演进上的坑。podman generate systemd 在 Podman 4.x 之后已经不推荐使用,官方主推的是 Quadlet:你写一个 .container 文件(语法接近 systemd unit,用 Image、Network、Volume 这些字段声明),放进 ~/.config/containers/systemd/,systemd generator 会自动把它转成对应的 unit。Quadlet 的好处是声明式、可以纳入版本管理、重建容器不会像 --new 那样留下旧容器残留。新部署建议直接上 Quadlet,老脚本再慢慢迁。
还有一个容易忽略的时序问题:/run/user/UID 这个目录是 user instance 的运行目录,它的生命周期和 linger 绑定。如果你在容器里挂载了依赖这个路径的东西,或者用了 podman system service 的 socket,机器重启后 socket 路径还没建好就被访问,会出现启动失败。用 systemd 的 After 和 Requires 把顺序定死,别依赖运气。
镜像层面基本不用操心。Podman 用的是 OCI 标准镜像格式,Docker Hub、Quay、各大云厂商的镜像仓库都能直接拉,docker build 出来的镜像 Podman 能跑,反过来也成立。Dockerfile 的语法兼容性很高,Podman 背后用 buildah 做构建,对绝大多数指令都支持,少数依赖 daemon 特性的指令在行为上有差异,但日常使用基本碰不到。
Compose 这块要说清楚。Podman 早期靠第三方的 podman-compose,兼容度参差不齐,复杂一点的 compose 文件经常踩坑。后来 Podman 提供了 podman compose,走的是调用 docker-compose 二进制、通过 socket 与 Podman 通信的路子,兼容度好了很多,但依然不是百分之百,尤其是用到依赖 daemon 行为的字段时。我的建议是:简单的一两组服务可以直接用,要上生产就把 compose 文件认真验一遍,或者干脆用 Podman 原生的 pod 概念(podman pod create)重写一遍,长期看后者更省心。
Kubernetes 方向反而是 Podman 的一个亮点。podman play kube 可以直接吃一份 K8s 的 Pod 或 Deployment YAML,在本机把容器组跑起来;podman generate kube 则可以把本机跑着的一组容器导出成 K8s YAML。这条链路对"本地开发用 Podman、生产跑 K8s"的团队很友好,能显著降低本地与线上环境不一致的问题。需要说明的是,K8s 生态里 containerd 和 CRI-O 是运行时层的选择,Podman 是单机层的工具,两者不在同一个层次上,也不冲突。
Podman 明显更合适的场景是这几种。单机多用户:几个研发共用一台机器,各自 rootless 跑容器,彼此隔离,谁也动不了别人的东西,这在 Docker 下要么全给 sudo、要么装 rootless docker,都不如 Podman 自然。安全合规要求高的环境:审计条款里明确写了"容器不得以 root 运行",rootless 是最直接的答案,而且不用解释。资源受限的边缘节点:少一个常驻 daemon 就少一份内存和进程开销。还有一类是已经全面 systemd 化的团队,容器用 unit 管理反而比 daemon 更符合他们既有的运维习惯。
不适合的方向同样清楚。要跑大规模 K8s 集群的,Podman 不是运行时层的答案,那一层是 containerd 和 CRI-O 的地盘,Podman 只在开发调试环节有用。要极致网络性能的,比如前面说的网关、代理、高并发接入层,rootless 的用户态转发路径会拖后腿,要么改 rootful,要么把这一层从 rootless 里剥离出去交给别的方案。重度依赖 Docker 生态工具的,比如某些只认 dockerd API 的 CI 插件、某些图形化管理面板,迁移成本会超出预期。还有一种情况容易被忽略:团队里没人愿意啃 subuid、linger、Quadlet 这些概念。技术选型要考虑人的因素,运维复杂度的账最终是人来还的。
下面是一份基于典型部署思路的规划参考,并非特指某一真实客户的配置,具体数值要在你自己的负载上校准。
CPU 这一项要在应用本身的用量之上单独加两块。第一块给 FUSE:如果存储驱动落在 fuse-overlayfs,按小文件密度预留一到两个核的量级,元数据密集的负载(构建、依赖安装、日志轮转频繁的服务)在此基础上往上加。第二块给网络:slirp4netns 或 pasta 是用户态进程,它的占用随 PPS 线性上升,连接数高的服务要按峰值 PPS 估,而不是按平均带宽估。两块加起来,一台跑十几个 rootless 容器的机器上留出两到四个核给"非业务"开销,是常见的规划口径。启用原生 rootless overlay 之后,第一块可以显著收回。
内存方面,rootless 本身没有额外的大额开销,FUSE 和网络转发进程各自是几十到几百 MB 的量级。真正要留意的是页缓存:rootless 容器的读写同样走页缓存,多个用户各自拉一份相同的基础镜像,缓存是各自独立的,同一份数据在内存里可能有多份副本。内存规划时按"业务用量 + 每个用户实例的系统开销"来算,别指望缓存共享能帮你省出空间。
磁盘 IO 是最容易被低估的一项。rootless 下的镜像层访问在 FUSE 路径上会放大 IOPS——一次逻辑上的文件访问可能触发多层查找,每一层都是真实的磁盘元数据操作。所以选盘时不要只看顺序带宽,要看 4K 随机读和元数据操作的 IOPS。NVMe 是这类负载的合理起点,SATA SSD 在小文件密集的场景下会明显吃力。容量上,镜像缓存按"用户数 × 平均镜像体积"估算,考虑到缓存不共享,这个数字通常比你习惯的大。
前面提过,每个用户的容器存储默认在 ~/.local/share/containers,天然隔离但镜像不共享。做规划时要在"隔离"和"空间"之间选一个落点,两个极端都不好。
偏隔离的做法:每个用户一个独立子卷(btrfs subvolume 或者 LVM 逻辑卷),家目录挂在这个子卷上,用配额设上限。好处是空间可控、故障域隔离、清理起来干净利落;代价是磁盘会有重复占用。偏共享的做法:建一个共享的镜像存储,让多个用户通过 podman --root 或 containers.conf 里的配置指向它,好处是省空间、拉取快,代价是权限模型变复杂,而且共享存储一旦被写坏,影响面是所有用户。
折中方案是分开处理镜像和容器:镜像层用共享的只读缓存(比如配置额外的 image store),容器可写层留在各自子卷里。这样既避免重复下载,又保住了运行时的隔离。不过这套配置对版本一致性和管理能力的要求更高,小规模团队未必值得上。我的建议是:人少、镜像不大就直接各用各的,简单可靠;人多了再上共享缓存,别一开始就追求完美方案。
还有一条别忘了:给每个用户设 CPU 和内存的约束。rootless 不等于资源隔离,一个用户跑失控的容器照样能把整台机器的 CPU 和内存吃光。用 systemd 的 slice 给每个 user instance 设 CPUQuota 和 MemoryMax,或者用 cgroup v2 的层级直接限。这一步不做,多用户方案就是纸糊的——安全边界划得再清楚,一次资源耗尽照样全员陪葬。
把上面的约束反过来推,一台适合跑 rootless Podman 的服务器,配置要求其实挺明确。CPU 核数要多留出两到四个核的余量给 FUSE 和网络转发,别按业务峰值刚够来买;内存按"业务用量加每用户实例开销"估算,并且假设镜像缓存不共享;磁盘优先 NVMe,容量按用户数乘以平均镜像体积再加容器可写层的增长来算,文件系统要么 ext4、要么明确开了 ftype=1 的 xfs;内核版本要新,5.15 LTS 或更高,这样才有机会用上原生 rootless overlay,把 FUSE 那一层开销直接省掉。
还有一条容易被采购环节忽略的:如果你打算在这台机器上再套一层虚拟化(比如把 rootless 容器跑在虚拟机里做租户隔离),就要确认机型支持嵌套虚拟化,并且宿主机内核足够新、能放开相应的 sysctl。这一点在下单前问清楚,比到手之后再折腾便宜得多。像一万网络(idc10000.net)这样深耕 IDC 19 年(成立于 2007 年)的服务商,可以提供支持嵌套虚拟化与高内核版本的机型,深圳自营机柜、最快 1 分钟上架,7×24 中文工单、平均 5 分钟响应,部署环境这类事提工单就能对接。价格需实时询价,价格主要受 CPU、内存、存储、带宽和 IP 影响。
采购之外还有一句话:先用月付或者短期方案把配置跑一遍再转长期。rootless 的很多问题——subuid 够不够、FUSE 吃多少 CPU、网络 PPS 顶不顶得住——只有真实负载跑起来才知道。上来就签一年,等发现内核版本不对或者文件系统没开 ftype=1,改起来代价就大了。
为什么坑:系统给每个用户默认分配的从属 ID 通常是 65536 个,日常小镜像完全够用,于是没人再去管它。等某天拉了一个带大量系统用户、属主散布很广的大镜像,映射数量不够,容器在启动阶段直接失败,报错信息还常常指不到 subuid 上,排查方向很容易跑偏。
怎么避:建机器时就按"一个用户至少 131072 个 ID"来分配,写进配置管理工具统一发放,不要依赖 useradd 的默认值。改完之后用 podman system migrate 重写已有容器存储的属主。多用户场景下务必确认号段不重叠。
为什么坑:命令行几乎一样,-p 用法也一样,小规模试跑时延迟和吞吐都正常,于是想当然认为两者等价。等连接数上来,用户态转发进程先被打满,表现为"应用变慢"而不是"网络打满",很容易误判成应用层问题,往错误方向优化。
怎么避:选型阶段就把业务的连接模型摊开看:峰值并发连接数、平均包大小、峰值 PPS。短连接密集或要跑满带宽的服务,直接走 rootful 或把这一层剥离出去。留下来的业务也要在峰值时段观察转发进程的 CPU 占用。
为什么坑:这块盘可能是老镜像格式化的,也可能是扩容时随手加的数据盘,ftype 没开。容器存储建在上面,轻则 Podman 拒绝启动,重则在旧版本上跑出莫名其妙的属主和权限问题,而且没有平滑补救办法,只能备份重来。
怎么避:格式化时显式加 -n ftype=1;验收时用 xfs_info 逐块盘核对一遍,把结果记进交付文档;容器存储单独挂到一个已经核对过参数的文件系统上,不要跟家目录混在一块盘。
为什么坑:systemd user instance 默认随用户登出而清理。很多人配好了 unit、enable 也做了,测试时因为自己一直登录着,一切正常;机器重启之后没有任何登录会话,容器一个都没起来,而且监控告警往往指向业务而不是 systemd。
怎么避:每个要跑容器的用户都执行 loginctl enable-linger,并把这一步写进装机脚本。验证方式要够狠:直接重启一次机器,看容器是否按预期起来。用 Quadlet 管理容器,别再用 generate systemd 的老路子。
为什么坑:rootless 解决的是 UID 映射问题,不等于所有限制都解开。绑定 1024 以下端口受 ip_unprivileged_port_start 限制,容器内 ping 受 ping_group_range 限制,挂载块设备、某些内核文件系统、NFS 这类操作依然拿不到权限。以为"rootless 全能",部署到一半才发现卡住。
怎么避:上线前把业务需要的能力列成清单逐条验证:端口、协议、挂载类型、需要的 cap。能放行的 sysctl 用配置文件持久化,放不了的就换架构(比如低端口交给前置反代)。
为什么坑:rootless 容器默认写在 ~/.local/share/containers,家目录通常没有配额。某个用户反复拉镜像、构建镜像,缓存和中间层把分区塞满,整台机器上的所有用户一起受影响,连 ssh 登录都可能出问题。
怎么避:每个用户一个独立子卷并设配额上限(xfs project quota 或 LVM/btrfs 子卷);镜像和容器分开存放;定期清理无用镜像写进例行任务。别把配额这件事留到出事之后。
没有放之四海皆准的数字,因为损耗取决于包的大小和频率,而不是带宽。长连接、大包、吞吐为主的负载,用户态转发多出来的一次拷贝和上下文切换摊薄在大数据块上,占比很小,多数业务无感。短连接、小包、PPS 高的负载就不一样了,瓶颈会先在转发进程的 CPU 上出现。判断方法很直接:估算你的峰值 PPS,再观察上线后转发进程的 CPU 占用,如果它长期在单核的百分之几十以上,就该考虑换方案了。
默认不能。net.ipv4.ip_unprivileged_port_start 这个 sysctl 默认值是 1024,非特权进程 bind 不了低端口。两种改法:一是把这个值调成 0 并写进 /etc/sysctl.d 持久化,容器就能直接发布 80;二是别让容器碰低端口,前面挂一层有 root 的 nginx 反代过去,容器只监听高位端口。我更倾向第二种,改动小,也不用为一两个容器去放宽整机的默认安全基线。
不一定,要看 IO 模型。它的开销是按操作次数收的,不是按字节收的。顺序读写大文件、文件数量少的服务——比如跑数据库的数据目录放在卷上、跑一个静态资源服务——差距很小,几乎可以忽略。真正吃亏的是元数据密集型负载:大量小文件的创建与查找、层数很多的镜像、频繁的依赖安装和构建。同样是 fuse-overlayfs,跑一个 Go 编译出来的单文件服务,和跑一个依赖树庞大的前端构建,体感完全是两回事。
技术上能装,但我不建议在同一台生产机上长期共存。两者都要操作 iptables/nftables、都要管 cgroup、都用 overlay,混在一起出问题很难定位是谁的锅,而且 Docker 的 daemon 会改变系统的一些默认参数。真有过渡需求,可以在开发机上并存做验证,生产环境还是分开机器或者干脆完成迁移。镜像层面不用担心,OCI 格式是通用的,两边拉同一个 tag 拿到的是同一份内容。
三步,缺一不可。第一步 loginctl enable-linger 用户名,让这个用户的 systemd user instance 开机就启动、不依赖登录会话;第二步写 unit,推荐用 Quadlet 的 .container 文件放进 ~/.config/containers/systemd/,让 generator 自动转 unit,比 podman generate systemd 更好维护;第三步 systemctl --user daemon-reload 再 enable --now。验证一定要重启一次机器,别只看当前会话里 systemctl status 是 active 就以为完事了。
单用户 65536 是常见默认值,对简单的单用途容器够用。但如果要跑多层构建、跑带大量系统用户的基础镜像、或者一个用户同时跑很多容器,建议给到 131072 甚至更大。这些是 ID 号段不是内存,给大了没有任何资源代价,给小了才会在某次拉大镜像时突然卡住。多用户场景按用户顺序递增分配、确保号段不重叠,改完记得跑 podman system migrate。
没有硬上限,真正的约束是资源而不是用户数。每个用户都会带来一份独立的镜像缓存(磁盘)、一份独立的页缓存(内存)、一个 systemd user instance 和一组转发进程(CPU 与进程数)。按经验,一台十六核、六十四 GB 内存的机器上跑五到十个活跃用户是比较舒服的区间;超过这个量级,镜像缓存的重复占用和转发进程的 CPU 竞争会开始明显。关键动作是给每个用户设配额和 CPU/内存上限,否则数量再多也守不住。
它显著提高门槛,但不是银弹。rootless 把容器里的 root 映射成宿主机上的普通 UID,逃逸出来拿到的是普通权限,动不了系统文件和其它用户的文件——这一层防护是实打实的。但它防不住内核漏洞本身,也防不住用户自己的家目录被破坏,更不解决资源耗尽的问题。正确的理解是:rootless 把"一次逃逸等于整机失守"降级成"一次逃逸影响一个普通用户",配合 cgroup 限额、只读根文件系统、最小能力集一起用,才有意义。
问:小规模试用完全没感觉,是不是说明开销可以忽略?不完全是。低负载时用户态转发和 FUSE 的单次开销被摊薄在空闲 CPU 里,确实看不出来。这两笔开销是按次数收的——按包的个数、按元数据操作的次数——所以它们随并发和文件数量线性增长,而不随带宽或数据量的增长而摊薄。判断要不要紧,看的是你的峰值的"次数",不是平均的"流量"。
问:那什么量级该开始警惕?经验上有两个信号值得盯。一是单核上转发进程或 fuse-overlayfs 的占用长期超过三成,说明这条路径已经从"顺带跑跑"变成"主开销之一";二是业务的 P99 延迟在低峰和高峰之间拉开明显差距,而应用本身的 CPU 并没有同步上升,多出来的部分多半就在 IO 路径上。出现这两个信号,就该考虑升内核换原生 overlay,或者把网络这一层换成 rootful 桥接。
问:有没有一劳永逸的做法?有,但代价是放弃一部分 rootless 的纯粹性:把存储升级到原生 rootless overlay(新内核加合适的文件系统),把网络这一层交给有 root 的桥接或者 macvlan。做完这两件事,rootless 剩下的开销就很小了,而你依然保留了 UID 映射带来的安全收益。真正需要权衡的是"网络要不要收回特权"这一件,答案取决于业务的连接模型。
本文涉及的内核版本门槛、存储驱动选择逻辑、subuid/subgid 分配格式、slirp4netns 与 pasta 的适用范围、ftype 相关要求等,均以 Podman 与 Linux 内核的公开文档口径为准,实际生效情况请以目标机器上 podman info 的输出、xfs_info 的结果、以及 /etc/subuid 与 /etc/subgid 的实际内容为准,不要照抄文档结论。文中给出的 CPU、内存、磁盘容量规划区间属于典型部署思路下的估算参考,并非特指某一真实客户的配置,请在自己的负载上校准。
一万网络相关机型、上架与工单服务能力,可查阅 https://www.idc10000.net/ 的服务器租用与 GPU 定制页面;价格需实时询价,价格主要受 CPU、内存、存储、带宽和 IP 影响,具体以签约时最新报价与合同为准。
Copyright © 2013-2020 idc10000.net. All Rights Reserved. 一万网络 科技有限公司 版权所有 深圳市科技有限公司 粤ICP备07026347号
本网站的域名注册业务代理北京新网数码信息技术有限公司的产品