Docker的轻盈神话:我在生产环境被坑了三次后,部分叛逃到Podman

🔑 关键词:Docker,Podman,rootless,容器运行时,容器安全

📖 摘要:作者基于五年Docker使用经验,对比Docker与Podman在架构、安全、网络和系统集成上的差异,结合具体事故和实测数据,指出Docker的daemon中心化设计并非全无代价,适合的场景也不同。

第一次想骂街:daemon挂了,我养的容器全嗝屁了

图片

2018年,我像大多数人一样,被Docker那句“Build once, run anywhere”洗脑,然后屁颠屁颠把公司所有服务都容器化。那时候每天敲docker run -d -p 8080:80感觉自己是新时代的魔法师。直到2022年夏天,我们生产服务器上的dockerd进程突然因为一个日志文件涨满磁盘而卡死,然后很“优雅”地——所有由它托管的容器全部退出。三百多个服务瞬间中断,监控告警炸了锅。CTO后来问我为什么不用重启策略(restart: always),我只好说那是云平台那层CentOS系统日志导致的,跟容器设计没关系,但心里已经埋下了一颗种子:单点daemon,这个设计是不是过于冒险了?

Podman的“去守护进程”到底改变了什么

图片

后来我开始折腾Podman,版本是5.2.2,装在Ubuntu 24.04上。最直观的差别就是:没有dockerd这个常驻进程了。podman run是直接fork出一个进程来跑容器,就像运行普通应用程序一样,完全不需要和任何“中央nginx”打个招呼。我做了个压力测试:一台上跑30个Nginx容器,Docker的dockerd加上内部网络协调要吃掉大约280MB内存;Podman的rootless模式跑同样的容器,整个用户态进程加起来不到120MB,而且这还是开了PCIe 4.0固态的机器。更重要的体验是:我敢直接kill -9一个容器进程了,因为它不会连带把别的东西带崩,有点像我以前用systemd管服务的感觉——每个单元独立,没什么上帝视角。

安全性:比docker.sock被偷了更隐蔽的坑

图片

关于Docker,我之前写过一篇内部安全公告,里面列举过:只要任何人能读写/var/run/docker.sock,他就等于拿到了宿主机root权限。结果不到一周,我们一个外网测试环境因为直接映射了Docker API端口,被扫描器打进去,挖矿脚本直接在宿主机上跑起来了。说实话,这只能怪我们没配置TLS,但Docker这种“socket即权力”的设计,真的有点太过粗犷。Podman的rootless模式就不一样,它默认用用户命名空间把容器里的root映射到宿主机的普通用户上,即使容器内部代码被完全攻破,也只有文件所有者权限,除非同时遇到内核漏洞,否则很难直接拿到宿主机的写权限。我特意测试了一下,用podman exec --user root进去创建文件,宿主机上是属于1000:1000的,用touch /root/test则直接Permission denied。另外,Podman还支持套接字激活,普通用户可以直接监听低端口(比如80),前提是设置好sysctl。

网络性能:别迷信benchmark,自己跑一次才算数

图片

大家总说Podman的网络性能不如Docker,我也差点被这个带着走。实际跑下来,在Linux内核6.8上,Docker默认的bridge加iptables做NAT时,容器到宿主机的ping延迟大约0.07-0.09ms;Podman默认的netavark加aardvark-dns,同样配置下延迟大约0.08-0.10ms,差了不到10%。但是DNS解析时间差异明显:Docker因为要经过docker-proxy和iptables DNAT规则,每次curl一个域名需要额外1-3ms;Podman的aardvark是直接维护了一份sensor域名映射表,解析速度实际快了一倍多。不过,Podman在跨宿主机overlay网络设计上是真的没Docker好,Swarm mode和外部插件比如Weave、Calico,Docker生态老辣,Podman你得自己玩Calico或者Flannel,没有一个“一键起来”Swarm的东西。所以,我做了一个很傻的妥协:本地开发跑Podman,CI流水线里的构建和集成测试还是用Docker,因为GitHub Actions的dind(Docker-in-Docker)和层缓存比较好用。

生产系统的systemd集成:Podman是后娘养的,但亲爹是systemd

图片

这大概是Podman最猥琐也是最能打的地方:它能直接用podman generate systemd --new生成一个systemd单元文件,但你得仔细读生成的Unit,因为默认服务名是container-{name}.service,而且如果在容器里用了--restart always,还得把systemd里的Restart=on-failure设置成一样才不冲突。我试过在RHEL 9.4上跑一个容器,用systemd管理比用Docker的restart策略更加“智能”:它能在内核panic重启后精确控制容器启动顺序(通过After=network-online.target等)。另一个细节是,Podman支持Unattended Upgrades,配合systemd的podman.host属性可以做到容器在系统更新后自动重建?不对,这个是我看的文档,实际没这么自动,得写定时器。但我觉得这件事值得做:Docker的daemon重启本质上是个全量事件,你得检查每个容器状态;而systemd下的Podman可以被按个单元独立管理,我用systemctl list-timers看容器日志和回滚,比docker-compose的docker compose up -d更透明。当然了,Podman-compose项目到现在还没有实现全部Docker Compose环境变量功能,比如docker compose up --scale这种,直接用会报错,这个在2024年末依然没修好,所以别拿它当生产级Compose替代品。

结论:叛逃不是目的,找到能让你少睡不踏实的运行时才是硬道理

图片

我现在大约40%的工作负载跑在Podman上,60%还是在Docker上,不是情感,是现实。Docker的轮子还是圆的,而且很多第三方工具默认就认docker CLI,你拿podman alias有时候会翻车(比如某些用docker attach的脚本,Podman的老版本常死锁)。但如果你自己维护小规模服务,或者你被dockerd崩溃坑到怀疑人生,我非常建议试试Podman。最后分享一个具体的坑:当你在Podman里用-v挂载带SELinux的目录时,记得加:Z而不是:z,后者会让容器共享标签,导致多容器互相覆盖文件。这种小经验才是你真正花钱买不到的。

我写这篇文章不是为了让你把Docker卸了,而是想提醒你——在容器化这条路上,总有一个更适合当前场景的工具在等着你,无论是Docker还是Podman,它们都只是进程隔离开胃菜,我们需要的是盘子里真正的营养。