在测试 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:一致性哈希算法也是使用取模的方法,只是一致性哈希算法是对 232 取模。
我们有 ABC 三台服务器,使用各自的 IP 地址进行哈希计算,使用哈希后的结果对 232 取模,计算结果映射到一个由 232 个点组成的哈希圆环上,可以得到如下的示意图:

假设有 4 个 hotkey,1234 需要缓存,根据 hash(hotkey)% 232 得到的映射图如下,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 带来的压力


↙↙↙阅读原文可查看相关链接,并与作者交流