4

内存分配

讨论Redis内存优化之前,有必要先聊下关于物理内存、虚拟内存的概念。

物理内存:就是主机安装的物理内存条,一般32位的CPU有32条地址线,最大寻址空间就是4G,而64位的CPU则能够支持更大的内存。可是,再大的内存,也有可能满足不了所有应用的内存需要;又或者是,大量使用频率极低的冷数据占用了大部分物理内存。这个时候,就是“虚拟内存”出场了。

虚拟内存(简称VM):由操作系统OS管理,本质就是硬盘中的一个特殊格的磁盘分区、或者一个文件(linux下是swap分区、windows下是ib3文件),其写入写出由OS掌管,如果出现以上两种情景,OS会将溢出物理内存部分数据或者部分冷数据交换到虚拟内存,释放更多空间提供其他应用运作。而对于所有应用而言,其运行可使用的内存理论上是:物理内存 + 虚拟内存

Redis跟所有的应用一样,其内存分配都是要由OS来管理。用户增加key-value对象,Redis则向OS申请内存划分到应用;反之,用户移除key-value对象,Redis向OS返还内存,并由OS决定该将热数据放入物理内存,冷数据交换到硬盘。

不一样的是,Redis有自己的内存分配器,当key-value对象被移除时,Redis不会马上向操作系统释放其占用内存(例如,当用户往一个实例填充了5G的数据,移除其中2G数据,但占用内存可能仍会保持在5G左右)。为什么Redis要这样处理?有两个原因:

1、OS可能会将释放内存交换到VM,但OS的VM又是物理文件,其IO读写效率较低,从而影响Redis性能表现;

2、OS的VM换入换出是基于Page机制,同一Page内的部分数据对象被释放,但其他数据对象依然被其他应用使用中,导致在该Page内的Redis对象没有被释放。

而Redis作者应该是考虑到以上问题,不希望Redis由此降低性能,所以在设计上Redis更倾向于自己掌控VM换入的粒度。

内存监控

redis-cli > info

图片描述

启动redis-cli,info命令可以观察Redis实例的运行情况,其中# Memory块查看内存使用情况。

提示:以上used_memory数据所包含的内存均包含有:

  • 用户定义的数据:内存被用来存储key-value值。

  • 内部开销: 存储内部Redis信息用来表示不同的数据类型。

再来看看具体每个数据所指具体含义

  • used_memory:当前Redis所有key-value值及内部开销理论上要占用的内存

  • used_memory_human:上一数据的可读版本

  • used_memory_rss:(Resident Set Size常驻数据集大小),可理解为OS为Redis分配的物理内存总量

  • used_memory_peak:峰值内存

  • used_memory_peak_human:峰值内存可读版本

  • used_memory_lua:lua引擎占用内存

  • mem_fragmentation_ratio:内存碎片率,used_memory_rss 和 used_memory 之间的比率(以下有更多说明)

  • mem_allocator: Redis 所使用的内存分配器。可以是 libc 、 jemalloc 或者 tcmalloc

其中mem_fragmentation_ratio(内存碎片率)是分析Redis性能的重要数据指标,计算公式是:

图片描述

大于1:OS为Redis分配的物理内存 > Redis所有key-value值及内部开销应占用的内存

产生原因:物理内存多出的部分,Redis内移除对象的占用内存,但这部分内存由Redis自带内存分配器占用,没有向操作系统返回。这一部分就是内存碎片。

小于1:OS为Redis分配的物理内存 < Redis所有key-value值及内部开销理应占用的内存

产生原因:应占内存比物理内存多出的部分,是被操作系统交换到虚拟内存,说明当前Redis的内存使用已经超出物理内存

问题是,这个ratio比率,多少比值最合理?回顾前文,Redis是自带内存分配器的,自带内存分配器会保留内部已释放的内存,目的是为了避免操作系统低性能交换,当然它也是比较灵活的,被释放的内存块是可以被重用。翻阅了下官方文档,也有对应的说明

  • However allocators are smart and are able to reuse free chunks of memory, so after you freed 2GB of your 5GB data set, when you start adding more keys again, you’ll see the RSS (Resident Set Size) to stay steady and don’t grow more, as you add up to 2GB of additional keys. The allocator is basically trying to reuse the 2GB of memory previously (logically) freed.

