如何将 Redis 的内存优化?

双十一、双十二都是买买买的节奏,但是你知道吗?像京东、淘宝这样的电子商城是如何扛得住几亿的请求吗?

下面是一个电商网站的基本架构,他包括了一个前端入口和一个缓存集群以及数据库集群。

一般来说,web 服务器作为前端入口,在 web 服务器内会把一些静态文件通过 CDN 分发到各个节点分摊服务器的请求压力。同时也能加速网站的访问。

当用户的一些查询请求,比如要查询一个商品的信息,会通过前端页面会经过缓存层,如果这些信息在缓存层就已经存在了,则直接返回给客户端,如果没有则去数据库集群查询,并经过缓存层缓存然后返回给客户端。

这样设计的好处是可以大大减小数据库集群的压力。我们都知道类似 MySQL 这种关系型数据库最大的瓶颈就是当出现像双十一这样的高并发请求时候会给 IO 带来巨大的压力,导致 IO 出现瓶颈。哪怕你的数据库优化做的再好,也不能改变这个基本事实。那么在数据库的上面加一道缓存层就可以大大缓解后端数据库的压力,通过类似 Redis 这样的内存型数据库将热点数据存储在内存中,可以大大提高读写效率和请求时延。

那么, 本文就来和大家分享下 Redis 内存的相关优化措施。

Redis 内存的消耗

要知道如何优化 Redis 的内存,我们需要先了解 Redis 都有哪些地方需要消耗内存。

众所周知,Redis 默认会把内存存储在内存中,当然你可以通过一些持久化方案,例如 AOF 或 RDB 的方式来将内存中的数据写入到磁盘中,但是 Redis 默认读取数据还是从内存中读取,因为内存的速度要比磁盘的读写速度快很多倍, 那在 Redis 中是如何对内存的消耗进行统计的呢?我们可以这样查看与内存相关的参数:

127.0.0.1:6379> infomemory# Memoryused_memory:812712used_memory_human:793.66Kused_memory_rss:6033408used_memory_rss_human:5.75Mused_memory_peak:812712used_memory_peak_human:793.66Ktotal_system_memory:1850044416total_system_memory_human:1.72Gused_memory_lua:37888used_memory_lua_human:37.00Kmaxmemory:0maxmemory_human:0Bmaxmemory_policy:noevictionmem_fragmentation_ratio:7.42mem_allocator:jemalloc-3.6.0

其中参数含义:

used_memory 以字节的形式表示 Redis 分配器分配的内存总量,也就是存储在 Redis 存储中的所有数据的占用量。

used_memory_rss 是从操作系统的角度显示 Redis 进程占用的物理内存总量。

used_memory_peak 是内存使用的最大值,表示 used_memory 的峰值。

total_system_memory 表示操作系统的最大内存。

used_memory_lua 表示 Lua 引擎所消耗的内存大小。

maxmemory Redis 可分配使用的最大内存量。

maxmemory_policy 当 Redis使用超过最大分配内存采用什么方式来剔除旧数据,默认是 noeviction

mem_fragmentation_ratio 表示 used_memory_rss/used_memory 比值,表示内存碎片率

mem_allocator 表示 Redis 的内存分配器,默认是 jemalloc

在 info memory 中,你会发现有类似 used_memory_human、used_memory_rss_human 等参数带 human 的,这些都是以可读的形式显示对应参数的信息。

在这些参数中,作为运维和相关开发人员,我们应该重点关注 used_memory_rss、used_memory、mem_fragmentation_ratio 这三个参数:

当 mem_fragmentation_ratio >1 时,说明used_memory_rss、used_memory 多出的部分内存并没有用于数据存储,而是被内存碎片所消耗,如果这两者相差很大,说明碎片率很严重。

当 mem_fragmentation_ratio < 1 时,这种情况通常是操作系统把 Redis Swap 到硬盘导致,出现这种情况要格外关注,由于硬盘的速度远远慢于内存,Redis 的性能会大幅下降。

内存消耗分类

介绍完了 Redis 的内存统计参数,我们来讲讲 Redis 的内存消耗主要是发生在哪些方面。Redis 的进程消耗内存主要包括:自身内存+对象内存+缓冲内存+内存碎片。Redis空进程自身内存消耗非常少,通常used_memory_rss在3MB左右, used_memory在800KB左右,一个空的Redis进程消耗内存可以忽略不计。

