作者:vivo 互联网服务器团队- Tang Wenjian
一、 背景
应用过 Redis 的同学应该都晓得,它基于键值对(key-value)的内存数据库,所有数据寄存在内存中,内存在 Redis 中表演一个外围角色,所有的操作都是围绕它进行。
咱们在理论保护过程中常常会被问到如下问题,比方数据怎么存储在 Redis 外面能节约老本、晋升性能?Redis内存告警是什么起因导致?
本文次要是通过剖析 Redis内存构造、介绍内存优化伎俩,同时联合生产案例,帮忙大家在优化内存应用,疾速定位 Redis 相干内存异样问题。
二、 Redis 内存治理
本章具体介绍 Redis 是怎么治理各内存构造的,而后次要介绍几个占用内存可能比拟多的内存构造。首先咱们看下Redis 的内存模型。
内存模型如图:
【used_memory】:Redis内存占用中最次要的局部,Redis分配器调配的内存总量(单位是KB)(在编译时指定编译器,默认是jemalloc),次要蕴含本身内存(字典、元数据)、对象内存、缓存,lua内存。
【本身内存】:本身保护的一些数据字典及元数据,个别占用内存很低。
【对象内存】:所有对象都是Key-Value型,Key对象都是字符串,Value对象则包含5品种(String,List,Hash,Set,Zset),5.0还反对stream类型。
【缓存】:客户端缓冲区(一般 + 主从复制 + pubsub)以及aof缓冲区。
【Lua内存】:次要是存储加载的 Lua 脚本,内存使用量和加载的 Lua 脚本数量无关。
【used\_memory\_rss】:Redis 主过程占据操作系统的内存(单位是KB),是从操作系统角度失去的值,如top、ps等命令。
【内存碎片】:如果对数据的更改频繁,可能导致redis开释的空间在物理内存中并没有开释,但redis又无奈无效利用,这就造成了内存碎片。
【运行内存】:运行时耗费的内存,个别占用内存较低,在10M内。
【子过程内存】:次要是在长久化的时候,aof rewrite或者rdb产生的子过程耗费的内存,个别也是比拟小。
2.1 对象内存
对象内存存储 Redis 所有的key-value型数据类型,key对象都是 string 类型,value对象次要有五种数据类型String、List、Hash、Set、Zset,不同类型的对象通过对应的编码各种封装,对外定义为RedisObject构造体,RedisObject都是由字典(Dict)保留的,而字典底层是通过哈希表来实现的。通过哈希表中的节点保留字典中的键值对,构造如下:
(起源:书籍《Redis设计与实现》)
为了达到极大的进步 Redis 的灵活性和效率,Redis 依据不同的应用场景来对一个对象设置不同的编码,从而优化某一场景下的效率。
各类对象抉择编码的规定如下:
string (字符串)
- 【int】:(整数且数字长度小于20,间接记录在ptr*外面)
- 【embstr】: (间断调配的内存(字符串长度小于等于44字节的字符串))
- 【raw】: 动静字符串(大于44个字节的字符串,同时字符长度小于 512M(512M是字符串的大小限度))
list (列表)
- 【ziplist】:(元素个数小于hash-max-ziplist-entries配置(默认512个),同时所有值都小于hash-max-ziplist-value配置(默认64个字节))
- 【linkedlist】:(当列表类型无奈满足ziplist的条件时,Redis会应用linkedlist作为列表的外部实现)
- 【quicklist】:(Redis 3.2 版本引入了 quicklist 作为 list 的底层实现,不再应用 linkedlist 和 ziplist 实现)
set (汇合)
- 【intset 】:(元素都是整数且元素个数小于set-max-intset-entries配置(默认512个))
- 【hashtable】:(汇合类型无奈满足intset的条件时就会应用hashtable)
hash (hash列表)
- 【ziplist】:(元素个数小于hash-max-ziplist-entries配置(默认512个),同时任意一个value的长度都小于hash-max-ziplist-value配置(默认64个字节))
- 【hashtable】:(hash类型无奈满足intset的条件时就会应用hashtable
zset(有序汇合)
- 【ziplist】:(元素个数小于zset-max-ziplist-entries配置(默认128个)同时每个元素的value小于zset-max-ziplist-value配置(默认64个字节))
- 【skiplist】:(当ziplist条件不满足时,有序汇合会应用skiplist作为外部实现)
2.2 缓冲内存
2.2 1 客户端缓存
客户端缓冲指的是所有接入 Redis 服务的 TCP 连贯的输入输出缓冲。有一般客户端缓冲、主从复制缓冲、订阅缓冲,这些都由对应的参数缓冲管制大小(输出缓冲无参数管制,最大空间为1G),若达到设定的最大值,客户端将断开。
【client-output-buffer-limit】: 限度客户端输入缓存的大小,前面接客户端品种(normal、slave、pubsub)及限度大小,默认是0,不做限度,如果做了限度,达到阈值之后,会断开链接,开释内存。
【repl-backlog-size】:默认是1M,backlog是一个主从复制的缓冲区,是一个环形buffer,假如达到设置的阈值,不存在溢出的问题,会循环笼罩,比方slave中断过程中同步数据没有被笼罩,执行增量同步就能够。backlog设置的越大,slave能够失连的工夫就越长,受参数maxmemory限度,失常不要设置太大。
2.2 2 AOF 缓冲
当咱们开启了 AOF 的时候,先将客户端传来的命令寄存在AOF缓冲区,再去依据具体的策略(always、everysec、no)去写入磁盘中的 AOF 文件中,同时记录刷盘工夫。
AOF 缓冲没法限度,也不须要限度,因为主线程每次进行 AOF会比照上次刷盘胜利的工夫;如果超过2s,则主线程阻塞直到fsync同步实现,主线程被阻塞的时候,aof\_delayed\_fsync状态变量记录会减少。因而 AOF 缓存只会存几秒工夫的数据,耗费内存比拟小。
2.3 内存碎片
程序呈现内存碎片是个很常见的问题,Redis的默认分配器是jemalloc ,它的策略是依照一系列固定的大小划分内存空间,例如 8 字节、16 字节、32 字节、…, 4KB、8KB 等。当程序申请的内存最靠近某个固定值时,jemalloc 会给它调配比它大一点的固定大小的空间,所以会产生一些碎片,另外在删除数据的时候,开释的内存不会立即返回给操作系统,但redis本人又无奈无效利用,就造成碎片。
内存碎片不会被统计在used\_memory中,内存碎片比率在redis info外面记录了一个动静值mem\_fragmentation\_ratio,该值是used\_memory\_rss / used\_memory的比值,mem\_fragmentation\_ratio越靠近1,碎片率越低,正常值在1~1.5内,超过了阐明碎片很多。
2.4 子过程内存
后面提到子过程次要是为了生成 RDB 和 AOF rewrite产生的子过程,也会占用肯定的内存,然而在这个过程中写操作不频繁的状况下内存占用较少,写操作很频繁会导致占用内存较多。
三、Redis 内存优化
内存优化的对象次要是对象内存、客户端缓冲、内存碎片、子过程内存等几个方面,因为这几个内存耗费比拟大或者有的时候不稳固,咱们优化内存的方向分为如:缩小内存应用、进步性能、缩小内存异样产生。
3.1 对象内存优化
对象内存的优化能够升高内存使用率,进步性能,优化点次要针对不同对象不同编码的抉择上做优化。
在优化前,咱们能够理解下如下的一些知识点:
(1)首先是字符串类型的3种编码,int编码除了本身object无需分配内存,object 的指针不须要指向其余内存空间,无论是从性能还是内存应用都是最优的,embstr是会调配一块间断的内存空间,然而假如这个value有任何变动,那么value对象会变成raw编码,而且是不可逆的。
(2)ziplist 存储 list 时每个元素会作为一个 entry; 存储 hash 时 key 和 value 会作为相邻的两个 entry; 存储 zset 时 member 和 score 会作为相邻的两个entry,当不满足上述条件时,ziplist 会降级为 linkedlist, hashtable 或 skiplist 编码。
(3)在任何状况下大内存的编码都不会降级为 ziplist。
(4)linkedlist 、hashtable 便于进行增删改操作然而内存占用较大。
(5)ziplist 内存占用较少,然而因为每次批改都可能触发 realloc 和 memcopy, 可能导致连锁更新(数据可能须要移动)。因而批改操作的效率较低,在 ziplist 的条目很多时这个问题更加突出。
(6)因为目前大部分redis运行的版本都是在3.2以上,所以 List 类型的编码都是quicklist,它是 ziplist 组成的双向链表linkedlist ,它的每个节点都是一个ziplist,思考了综合均衡空间碎片和读写性能两个维度所以应用了个新编码quicklist,quicklist有个比拟重要的参数list-max-ziplist-size,当它取负数的时候,负数示意限度每个节点ziplist中的entry数量,如果是正数则只能为-1~-5,限度ziplist大小,从-1~-5的限度别离为4kb、8kb、16kb、32kb、64kb,默认是-2,也就是限度不超过8kb。
(7)【rehash】: redis存储底层很多是hashtable,客户端能够依据key计算的hash值找到对应的对象,然而当数据量越来越大的时候,可能就会存在多个key计算的hash值雷同,这个时候这些雷同的hash值就会以链表的模式寄存,如果这个链表过大,那么遍历的时候性能就会降落,所以Redis定义了一个阈值(负载因子 loader_factor = 哈希表中键值对数量 / 哈希表长度),会触发渐进式的rehash,过程是新建一个更大的新hashtable,而后把数据逐渐挪动到新hashtable中。
(8)【bigkey】:bigkey个别指的是value的值占用内存空间很大,然而这个大小其实没有一个固定的规范,咱们本人定义超过10M就能够称之为bigkey。
优化倡议:
- key尽量管制在44个字节数内,走embstr编码,embstr比raw编码缩小一次内存调配,同时因为是间断内存存储,性能会更好。
- 多个string类型能够合并成小段hash类型去保护,小的hash类型走ziplist是有很好的压缩成果,节约内存。
- 非string的类型的value对象的元素个数尽量不要太多,防止产生大key。
- 在value的元素较多且频繁变动,不要应用ziplist编码,因为ziplist是间断的内存调配,对频繁更新的对象并不敌对,性能损耗反而大。
- hash类型对象蕴含的元素不要太多,防止在rehash的时候耗费过多内存。
- 尽量不要批改ziplist限度的参数值,因为ziplist编码尽管能够对内存有很好的压缩,然而如果元素太多应用ziplist的话,性能可能会有所降落。
3.2 客户端缓冲优化
客户端缓存是很多内存异样增长的罪魁祸首,大部分都是一般客户端输入缓冲区异样增长导致,咱们先理解下执行命令的过程,客户端发送一个或者通过piplie发送一组申请命令给服务端,而后期待服务端的响应,个别客户端应用阻塞模式来期待服务端响应,数据在被客户端读取前,数据是寄存在客户端缓存区,命令执行的繁难流程图如下:
异样增长起因可能如下几种:
- 客户端拜访大key 导致客户端输入缓存异样增长。
- 客户端应用monitor命令拜访Redis,monitor命令会把所有拜访redis的命令继续寄存到输入缓冲区,导致输入缓冲区异样增长。
- 客户端为了放慢拜访效率,应用pipline封装了大量命令,导致返回的后果集异样大(pipline的个性是等所有命令全副执行完才返回,返回前都是暂存在输入缓存区)。
- 从节点利用数据较慢,导致输入主从复制输入缓存有很多数据积压,最初导致缓冲区异样增长。
异样体现:
- 在Redis的info命令返回的后果外面,client局部client\_recent\_max\_output\_buffer的值很大。
- 在执行client list命令返回的后果集外面,omem不为0且很大,omem代表该客户端的输入代表缓存应用的字节数。
- 在集群中,可能少部分used_memory在监控显示存在异样增长,因为不论是monitor或者pipeline都是针对单个实例的下发的命令。
优化倡议:
- 利用不要设计大key,大key尽量拆分。
- 服务端的一般客户端输入缓存区通过参数设置,因为内存告警的阈值大部分是使用率80%开始,理论倡议参数能够设置为实例内存的5%~15%左右,最好不要超过20%,防止OOM。
- 非非凡状况下防止应用monitor命令或者rename该命令。
- 在应用pipline的时候,pipeline不能封装过多的命令,特地是一些返回后果集较多的命令更应该少封装。
- 主从复制输入缓冲区大小设置参考: 缓冲区大小=(主库写入命令速度 * 操作大小 – 主从库间网络传输命令速度 * 操作大小)* 2。
3.3 碎片优化
碎片优化能够升高内存使用率,进步拜访效率,在4.0以下版本,咱们只能应用重启复原,重启加载rdb或者重启通过高可用主从切换实现数据的从新加载能够缩小碎片,在4.0以上版本,Redis提供了主动和手动的碎片整顿性能,原理大抵是把数据拷贝到新的内存空间,而后把老的空间开释掉,这个是有肯定的性能损耗的。
【a. redis手动整顿碎片】:执行memory purge命令即可。
【b.redis主动整顿碎片】:通过如下几个参数管制
- 【activedefrag yes 】:启用主动碎片清理开关
- 【active-defrag-ignore-bytes 100mb】:内存碎片空间达到多少才开启碎片整顿
- 【active-defrag-threshold-lower 10】:碎片率达到百分之多少才开启碎片整顿
- 【active-defrag-threshold-upper 100 】:内存碎片率超过多少,则尽最大致力整顿(占用最大资源去做碎片整顿)
- 【active-defrag-cycle-min 25 】:内存主动整顿占用资源最小百分比
- 【active-defrag-cycle-max 75】:内存主动整顿占用资源最大百分比
3.4 子过程内存优化
后面谈到 AOF rewrite和 RDB 生成动作会产生子过程,失常在两个动作执行的过程中,Redis 写操作没有那么频繁的状况下fork进去的子过程是不会耗费很多内存的,这个次要是因为 Redis 子过程应用了 Linux 的 copy on write 机制,简称COW。
COW的外围是在fork出子过程后,与父过程共享内存空间,只有在父过程产生写操作批改内存数据时,才会真正去分配内存空间,并复制内存数据。
然而有一点须要留神,不要开启操作系统的大页THP(Transparent Huge Pages),开启 THP 机制后,原本页的大小由4KB变为 2MB了。它尽管能够放慢 fork 实现的速度( 因为要拷贝的页的数量缩小 ),然而会导致 copy-on-write 复制内存页的单位从 4KB 增大为 2MB,如果父过程有大量写命令,会减轻内存拷贝量,从而造成适度内存耗费。
四、内存优化案例
4.1 缓冲区异样优化案例
线上业务 Redis 集群呈现内存告警,内存使用率增长很快达到100%,值班人员先进行了紧急扩容,同时反馈至业务群是否有大量新数据写入,业务反馈并无大量新数据写入,且同时扩容后的内存还在涨,很快又要触发告警了,业务 DBA 去查监控看看具体起因。
首先咱们看used_memory增长只是集群的少数几个实例,同时内存异样的实例的key的数量并没有异样增长,阐明没有写入大批量数据导致。
咱们再往下剖析,可能是客户端的内存占用异样比拟大,查看实例 info 外面的客户端相干指标,察看发现output\_list的增长曲线和used\_memory统一,能够断定是客户端的输入缓冲异样导致。
接下来咱们再去通过client list查看是什么客户端导致output增长,客户端在执行什么命令,同时去剖析是否拜访大key。
执行 client list |grep -i omem=0 发现如下:
id=12593807 addr=192.168.101.1:52086 fd=10767 name= age=15301 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=32768 obl=16173 oll=341101 omem=5259227504 events=rw cmd=get
阐明下相干的几个重点的字段的含意:
【id】:就是客户端的惟一标识,常常用于咱们kill客户端用到id;
【addr】:客户端信息;
【obl】:固定缓冲区大小(字节),默认是16K;
【oll】:动静缓冲区大小(对象个数),客户端如果每条命令的响应后果超过16k或者固定缓冲区写满了会写动静缓冲区;
【omem】: 指缓冲区的总字节数;
【cmd】: 最近一次的操作命令。
能够看到缓冲区内存占用很大,最近的操作命令也是get,所以咱们先看看是否大key导致(咱们是间接剖析RDB发现并没有大key),然而发现并没有大key,而且get对应的必定是string类型,string类型的value最大是512M,所以单个key也不太可能产生这么大的缓存,所以判定是客户端缓存了多个key。
这个时候为了尽快恢复,和业务沟通长期kill该连贯,内存开释,而后为了防止避免前面还产生异样,和业务方沟通设置一般客户端缓存限度,因为最大内存是25G,咱们把缓存设置了2G-4G, 动静设置参数如下:
config set client-output-buffer-limit normal 4096mb 2048mb 120
因为参数限度也只是针对单个client的输入缓冲这么大,所以还须要查看客户端应用应用 pipline 这种管道命令或者相似实现了封装大批量命令导致后果对立返回之前被阻塞,前面确定的确会有这个操作,业务层就须要去逐渐优化,不然咱们限度了输入缓冲,达到了下限,会话会被kill, 所以业务不改的话还是会有抛错。
业务方反馈用的是 C++ 语言 brpc 自带的 Redis客户端,第一次间接搜寻没有pipline的关键字,然而景象又指向应用的管道,所以持续认真看了下代码,发现其外部是实现了pipline相似的性能,也是会对多个命令进行封装去申请redis,而后对立返回后果,客户端GitHub链接如下:
https://github.com/apache/incubator-brpc/blob/master/docs/cn/redis_client.md
总结:
pipline 在 Redis 客户端中应用的挺多的,因为的确能够提供拜访效率,然而使用不当反而会影响拜访,应该管制好拜访,生产环境也尽量加这些内存限度,防止局部客户端的异样拜访影响全局应用。
4.2 从节点内存异样增长案例
线上 Redis 集群呈现内存使用率超过 95% 的劫难告警,然而该集群是有190个节点的集群触发异样内存告警的只有3个节点。所以查看集群对应信息以及监控指标发现如下有用信息:
- 3个从节点对应的主节点内存没有变动,从节点的内存是逐渐增长的。
- 发现集群整体ops比拟低,阐明业务变动并不大,没有发现无效命令突增。
- 主从节点的最大内存不统一,主节点是6G,从节点是5G,这个是导致劫难告警的重要起因。
- 在出问题前,主节点比从节点的内存大略多出1.3G,前面从节点used_memory逐渐增长到超过主节点内存,然而rss内存是最初放弃了一样。
- 主从复制呈现提早也内存增长的那个时间段。
处理过程:
首先想到的应该是放弃主从节点最大内存统一,然而因为主机内存使用率比拟高临时没法扩容,因为想到的是从节点可能什么起因阻塞,所以和业务方沟通是重启下2从节点缓解下,重启后从节点内存开释,降到产生问题前的程度,如上图,前面主机空出了内存资源,所以优先把内存调整统一。
内存调整好了一周后,这3个从节点内存又告警了,因为当初主从内存是统一的,所以触发的是重大告警(>85%),查看监控发现状况是和之前一样,猜想这个是某些操作触发的,所以还是决定问问业务方这 两个时间段都有哪些操作,业务反馈这段时间就是在写业务,那2个时间段都是在写入,也看了写redis的那段代码,用了一个比拟少见的命令append,append是对string类型的value进行追加。
这里就得提下string类型在 Redis 外面是怎么分配内存的:string类型都是都是sds存储,以后调配的sds内存空间有余存储且小于1M时候,Redis会重新分配一个2倍之前内存大小的内存空间。
依据下面到知识点,所以能够大抵能够解析上述一系列的问题,大略是过后做 append 操作,从节点须要调配空间从而产生内存收缩,而主节点不须要调配空间,因为内存重新分配设计malloc和free操作,所以过后有lag也是失常的。
Redis的主从自身是一个逻辑复制,加载 RDB 的过程其实也是拿到kv一直的写入到从节点,所以主从到内存大小也常常存在不雷同的状况,特地是这种values大小常常扭转的场景,主从存储的kv所用的空间很多可能是不一样的。
为了证实这一猜想,咱们能够通过获取一个key(value大小要比拟大)在主从节点占用空间的大小,因为是4.0以上版本,所以咱们能够应用memory USAGE 去获取大小,看看差别有多少,咱们随机找了几个略微大点的key去查看,发现在有些key从库占用空间是主库的近2倍,有的差不多,有的也是1倍多,rdb解析进去的这个key空间更小,阐明从节点重启后加载rdb进行寄存是最小的,而后因为某段时间大批量key操作,导致从节点的大批量的key调配的空间有余,须要扩容1倍空间,导致内存呈现增长。
到这就剖析的其实差不多了,因为append的个性,为了防止内存再次出现内存告警,决定把该集群的内存进行扩容,管制内存使用率在70%以下(防止可能产生的大量key应用内存翻倍的状况)。
最初还有1个问题:下面的used\_memory为什么会比memory\_rss的值还大呢?(swap是敞开的)。
这是因为jemalloc内存调配一开始其实调配的是虚拟内存,只有往调配的page页外面写数据的时候才会真正分配内存,memory\_rss是理论内存占用,used\_memory其实是一个计数器,在 Redis做内存的malloc/free的时候,对这个used_memory做加减法。
对于used\_memory大于memory\_rss的问题,redis作者也做了答复:
https://github.com/redis/redis/issues/946#issuecomment-13599772
总结:
在通晓 Redis内存调配原理的状况下,数据库的内存异样问题进行剖析会比拟疾速定位,另外可能某个问题看起来和业务没什么关联,然而咱们还是应该多和业务方沟通获取一些线索排查问题,最初主从内存肯定依照标准保持一致。
五、总结
Redis在数据存储、缓存都是做了很奇妙的设计和优化,咱们在理解了它的内部结构、存储形式之后,咱们能够提前在key的设计上做优化。咱们在遇到内存异样或者性能优化的时候,能够不再局限于外表的一些剖析如:资源耗费、命令的复杂度、key的大小,还能够联合依据Redis的一些外部运行机制和内存治理形式去深刻发现是否还有可能哪些方面导致异样或者性能降落。
参考资料
- 书籍《Redis设计与实现》
发表回复