运维 Redis 的苦恼
我们都知道 Redis 是互联网产品开发中不可缺少的常备武器,速度快、数据结构丰富、简单易用是它的优点,但同时也是因为太容易用了,我们的开发同学不管什么数据、不管这数据有多大、不管数据有多少通通塞进去,要查的时候通通一把梭全取出来,你说这是多么信任 Redis 啊,运维 Redis 的同学听到这种说法一定会露出下边这种表情:
为了更好的了解我们在如何使用 Redis 除了对 Redis 做一些使用规范,还需要对线上使用的 Redis 的有充分的了解。那么问题来了:一个 Redis 的实例用了那么大的内存,里边到底存了啥?都有那些 Key?每个 Key 用了多少空间?Redis 内存使用越来越大,却不知道里面都存了啥,想要分析一次费时费力,一个搞不好还容易影响业务。
那有没有什么办法让我们安全高效的看到 Redis 内存消耗的详细报表呢?就像这样又直观又漂亮:
办法总比问题多,有需求就有解决方案。上面就是雪球 SRE 团队做出来的 Redis 数据可视化平台 RDR (Redis Data Reveal)。RDR 可以非常方便的对 Reids 的内存进行分析了解一个 Reids 里都有那些 Key,那些 Key 占用的空间是多少,占比如何,非常直观业。那么接下来聊聊我们是如何设计并实现它的。
设计思路
首先想有什么办法可以拿到 Redis 的所有数据呢?
先通过 keys * 命令,拿到所有的 key,然后根据 key 再获取所有的内容。
优点:可以不使用 redis 机器的硬盘,直接网络传输
缺点:如果key数量特别多,keys 命令可能会导致 Redis 卡住影响业务;需要对 Redis 请求非常多次,资源消耗多;遍历数据太慢
开启 aof,通过 aof 文件获取到所有数据。
优点:无需影响 Redis 服务,完全离线操作,足够安全;
缺点:有一些 Redis 实例写入频繁,不适合开启 aof,普适性不强;aof 文件有可能特别大,传输、解析起来太慢,效率低。
使用 bgsave,获取 rdb 文件,解析后获取数据。
优点:机制成熟,可靠性好;文件相对小,传输、解析效率高
缺点:bgsave 虽然会 fork 子进程,但还是有可能导致主进程卡住一段时间,对业务有产生影响的风险
以上几种方式我们评估之后,决定采用低峰期在从节点做 bgsave 获取 rdb 文件,相对安全可靠,也可以覆盖所有业务的 Redis 集群。每个实例每天在低峰期自动生成一个 rdb 文件,即使报表数据有一天的延迟也是可以接受的。
拿到了 rdb 文件就相当于拿到了 Redis 实例的所有数据,接下来就是生成报表的过程了。
解析 rdb 文件,获取到 key 和 value 的内容。
根据相对应的数据结构及内容,估算内存消耗等
统计并生成报表
逻辑很简单,所以设计思路很清晰。数据流图如下:
然后我们看下具体该如何实现,雪球 SRE 自研的组件基本都是用 GO 做的后端,所以语言选型没什么纠结,直接用 GO。
解析 RDB
按照 Redis 的协议来做就可以了,这个在 github 上各种语言基本都有相关的库,拿过来使用即可。需要注意的是语言本身的性能,对解析效率有比较大的影响。
估算内存消耗
一条记录会有哪些内存使用呢?
我们知道 Redis 的实现里面有一些基础的数据结构,就是用这些结构来实现了对外暴露的各种数据类型:比如 sds、dict、intset、zipmap、adlist、ziplist、quicklist、skiplist 等等。只要根据这条记录的数据类型,找出使用了哪些数据结构,再计算出这些基础数据结构的内存消耗,再加上数据的内存使用,以及一些额外开销比如过期时间等,就可以估算出一条记录到底使用了多少内存。
但是由于 Redis 做了非常多的优化,同样的一种数据类型,在不同场景下使用的数据结构有可能是不同的。比如 List ,比较老版本的 Redis,会根据list元素的数量来决定来使用哪种结构,较短的时候使用 adlist,长之后使用 ziplist,数值可以通过 list-max-ziplist-entries 来配置。3.2 版本以后全都使用了 quicklist。而不同结构对于内存的使用其实是有区别的,我们计算的时候也没办法拿到具体的配置,所以都按默认配置来计算,***得出的值是一个估算的值,不过也基本可以反应使用情况了。如果大家对于 Redis 使用的各种数据结构感兴趣,想了解其设计及适用场景,可以多搜索一下相关的资料以及阅读 Redis 源码。
举个计算内存使用的例子:
假如我们通过解析 rdb,获取到了一个 key 为 hello,value 为 world,类型为 string ,ttl 为 1440 的一条记录,它的内存使用是这样的:
一个 dictEntry 的消耗,因为是 redis db dict 中的一个元素
一 个 robj 的消耗,robj 是为了在同一个dict内能够存储不同类型的value,而使用的一个通用的数据结构,全名是 Redis Object
存储 key 的 sds 消耗,sds 是 Redis 中存储字符串使用的数据结构
存储过期时间消耗
存储 value 的 sds 消耗
前四项基本是存储任何一个 key 都需要消耗的,***一项根据 value 的数据结构不同而不同。
一个 dictEntry 有 2 个指针,一个 int64 的内存消耗(https://github.com/antirez/redis/blob/unstable/src/dict.h)
一个 robj 有 1 指针,一个 int,以及几个使用位域的字段共消耗 4 字节(https://github.com/antirez/redis/blob/unstable/src/server.h)
过期时间也是存储为一个 dictEntry,时间戳为 int64(https://github.com/antirez/redis/blob/unstable/src/db.c)
存储 sds 需要存储 header 以及字符串长度 +1 的空间,header 长度根据字符串长度不同也会有所不同(https://github.com/antirez/redis/blob/unstable/src/sds.h)
我们根据以上信息可以算出,向操作系统申请这些内存,真正需要多少内存。由于 redis 支持多种 malloc 算法,我们就按 jemalloc 的分配方式算,这里也是可能存在误差的点。
所以*** key 为 hello 的这条记录在 64 位操作系统上一共会消耗 92 字节。
其他类型的计算也大致是同样的思路,只不过根据不同的数据结构需要计算不同的内存消耗,计算的时候要记得考虑内存对齐的情况。还有由于 zset 的算法涉及到了随机生成层数,我们也使用同样的算法来随机,但是算出来的值肯定不是精确的,也是一个误差点。
统计计数
终于可以拿到任何一个 key 的内存使用了,哪些是最有意义最有价值的数据呢?
top N,毫无疑问***的前 N 个 key 一定是要关注的
不同数据类型的 key 数量元素数量分布以及内存使用情况
按照前缀分类,统一的前缀一般意味着某个特定的业务在使用,计算各个分类的 key 数量及内存使用情况
这几个需求实现起来也都很容易:
维护一个小顶堆来存储前 N 个***的即可,***取出堆中的数据即可
计数即可
一般都会有特定的分隔符,比如 :|._ 等字符,按照这些字符切出公共前缀再统计,同时把所有的数字都替换为0,便于分类
写在***
可以每天打开个网页就可以看到某个 Redis 实例的内存使用的详细情况,是件非常幸福的事情,Redis 的内存使用再也不是黑盒。