下面对这几个方面进行一一讲解:

对象内存

这块内存消耗最大,它存储着用户所有的数据 ,我们都知道 Redis 存储的数据都是以 key-value 形式的数据类型,当你每次创建键值对时,至少要创建 2 个类型的对象,即key 和 value 对象。对象内存消耗我们可以简单理解为sizeof(keys)+sizeof(values)。

缓存内存

这块主要包括客户端缓存、复制积压缓存区、AOF 缓存区。

客户端缓存

对于客户端缓存,我们可以使用 client-output-buffer-limit 查看相关配置,默认情况下,这部分配置为

client-output-buffer-limit normal 0 0 0 # 客户端client-output-buffer-limit replica 256mb 64mb 60 # 从客户端client-output-buffer-limit pubsub 32mb 8mb 60 # 订阅客户端

对于普通客户端来说,限制为0,也就是不限制。因为普通客户端通常采用阻塞式的消息应答模式,何谓阻塞式呢?如:发送请求,等待返回,再发送请求,再等待返回。这种模式下,通常不会导致Redis服务器输出缓冲区的堆积膨胀;

对于Pub/Sub客户端(也就是发布/订阅模式),大小限制是8M,当输出缓冲区超过8M时,会关闭连接。持续性限制是,当客户端缓冲区大小持续60秒超过2M,则关闭客户端连接;

对于slave客户端来说,大小限制是256M,持续性限制是当客户端缓冲区大小持续60秒超过64M,则关闭客户端连接。

我们可以使用如下方式查看这三种客户端的配置

127.0.0.1:6379> CONFIGGETclient-output-buffer-limit1) "client-output-buffer-limit"2) "normal 0 0 0 slave 268435456 67108864 60 pubsub 33554432 8388608 60"

这里的 normal 0 0 0 表示普通客户端,值都为 0,表示 Redis 默认不做限制,一般普通客户端的内存消耗几乎可以忽略不计,但是如果有大佬的慢连接客户端接入就会存在问题,我们可以使用 maxclients 来限制最大客户端连接

127.0.0.1:6379> configgetmaxclients1) "maxclients"2) "10000"

slave 268435456 67108864 60 表示的是从客户端的连接,默认的限制是最大 256M 和 60s 内不超过 64M ,如需调整,可以使用如下方式设置,其中 1 表示最大限制,3 表示多少秒,2 表示多少秒内不能超过的最大限制。

config setclient-output-buffer-limit'slave 1 2 3'

复制积压缓冲区

复制积压缓冲区是 Redis 2.8 版本以后提供的一个可重用的固定大小的缓冲区,目的是用于实现部分复制功能,它主要根据参数 repl-backlog-size 来控制,默认是 1MB,对于复制积压缓冲区整个主节点只有一个,所有的从节点共享此缓冲区,因此可以设置较大的缓冲区空间,如100MB,它可以有效避免全量复制。

AOF 缓冲区

这部分空间用于在Redis重写期间保存最近的写入命令 ,这部分的消耗主要取决于 AOF 的重写时间和写入量。,通常占用很小。

内存碎片

Redis默认的内存分配器采用jemalloc,jemalloc在64位系统中将内存空间划 分为:小、大、巨大三个范围:

小:8byte,192byte,256byte,...,512byte

大:[4KB,8KB,12KB,...,4072KB]

巨大:[4MB,8MB,12MB,...]

比如当保存5KB对象时jemalloc可能会采用8KB的块存储,而剩下的3KB 空间变为了内存碎片不能再分配给其他对象存储。内存碎片问题虽然是所有 内存服务的通病,但是jemalloc针对碎片化问题专门做了优化,一般不会存在过度碎片化的问题,正常的碎片率(mem_fragmentation_ratio)在1.03左右。

