白盒测试 Redis 集群缓存测试要点--关于 线上 token 失效 BUG 的总结

1875884881 · 2018年03月05日 · 最后由 jim_lin 回复于 2018年03月07日 · 4696 次阅读
本帖已被设为精华帖!

在测试xx系统过程中遇到了线上大面积用户登录态失效的严重问题,事后对于其原因及测试盲点做了一些总结记录以便以后查阅,总结分为以下7点,其中原理性的解释有些摘自网络。

1.xx系统token失效问题复盘
2.Redis 经典流程
3.Redis分片部署方式
4.Redis扩容导致缓存数据失效
5.Redis Sharding一致性hash算法
6.缓存失效,缓存击穿,缓存穿透
7.Redis缓存测试总结

xx系统token失效问题复盘
现象:redis扩容后线上大量用户登录态失效,需要重新登录。由于登录态可以持续保持,部分用户忘记密码,需要修改密码后再次登录。在测试验证中,由于切换环境、登录登出导致这个问题难以发现和注意。

原因:sharded-redis-pool分片规则中有域名因子(框架源码中),扩容修改了redis域名,导致redis中数据虽然存在,概率性获取不到。
PS:失效问题复盘中有较多关于BUG修复前后代码差异的片段由于保密未贴出,一般来说测试复盘过程中对于代码的解析是很重要的一环。

Redis经典流程

前端测试盲点:
1.有些应用临时数据都存储在redis里,不存储在DB里
2.上面流程中redis的数据不管有没有生效,程序都可以正常进行,且功能正常
3.redis如果是集群的方式,缓存数据的读取和写入有没有进入正确的分片

Redis分片部署方式
(1)在客户端(jedis)做分片(Redis Sharding);这种方式在客户端确定要连接的redis实例,然后直接访问相应的redis实例。
(2)在代理中做分片;这种方式中,客户端并不直接访问redis实例,它也不知道自己要访问的具体是哪个redis实例,而是由代理转发请求和结果;其工作过程为:客户端先将请求发送给代理,代理通过分片算法确定要访问的是哪个redis实例,然后将请求发送给相应的redis实例,redis实例将结果返回给代理,代理最后将结果返回给客户端。
(3)在redis服务器端做分片(Redis Cluster);这种方式被称为“查询路由”,在这种方式中客户端随机选择一个redis实例发送请求,如果所请求的内容不再当前redis实例中它会负责将请求转交给正确的redis实例,也有的实现中,redis实例不会转发请求,而是将正确redis的信息发给客户端,由客户端再去向正确的redis实例发送请求。

Redis扩容导致缓存数据失效
假设有三台缓存服务器,缓存hotkey,希望hotkey被均匀的缓存到这三台服务器上,原始的做法是对缓存项的键进行哈希,将哈希后的结果对缓存服务器的数量进行取模操作。

假设三台缓存服务器已经不能满足业务缓存需求,需要增加机器,就会出现一些缺陷。假设增加一台服务器,缓存服务器的数量由三台变为四台,此时,如果仍用取模的方法对同一hotkey进行缓存,那么这个hotkey所在的服务器编号就肯定与原来三台服务器时所在的编号不同。这就导致了缓存在一定时间内是失效的,当应用无法从缓存中获取数据,则会向后端服务请求数据,由于大量缓存同一时间失效,造成缓存的雪崩,可能导致系统被压垮。

Redis Sharding一致性hash算法
一致性hash:一致性哈希算法也是使用取模的方法,只是一致性哈希算法是对2^32取模。
我们有ABC三台服务器,使用各自的IP地址进行哈希计算,使用哈希后的结果对2^32取模,计算结果映射到一个由2^32个点组成的哈希圆环上,可以得到如下的示意图:

假设有4个hotkey,1234需要缓存,根据hash(hotkey)% 2^32得到的映射图如下,hotkey1、2存储到A中,hotkey3存储到B中,hotkey4存储到C中。

假设机器B出现故障,需要移除服务器B,那么移除后的示意图如下。

