共计 9792 个字符,预计需要花费 25 分钟才能阅读完成。
作者: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 设计与实现》