此外子进程也会存在一些内存消耗,这部分消耗主要是耗费在 AOF/RDB 重写时创建子进程的内存消耗,Redis执行fork操作产生的子进程内存占用量对外表现为与父进程相同, 理论上需要一倍的物理内存来完成重写操作。但Linux具有写时复制技术 (copy-on-write),父子进程会共享相同的物理内存页,当父进程处理写请求时会对需要修改的页复制出一份副本完成写操作,而子进程依然读取fork 时整个父进程的内存快照。

内存优化

我们从操作系统和 Redis 本身的优化来讲讲如何优化内存。

操作系统层面

1.vm.overcommit_memory

Linux操作系统对大部分申请内存的请求都回复 yes,以便能运行更多的程序。因为申请内存后,并不会马上使用内存,这种技术叫做 overcommit。vm.overcommit_memory用来设置内存分配策略,有三个可选值:

0 表示内核将检查是否有足够的可用内存,如果有足够的内存,内存申请通过,否则申请失败。

1 表示允许超量使用内存直到用完为止。

2 表示内核绝不过量使用内存,即系统整个内存地址空间不能超过 swap+50%的 RAM 值。

建议将值设置为 1,避免发生 fork 失败。

echo "vm.overcommit_memory=1">> /etc/sysctl.confsysctl vm.overcommit_memory=1

Redis设置合理的maxmemory,保证机器有20%~30%的闲置内存。

2.swappiness

当物理内存不足时,可以将一部分内存页进行swap操作,但是swap 空间是由硬盘提供的,对于需要高并发、高吞吐的应用来说,磁盘IO通常会成为系统瓶颈。

在Linux中,并不是要等到所有物理内存都使用完才会使用到swap,系 统参数swppiness会决定操作系统使用swap的倾向程度。

swappiness的取值范 围是0~100,swappiness的值越大,说明操作系统可能使用swap的概率越 高,swappiness值越低,表示操作系统更加倾向于使用物理内存。默认是 60,它的含义如下:

0 Linux kernel 3.5+ 宁愿 OOM 也不用 swap,Linux kernel 3.4以下,宁愿 swap 也不用 OOM

1 Linux kernel 3.5+ 宁愿 swap 也不 OOM

60 默认值

100 操作系统会主动的使用 swap

可以如下方式设置该参数,{bestvalue}为swappiness 的值:

echo vm.swappiness={bestvalue} >> /etc/sysctl.conf

3.THP

Linux Kernel内存中的Transparent Huge Pages(THP)机制,默认会开启 THP,虽然开启THP可以降 低fork子进程的速度,但之后copy-on-write期间复制内存页的单位从4KB变 为2MB,如果父进程有大量写命令,会加重内存拷贝量,从而造成过度内存 消耗。

对于子进程的消耗,我们可以通过如下方式来进行优化:

设置sysctl vm.overcommit_memory=1允许内核可以分配所有的物理 内存,防止Redis进程执行fork时因系统剩余内存不足而失败。

关闭 THP,防止copy-on- write期间内存过度消耗。

echo never > /sys/kernel/mm/transparent_hugepage/enabled

在设置THP配置时需要注意:有些Linux的发行版本没有将THP放 到/sys/kernel/mm/transparent_hugepage/enabled中,例如Red Hat6以上的THP配 置放到/sys/kernel/mm/redhat_transparent_hugepage/enabled中。而有些版本的 Redis源码中 检查THP时,把THP位置写死:

FILE *fp = fopen("/sys/kernel/mm/transparent_hugepage/enabled","r"); if (!fp) return0;

所以在发行版中,虽然没有THP的日志提示,但是依然存在THP所带来 的问题:

echo never > /sys/kernel/mm/redhat_transparent_hugepage/enabled

4.OOM Killer

OOM killer会在可用内存不足时选择性地杀掉用户进程,OOM killer进程会为每个用户进程设置一个权值,这个权值越高,被 Kill 的概率就越高,反之概率越低。每个进程的权值存放在/proc/{progress_id}/oom_score中,这个值是受/proc/{progress_id}/oom_adj的控制,oom_adj在不同的Linux版本中最小值不同,设置方法:

echo {value} > /proc/${process_id}/oom_adj

对于Redis所在的服务器来说,可以将所有Redis的oom_adj设置为最低 值或者稍小的值,降低被OOM killer杀掉的概率:

for redis_pid in $(pgrep -f "redis-server")doecho -17 > /proc/${redis_pid}/oom_adjdone

