九月底老板在季度管理会上把财务报表往桌上一拍:“大盘业务增长放缓,每个月的云账单却像个无底洞,Q4 技术部必须交出至少 15% 的基建降本成果,否则年终奖免谈。”
作为背着房贷、上有老下有小的二线小厂架构师,我太清楚这种时刻没有讨价还价的余地。大厂有专门的 SRE 和 FinOps 团队搞智能弹性调度,但我们这种几十号研发的小团队,架构常常充满历史包袱。过去三年为了追赶所谓的“微服务架构先进性”,团队把一个电商商城硬生生拆成了 30 多个微服务,每个服务为了高可用,标配至少两台 8C16G 或 4C8G 的云虚机。
等我写了脚本拉出 Prometheus 全量负载画像时,眼前的数字让人啼笑皆非:有整整 14 台云主机的常年月均 CPU 利用率低于 6%,内存峰值使用率甚至没突破过 18%。很多机器只为了跑一个每天凌晨执行十分钟的报表导出服务,或者承载几个根本没人访问的内部运营后台。
这种把算力当自来水随意挥霍的账,是时候彻底清算了。
资产盘点:揪出那 10 台“吸血”的闲置节点
我们先对云平台上的 42 台计算节点进行了为期一个月的基线监控审计,梳理出三个典型的资源黑洞:
- 僵尸微服务节点:比如“积分兑换商城后台”、“供应商合同审批”,平时每天只有两三个请求,却各自霸占着独立的 2 台 8C16G 虚机。
- 测试与预发环境长期不关机:很多开发同学在预发环境拉起完整的压测集群,测完后拍拍屁股走人,虚机空转了半年还在按量计费。
- 过分保守的资源预留(Over-Provisioning):开发在提交部署清单时,习惯性将容器的
resources.requests填得奇高无比,一台物理宿主机因为虚拟分配超额(CPU Limit Sum),哪怕真实 CPU 负载只有 5%,也无法再调度新的 Pod 上去。
我们划定了缩容红线:连续 30 天 P95 CPU 利用率低于 15%、内存空闲大于 70% 的非核心承载节点,列入退役清退名单。最终锁定 10 台高配云主机(8C16G,单台月费约 1200 元)作为首期绞杀目标。
核心战术一:打破盲目微服务,冷门应用回归单体
在小厂,微服务拆得过细是导致服务器成本暴增的罪魁祸首。每个微服务都要有一套独立的进程开销、运行时 GC 开销、日志采集 Daemon、链路追踪探针。一个最简单的 Go 基础进程常驻就要吃掉 60MB 内存,如果用 Java SpringBoot 那更是起步 800MB。
我们将 6 个冷门的内部管理、定时对账微服务代码重新打包,合并为一个“综合内部运营服务”。得益于 Go 1.27.1 对小对象内存分配的改良(小于 80 字节对象分配开销降低 30%)以及优秀的并发机制,合并后的单个单体进程常驻内存仅有 180MB,并发吞吐反而比原来跨网络 RPC 调用提升了 3 倍。
核心战术二:容器精准混部与 Request/Limit 水位下调
原来各个虚机上的应用各占山头,我们利用现有的 Kubernetes 主力集群,推行容器混部:
- 真实用量对齐 Request:利用监控回溯,将原本动辄申请
cpu: 2000m, memory: 4Gi的非核心 Pod,压实到cpu: 200m, memory: 512Mi。 - 节点超卖与 QoS 调度:设置合理的 Limit 上限,并通过
BestEffort与Burstable级别进行服务分级混部。让白天的用户下单服务与夜间的报表计算服务错峰使用同一批宿主机算力。
为了在缩容混部后防止突发任务打崩节点,我们用 Go 编写了一个极轻量的轻度混部看门狗服务,挂在宿主机上实时监测内核 cgroup 状态并自动做低优任务的流控:
package cgroupwatch import ( "context" "fmt" "os" "strconv" "strings" "syscall" "time" ) // NodeHealthWatcher 监控本地节点 cgroup 内存压力,防止多服务混部导致 OOM 连锁反应 type NodeHealthWatcher struct { memPressureThreshold float64 // 内存压力告警水位 lowPriorityPIDs []int // 低优先级后台任务 PID 列表 } func NewNodeHealthWatcher(threshold float64) *NodeHealthWatcher { return &NodeHealthWatcher{ memPressureThreshold: threshold, lowPriorityPIDs: make([]int, 0), } } // RegisterLowPriorityJob 注册可以随时挂起或降级的离线批处理任务 func (w *NodeHealthWatcher) RegisterLowPriorityJob(pid int) { w.lowPriorityPIDs = append(w.lowPriorityPIDs, pid) } // MonitorLoop 周期性检测宿主机内存压力 func (w *NodeHealthWatcher) MonitorLoop(ctx context.Context) { ticker := time.NewTicker(3 * time.Second) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: ratio, err := w.calculateHostMemUsage() if err != nil { continue } // 如果宿主机真实内存水位超过 85%,立刻向混部的低优离线任务发 SIGSTOP 暂停执行,优先保在线交易 if ratio > w.memPressureThreshold { w.pauseLowPriorityJobs() } else if ratio < (w.memPressureThreshold - 0.15) { w.resumeLowPriorityJobs() } } } } func (w *NodeHealthWatcher) calculateHostMemUsage() (float64, error) { data, err := os.ReadFile("/proc/meminfo") if err != nil { return 0, err } var memTotal, memAvailable float64 lines := strings.Split(string(data), "\n") for _, line := range lines { fields := strings.Fields(line) if len(fields) < 2 { continue } if fields[0] == "MemTotal:" { memTotal, _ = strconv.ParseFloat(fields[1], 64) } else if fields[0] == "MemAvailable:" { memAvailable, _ = strconv.ParseFloat(fields[1], 64) } } if memTotal == 0 { return 0, fmt.Errorf("invalid meminfo") } return (memTotal - memAvailable) / memTotal, nil } func (w *NodeHealthWatcher) pauseLowPriorityJobs() { for _, pid := range w.lowPriorityPIDs { // 发送 SIGSTOP 临时挂起进程,不占 CPU,让出主干通道 _ = syscall.Kill(pid, syscall.SIGSTOP) } } func (w *NodeHealthWatcher) resumeLowPriorityJobs() { for _, pid := range w.lowPriorityPIDs { // 发送 SIGCONT 恢复任务继续计算 _ = syscall.Kill(pid, syscall.SIGCONT) } }缩容实施与退役复盘:实打实的降本账单
我们按照“灰度切流 -> 隔离观察 7 天 -> 停机保留快照 3 天 -> 彻底退订销毁”的标准流程,将这 10 台闲置云主机上的业务无缝迁移至现有 Kubernetes 集群中。
降本成效收益清单:
- 直接资金节省:退订 10 台 8C16G 高配虚机,每台月单价 1200 元,每个月实打实节省 12,000 元,全年折合节约 14.4 万元。
- 集群整体利用率提升:主力 Kubernetes 节点的 CPU 月均利用率从原来的 11% 提升到了稳健的 42%,内存利用率从 24% 提升到 58%,依然保有充裕的安全应急冗余。
- 运维人效大幅提升:减少了 10 台虚机的系统补丁更新、日志采集探针维护和漏洞扫描工单,团队整体的维护精力更加集中。
在小厂过日子,技术人员最大的价值绝不是堆砌华而不实的新词汇,而是能用硬核的工程手段把每一分算力榨出最大价值。1.2 万元在巨头眼里可能连一天的零头都算不上,但对于一个自负盈亏的小团队,这笔钱足够覆盖两个初级实习生的一年薪资,或者给兄弟们大促后发一份实打实的奖金。