因此,内存碎片率保持在1.0至1.5之间是最理想的状态。假若碎片率超过了1.5,我所知道的最有效解决手段就是重启Redis服务器,释放内存回到操作系统;反之,若碎片率为0.9,说明物理内存已不够用,应增添硬件,或设置Redis最大内存限制maxmemory

最大内存限制maxmemory的设置非常重要,如果不设置maxmemory,Redis一直会为其分配内存,直至耗尽所有物理内存,直到操作系统进行虚拟内存交换。因此,一般情况下,作者建议还是把峰值设置设上。开启此配置,当超出限定内存情况发生,Redis会返回异常消息,操作系统不会因内存溢出而奔溃。还有一点建议是,开发者在系统设计之初,就应当制定Redis内存使用划分计划,而划分原则是,为Redis准备系统可能使用的峰值内存,而不是平均使用内存。例如系统大部分情况会以Redis作为分布式缓存写入10G数据,但大部分情况下只会跑到4G,但Redis依然推荐用户为其预留10G内存(used_memory_peak峰值)。

redis.conf--> maxmemory 10000000

maxmemory的单位是bytes,默认为0,即不限制最大内存。

内存限制与Key回收

maxmemory以外,仍然需要指定Redis在最大内存溢出后的处理行为——maxmemory-policy。同时设置了maxmemorymaxmemory-policy选项,redis内存使用达到上限。可以通过设置LRU算法(Least Recently Used 近期最少使用算法)来删除部分key,释放空间。相关内容参考:将redis当做使用LRU算法的缓存来使用

提示:Redis32位实例最大可用内存为3G,64位则无限制,而RDB与AOF持久化文件都兼容支持32位或64位实例,因此可以自由切换在32位与64位之间切换。

volatile-lru -> 根据LRU算法生成的过期时间来删除。
allkeys-lru -> 根据LRU算法删除任何key。
volatile-random -> 根据过期设置来随机删除key。
allkeys-random -> 无差别随机删。
volatile-ttl -> 根据最近过期时间来删除(辅以TTL)
noeviction -> 谁也不删,直接在写操作时返回错误。

String与Hash

String与Hash毫无疑问是最常用的两种数据结构,而且AB两个方案互换使用往往都可以满足系统的业务需求,该如何选型,才能在复杂度、性能以及内存间取得一个平衡点?

举个例子,存储以下数据,分别采取String与Hash的形式:

  • object:1

  • object:2

  • object:3

  • object:……

  • object:100000

String存储:以object:x作为key,(1)

stringexp

Hash存储:以object:x作为Key,后两位字节xx作为hash key,(2)

hash100exp

(1)存储结果:
string10w

(2)存储结果:
hash100-10w

从以上结果可以得出:hash散列比string更节省内存,原因有3点:

  1. Redis的Key承载了很多特性的,过期时间、LRU、Cluster节点信息、系统关键信息等,因此和String类型的key-value相比,hash类型可更大限度地减少key的数量,从而节省内存空间的使用率;

  2. key对应多种数据结构,而hash key对应的value只能是string,这种简单结构比多重复杂结构更加高效。(Redis是不支持嵌套数据的,例如hash中value还嵌套有hash);

  3. hash在额定数量及容积下,将会以一维线性的紧凑格式存储(ziplist),这种存储形式可节省更多空间。

注意一点是:hash只会在额定的要求范围内,才会以ziplist存储,当超过额定阈值后会转换成真正hashtable格式并重新存储。而hashtable的存储对于内存的使用不占优势。关于这个额定值,redis提供了两个可配置参数hash-max-ziplist-entrieshash-max-ziplist-value

  • hash-max-ziplist-entries:hash以ziplist存储的最大对象数量,单位:个

  • hash-max-ziplist-value:hash以ziplist存储的单个对象占用空间,单位:bytes

默认值是:

# Hashes are encoded using a memory efficient data structure when they have a
# small number of entries, and the biggest entry does not exceed a given
# threshold. These thresholds can be configured using the following directives.
hash-max-ziplist-entries 512
hash-max-ziplist-value 64

修改配置hash-max-ziplist-entries为20万,再调整以上例子写入10万数据到单一散列中,

advconfig20w

结果是,与(2)例子接近,以ziplist格式存储更节省空间,但缺点是ziplist格式读写速度相当低效,10万数据的写入花费了99356毫秒,接近1.5分钟。因此,在实际应用中,对应存储格式的选型,还是需要根据实际需求作为衡量标准。

onehash10w


Tgor
1.3k 声望57 粉丝

井底之蛙