持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第21天,点击查看活动详情
背景:
近五年redis内存都维持再一个稳定的状态,运维会定期做一些维护清理,近期没有运维发现生产环境reids内存爆满。阿里云统计数据超过了100%。
接下来就开始比较犯难了,redis超过100%阿里云既不能升级,生产环境也无法写入,即便这是一个低级错误但是已经发生了。问题先放一放,先了解一下redis集群的相关知识原理,再去解决问题。
平台redis整体采用的是集群模式,大体架构模式:
redis集群是一个由多个主从节点群组成的分布式服务器群,它具有复制、高可用和分片特性。Redis集群不需要sentinel哨兵也能完成节点移除和故障转移的功能。需要将每个节点设置成集群模式,这种集群模式没有中心节点,可水平扩展,据官方文档称可以线性扩展到上万个节点(官方推荐不超过1000个节点)。redis集群的性能和高可用性均优于之前版本的哨兵模式,且集群配置非常简单。
Redis集群原理分析
Redis Cluster 将所有数据划分为 16384 个 slots(槽位) ,每个节点负责其中一部分槽位。槽位的信息存储于每个节点中。
当 Redis Cluster 的客户端来连接集群时,它也会得到一份集群的槽位配置信息并将其缓存在客户端本地。这样当客户端要查找某个 key 时,可以直接定位到目标节点。同时因为槽位的信息可能会存在客户端与服务器不一致的情况,还需要纠正机制来实现槽位信息的校验调整。
Redis缓存淘汰算法
缓存淘汰策略
因为Redis是基于内存的,内存的空间是非常宝贵的,所以数据不可能无上限的存储,必定会存在一个淘汰策略定期删除一些key。
那Redis的缓存淘汰策略有两种:定时删除和惰性删除
-
定时删除: Redis将每个设置了过期时间的key放到一个独立的Hash表中,默认每秒定时遍历这个hash而不是整个Redis内存空间,并且Redis不会遍历所有的key,而是采用一种贪心策略。步骤如下:
-
从过期key字典中,随机找20个key。
-
删除20个key中过期的key。
-
如果2中过期的key超过1/4,则重复第一步。
如果有大量的key在同一时间段内过期,就会造成数据库的集中访问,就是缓存雪崩!
- 惰性删除: 因为定时删除会漏掉一部分已过期的key而没有被删除,所以Redis引入一个惰性删除来删除那些漏掉了的key
客户端访问的时候,会对这个key的过期时间进行检查,如果过期了就立即删除。
内存淘汰机制
思考一下,如果定期删除漏掉了大量的key, 且我们后面也没有访问这些key, 没有触发惰性删除,那么内存中会残留大量垃圾key, 直到某一个时刻,Redis内存总会被填满,此时Redis会触发他的内存淘汰机制。
Redis配置文件中可以设置maxmemory,内存的最大使用量,到达限度时会执行内存淘汰机制。没有配置时,默认为no-eviction
Redis中的内存淘汰机制:
当 Redis 的内存超过最大允许的内存之后,Redis 会触发内存淘汰策略。(过期策略是指正常情况下清除过期键,内存淘汰是指内存超过最大值时的保护策略)。内存淘汰策略可以通过maxmemory-policy进行配置,目前Redis提供了以下几种(2个LFU的策略是4.0后出现的):
volatile-lru,针对设置了过期时间的key,使用lru算法进行淘汰。
allkeys-lru,针对所有key使用lru算法进行淘汰。
volatile-lfu,针对设置了过期时间的key,使用lfu算法进行淘汰。
allkeys-lfu,针对所有key使用lfu算法进行淘汰。
volatile-random,从所有设置了过期时间的key中使用随机淘汰的方式进行淘汰。
allkeys-random,针对所有的key使用随机淘汰机制进行淘汰。
volatile-ttl,针对设置了过期时间的key,越早过期的越先被淘汰。
noeviction,不会淘汰任何数据,当使用的内存空间超过 maxmemory 值时,再有写请求来时返回错误。
除了比较特殊的noeviction与volatile-ttl,其余6种策略都有一定的关联性。我们可以通过前缀将它们分为2类,volatile-与allkeys-,这两类策略的区别在于二者选择要清除的键时的字典不同,volatile-前缀的策略代表从设置了过期时间的key中选择键进行清除;allkeys-开头的策略代表从所有key中选择键进行清除。这里面值得介绍的就是lru与lfu了,下面会对这两种方式进行介绍。
LRU是Least Recently Used的缩写,也就是表示最近很少使用,也可以理解成最久没有使用。也就是说当内存不够的时候,每次添加一条数据,都需要抛弃一条最久时间没有使用的旧数据。(一个key上一次的访问时间存储redisObject中的lru字段中。
LRU 是基于链表结构实现的,链表中的元素按照操作顺序从前往后排列,最新操作的键会被移动到表头,当需要进行内存淘汰时,只需要删除链表尾部的元素即可
需要注意的是Redis并没有使用标准的LRU实现方法作为LRU淘汰策略的实现方式,这是因为: 要实现LRU,需要将所有数据维护一个链表,这就需额外内存空间来保存链表每当有新数据插入或现有数据被再次访问,都要调整链表中节点的位置,尤其是频繁的操作将会造成巨大的开销(不要忘了Redis就是为了存储热点数据而出现的,这必然导致数据出现高频的访问)
在Redis中,键值对(Key-Value)存储方式是由字典(Dict)保存的,而字典底层是通过哈希表来实现的。通过哈希表中的节点保存字典中的键值对。我们知道当HashMap中由于Hash冲突(负载因子)超过某个阈值时,出于链表性能的考虑,会进行Resize的操作。Redis也一样
在Redis的具体实现中,使用了一种叫做 渐进式哈希(Rehash) 的机制来提高字典的缩放效率,避免 rehash 对服务器性能造成影响,假如Redis中有大量的key, 如果一次性对全部的数据进行Rehash, 可能会导致Redis在一段时间内停止服务。
在Redis中,哈希表扩容需要将 哈希表0 里面的所有键值对 rehash 到 哈希表1 里面, 但是, 这个 rehash 动作并不是一次性完成的, 而是分多次、渐进式地完成的。
数据分布
数据库首先要解决把整个数据集按照分区规则映射到多个节点的
问题,即把数据集划分到多个节点上,每个节点负责整体数据的一个子集。
如图所示。
需要重点关注的是数据分区规则。常见的分区规则有哈希分区和顺序分
区两种,表对这两种分区规则进行了对比。
分布式存储数据分区
由于Redis Cluster采用哈希分区规则,这里我们重点讨论哈希分区,常 见的哈希分区规则有几种,下面分别介绍。
1.节点取余分区
使用特定的数据,如Redis的键或用户ID,再根据节点数量N使用公式: hash(key)%N计算出哈希值,用来决定数据映射到哪一个节点上。这种方 案存在一个问题:当节点数量变化时,如扩容或收缩节点,数据节点映射关 系需要重新计算,会导致数据的重新迁移。 这种方式的突出优点是简单性,常用于数据库的分库分表规则,一般采 用预分区的方式,提前根据数据量规划好分区数,比如划分为512或1024张 表,保证可支撑未来一段时间的数据量,再根据负载情况将表迁移到其他数 据库中。扩容时通常采用翻倍扩容,避免数据映射全部被打乱导致全量迁移 的情况,
如图
2.一致性哈希分区
一致性哈希分区(Distributed Hash Table)实现思路是为系统中每个节 点分配一个token,范围一般在0~2 32 ,这些token构成一个哈希环。数据读写 执行节点查找操作时,先根据key计算hash值,然后顺时针找到第一个大于 等于该哈希值的token节点,如图所示。
这种方式相比节点取余最大的好处在于加入和删除节点只影响哈希环中 相邻的节点,对其他节点无影响。但一致性哈希分区存在几个问题:
-
加减节点会造成哈希环中部分数据无法命中,需要手动处理或者忽略
-
这部分数据,因此一致性哈希常用于缓存场景。
-
当使用少量节点时,节点变化将大范围影响哈希环中数据映射,因此
-
这种方式不适合少量数据节点的分布式方案。
-
普通的一致性哈希分区在增减节点时需要增加一倍或减去一半节点才 能保证数据和负载的均衡。
正因为一致性哈希分区的这些缺点,一些分布式系统采用虚拟槽对一致 性哈希进行改进,比如Dynamo系统。
虚拟槽分区
虚拟槽分区巧妙地使用了哈希空间,使用分散度良好的哈希函数把所有 数据映射到一个固定范围的整数集合中,整数定义为槽(slot)。这个范围 一般远远大于节点数,比如Redis Cluster槽范围是0~16383。槽是集群内数据 管理和迁移的基本单位。采用大范围槽的主要目的是为了方便数据拆分和集 群扩展。每个节点会负责一定数量的槽,如图、
当前集群有5个节点,每个节点平均大约负责3276个槽。由于采用高质
量的哈希算法,每个槽所映射的数据通常比较均匀,将数据平均划分到5个
节点进行数据分区。Redis Cluster就是采用虚拟槽分区。
问题解决方法:
问题1背景中提到问题是当前redis爆满,无法升级扩容。 问题2如何扩容,扩容多大比较合理。 问题3如何维护redis内存空间。
问题1的解决方式是基于现有redis淘汰机制以外,需要人工删除一些业务废弃的key,统计redis中占用内存空间比较大,不实用或者年限久远未设置过期时间的key。腾出一定的内存空间,让redis维持在100%以下的范围。 问题2升级redis。基于以上提到的redis的卡槽数据分布。扩容时通常采用翻倍扩容,避免数据映射全部被打乱导致全量迁移的情况,如现有是32G,下次扩容就要升级到64G。 问题3开发阶段就要规划好至少最好以下几点: 1.redis的使用规则和淘汰时机,如合理分配DB,一边后期维护管理。 2.命名规则等,后期方便业务手动清理一些废弃的业务模块存储的相关数据等。 3.对于有时效性的key设置合理的过期时间,便于节约空间,redis内存是比较昂贵的。