redis 性能调优 -- 内存使用率过高
一.背景知识
要针对 redis 的内存使用率进行调优,首先需要了解 redis 是如何进行数据存储的。
1.1 redis 所有键的存储方式
Redis 是一个键值对数据库服务器,服务器中的每个数据库都由一个 redis.h/redisDb 结构表示,其中 redisDb 结构的 dict 字典保存了数据库中所有键值对,我们称这个字典为键空间。(dict 详细数据结构可参考redis--zset解析)
键空间的键就是数据库的键,每个键都是一个字符串对象。
键空间的值就是数据库的值,每个值可以是字符串对象,列表对象,哈希表对象,集合对象和有序集合对象中的任意一种 Redis 对象
从 redis 数据库的结构方式可知,对数据库键中的任意一种增,删,改,查操作实际上都是针对的字典进行的操作。
1.2 redis 键的过期机制
1.2.1 过期时间存储
redisDb 结构体用 dict 字典存储键值对,用 expires 字典保存了数据库中所有键的过期时间,这个字典就是过期字典。
过期字典的键是一个指针,这个指针指向键空间中的某个键对象(也即是某个数据库键)
过期字典的值是一个 long long 类型的整数,这个整数保存了键所指向的数据库键的过期时间--一个毫秒精度的 unix 时间戳。
1.2.2 过期键删除
最简单的定时删除
和惰性删除
都有他们自己的缺点
定时删除,如果过期键太多,每次删除过程会占用大量的 cpu 时间,影响服务器的响应时间和吞吐量。
惰性删除,如果有大量的过期 key 不被访问,就相当于内存泄漏,浪费太多内存,有内存泄漏的风险。
定期删除
每隔一段时间执行一次删除过期键的操作,并通过限制删除操作执行的时长和频率来减少删除对 cpu 时间的影响
通过定期删除过期键,减少了太多过期键的内存浪费。
redis 采用的是定期删除+惰性删除的综合方式
二.内存调优方式
2.1 官方内存优化方式
2.1.1 小型聚合数据类型的特殊编码(Special encoding of small aggregate data types)
介绍
Redis2.2 之后针对许多数据类型进行了优化,用于减少空间使用,list
,hash
,set
长度较小且其中存储的值的大小不超过限制的场景下会使用ziplist
结构存储--以序列化方式存储,需要解码和编码,这是一种 cpu 换内存的权衡,可以在配置文件中调整特殊编码类型的最大元素数量和最大元素大小
如果特殊编码的值超出了配置的最大大小,Redis 会自动将其转换为正常编码。 对于较小的值,此操作非常快,但如果您更改设置以便为更大的聚合类型使用特殊编码的值,建议运行一些基准和测试来检查转换时间。
思考
在需要的场景下通过调整 redis 存储的 k/v 存储方式,可以利用上ziplist
的特性,减少内存消耗,但是注意减少内存消耗的同时会增加 cpu 的消耗
补充
ziplist 是一个经过特殊编码的双向链表连续的一整块内存,减少了内存碎片,增大了内存利用率;通过地址长度来进行数据索引,而不是指针,减少了内存的消耗。
2.1.2 使用 32 位实例(Using 32 bit instances)
介绍
使用 32 位目标编译的 Redis 每个键使用的内存要少得多,因为指针很小,但是这样的实例将被限制为 4 GB 的最大内存使用量。 要将 Redis 编译为 32 位二进制文件,请使用 make 32bit。 RDB 和 AOF 文件在 32 位和 64 位实例之间兼容(当然在小端和大端之间),因此您可以从 32 位切换到 64 位,或者相反,没有问题。
思考
这个感觉偏运维一些,是用 64 位选择单机存储更多还是用 32 位选择单台少,但是对内存利用率更高的方式,两者没有什么优劣之分,更看公司在使用 redis 上的取舍。
2.1.3 位与字节级操作(Bit and byte level operations)
介绍
Redis 2.2 引入了新的位和字节级别操作:GETRANGE、SETRANGE、GETBIT 和 SETBIT。 使用这些命令,您可以将 Redis 字符串类型视为随机访问数组。
例如一些大量的 bool 类型的存取,一个用户 365 天的签到记录,签到了是 1,没签到是 0,如果用普通的 key/value 进行存储,当用户量很大的时候,需要的存储空间是很大的。
如果使用位图进行存储,一年 365 天,用 365 个 bit 就可以存储,365 个 bit 换算成 46 个字节(一个稍长的字符串),如此就节省了很多的存储空间,
思考
大量的 bool 类型的存取,且这些值最好是连续的,可以用位图减少我们存储的内存消耗
2.1.4 尽可能的使用 hashes(Use hashes when possible)
小散列被编码在一个非常小的空间中,因此您应该尽可能尝试使用散列来表示您的数据。 例如,如果您在 Web 应用程序中有代表用户的对象,而不是使用不同的键来表示姓名、姓氏、电子邮件、密码,而是使用包含所有必填字段的单个哈希,详细信息可见下一节。
2.1.5 使用哈希在 Redis 之上抽象出一个非常节省内存的普通键值存储(Using hashes to abstract a very memory efficient plain key-value store on top of Redis)
核心归纳还是利用了 hash 的特殊编码,在特殊编码下 hash 会是一个线性数组--ziplist,比一般的 set 能节省 10 倍内存消耗,所以可以根据要存储的对象进行 hash 建模,保证他的存储结构式 zipilist 即可节省内存。
举个例子,假设我们有一堆编码的对象:
object:102393
object:1234
object:5
一般我们可以使用 set 命令对这些对象进行存储
set object:1234 somevalue
为了节省内存,我们可以将对象名称进行拆分
hset object:1023 93 somevalue
hset object:12 34 somevalue
hset object: 5 somevalue
这样做的好处是保证每一个 key 只有 100 个 field,可以达到 cpu 与节省内存之间的最佳折衷。
2.1.6 内存分配(Memory allocation)
关于 redis 的内存分配有几个需要注意的点:
删除键时,redis 并不总是向操作系统释放内存。例如,如果您用 5GB 的数据填充一个实例,然后删除相当于 2GB 的数据,则驻留集大小(也称为 RSS,它是进程消耗的内存页数)可能仍然是大约 5GB,即使 Redis 声称用户内存在 3GB 左右。发生这种情况是因为底层分配器不能轻易释放内存。例如,大多数已删除的键通常与仍然存在的其他键分配在相同的页面中。
前一点意味着您需要根据您的峰值内存使用情况来配置内存。如果您的工作负载不时需要 10GB,即使大多数时候 5GB 可以做到,您也需要预置 10GB。
然而分配器很聪明并且能够重用空闲的内存块,所以在你释放了 5GB 数据集中的 2GB 之后,当你再次开始添加更多键时,你会看到 RSS(驻留集大小)保持稳定并且不会增长更多,当您添加多达 2GB 的额外密钥时。分配器基本上试图重用之前(逻辑上)释放的 2GB 内存。
由于所有这些,当您的内存使用量在峰值时远大于当前使用的内存时,碎片率是不可靠的。碎片计算为实际使用的物理内存(RSS 值)除以当前使用的内存量(作为 Redis 执行的所有分配的总和)。因为 RSS(Redis 进程占用的物理内存总量 这是直观在 redis 显示的,也是最关注的) 反映了峰值内存,当(虚拟)使用的内存由于很多键/值被释放而低,但 RSS 高时,比率 RSS / mem_used 将非常高。
如果未设置 maxmemory,Redis 将继续分配它认为合适的内存,因此它可以(逐渐)耗尽所有可用内存。因此,通常建议配置一些限制。您可能还想将 maxmemory-policy 设置为 noeviction(在某些旧版本的 Redis 中这不是默认值)。
它使 Redis 在达到限制时返回写入命令的内存不足错误 - 这反过来可能会导致应用程序出现错误,但不会因为内存不足而导致整个机器死机。
2.2 业务内存使用率优化方向
2.2.1 修改业务对象缓存结构
将 set 的改成 hash,由于 hash 的编码,在业务对象小于配置限制的场景下业务对象会被存储为 ziplist,能节省 10 倍的存储。
2.2.2 缩短业务对象的过期时间
通过前面的背景知识,我们知道 redis 存储有过期时间的业务对象的时候会将对象指针和过期时间存储到一个过期字典
中。
redis 在清除过期时间的对象的时候使用的是定时+惰性
的清楚策略。
在 redis 的内存分配中我们又能知晓删除键时,redis 并不总是向操作系统释放内存。例如,如果您用 5GB 的数据填充一个实例,然后删除相当于 2GB 的数据,则驻留集大小(也称为 RSS,它是进程消耗的内存页数)可能仍然是大约 5GB,即使 Redis 声称用户内存在 3GB 左右。发生这种情况是因为底层分配器不能轻易释放内存。例如,大多数已删除的键通常与仍然存在的其他键分配在相同的页面中。(虽然由于分配器的优化会重用空闲的内存块)
所以如果出现对象过期时间还没到,又有新的对象需要存储的;或者对象过期时间与新对象进入时间几乎一致,redis的内存消耗一定会飞速上涨。
所以针对业务场景,我们可以结合业务对象的应用场合,尽量的设置合适的过期时间,来达到节省内存使用的目的,当然,如果缓存时间过短,也会出现大量缓存失效,从而导致雪崩的发生,一味追求节省内存空间肯定也是不对的。
评论