一、故障初现:只读副本的“掉队”警报
上周三上午10点整,公司核心电商平台的订单系统突然弹出告警:只读副本的查询延迟突破30秒,且持续攀升。一开始我们以为是临时波动,结果10分钟后延迟涨到了2分钟,到10点20分直接卡死——用户查询商品库存、订单状态的请求要么超时,要么返回旧数据,差点引发大规模投诉。
我们的架构是典型的跨可用区读写分离:主库放在可用区A(AZ-A),专门处理写请求(比如用户下单、修改地址);两个只读副本分别放在可用区B(AZ-B)和可用区C(AZ-C),专门处理读请求(比如商品搜索、订单查询),核心是靠Amazon Aurora的自动复制机制同步数据。之前这套架构跑了半年都稳得很,怎么突然就掉链子了?
二、逐层排查:从表象到根因的“抽丝剥茧”
我们按“先表象后本质”的顺序排查,每一步都做了具体验证,没有瞎猜。
2.1 第一层:先排除最容易想到的网络问题
跨可用区的网络延迟是读写分离的常见坑,比如AZ之间的带宽被占满、网络丢包。我们先查了云平台的网络监控:
- 先看AZ-A到AZ-B、AZ-C的网络带宽,峰值只有120Mbps,远低于可用的1Gbps专线带宽,没占满;
- 再测网络延迟,用ping命令测主库到只读副本的往返延迟:
# 测主库(AZ-A)到AZ-B只读副本的延迟,连续测10次
ping -c 10 10.0.2.10 # 10.0.2.10是AZ-B只读副本的内网IP
结果显示平均往返延迟只有3.2ms,丢包率0%,完全正常。
那会不会是主库的写请求太猛,导致同步队列堆不下?我们查了主库的写TPS(每秒写请求数),平时是150-200,故障时最高才220,没超过主库的承载极限。
2.2 第二层:再挖Aurora的复制机制细节
Aurora的复制和普通MySQL不一样,它是“存储层同步”:主库的写请求会先写到自己的分布式存储层,然后存储层自动同步到只读副本的存储层,不是主库主动推数据。我们查了Aurora的专属监控指标:
AuroraReplicaLag:这个指标是只读副本和主库的时间差,故障时显示是120秒,而且还在涨;AuroraReplicaApplyLag:这个是只读副本把存储层同步过来的数据,应用到自己的查询缓存的时间差,故障时显示是0秒——这说明数据已经同步到只读副本的存储层了,只是没应用到查询缓存里。
那问题出在只读副本的存储层?我们再查只读副本的存储监控,发现一个异常:AZ-B和AZ-C的只读副本的“存储IO等待时间”突然从1ms涨到了80ms。
2.3 第三层:定位到具体的存储操作
为什么存储IO等待会涨?我们查了只读副本的慢查询日志,发现所有的读请求都在查一张大表:order_log(订单日志表)。这张表有1.2亿条数据,大小150GB,而且没有建合适的索引——之前为了写性能,我们没给这张表建索引,没想到读的时候出问题了。
我们做了个验证:临时把AZ-B的只读副本改成主库(通过Aurora的故障转移功能),然后在这张表上建了索引:
-- 给order_log表的order_id和create_time建联合索引,优化查询
CREATE INDEX idx_order_id_create_time ON order_log(order_id, create_time);
建完索引后,我们把这个实例改回只读副本,结果AuroraReplicaApplyLag立刻降到了0.1秒,延迟问题解决了。
2.4 根因总结
整个故障的链条是:
- 上周二我们上线了一个新功能:给用户展示近7天的订单日志,这个功能大量查询
order_log表; order_log表没有索引,每次查询都要全表扫描,导致只读副本的存储IO等待时间飙升;- 存储层的IO等待占满了只读副本的存储处理能力,导致数据从存储层同步到查询缓存的速度变慢,最终引发了只读副本“掉队”。
三、故障处理:快速恢复的完整流程
我们的处理步骤是分阶段的,先救业务,再彻底解决问题:
3.1 紧急救业务:临时切换读流量
故障发生后,我们先把AZ-B和AZ-C的只读副本的读流量,临时切到主库上(主库专门开了一个只读端口,用来临时处理读请求):
# 用AWS CLI把读流量从只读副本切到主库的只读端口
aws rds modify-db-cluster --db-cluster-identifier my-ecommerce-cluster --read-write-endpoint 10.0.1.10:3306 --read-only-endpoint 10.0.1.10:3307
这个操作花了2分钟,之后用户的读请求恢复正常,投诉立刻停止。
3.2 临时解决:给只读副本建索引
因为主库上的order_log表是全表扫描的,如果在主库建索引,会导致主库的写性能下降(建索引会锁表),所以我们先在两个只读副本上建索引:
-- 在AZ-B的只读副本上建索引,只读副本建索引不会影响主库
CREATE INDEX idx_order_id_create_time ON order_log(order_id, create_time);
-- 建完后,等10分钟让索引生效,然后验证查询速度
SELECT count(*) FROM order_log WHERE order_id = '12345' AND create_time > '2024-05-20 00:00:00';
验证发现查询时间从原来的15秒降到了0.02秒,存储IO等待时间也降到了1ms以下。
3.3 彻底解决:主库补索引+架构优化
等业务低峰期(凌晨1点),我们在主库上建索引,为了不影响写性能,我们用了Aurora的在线索引创建功能:
-- Aurora支持在线建索引,不会锁表,对写性能影响很小
CREATE INDEX idx_order_id_create_time ON order_log(order_id, create_time) ONLINE;
同时,我们优化了架构:
- 专门加了一个只读副本,专门处理
order_log表的查询,和其他读请求隔离; - 给只读副本设置了单独的存储IO配额,避免某一个只读副本的IO占用影响其他副本;
- 给新上线的功能加了监控,当读请求量超过阈值时,自动触发告警。
四、相关技术详解:Aurora跨可用区读写分离的核心逻辑
为了让大家彻底搞懂,我们专门拆解一下这套架构的核心:
4.1 跨可用区读写分离的设计思路
跨可用区的好处是容灾:如果一个可用区挂了,其他可用区的实例还能正常工作。读写分离的好处是:主库只处理写请求,性能更稳;只读副本处理读请求,能横向扩容(加多少个只读副本都行)。
4.2 Aurora复制和普通MySQL的区别
普通MySQL的复制是“主库推数据”:主库写完数据后,把数据写到binlog,然后主动推给只读副本,只读副本再应用binlog。这种方式如果主库和只读副本之间的网络断了,复制就断了。
Aurora的复制是“存储层同步”:主库和只读副本的存储层是同一个分布式存储集群,主库写完数据后,存储层自动同步到所有只读副本的存储层,然后只读副本再把存储层的数据应用到自己的查询缓存。这种方式的好处是:网络断了也能同步,复制延迟更低。
4.3 只读副本的查询缓存作用
只读副本的查询缓存是专门用来处理读请求的,它会把存储层同步过来的数据,转换成自己的查询格式(比如索引、缓存)。如果查询缓存的处理速度跟不上存储层的同步速度,就会出现“只读副本掉队”的问题。
五、应用场景、优缺点和注意事项
5.1 应用场景
这套架构适合所有需要高可用、高并发的互联网业务,比如电商、社交、视频平台,尤其是读请求量远大于写请求量的业务(读请求量是写请求量的10倍以上)。
5.2 技术优缺点
优点:
- 容灾能力强:跨可用区部署,一个可用区挂了,其他可用区的实例还能正常工作;
- 性能高:读写分离,主库专门处理写请求,只读副本专门处理读请求,能横向扩容;
- 成本低:只读副本的价格比主库便宜很多,能节省成本。
缺点:
- 架构复杂:需要维护主库、多个只读副本,还要处理跨可用区的网络、存储问题;
- 同步延迟:虽然Aurora的同步延迟很低,但还是存在,尤其是当只读副本的查询缓存处理速度跟不上的时候;
- 索引冲突:主库和只读副本的索引需要保持一致,否则会出现数据不一致的问题。
5.3 注意事项
- 一定要给大表建索引:尤其是经常被查询的大表,没有索引会导致全表扫描,占用大量IO;
- 隔离不同类型的读请求:比如把处理实时查询的只读副本和处理报表查询的只读副本分开,避免互相影响;
- 监控复制延迟:一定要监控
AuroraReplicaLag和AuroraReplicaApplyLag这两个指标,一旦超过阈值就触发告警; - 避免在主库上做全表扫描:主库上的全表扫描会导致主库的写性能下降,甚至引发故障。
六、故障总结
这次故障的核心原因是我们对只读副本的查询缓存处理能力的认知不足,以为只读副本的IO是无限的,没想到大表的全表扫描会占满IO,导致同步延迟。
通过这次故障,我们总结出三个经验:
- 上线新功能前,一定要做性能测试:尤其是涉及到大表查询的功能,一定要测查询速度、IO占用;
- 要给只读副本加单独的监控:除了复制延迟,还要监控存储IO、查询缓存的处理速度;
- 架构设计要考虑极端情况:比如某一个只读副本的IO被占满时,怎么避免影响其他副本。
评论
围绕“Amazon Aurora只读副本掉队探因:跨可用区读写分离架构中复制延迟持续加剧,从网络延迟到存储引擎逐层深挖,定位根因并恢复主从同步”参与讨论