用户登录卡死、报表加载转圈、凌晨3点数据库主从切换导致服务抖动......这些小概率事件,正在一点一点吃掉用户对产品的信任。
99.99%的可用性意味着什么?一年宕机时间不超过52分钟。本文从实战角度,完整复盘一家SaaS CRM从单点故障到多活架构的演进之路。
一、99.99%可用性的真实含义
很多人在谈论高可用时,往往只关注服务能不能通,忽略了更关键的维度。
| 可用性级别 | 年故障时间 | 月故障时间 | 典型特征 |
|---|---|---|---|
| 99.9% | 8.76小时 | 43分钟 | 单机房主备,切换需人工介入 |
| 99.99% | 52.6分钟 | 4.3分钟 | 同城双活,故障自动切换 |
| 99.999% | 5.26分钟 | 26秒 | 异地多活,金融级要求 |
为什么选99.99%作为目标? 三个9是及格线,五个9的边际成本是指数级上升的。对于绝大多数SaaS产品,四个9是最具性价比的高可用目标,用户几乎感知不到故障,而成本仍在可控范围内。
二、第一阶段:从单机到主从
2.1 最初的架构
项目上线初期,用户量不大,架构非常简单:
前端:静态资源放在Nginx
后端:单台ECS部署Spring Boot
数据库:单台MySQL
缓存:单台Redis
当时的想法:服务器配置高、数据库优化过、代码质量好,应该没问题吧?
第一次教训:某个周末,MySQL实例所在物理机磁盘损坏,数据库整整宕机6小时。备份恢复后,才发现最近一次有效备份是3天前的。
2.2 从单机到主从的演进
这次事故后的改进:
| 组件 | 改进方案 | 效果 |
|---|---|---|
| MySQL | 一主一从 + 半同步复制 | 主库宕机可手动切从库 |
| Redis | 主从 + 哨兵 | 自动故障转移 |
| 应用 | 单台仍为单点 | 待解决 |
经验教训:备份不仅要做,更要定期验证可恢复性。
2.3 主从架构的核心问题
这个阶段的架构仍然存在几个致命缺陷:
-
主从切换需要人工介入:半夜出故障,等DBA起床就已经过了半小时
-
从库无法分担写压力:写操作仍全部在主库
-
网络抖动导致主从复制延迟:大量从库读取请求可能读到旧数据
三、第二阶段:同城双活
3.1 为什么要做同城双活?
随着用户量增长,单个机房的局限性越来越明显:
机房级别的故障无法应对:光纤被挖断、机房断电等黑天鹅事件
主从切换有不可控的黑窗期:即使自动化,仍有几十秒到几分钟的切换时间
读写分离效果有限:主库仍然是写瓶颈
3.2 同城双活架构设计
┌─────────────────────────────────────┐
│ DNS智能解析 │
│ (根据用户IP分配就近入口) │
└─────────────────┬───────────────────┘
│
┌────────────────────────┼────────────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 可用区A │ │ 可用区B │ │ 可用区C │
│ (主流量入口) │◄────►│ (主流量入口) │ │ (仲裁节点) │
│ │ DTS │ │ │ │
│ MySQL 主库(写) │ │ MySQL 从库(读) │ │ MySQL 从库 │
│ Redis 主(写) │ │ Redis 从(读) │ │ Redis 从 │
│ 应用实例 x N │ │ 应用实例 x N │ │ │
└─────────────────┘ └─────────────────┘ └─────────────────┘
3.3 数据层双活方案
MySQL同城双活的核心难点在于双写冲突。没有采用双主模式,而是用了以下方案:
读写分离策略:
写操作: 100% 路由到主写节点
读操作:
- 用户维度: 按user_id哈希,同一用户请求固定路由
- 跨区读: 允许从可用区B的从库读取,容忍秒级延迟
故障切换: 主写节点故障时,30秒内将写流量切到可用区B
关键配置:
sql
-- MySQL半同步复制配置
SET GLOBAL rpl_semi_sync_master_enabled = 1;
SET GLOBAL rpl_semi_sync_master_wait_for_slave_count = 1;
SET GLOBAL rpl_semi_sync_master_wait_point = AFTER_SYNC;
-- 从库延迟监控告警
SET GLOBAL slave_net_timeout = 30;
3.4 应用层无状态化改造
这是双活的前提。所有应用实例必须无状态:
| 改造项 | 原方案 | 改造后 |
|---|---|---|
| Session存储 | 本地内存 | Redis集中存储 |
| 定时任务 | 各实例独立执行 | 分布式调度 |
| 文件上传 | 本地存储 | OSS对象存储 |
| 配置管理 | 本地配置文件 | 配置中心 |
3.5 同城双活的代价
| 维度 | 1.0架构 | 2.0架构 |
|---|---|---|
| 服务器数量 | 5台 | 25台 |
| 年可用性 | 99.5% | 99.95% |
| 运维复杂度 | 低 | 中 |
| 故障恢复时间 | 小时级 | 分钟级 |
四、第三阶段:CDN + 边缘缓存
4.1 发现新的瓶颈
双活架构上线后,后端服务稳定了很多,但用户反馈报表加载慢、大屏展示卡顿。
分析后发现:
静态资源从应用服务器传输,效率低
API响应快,但数据量大
跨国用户访问延迟高
4.2 CDN加速静态资源
CDN配置策略:
静态资源:
规则: *.js, *.css, *.png, *.jpg
缓存TTL: 30天
回源: 对象存储OSS
API动态内容:
规则: /api/report/*(报表数据)
缓存TTL: 5分钟(边缘节点缓存)
回源: 双活应用集群
效果:
静态资源加载速度提升70%
源站带宽消耗降低85%
海外用户访问延迟从800ms降到150ms
4.3 边缘缓存:在离用户最近的地方缓存数据
对于一些准静态数据,我们引入了边缘缓存,在CDN节点直接缓存API响应。
技术实现:
# Nginx边缘节点配置
location ~ ^/api/config/ {
# 缓存配置项API响应
proxy_cache config_cache;
proxy_cache_valid 200 5m;
proxy_cache_key "$request_uri";
add_header X-Cache-Status $upstream_cache_status;
proxy_pass http://backend;
}
location ~ ^/api/report/daily {
# 日报数据:缓存10分钟
proxy_cache report_cache;
proxy_cache_valid 200 600s;
proxy_cache_key "$request_uri|$http_x_user_id";
proxy_pass http://backend;
}
4.4 缓存一致性问题
边缘缓存最大的风险是数据更新后用户看到旧数据。
解决方案:主动失效机制
python
# 配置变更时,主动清除边缘缓存
def invalidate_edge_cache(urls):
for url in urls:
# 调用CDN API清除缓存
cdn_client.purge(url)
# 同时清理Redis中的缓存标记
redis_client.delete(f"cache_version:{resource_type}")
兜底策略:设置合理的缓存时间,并在业务可接受范围内选择最终一致性。
五、第四阶段:全链路压测与混沌工程
5.1 为什么需要主动搞破坏?
系统架构再完善,如果不经过真实故障的考验,永远不知道哪里会出问题。
混沌工程的核心原则:在生产环境中主动注入故障,观察系统反应,提前发现薄弱环节。
5.2 我们的混沌实验清单
| 故障类型 | 注入方式 | 预期表现 | 实际结果 |
|---|---|---|---|
| 单ECS实例宕机 | 随机kill一个应用容器 | 流量自动切换到其他实例 | 通过 |
| 整个可用区A网络中断 | 模拟交换机故障 | 流量全部切到可用区B | 部分通过 |
| MySQL主库宕机 | kill mysql进程 | MHA自动切换,30秒内恢复 | 失败 |
| Redis主节点故障 | 模拟节点宕机 | 哨兵选主,自动切换 | 通过 |
| 缓存穿透/击穿 | 高频请求不存在的Key | 限流/布隆过滤器生效 | 通过 |
| 数据库连接池耗尽 | 模拟慢查询占满连接 | 熔断降级,返回默认值 | 部分通过 |
这次混沌实验最大的收获:发现了MySQL自动切换脚本在大促流量下的bug,提前修复后,避免了一次真实的生产事故。
六、效果与总结
6.1 各阶段可用性对比
| 架构阶段 | 年可用性 | 主要瓶颈 | 月成本(估算) |
|---|---|---|---|
| 单机部署 | 99.0% | 单点故障 | 3000元 |
| 主从架构 | 99.5% | 切换需人工介入 | 8000元 |
| 同城双活 | 99.95% | 跨区延迟 | 2.5万元 |
| CDN+边缘缓存 | 99.99% | 缓存一致性 | 3万元 |
6.2 核心经验总结
-
高可用是分层构建的:DNS、接入层、应用层、数据层,每一层都要考虑冗余和故障转移
-
没有银弹:双活在提升可用性的同时,也带来了架构复杂度和运维成本的上升
-
缓存是双刃剑:用得好性能翻倍,用不好数据一致性问题会让你头疼
-
混沌工程不是可选项:没有经过故障考验的系统,永远不知道哪里会出问题