对中间件的熟悉程度会直接影响到你的编码质量和查错效率
一、问题背景
某天早上到公司,习惯性地看下监控大盘,一如既往一样正常,但是在浏览到redis监控时,发现了一个奇怪的现象,从前一天晚上开始,redis的内存用量出现频繁毛刺现象,如下图:
最近没有上线新业务,按道理不应该出现这么频繁地写入(内存占用)和删除/过期(内存释放)。问了下其他同事,都反馈没有任何调整,运维也反馈没有调整。那就奇怪了,还有这种诡异的事情发生。虽然影响不大,但是出于对技术的探索角度,还是继续探究个为什么。
二、排查过程
先介绍下,redis里面存储的都是hash类型的key,过期时间都很久(365天左右),redis内存大小为128M。
首先去找运维(只有运维有线上机器权限),登上redis,看了下key的个数(命令:dbsize),结果发现并没有什么变化(我们的key和用户相关,个数增长不会很快)。然后看了下redis连接情况(命令:client list),发现请求来源都是合法的线上机器。此时排查十分没有头绪。无意间看了眼执行命令列表,发现有一些hgetall,这种O(n)的查询命令,一定是要慎用的(如果可以,都建议用hscan代替,除非确定hash很小)。
反正没有头绪,就顺着hgetall查下去,看能不能查出来什么。于是继续查下redis连接情况(命令:client list),目的是搜下这个hgetall是哪里来的。一番查找后,终于找到了,如下图
然后发现了一个诡异的地方,这里有个omem指标很大,足足26214496字节,约26M,一番回忆后想到,这个omem不就是查询连接的输出区大小嘛,redis会把输出结果放到输出缓冲区返回给客户端,而这个输出缓冲区是占用redis本身的内存资源的。瞬间感觉离真像近在咫尺了,八九不离十就是这个原因了:有某些功能在执行hgetall,而且查询了一个超大的hash key!进而占用了较多输出缓冲区,造成的现象就是内存毛刺(我redis内存本身比较小)。
立刻查看redis快照分析统计,发现果真有几个超大的hash key,打开代码全局搜索后,找到了这块代码,看了下,是一个“祖传”老代码,粗暴地使用了hgetall查询redis内容,然后放入内存缓存(应该是为了进一步提高接口的性能),可能当时hash比较小,没有问题,但是随着业务的发展,hash的field个数越来越多,hash越来越大,问题就凸显出来了!
问题告破。当即改了这位“前辈”的代码,把hgetall改为hscan,如下图,hscan是O(1)复杂度的查询命令,增量的查询hash的field(一般一次返回10个,可设置)直到迭代完毕所有hash的field,完美避免hgetall带来的慢查询和输出缓冲区过大问题。
三、总结发散
redis作为一个高性能缓存非关系数据库(KV数据库),提供了非常高的性能保证(单机可达10w qps),但是如果使用不当,不仅得不到好的效果,还会影响到其他redis操作。比如像keys * ,hgetall,smembers等O(n)时间复杂度的命令,阻塞redis查询(redis连接是epoll模型但是worker是单线程),严重的会出重大线上问题。
其次最重要的是,各位同学不要单纯以为存入redis多少数据,就占用多少内存,大错特错。为什么这么说,因为除了我们原始存入的内容,redis还有另外的数据结构,如:全局的hashTable,每个key的dictEntry,以及大家熟悉的redisObject结构(各种类型的key都是基于此结构体进行构建)等等,这些结构帮助redis来管理我们写入的数据。(就像我们买了个128g内存的iphone,拿到手可用的其实也就110g左右)。
只有不断加深对中间件的熟悉程度、框架底层原理的探索和理解,才会更进一步提升我们的编程能力、架构能力、避免踩一些隐藏的坑,也才是真正的“专业”和“业余”的分水岭!
留2个问题给各位:
1、你写入的key带了ttl,这个ttl在redis中是怎么记录的?
2、hash结构扩容,是怎么样的过程?
欢迎各位在评论区留言讨论。