Oom_adj 只是起到辅助作用,合理的分配和使用内存才是最重要的。

4.ulimit

Linux中,可以通过ulimit查看和设置系统当前用户进程的资源数。其中ulimit-a命令包含的open files参数,是单个用户同时打开的最大文件个数:

[root@redis ~]# ulimit -acore file size (blocks, -c) 0data seg size (kbytes, -d) unlimitedscheduling priority (-e) 0file size (blocks, -f) unlimitedpending signals (-i) 6972max locked memory (kbytes, -l) 64max memory size (kbytes, -m) unlimitedopen files (-n) 1024pipe size (512 bytes, -p) 8POSIX message queues (bytes, -q) 819200real-time priority (-r) 0stack size (kbytes, -s) 8192cpu time (seconds, -t) unlimitedmax user processes (-u) 6972virtual memory (kbytes, -v) unlimitedfile locks (-x) unlimited

Redis允许同时有多个客户端通过网络进行连接,可以通过配置 maxclients来限制最大客户端连接数。对Linux操作系统来说,这些网络连接 都是文件句柄。

设置方法如下:

ulimit –Sn {max-open-files}

通常来说,ulimit的值要 >= maxclients 的值。

5.TCP backlog

Redis默认的tcp-backlog值为511,可以通过修改配置tcp-backlog进行调整:

echo 511 > /proc/sys/net/core/somaxconn

Redis 自身优化

1.设置内存上限

使用maxmemory参数限制最大可用内存,当超出内存上限maxmemory时使用LRU等删除策略释放空间以及防止所用内存超过服务器物理内存。

2.配置内存回收策略

Redis所用内存达到maxmemory上限时会触发相应的溢出控制策略。具体策略受maxmemory-policy参数控制,Redis支持6种策略,如下所示:

noeviction:默认策略,不会删除任何数据,拒绝所有写入操作并返回客户端错误信息(error)OOM command not allowed when used memory,此 时Redis只响应读操作。

volatile-lru:根据LRU算法删除设置了超时属性(expire)的键,直到腾出足够空间为止。如果没有可删除的键对象,回退到noeviction策略。

allkeys-lru:根据LRU算法删除键,不管数据有没有设置超时属性, 直到腾出足够空间为止。

allkeys-random:随机删除所有键,直到腾出足够空间为止。

volatile-random:随机删除过期键,直到腾出足够空间为止。

volatile-ttl:根据键值对象的ttl属性,删除最近将要过期数据。如果 没有,回退到noeviction策略。

3.键值对优化

降低Redis内存使用最直接的方式就是缩减键(key)和值(value)的长度。在完整描述业务情况下,键值越短越好。值对象缩减比较复杂,应该在业务上精简业务对象,去掉不必要的属性避免存储无效数据。其次在序列化工具选择上,应该选择更高效的序列化工具来降低字节数组大小。

4.共享对象池

共享对象池是指Redis内部维护[0-9999] [0-9999]的整数对象 池,用于节约内存。

但是共享对象池与maxmemory+LRU策略冲突,使用时需要注意。对于ziplist编码的值对象,即使内部数据为整数也无法使用共享对象池,因为ziplist使用压缩且内存连续的结构,对象共享判断成本过高。

5.字符串优化

字符串对象是Redis内部最常用的数据类型。所有的键都是字符串类 型,值对象数据除了整数之外都使用字符串存储。在使用过程中应当尽量优先使用整数,比字符串类型更节省空间。并且要优化字符串使用,避免预分配造成的内存浪费。使用ziplist压缩编码优化hash、list等结构,注重效率和空间的平衡,使用intset编码优化整数集合。使用ziplist编码的hash结构降低小对象链规模。

6.编码优化

Redis对外提供了多种数据类型,但是Redis内部对于不同类型的数据使用的内部编码不一样。内部编码不同将直接影响数据的内存占用和读写效率。

7.控制键的数量

当使用Redis存储大量数据时,通常会存在大量键,过多的键同样会消 耗大量内存。

发布了61 篇原创文章 · 获赞 34 · 访问量 1831

猜你喜欢

转载自blog.csdn.net/qq_43162613/article/details/103794769