摘要:使用 Redis 的开发者必看,吸取教训啊!
原文:Redis 的 KEYS 命令引起 RDS 数据库雪崩,RDS 发生两次宕机,造成几百万的资金损失
作者:陈浩翔
Fundebug 经授权转载,版权归原作者所有。
最近的互联网线上事故发生比较频繁,2018 年 9 月 19 号顺丰发生了一起线上删库事件,在这里就不介绍了。
在这里讲述一下最近发生在我公司的事故,以及如何避免,并且如何处理优化。
间接原因还有很多,技术跟不上业务的发展,由每日百万量到千万级是一个大的跨进,公司对于系统优化的处理优先级不高,技术开发人手的短缺
第一次宕机
2018 年 9 月 13 号某个点,公司某服务化项目的 RDS 实例连接飙升,CPU 升到 100%,拒绝了其他应用的所有请求服务
整个过程如下:
监控报警,显示 RDS 的 CPU 使用率达到 80% 以上,DBA 介入,准备 KILL 慢 SQL
1 分钟内,没有发现明显阻塞的 SQL,CPU 持续上升到 99%
5 分钟内,大量应用报警,并且拒绝服务,RDS 的监控显示出现大量慢 SQL,联系服务器数据库提供商进行协助
8 分钟内,进行数据库主备切换(业务会受损,但是也没办法,没有定位到问题)
9 分钟内,部分业务恢复,但是一些业务订单的回调消息堆积超过 20w,备库的 CPU 使用率也持续上升
15 分钟内,备库 CPU 使用率超过 97%,业务再次中断,进行切回主库,并进行限流
20 分钟内,关闭一些次要应用的流量入口
25 分钟内,主库 CPU 使用率恢复正常
30 分钟内,逐步开启关闭的限流应用
35 分钟内,所有应用恢复正常
接下来就是与服务器数据库提供商成立应急小组紧急优化可能出现的慢 SQL,虽然说可能解决了一些慢 SQL,但此次并没有定位到具体的问题,也就为几天后再次发生宕机事件埋下了伏笔
事故影响
某服务化项目服务不可用几十分钟,造成订单数减少几十万笔,损失百万资金。
原因分析
当时是没有定位到具体的原因的,但是下面的原因也是一部分可能引起宕机的情况。
某服务化项目的业务增速非常快,在高峰期,数据库 QPS 突破 35000,系统处于高负荷状态。
在高峰期如果同时执行几个全表扫描的 SQL,会造成数据库压力急剧上升,应用超时增多,前端应用超时,用户重试,流量飙升,形成了雪崩效应。
主要原因在与一些老项目的 SQL 查询性能较差,并且使用的主库,对数据库影响较大。数据库 QPS 太高,但是缓存方案因为人手原因一直没有落地,慢 SQL 的问题处理优先级应该提升
改进方案
针对每个应用建一个数据库账号,严格按照规范使用
缓存优化方案即时落地,慢 SQL 问题优先处理,集中处理目前已经发现的慢 SQL(查询时间超过 1S)
升级数据库配置
迁移非核心业务到新的 RDS 实例中去
第二次宕机
由于上一次的宕机原因未找到,所以此次的宕机是可以预见的。
2018 年 9 月 19 号,还是一样的 ” 配方 ”,还是原来的 ” 味道 ”。同一个 RDS,CPU 飙升至 100%,接下来就是拒绝服务,宕机。当然,有了第一次的经验,直接主从切换,在几十秒左右就恢复了所有业务,但还是严重影响了公司的业务和形象。
原因分析
恢复业务后,公司紧急召开了紧急事故研究会议,当然,我的级别是参与不了的。公司的高管,高层技术架构、DBA、各个项目的主负责人一起进行了会议。
在此次会议中,经过查看各个项目的日志,后台的监控数据,发现在那台 RDS 数据库 CPU 飙升时,有一台 Redis 数据库内存将近 100%,然后急剧下降。联系第一次的宕机情况,也是类似的。
接下来就是联系服务器数据库提供商,将那台 Redis 最近一周的命令全部调用出来,最后发现,在那个时间点运行了一条 keys *…* 命令。公司的一个工程师执行 keys 模糊的匹配命令是为了清理没用的键,但是没有考虑到 keys * 进行模糊匹配引发 Redis 锁,造成 Redis 锁住,CPU 飙升,引起了所有调用链路的超时并且卡住,等 Redis 锁的那几秒结束,所有的请求流量全部请求到 RDS 数据库中,使数据库产生了雪崩,使数据库宕机。
改进方案
所有线上操作,全部要经过运维通过后方可执行,运维部门逐步快速收回各项权限
新增 Redis 实例,进行分离
如果有使用类似 keys 正则命令需求,使用 scan 命令代替
总结
该事件中出现的两次事故,完全是由于人为操作引起的,如果那位工程师,看过 Redis 的开发规范,会发现是建议禁用 keys 命令的。另外,有线上的命令操作,一定要经过运维评估后方可进行操作,估计那个工程师是老员工吧,有权限,然后直接就进行操作了。
另外,公司的业务发展确实很快,技术跟不上,这是非常非常危险的,极大的增加了宕机的概率。
在业务量不大的情况下,那位工程师的操作是完全没什么问题的,毕竟并发也不大,但是现在,随着公司的发展,业务量的成倍成倍增加,技术的扩展却没有随着增长那么快。
公司的技术人手不足也是一方面,绝大多数人都是边维护老项目边做新功能,但是对于项目的重构优化,人手却少了很多,项目优化的优先级不高,这也是很大的一个原因,极有可能出现类似的情况,新服务化构建迫在眉睫。
最后的最后,线上操作的任何一条命令,再小心也不为过,因为由于你的一个符号而引起的事故可能是你所承担不起的。
Redis 开发建议
最后附上 Redis 的一些开发规范和建议
1. 冷热数据分离,不要将所有数据全部都放到 Redis 中
虽然 Redis 支持持久化,但是 Redis 的数据存储全部都是在内存中的,成本昂贵。建议根据业务只将高频热数据存储到 Redis 中【QPS 大于 5000】,对于低频冷数据可以使用 MySQL/ElasticSearch/MongoDB 等基于磁盘的存储方式,不仅节省内存成本,而且数据量小在操作时速度更快、效率更高!
2. 不同的业务数据要分开存储
不要将不相关的业务数据都放到一个 Redis 实例中,建议新业务申请新的单独实例。因为 Redis 为单线程处理,独立存储会减少不同业务相互操作的影响,提高请求响应速度;同时也避免单个实例内存数据量膨胀过大,在出现异常情况时可以更快恢复服务!在实际的使用过程中,redis 最大的瓶颈一般是 CPU,由于它是单线程作业所以很容易跑满一个逻辑 CPU,可以使用 redis 代理或者是分布式方案来提升 redis 的 CPU 使用率。
3. 存储的 Key 一定要设置超时时间
如果应用将 Redis 定位为缓存 Cache 使用,对于存放的 Key 一定要设置超时时间!因为若不设置,这些 Key 会一直占用内存不释放,造成极大的浪费,而且随着时间的推移会导致内存占用越来越大,直到达到服务器内存上限!另外 Key 的超时长短要根据业务综合评估,而不是越长越好!
4. 对于必须要存储的大文本数据一定要压缩后存储
对于大文本【+ 超过 500 字节】写入到 Redis 时,一定要压缩后存储!大文本数据存入 Redis,除了带来极大的内存占用外,在访问量高时,很容易就会将网卡流量占满,进而造成整个服务器上的所有服务不可用,并引发雪崩效应,造成各个系统瘫痪!
5. 线上 Redis 禁止使用 Keys 正则匹配操作
Redis 是单线程处理,在线上 KEY 数量较多时,操作效率极低【时间复杂度为 O(N)】,该命令一旦执行会严重阻塞线上其它命令的正常请求,而且在高 QPS 情况下会直接造成 Redis 服务崩溃!如果有类似需求,请使用 scan 命令代替!
6. 可靠的消息队列服务
Redis List 经常被用于消息队列服务。假设消费者程序在从队列中取出消息后立刻崩溃,但由于该消息已经被取出且没有被正常处理,那么可以认为该消息已经丢失,由此可能会导致业务数据丢失,或业务状态不一致等现象发生。
为了避免这种情况,Redis 提供了 RPOPLPUSH 命令,消费者程序会原子性的从主消息队列中取出消息并将其插入到备份队列中,直到消费者程序完成正常的处理逻辑后再将该消息从备份队列中删除。同时还可以提供一个守护进程,当发现备份队列中的消息过期时,可以重新将其再放回到主消息队列中,以便其它的消费者程序继续处理。
7. 谨慎全量操作 Hash、Set 等集合结构
在使用 HASH 结构存储对象属性时,开始只有有限的十几个 field,往往使用 HGETALL 获取所有成员,效率也很高,但是随着业务发展,会将 field 扩张到上百个甚至几百个,此时还使用 HGETALL 会出现效率急剧下降、网卡频繁打满等问题【时间复杂度 O(N)】, 此时建议根据业务拆分为多个 Hash 结构;或者如果大部分都是获取所有属性的操作, 可以将所有属性序列化为一个 STRING 类型存储!同样在使用 SMEMBERS 操作 SET 结构类型时也是相同的情况!
8. 根据业务场景合理使用不同的数据结构类型
目前 Redis 支持的数据库结构类型较多:字符串(String),哈希(Hash),列表(List),集合(Set),有序集合(Sorted Set), Bitmap, HyperLogLog 和地理空间索引(geospatial)等, 需要根据业务场景选择合适的类型。
常见的如:String 可以用作普通的 K-V、计数类;Hash 可以用作对象如商品、经纪人等,包含较多属性的信息;List 可以用作消息队列、粉丝 / 关注列表等;Set 可以用于推荐;Sorted Set 可以用于排行榜等!
9. 命名规范
虽然说 Redis 支持多个数据库(默认 32 个,可以配置更多),但是除了默认的 0 号库以外,其它的都需要通过一个额外请求才能使用。所以用前缀作为命名空间可能会更明智一点。
另外,在使用前缀作为命名空间区隔不同 key 的时候,最好在程序中使用全局配置来实现,直接在代码里写前缀的做法要严格避免,这样可维护性实在太差了。
如:系统名: 业务名: 业务数据: 其他
但是注意,key 的名称不要过长,尽量清晰明了,容易理解,需要自己衡量
10. 线上禁止使用 monitor 命令
禁止生产环境使用 monitor 命令,monitor 命令在高并发条件下,会存在内存暴增和影响 Redis 性能的隐患
11. 禁止大 string
核心集群禁用 1mb 的 string 大 key(虽然 redis 支持 512MB 大小的 string),如果 1mb 的 key 每秒重复写入 10 次,就会导致写入网络 IO 达 10MB;
12. redis 容量
单实例的内存大小不建议过大,建议在 10~20GB 以内。redis 实例包含的键个数建议控制在 1kw 内,单实例键个数过大,可能导致过期键的回收不及时。
13. 可靠性
需要定时监控 redis 的健康情况:使用各种 redis 健康监控工具,实在不行可以定时返回 redis 的 info 信息。客户端连接尽量使用连接池(长链接和自动重连)。