容器化和传统虚拟机到底该选谁?一个被K8s折腾三年的运维说出大实话

🔑 关键词:容器化,虚拟化,Docker,Kubernetes,运维对比

📖 摘要:不吹不黑,从隔离边界、镜像供应链、故障域三个维度拆解容器与虚拟机的真实区别。用三次线上事故告诉你,容器不是虚拟机的廉价平替,盲目迁移可能更麻烦。

刚接触容器化的时候,我是挺抵触的那批人。从 systemd 到 shell 脚本,明明写得好好的,为什么要被 Dockerfile 和 YAML 再套一层?后来被逼着把生产 Java 应用往 Kubernetes 迁移,三年里遇到容器假死、镜像层损坏、DNS 解析超时,还有一次所有节点 CPU steal 异常飙高。这些真实事故让我重新思考一个没人愿意明说的问题——容器跟虚拟机之间,根本不是替代关系,而是两种维度完全不同的基础设施。

图片

第一件事让我明白 namespace 和 cgroup 的隔离到底多脆弱。虚拟机有独立内核,GuestOS 崩了宿主机不在乎;容器只是共享宿主机内核的进程,一个高危的 sysctl 改到全局网络栈,整台节点立刻遭殃。有个老项目跑在裸金属虚拟机上,内存占用稳定,线程 dump 也正常;可把它丢进容器后,Java 进程竟然在 /proc 里看不出真实 pid,metrics 里的 CPU steal 直接飙到 37%。折腾一整天,最后发现是宿主机内核的 pid.max 设置太低,导致容器里创建线程失败,而其他不相关的容器也被同样的内核参数影响。这种故障半径,谁要再跟我说容器隔离跟虚拟机一样,我就把这个案例扔他脸上。

图片

镜像这件事被过度神话了。Docker 镜像解决的问题,严格来说不是环境一致,而是软件分发的格式统一。我们的 CI 系统里,Dockerfile 每一条 RUN 都能生成一层,构建缓存看着省时间,但清理起来真的要命。某次我们推镜像到生产 registry 后,发现 containerd 拉取时一直卡在 node_modules 那一层,控制台直接 404。后来查完才知道,是 registry 的 media type 兼容问题,而我们的镜像层数量超过 40 层,其中一个大层有将近 800MB。搞过分布式部署的人心里都清楚,这种层依赖在 NFS 存储上尤其脆弱。把这套东西搬进 Kubernetes 之后,新的问题又出现了——CNI 插件一升级,Pod 里已有的 conntrack 表立刻失效,结果业务流量从端口通到不通只有不到两小时的反应窗口。

图片

关于故障域,Kubernetes 把自己宣传得像高可用救世主,但实际出问题的时候,故障单元还是那一台活生生的物理机。Pod 跟节点共存亡,风险不在容器本身,而在 kubelet、CSI、CNI 这一整条链路。比如节点磁盘满时,Eviction API 会冷酷地驱逐一堆 Pod,如果你的 StatefulSet 没有预先定义备份策略,MySQL 的 PVC 可能直接被标记为异常而无法自动恢复。虚拟机迁移时,可以把整块内存状态拷贝到另一台物理机;而容器在节点宕机后重新拉起,需要等 kubelet 重新注册、volume 重新挂载、CSI 等待超时,我们实际统计的 p99 恢复时间是 47 秒,比传统虚拟机的冷启动还慢。这就让所谓「容器秒级恢复」显得特别尴尬,至少在自建机房里,它根本做不到。

图片

那容器化到底好在哪?我的答案可能很反主流:容器最适合的不是当虚拟机用,而是把应用变成一种「可以编排的资产」。当你需要把业务拆成很多个可以独立扩缩容的副本,而且有一套完整的自动运维体系管理这些副本时,容器才真正发挥价值。如果你需要的只是一个稳定、隔离、审计简单的长周期服务,裸金属 + 虚拟机依然是更省心的选择。别一上来就搞 K8s,你可以先在单机上用 containerd + Kubernetes 的 node 模式练手,跑一个无状态 API 看看。要迁移的兄弟,我劝你先压测环境里随机杀一台节点,然后查 /var/log/messages 里的内核记录,而不是对着官方 whitepaper 自我感动。容器化真正麻烦的地方从来不是启动镜像,而是后续的地基加固。

图片