1.3 机器准备:CVM 规格与弹性扩容
场景:一台机器扛全场,洪峰一来全壮烈
第一版秒杀我们只有两台 4 核 8G 的云服务器 CVM,一台跑 Nginx 加 PHP-FPM,一台自建 MySQL。开场五分钟,负载均衡后面这两台机器先后报警,页面直接 502,活动在一片骂声中紧急下线。当晚复盘定下规矩:秒杀的机器准备必须按峰值预估,而不是按日常均值;而且数据库必须迁到云数据库,不能自己折腾。
机器准备要回答三个问题:买什么规格、买几台、洪峰来了怎么扩。我们的整个秒杀链路都跑在腾讯云 CVM 上,产品文档入口是 cloud.tencent.com/document/product/213。预算紧张的同学可以先从这个特惠入口下手 云服务器 CVM 特惠,新用户价格友好,适合先跑通链路再升配。
规格选择上还有一条经验值得分享:秒杀活动是周期性的,大促当天的机器需求和平常差着好几倍。如果按大促规格常备机器,平时机器闲到发霉成本浪费;按日常规格常备,大促必然不够用。这就是为什么弹性扩容在秒杀场景不是可选项而是必选项。另外磁盘类型也别省,应用机器选高性能云硬盘可以让 OPcache 预热和日志写入都快一截,我们对比过普通云硬盘与增强型 SSD 的启动耗时,镜像拉起时间差了一半,这对分钟级扩容的灵敏度影响很明显。
方案:规格与弹性怎么选
我们按角色给机器分类,秒杀链路至少四个角色,每个角色的瓶颈点完全不同,所以规格也别一刀切:
| 角色 | 瓶颈点 | 推荐规格 | 数量建议 |
|---|---|---|---|
| 接入层 Nginx | 网络带宽、连接数 | 4 核 8G,带宽按峰值 3 倍 | 2 台起 |
| 应用层 PHP-FPM | CPU、FPM 进程数 | 8 核 16G | 4 台起 |
| 库存热点 Redis | 内存、单线程 CPU | 直接用云数据库 Redis | 1 实例 |
| 数据库 MySQL | 磁盘 IO、连接数 | 直接用云数据库 MySQL | 一主一从 |
为什么 Redis 和 MySQL 不自己买 CVM 装?因为云数据库自带主从切换、自动备份和监控告警,秒杀场景下我们紧张的运维人力应该花在链路调优上而不是装数据库。这部分成本在 5.4 节有详细的账单复盘,结论是自建的总持有成本反而更高。
应用层的扩容方案我们对比过三种,各有各的坑:
| 方案 | 生效速度 | 日常成本 | 主要风险 |
|---|---|---|---|
| 固定多台常备 | 常年在线 | 高 | 无,但浪费 |
| 伸缩组定时扩容 | 分钟级 | 中 | 新机器冷启动需要预热 |
| 手动临时扩容 | 看人 | 低 | 大促当晚谁也不敢睡 |
最终选择伸缩组定时扩容:大促前 30 分钟自动扩出 2 倍机器,活动结束后 30 分钟缩回。新机器冷启动的问题用预热脚本解决,见下方配置。
规格选定后还有一道算术必须做:机器数、FPM 进程数与数据库连接数的匹配。我们团队内部叫它容量三角,算错任何一角都会在洪峰时露馅:
| 算式 | 例子 | 算错的后果 |
|---|---|---|
| FPM 进程数 × 机器数 ≤ MySQL 最大连接数 | 256 × 6 = 1536 | 数据库连接打满,全站报错 |
| 各机带宽之和 ≥ 峰值带宽 × 冗余系数 | 20 Mbps × 6 台 | 接入层拥塞,首屏白屏 |
| 伸缩组扩容数 ≤ 镜像与子网余量 | 镜像限 50 台 | 扩容发不出机器,活动裸奔 |
第三行特别容易被忽略。云服务器 CVM 的子网与镜像配额都有上限,大促前必须申请提额,否则点扩容按钮的时候才会发现机器根本发不出来。首次活动我们就栽在配额上,眼睁睁看着伸缩组扩容失败告警刷屏,应急的临时方案是把另外两个测试环境的机器降配顶上去,才勉强扛过那场活动。
配置:弹性伸缩组与发版预热
CVM 伸缩组的核心是启动配置与伸缩策略。我们用打好补丁的 Laravel 镜像作为启动配置基底,新机器启动时通过用户数据脚本自动完成预热:
#!/bin/bash
# 用户数据脚本:新机器启动后自动预热
cd /var/www/shop
php artisan config:cache
php artisan route:cache
php artisan warm:seckill # 自定义命令:拉取活动配置与库存快照
systemctl restart php-fpm
curl -sX POST http://monitor.internal/register -d host=$(hostname)
伸缩策略设为定时任务而非按 CPU 阈值触发。原因很实在:CPU 到 80% 再开始扩容,等新机器拉起、预热、进负载均衡,30 秒的秒杀洪峰早过去了。秒杀场景里扩容救不了火,只能提前布好,这是血的教训。定时扩容还有一个好处是可预期,运维同学知道每天几点机器会变多、几点会缩回,排班和监控都可以围绕这个节奏安排,比被动触发的扩容好管理得多。
// warm:seckill 预热命令核心逻辑
public function handle(): int
{
$sessions = SeckillSession::where('status', 'live')->get();
foreach ($sessions as $s) {
Cache::remember("seckill:info:{$s->sku_id}", 600, fn () => $s->toArray());
}
$this->info('seckill cache warmed: ' . $sessions->count());
return self::SUCCESS;
}
预热命令之外还有一个校验命令,两条命令配合才敢放心扩容。校验命令的原理很简单:随机抽查几个活动键,比对 Redis 里的数字与 MySQL 活动表是否一致,不一致就把告警打到群里。预热负责让数据到位,校验负责确认数据真的到位,二者缺一不可。我们的排班表上,大促前一小时必须人工跑一次校验命令并截图留证,这条流程后来救过一次场:有一次运维同学手动改了 MySQL 里的活动库存却忘了同步,校验命令当场报出来,避免了一场超卖事故。
踩坑:扩容不是加机器那么简单
坑一:镜像里带了旧代码。 有一次大促前扩容,新机器跑的还是上周的镜像,秒杀接口里的限流阈值是旧值,一开场限流形同虚设,差点又翻车。后来所有扩容前必须验证镜像版本与配置版本一致,变成上线 checklist 的第一条。
坑二:只扩应用不扩数据库。 PHP 机器从 4 台扩到 12 台,云数据库 MySQL 的最大连接数没有同步调高,结果应用扩得越多数据库越接近崩溃。连接数容量要按 FPM 进程数乘机器数提前评估,规格说明见 cloud.tencent.com/document/product/236。云数据库 Redis 同理,带宽峰值也要提前看一眼,洪峰期 Redis 的入流量可能轻松超过日常的十倍,实例规格太小会直接触发限速,库存扣减的耗时就全乱了。
坑三:健康检查没覆盖预热完成。 伸缩组把还在跑预热脚本的机器加进负载均衡,第一批分过去的请求全部超时。解决办法是预热脚本全部跑完后,才把健康检查路由注册到负载均衡。健康检查一定要探真实业务可用性,而不是只探 80 端口通不通,这一点在 5.2 节监控告警还会强调。
坑四:安全组规则把 CLS 日志端口封了。 扩出来的新机器沿用了默认安全组,结果应用日志投递到 CLS 的链路被阻断,洪峰期排查问题时日志主题里空空如也,急出一身冷汗。事后把出站规则统一梳理了一遍,所有涉及日志投递与回调通知的出站端口都加进了基准安全组,并作为镜像审计的一部分。
小结
机器准备的口诀:按峰值预估、按角色选型、提前定时扩容、改检查项覆盖预热。秒杀洪峰只有几十秒,扩容永远在洪峰之前完成才有意义,事后扩容只是给事后复盘加素材。机器清单、容量算式、预热脚本、健康检查这四样东西配齐,机器准备工作就算及格了,剩下的事情交给后面的篇章继续打磨。还有一条容易忽略的经验:扩容计划要写成可执行的脚本而不是口头约定。脚本化的扩容在不同环境间可复用,换人值班也不会因为不熟悉流程而手忙脚乱。顺带一个提醒:扩容脚本本身也要放进版本仓库走代码评审,它和生产代码一样能引发故障,临时拼出来的脚本最容易在关键时刻掉链子。另外,平时每周演练一次扩容流程,大促前的才不会手生。弹性扩容这套机制省下的钱我们也算过:按峰值常备机器计费与按需扩容相比,两个月就把全年云预算省出了两成,这笔账财务看了也开心,技术投入的价值用钱说清楚最有力,机器准备这件事从此再也没被质疑过预算,反而成为跨部门协作的典范案例,财务与运维第一次因为同一张账单坐到了一条战线上。
