一、容器化基础与成本意识:为什么资源规划如此重要?
说实话,我见过太多团队在容器化初期就栽了跟头。大家一窝蜂地把应用往 Docker 里塞,觉得「容器嘛,轻量、快速、随便跑」。结果呢?月底账单出来,云服务费用翻了三倍。老板问怎么回事,谁也说不清楚。
我个人习惯是,在动手写第一个 Dockerfile 之前,先想清楚一个问题:这个容器到底要吃多少资源? 别笑,这个问题真没几个人能立刻答上来。
1.1 为什么资源规划如此重要?
你想想看,传统物理机时代,一台机器跑一个应用,资源是固定的。虚拟化时代,一台物理机跑几个虚拟机,资源分配也相对粗放。但到了容器时代,一台宿主机上可能跑几十上百个容器。每个容器多吃一点,累积起来就是天文数字。
我遇到过一家创业公司,他们的微服务架构有 60 多个服务。每个服务的容器都用了默认配置——没设 CPU 限制,没设内存限制。结果某个服务出现内存泄漏,直接拖垮了整台宿主机上的所有容器。嗯,这就是典型的「一个坏苹果毁了一筐」。
资源规划的核心目的就两个:
- 避免资源争抢:防止某个容器吃掉所有资源,导致其他容器「饿死」
- 控制成本:精确分配资源,不浪费一分钱在云厂商那里
核心观点:容器化不是免费的午餐。资源规划做不好,成本失控是必然的。
1.2 Docker 资源隔离原理(Cgroups 与 Namespace)
要理解资源规划,得先知道 Docker 是怎么「骗」过容器的。说白了,容器里的进程以为自己独占了一台机器,其实它只是被关在了一个「隔间」里。
这个隔间由两个机制构成:
Namespace:让容器「看」不到外面
Namespace 负责隔离「视图」。每个容器有自己的:
- PID Namespace:容器里只能看到自己的进程,看不到宿主机的进程
- Network Namespace:容器有自己的网络栈,IP 地址、端口都是独立的
- Mount Namespace:容器有自己的文件系统挂载点
- UTS Namespace:容器有自己的主机名
- IPC Namespace:容器有自己的进程间通信机制
- User Namespace:容器有自己的用户 ID 映射
我记得刚开始学 Docker 时,最让我惊讶的是:容器里的 root 用户,在宿主机上可能只是个普通用户。这就是 User Namespace 的功劳。
Cgroups:让容器「吃」不到更多
Namespace 解决了「看得见」的问题,但没解决「吃得多」的问题。Cgroups(Control Groups)就是干这个的。
Cgroups 负责限制「用量」。它像一个严格的管家,告诉每个容器:
- 你最多只能用 2 个 CPU 核心
- 你最多只能用 1GB 内存
- 你的磁盘 IO 不能超过 100MB/s
- 你的网络带宽不能超过 100Mbps
来看一个实际的例子。我在项目中遇到过这样的情况:一个 Java 应用容器没设内存限制,结果 JVM 的堆内存直接撑到了 4GB,把宿主机内存吃光了。后来加了限制:
docker run -d \
--memory="1g" \
--memory-swap="1g" \
--cpus="1.5" \
my-java-app:latest
加了限制之后,这个容器的内存使用被严格控制在 1GB 以内。即使 JVM 想申请更多内存,也会被 Cgroups 拒绝。应用会抛出 OOM 异常,但至少不会拖垮整个宿主机。
小技巧:设置 --memory-swap 和 --memory 一样大,可以禁用 swap。容器用 swap 会导致性能急剧下降,我建议你直接关掉。
1.3 成本失控的常见场景
讲完了原理,咱们聊聊真金白银的事。成本失控不是突然发生的,它是一点一点积累起来的。我总结了几个最常见的场景:
场景一:无限制的资源分配
这是最常见的问题。很多团队在开发环境里跑容器,不加任何资源限制。开发环境机器多、资源充裕,看不出问题。但到了生产环境,同样的镜像部署上去,资源立刻吃紧。
我曾经帮一个团队排查过:他们的开发环境有 32 核 128GB 内存,跑 20 个容器毫无压力。但生产环境只有 8 核 32GB 内存,同样的 20 个容器上去,CPU 直接飙到 100%。原因很简单——开发环境没设限制,容器们「放开了吃」。
场景二:过度预留资源
另一个极端是「怕出事,多给点」。每个容器都申请 4 核 8GB 内存,实际使用率只有 10%。你想想看,100 个容器就是 400 核 800GB 内存,但实际只用了 40 核 80GB。剩下的资源全浪费了,但云厂商照样收钱。
警告:过度预留比资源不足更可怕。资源不足最多导致性能下降,过度预留直接导致成本翻倍。
场景三:无状态应用的资源浪费
很多无状态应用(比如 Web 服务、API 网关)在低峰期根本不需要那么多资源。但大多数团队的做法是:不管高峰低峰,资源配给都一样。结果就是低峰期大量资源闲置。
我建议的做法是:
- 使用 HPA(Horizontal Pod Autoscaler)根据 CPU/内存使用率自动扩缩容
- 低峰期减少副本数,高峰期自动增加
- 配合 Cluster Autoscaler 自动调整集群节点数
场景四:日志和数据的存储成本
这个坑我踩过。容器默认会把日志输出到 stdout/stderr,Docker 会把这些日志收集到宿主机上。如果不加限制,一个容器的日志文件可以轻松涨到几十 GB。这些日志占用的磁盘空间,也是要花钱的。
解决方案很简单:
docker run -d \
--log-opt max-size=10m \
--log-opt max-file=3 \
my-app:latest
这样每个容器的日志文件最大 10MB,最多保留 3 个文件。既保证了日志可查,又不会撑爆磁盘。
场景五:镜像体积过大
镜像越大,拉取越慢,存储越贵。我见过一个团队的基础镜像有 2GB,每次部署都要拉取 2GB 的数据。几十个节点同时拉取,网络带宽被占满,部署时间从几分钟拖到半小时。
更关键的是,镜像存储在镜像仓库里,也是按大小收费的。一个 2GB 的镜像,保留 10 个版本,就是 20GB 的存储费用。
建议:使用多阶段构建,把最终镜像控制在 200MB 以内。基础镜像选择 Alpine 或 Distroless,能省不少空间。
1.4 总结:成本意识要从第一天开始
说了这么多,其实就一句话:资源规划不是事后补救,而是事前设计。
我个人习惯是,每个新项目启动时,先做三件事:
- 估算每个容器的资源需求(CPU、内存、磁盘、网络)
- 设置合理的资源限制(上限和下限)
- 建立资源监控和告警机制
这三件事做好了,成本失控的概率至少降低 80%。剩下的 20%,靠持续优化和复盘来解决。
下一章,我会详细讲如何做资源估算和容量规划。咱们到时候见。