2021年遇到的第一个有意思的线上技术故障,是由redis的big key引起的,虽然不是我负责的系统,但是又还是我去排查,花了不少时间和DBA一起刨根究底位到问题根本,记录下这个过程。

故障描述

某个撸码的早晨,组内的一个哥们收到一个千年老系统的短时间内大量接口超时告警,哥们通过链路监控定位了是redis(单机主从)操作请求超时,找了DBA排查了下,DBA给的反馈是某个key过期失效,阻塞了1.3S,但是我们的redis请求超时设置了为5S。所以出现了2个问题,一是为什么一个key过期失效会阻塞长达1.3S,另外即使阻塞1.3S为什么还是会有很多redis的操作超时。

排查过程

阻塞1.3S是怎么判断的

一般遇到redis服务端处理请求超时,直接访问redis的master,查看slowlog,发现只有某个set的ttl操作耗时很长,有1.8S,而且2者之间隔了刚好2S,而且这个ttl的操作很频繁,基本都是每隔2S请求一次

1) 1) (integer) 5994  
   2) (integer) 1611914411    2021-01-29 18:00:11  
   3) (integer) 1877766  
   4) 1) "TTL"  
      2) "is_a_big_set"  
2) 1) (integer) 5993  
   2) (integer) 1611914409    2021-01-29 18:00:09  
   3) (integer) 1824734   
   4) 1) "TTL"  
      2) "is_a_big_set"  

然后跑到slave去查看下slowlog,发现的是在TTL操作中有一次这个key的del

1) 1) (integer) 1047
   2) (integer) 1611914413    2021-01-29 18:00:13
   3) (integer) 1333533
   4) 1) "DEL"
      2) "is_a_big_set"
2) 1) (integer) 1046
	2) (integer) 1611914411    2021-01-29 18:00:11
	3) (integer) 1211249
	4) 1) "DEL"
  	   2) "is_a_big_set"   

根据redis的描述一个key超时被清理之后,会往其slave发送一个DEL的命令,由此可判断,slave的del操作是由master过期清理is_a_big_set 这个key,但是为什么会引起阻塞了1.3S,查看了下业务相关代码,确认这个key是一个有大约167W个元素的Set(别问我为什么会有这种设计,我只是擦别人屁股的),是非常大的了。根据redis的文档(https://redis.io/commands/del)我们了解到,del一个set的时间复杂度是O(n),n是该key的元素数量.也就是元素越大删除的耗时越长。

故障还原

为了验证大key删除耗时问题,我手动写了个shell脚本,往2个set分别写了300W和1000W个元素,然后测试删除的执行时间,脚本核心内容如下:

for (( i=10000000; i<20000000; i++ ))
do
redis-cli -h 10.100.100.101 -p 7001 -c sadd testbigkey ${i} >/dev/null
done

通过time命令和slowlog信息来确认,300W的set删除耗了1.8S,1000W的set删除耗了6.8S。所以第一个问题得以解释,阻塞1.3S是因为key的元素量过大,过期清理的时候阻塞了操作。

客户端的超时

虽然我们确定是某个big key导致这个问题,但是完全没有解释清楚为什么会让客户端这么多请求超时。
再次抓了DBA过来一起排查,在redis的slow log发现的确是在业务应用里面每隔2S就去对这个key做一次ttl,而且出奇的耗时,每次ttl耗时1.8S,如果每次都耗时1.8S,每2次中间间隔2S,如果中间出现一次del操作,很可能会一下子这几个操作一起并发,耗时就会比较长,就会导致是客户端的请求超时。
我们尝试在测试环境对一个200W元素的set做ttl操作,发现每次耗时仅在60毫秒左右,跟上生产的1.8S相差甚远。后来DBA找了1台生产的机器进行一样的操作,居然重现了这个耗时的ttl操作。所以这个ttl我们暂时归属为网络原因,细节还需要运维去排查。
也就是我们定位这个客户端的超时根因归属为应用频繁的ttl操作外加该key的过期清理操作一起 引起了客户端的请求超时。

解决思路

1.这个ttl操作的频率降低
2.从业务实现上改造,将167W个元素的set给拆分掉,避免出现bigkey

在redis4.0推出了unlink命令(https://redis.io/commands/unlink),用于异步去释放某个key所占用的内存。相比del,它是非阻塞的,本质上是将key与key所占用的内存空间断开连接,实际的删除再异步进行。在应对bigkey这种场景非常有效。