当服务器移除以后,按照之前的一致性哈希算法的规则,hotkey3应该被缓存到服务器C中,hotkey3的缓存位置发生了改变。但是hotkey1、2仍被缓存到服务器A中,hotkey4仍被缓存到服务器C中,这就是一致性哈希算法的优点,当服务器数量发生改变,并不是缓存都会失效,而是只有部分缓存会失效,前端的缓存仍能分担整个系统的压力,不至于所有压力在同一时间集中到后端服务器上。

Hash环的偏斜及虚拟结点:

在实际的映射中,服务器可能会被映射成如下图:

虚拟节点是实际节点在hash环上的复制品,一个实际节点可以对应多个虚拟节点。虚拟节点可以解决hash环的偏斜以及缓存雪崩的问题。

缓存失效、缓存击穿、缓存穿透
缓存穿透
缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。
解决方案
有很多种方法可以有效地解决缓存穿透问题,最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。另外也有一个更为简单粗暴的方法(我们采用的就是这种),如果一个查询返回的数据为空(不管是数 据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。

缓存雪崩
缓存雪崩是指在我们设置缓存时采用了相同的过期时间或者其他情况,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。
解决方案
缓存失效时的雪崩效应对底层系统的冲击非常可怕。大多数系统设计者考虑用加锁或者队列的方式保证缓存的单线 程(进程)写,从而避免失效时大量的并发请求落到底层存储系统上。这里分享一个简单方案就时讲缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

缓存击穿
对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题,这个和缓存雪崩的区别在于这里针对某一key缓存,前者则是很多key。
缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
解决方案
使用互斥锁(mutex key)
简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。

Redis集群缓存测试总结
功能:
1.系统运行过程中,redis缓存数据生效。缓存的数据读取正确、数据写入落地正确,数据有效期设置合理。
2.redis集群分片策略验证正确。
3.缓存与数据库的数据一致性检测。
4.DB事务性导致回滚,缓存是否回滚,有没有产生脏数据。
5.注意测试环境与线上环境的区别,尤其是单例与集群分片、读写分离。尽量保持测试环境与线上一致或者是其缩小版。

自动化:
1.自动化用例中断言部分设计缓存层断言并且自动化框架本身对于断层层次可配置。

性能及稳定性:
1.关注业务本身应用场景及缓存结构,是否使用缓存。
2.预防缓存穿透、缓存雪崩、缓存击穿引发的系统风险。

扩容:
1.关注扩容方案设计、老数据备份策略、回滚方案
2.关注扩容后分片策略的变化
3.扩容后热点数据失效率或命中率以及对后端DB带来的压力

共收到 11 条回复 时间 点赞

不错,以前公司内做case study也是这样的

点个赞 学习了先

Lihuazhang 将本帖设为了精华贴 03月06日 00:29

写的很棒,缓存穿透处小弟有疑问,作者说你们采用的是最简单暴力的方式,如果查询数据为空,也会把查询到数据(null)存缓存中,失效时间是5分钟,那这里如果有人采用高并发来查询你不存在的数据,会出现什么结果?这里有点类似拒绝服务

很详细,一些开源中间件确实可能存在问题,13年测过redis,也发现了一个数据一致性的bug并提交给了redis官方

seveniruby 回复

谢谢加精😀

_leaves_ 回复

嗯.这次主要是测试对于中间件的分片逻辑不了解,上线以后有概率登录态失效,不好验证。

samfu 回复

我个人意见有已下以点哈。1.系统对于接口的请求频次和参数校验有限制和强校验 2.查询key需要有一定组成规则,不符合规则的数据可以直接抛弃 3.数据库主从分离,并且业务端低耦合。

githubtest007 回复

谢谢

seveniruby 回复

居然被TesterHome公众号推送了,谢谢社区。

1875884881 回复

也给大家看看内部case study的风格,有参考意义

seveniruby 回复

黄先生,您好。sevenirubu@gmail.com这个邮箱还有在使用吗,给您发了邮件,麻烦看下。

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册