内存消耗
目的:理解内存消耗在哪里
内存使用统计
127.0.0.1:6379> info memory
# Memory
used_memory:586328
used_memory_human:572.59K
used_memory_rss:8495104
used_memory_rss_human:8.10M
used_memory_peak:586328
used_memory_peak_human:572.59K
used_memory_peak_perc:100.01%
used_memory_overhead:541330
used_memory_startup:524344
used_memory_dataset:44998
used_memory_dataset_perc:72.60%
allocator_allocated:733648
allocator_active:950272
allocator_resident:3510272
total_system_memory:8182054912
total_system_memory_human:7.62G
used_memory_lua:37888
used_memory_lua_human:37.00K
used_memory_scripts:0
used_memory_scripts_human:0B
number_of_cached_scripts:0
maxmemory:0
maxmemory_human:0B
maxmemory_policy:noeviction
allocator_frag_ratio:1.30
allocator_frag_bytes:216624
allocator_rss_ratio:3.69
allocator_rss_bytes:2560000
rss_overhead_ratio:2.42
rss_overhead_bytes:4984832
mem_fragmentation_ratio:15.58
mem_fragmentation_bytes:7949800
mem_not_counted_for_evict:0
mem_replication_backlog:0
mem_clients_slaves:0
mem_clients_normal:16986
mem_aof_buffer:0
mem_allocator:jemalloc-5.1.0
active_defrag_running:0
lazyfree_pending_objects:0
属性名 | 属性说明 |
used_memory | Redis分配器分配的内存总量,也就是内部存储的所有数据内存占用量 |
used_memory_human | 以可读格式返回 Redis 使用的内存总量 |
used_memory_rss | 从操作系统的角度,Redis进程占用的总物理内存 |
used_memory_peak | 内存分配器分配的最大内存,代表used_memory的历史峰值 |
used_memory_peak_human | 以可读的格式显示内存消耗峰值 |
used_memory_lua | Lua引擎所消耗的内存 |
mem_fragmentation_ratio | used_memory_rss /used_memory比值,表示内存碎片率 |
mem_allocator | Redis 所使用的内存分配器。默认: jemalloc |
需要重点关注的指标:used_memory_rss、used_memory,以及他们的比值mem_fragmentation_ratio
- 如果
mem_fragmentation_ratio>1
(jemalloc在1.03左右比较正常)时
- 说明used_memory_rss-used_memory多出的部分内存并没用用于数据存储,而是被内存碎片所消耗
- 如果两者相差很大,说明碎片率严重
- 这时便可以考虑重启redis服务,在内存中对数据进行重排,减少内存碎片。
- 如果
mem_fragmentation_ratio<1
时
- 这种情况一般出现在操作系统把redis内存交换到硬盘导致(redis内存不足,部分数据使用了虚拟内存)
- 出现了这种情况需要格外关注,由于硬盘速度远远慢于内存,redis性能会变得很差,甚至僵死
- 因此必须设法增大物理内存(可以增加服务器节点数量,或提高单机内存),或减少redis中的数据
- 要减少redis中的数据,除了选用合适的数据类型、利用共享对象等,还有一点是要设置合理的数据回收策略(maxmemory-policy),当内存达到一定量后,根据不同的优先级对内存进行回收。
建议要设置和内存一样大小的交换区:
- 如果没有交换区,一旦 Redis 突然需要的内存大于当前操作系统可用内存时,Redis 会因为 out of memory 而被 Linix Kernel 的 OOM Killer 直接杀死。
- 虽然当 Redis 的数据被换出 (swap out) 时,Redis的性能会变差,但是总比直接被杀死的好。
used_memory = 自身内存+对象内存+缓冲内存+lua内存
used_rss = used_memory + 内存碎片
内存消耗划分
Redis 进程内消耗主要包括:自身内存 + 对象内存 + 缓冲内存 + 内存碎片
自身内存
- -Redis 空进程自身内存消耗非常少,通常 used_memory_rss 在 3MB 左右时,used_memory 一般在 800KB 左右
- 一个空的 Redis 进程消耗内存可以忽略不计。因此我们不必要关注这里
对象内存
对象内存是redis内存占用最大的一块,存储这用户的所有数据
redis所有数据都采用key-value数据类型,至少创建两个类型对象:key对象和value对象,也就是说,对象内存=sizeof(keys)+sizeof(values)
- key对象都是字符串,在使用redis时很容易忽略键对内存的影响,应当避免使用过长的键
-value对象复杂些:
- 主要包含五中基础类型:字符串、列表、hash、集合、有序集合,其他数据类型都是建立在这五种数据结构之上的
- 每种value对象类型根据使用规模不同,占用内存不同。因此,在使用时一定要合理预估并监控value对象占用情况,避免内存溢出
缓冲内存
缓冲内存主要包括:客户端缓冲、复制积压缓冲区、AOF缓冲区
客户端缓冲
客户端缓冲指的是所有接入到redis服务器的TCP连接的输入输出缓存。输入缓存无法控制,最大空间为1G,如果超过将断开连接。输出缓冲通过参数client-output-buffer-limit控制,如下所示:
- 普通客户端:
- 除了复制和订阅的客户端之外的所有连接
- Redis的默认配置是:client-output-buffer-limit normal 0 0 0,也就是说redis并没有对普通客户端的输出缓冲区做限制
- 一般普通客户端的内存消耗可以忽略不计,但是当有大量慢连接客户端接入时这部分内存消耗就不能忽略了,可以设置maxclients做限制
- 特别是当使用大量数据输出的命令而且数据无法及时推送给客户端时,比如monitor命令,容易造成redis服务器内存突然飙升
- 从客户端
- 主节点会为每个从节点单独建立一条连接用于命令复制
- 默认配置是:client-output-buffer-limit slave 256mb 64mb 60
- 当主从节点之间网络延迟较高或者主节点挂载大量从节点时这部分内存消耗将占用很大一部分
- 建议:主节点挂载的从节点不要多于两个,主从节点不要部署在较差的网络环境下,比如异地跨机房部署,防止复制客户端连接缓慢造成溢出
- 订阅客户端:
- 当使用发布订阅功能时,连接客户端使用单独的输出缓冲区
- 默认配置为:client-output-buffer-limit pubsub 32mb 8mb 60
- 当订阅服务的消息生产快于消费速度时,输出缓冲区会产生积压造成输出缓冲区空间溢出
输入输出缓冲区在大流量的场景中容易失控,造成redis内存的不稳定,需要重点监控
复制积压缓存
- Redis在2.8版本之后提供了一个可重用的固定大小缓冲区用于实现部分复制功能,根据repl-backlog-size参数控制,默认1MB
- 对于复制积压缓冲区整个主节点只有一个,所有的从节点共享此缓冲区,因此 可以设置较大的缓冲区空间,如100MB,这部分内存投入是有价值的,可以有效避免全量复制
AOF缓冲区
- 这部分空间用于在redis重写期间保存最近的写入命令。
- AOF缓冲区消耗用户无法控制,消耗的内存取决于AOF重写时间和写入命令量,这部分占用空间通常很小
内存碎片
- redis默认的内存分配器采用
jemalloc
,可选的分配器还有:glibc、tcmalloc。内存分配器为了更好的管理和重复利用内存,分配内存策略一般采用固定范围的内存块进行分配 - 例如jemalloc在64位系统中将内存空间划分为:小、大、巨大三个范围。每个范围内又划分为多个小的内存块单位, 如下所示:
- 小:[8byte],[16byte,32byte,48byte,…,128byte],[192byte,256byte,…,512byte],[768byte,1024byte,…,3840byte]
- 大:[4KB,8KB,12KB,…,4072KB]
- 巨大:[4MB,8MB,12MB,…]
- 比如当保存5KB对象时jemalloc可能会采用8KB的快存储,而剩下的3KB空间就变成了内存碎片不能再分配给其他对象存储。内存碎片问题虽然是所有内存访问的同比,而jemalloc针对碎片化问题专门做了优化,一般不会存在过度碎片化的问题,正常的碎片率(mem_fragmentation_ratio)在1.03左右。但是当存储的数据长度长短差异较大时,以下场景容易出现高内存碎片的问题
- 频繁的更新操作,比如频繁对已存在的键执行append、setrange更更新操作
- 大量的过期删除,键对象过期删除后,释放的空间无法得到充分利用,导致碎片率上升
- 出现高内存碎片问题时解决方法:
- 数据对齐:在条件允许的情况下尽量做数据对齐,比如数据尽量采用数字类型或者固定长度字符串等,但是这要视具体的业务而定,有些场景无 法做到
- 安全重启:重启节点可以做到内存碎片重新整理,因此可以利用高可用架构,比如sentinel或者cluster,将碎片率过高的主节点转换为从节点,进行安全重启。
子进程内存消耗
子进程内存消耗主要是指执行AOF/RDB重写时redis创建的子进程的内存消耗
写时复制技术
- redis执行fork操作产生的子进程内存占用量对外表现为与父进程相同,理论上需要一倍的物理内存来完成重写操作
- 但是linux具有写时复制技术,父子进程会共享相同的物理内存页,当父进程执行写请求时会对需要修改的页复制出一份副本来完成写操作,而子进程依然读取fork时整个父进程的内存快照
TRANSPARENT HUGE PAGES(THP)机制
- Linux Kernel在2.6.38内核增加了THP机制,而有些Linux发行版即使内核达不到2.6.38也会默认加入并开启这个功能,如 Redhat Enterprise Linux在6.0以上版本默认会引入THP
- 虽然开启THP可以降低fork子进程的速度,但是之后copy-on-write期间复制内存页的单位从4KB变成了2MB,如果父进程有大量写命令,会加重内存拷贝量,从而造成过度内存消耗
- 如果在高并发写的场景下开启THP,子进程内存消耗可能是父进程的数倍,极易造成机器物理内存溢出,从而触发SWAP或者OOM killer
子进程内存消耗总结如下:
- redis产生的子进程并不需要消耗一倍的父进程内存,实际消耗根据期间写入命令量决定,但是依然需要预留出一些内存来防止溢出
- 需要设置sysctlvm.overcommit_memory=1允许内核可以分配所有的物理内存,防止redis进程执行fork时因为系统剩余内存不足而失败
- 排查当前系统是否支持并开启THP,如果开启建议关闭,防止copy-onwirte期间内存过度消耗