首先,并不是说redis是内存应用就完全没性能问题,用的不好,还是会出现各种状况,例如RDB频繁,碎片太多等.


性能分析

info信息:

在redis-cli进入登录界面后,输入info all,或者redis-cli -h ${ip} -p ${post} -a "${pass}" -c info all,通常我们只输入info就够了,是简介模式的意思,info all是详细模式

之后,就会获取所有与Redis服务相关的实时性能信息,类似于linux命令top那样的东西.

info命令输出的数据可分为10个类别,分别是:

  • server

  • clients

  • memory

  • persistence

  • stats

  • replication

  • cpu

  • commandstats

  • cluster

  • keyspace

下面展开解析一些重点信息.


server 部分:

redis_version :     Redis 服务器版本,不同版本会有些功能和命令不同

arch_bits :     架构(32 或 64 位),某些情况,容易被忽略的坑

tcp_port :     TCP/IP 监听端口,确认你操作的对不对

uptime_in_seconds :     自 Redis 服务器启动以来,经过的秒数,可以确认有没有被重启过

uptime_in_days :     自 Redis 服务器启动以来,经过的天数,可以确认有没有被重启过


clients 部分:

connected_clients :     已连接客户端的数量(不包括通过从属服务器连接的客户端)

client_longest_output_list :     当前连接的客户端当中,最长的输出列表

client_longest_input_buf :     当前连接的客户端当中,最大输入缓存

blocked_clients :     正在等待阻塞命令(BLPOP、BRPOP、BRPOPLPUSH)的客户端的数量


memory部分:

maxmemory/maxmemory_human:    配置文件redis.conf限制的可分配的最大内存总量,当超过之后,就会触发LRU删除旧数据.

used_memory/used_memory_human:    当前redis-server实际使用的内存总量,如果used_memory > maxmemory ,那么操作系统开始进行内存与swap空间交换,以便腾出新的物理内存给新页或活动页(page)使用,那是有多糟糕大家可以想得到.

used_memory_rss/used_memory_rss_human:    从操作系统上显示已经分配的内存总量,也就是这个redis-server占用的系统物理内存实际值,比used_memory多出来的就可能是碎片.

mem_fragmentation_ratio:    内存碎片率,内存碎片率稍大于1是合理的,说明redis没有发生内存交换,如果内存碎片率超过1.5,那就说明Redis消耗了实际需要物理内存的150%,其中50%是内存碎片率。若是内存碎片率低于1的话,说明Redis内存分配超出了物理内存,操作系统正在进行内存交换。内存交换会引起非常明显的响应延迟.

    下面是计算公式:

        wKiom1jCaDyBrpuiAAAuFTm2d-w931.png

    当碎片率出现问题,有3种方法解决内存管理变差的问题,提高redis性能:

        1. 重启Redis服务器:如果内存碎片率超过1.5,重启Redis服务器可以让额外产生的内存碎片失效并重新作为新内存来使用,使操作系统恢复高效的内存管理。

        2.限制内存交换: 如果内存碎片率低于1,Redis实例可能会把部分数据交换到硬盘上。内存交换会严重影响Redis的性能,所以应该增加可用物理内存或减少实Redis内存占用

        3.修改内存分配器:
Redis支持glibc’s malloc、jemalloc11、tcmalloc几种不同的内存分配器,每个分配器在内存分配和碎片上都有不同的实现。不建议普通管理员修改Redis默认内存分配器,因为这需要完全理解这几种内存分配器的差异,也要重新编译Redis。

used_memory_lua:    Lua脚本引擎所使用的内存大小。redis默认允许使用lua脚本,不过太多了的话就占用了可用内存

mem_allocator:    在编译时指定的Redis使用的内存分配器,可以是libc、jemalloc、tcmalloc.


persistence 部分:

RDB信息,RDB的操作要用到bgsave命令,是比较耗费资源的持久化操作,而且不是实时的,容易造成宕机数据消失,如果内存容量满了,不能做bgsave操作的话,隐患会很大.

rdb_changes_since_last_save :     距离最近一次成功创建持久化文件之后,经过了多少秒。持久化是需要占用资源的,在高负载下需要尽量避免持久化的影响,下列参数均有参考价值.

rdb_bgsave_in_progress:    当前是否在进行bgsave操作。是为1

rdb_last_save_time :     最近一次成功创建 RDB 文件的 UNIX 时间戳。

rdb_last_bgsave_time_sec :     记录了最近一次创建 RDB 文件耗费的秒数。

rdb_last_bgsave_status:    上次保存的状态

rdb_current_bgsave_time_sec :     如果服务器正在创建 RDB 文件,那么这个域记录的就是当前的创建操作已经耗费的秒数。

AOF信息,AOF是持续记录命令到持久化文件的方法,比较节省资源,但是AOF存储文件会没有限制存储,时间一长,或者操作太频繁,那就会出现AOF文件过大,撑爆硬盘的事.而且,这个方法还是会定期bgsave操作.

aof_enabled:    AOF文件是否启用

aof_rewrite_in_progress:    表示当前是否在进行写入AOF文件操作

aof_last_rewrite_time_sec :     最近一次创建 AOF 文件耗费的时长。

aof_current_rewrite_time_sec :     如果服务器正在创建 AOF 文件,那么这个域记录的就是当前的创建操作已经耗费的秒数。

aof_last_bgrewrite_status:    上次写入状态

aof_last_write_status:    上次写入状态

aof_base_size :     服务器启动时或者 AOF 重写最近一次执行之后,AOF 文件的大小。

aof_pending_bio_fsync :     后台 I/O 队列里面,等待执行的 fsync 调用数量。

aof_delayed_fsync :     被延迟的 fsync 调用数量。


stats部分:

total_commands_processed:    显示了Redis服务处理命令的总数,且值是递增的.因为Redis是个单线程模型,客户端过来的命令是按照顺序执行的,如果命令队列里等待处理的命令数量比较多,命令的响应时间就变慢,甚至于后面的命令完全被阻塞,导致Redis性能降低.所以这个时候就需要记录这个参数的值,是不是增长过快,导致了性能降低.

instantaneous_ops_per_sec :     服务器每秒钟执行的命令数量,同上,如果增长过快就有问题。

expired_keys :     因为过期而被自动删除的数据库键数量,具有参考意义。

evicted_keys:    显示因为maxmemory限制导致key被回收删除的数量.根据配置文件中设置maxmemory-policy值来确定Redis是使用lru策略还是过期时间策略.如果是过期回收的,不会被记录在这里,通常这个值不为0,那就要考虑增加内存限制,不然就会造成内存交换,轻则性能变差,重则丢数据.

latest_fork_usec:     最近一次 fork() 操作耗费的微秒数。fork()是很耗费资源的操作,所以要留意一下.


commandstats部分:

cmdstat_XXX:    记录了各种不同类型的命令的执行统计信息,命令包含有读有写,分得比较细,其中calls代表命令执行的次数,usec代表命令耗费的 CPU 时间,usec_per_call代表每个命令耗费的平均 CPU 时间,单位为微秒.对于排错有一定用途.

---------------------------------------------------------------------------------------------------


其他问题的分析方法:

查看redis的网络延时:

Redis的延迟数据是无法从info信息中获取的。倘若想要查看延迟时间,可以用 Redis-cli工具加--latency参数运行

redis-cli --latency -h 10.1.2.11 -p 6379

他将会持续扫描延迟时间,直到按ctrl+C退出,以毫秒为单位测量Redis的响应延迟时间,由于服务器不同的运行情况,延迟时间可能有所误差,通常1G网卡的延迟时间是0.2毫秒,若延时值远高于这个参考值,那明显是有性能问题了。这个时候我们要考虑检查一下网络状况.


查看redis的慢查询:

Slow log 是 Redis 用来记录查询执行时间的日志系统。Redis中的slowlog命令可以让我们快速定位到那些超出指定执行时间的慢命令,默认情况下会记录执行时间超过10ms的记录到日志,由参数slowlog-log-slower-than控制.最多记录128条,由参数slowlog-max-len控制,超过就自动删除.

通常这个默认参数够用,也可以在线CONFIG SET修改参数slowlog-log-slower-than和slowlog-max-len来修改这个时间和限制条数。

通常1gb带宽的网络延迟,预期在0.2ms左右,倘若一个命令仅执行时间就超过10ms,那比网络延迟慢了近50倍。可以通过使用Redis-cli工具,输入slowlog get命令查看,返回结果的第三个字段以微妙位单位显示命令的执行时间。假如只需要查看最后3个慢命令,输入slowlog get 10即可。

127.0.0.1:6379> slowlog get 10
    .
    .
    .
4) 1) (integer) 215
    2) (integer) 1489099695
    3) (integer) 11983
    4) 1) "SADD"
       2) "USER_TOKEN_MAP51193"
       3) "qIzwZKBmBJozKprQgoTEI3Qo8QO2Fi!4"
 5) 1) (integer) 214
    2) (integer) 1489087112
    3) (integer) 18002
    4) 1) "SADD"
       2) "USER_TOKEN_MAP51192"
       3) "Z3Hs!iTUNfweqvLLf!ptdchSV2JAOrrH"
 6) 1) (integer) 213
    2) (integer) 1489069123
    3) (integer) 15407
    4) 1) "SADD"
       2) "USER_TOKEN_MAP51191"
       3) "S3rNzOBwUlaI3QfOK9dIITB6Bk7LIGYe"

  • 1=日志的唯一标识符

  • 2=被记录命令的执行时间点,以 UNIX 时间戳格式表示

  • 3=查询执行时间,以微秒为单位。例子中命令使用11毫秒。

  • 4= 执行的命令,以数组的形式排列。完整命令是拼在一起.


监控客户端的连接:

因为Redis是单线程模型(只能使用单核),来处理所有客户端的请求, 但由于客户端连接数的增长,处理请求的线程资源开始降低分配给单个客户端连接的处理时间,这时每个客户端需要花费更多的时间去等待Redis共享服务的响应。

#查看客户端连接状态
127.0.0.1:6379> info clients
# Clients
connected_clients:11
client_longest_output_list:0
client_biggest_input_buf:0
blocked_clients:0

第一个字段(connected_clients)显示当前实例客户端连接的总数,Redis默认允许客户端连接的最大数量是10000。若是看到连接数超过5000以上,那可能会影响Redis的性能。倘若一些或大部分客户端发送大量的命令过来,这个数字会低的多。

查当前客户端状态

#查看所有正在连接的客户端状态,
127.0.0.1:6379> client list
id=821882 addr=10.25.138.2:60990 fd=8 name= age=53838 idle=24 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=ping

这个看起来有点绕,因为和历史数据是混在一起的:

addr:客户端的地址和端口,包含当前连接和历史连接

age:这个客户端连进来的生命周期,也就是连进来之后的持续时间,单位是秒

idle:这个客户端的空闲时间,也就是说这个时间内,客户端没有操作,单位是秒

db:操作的数据库,redis默认有db0~db15可用选择

cmd:客户端最后一次使用的命令

也就是说,idle越少,那就代表这个客户端刚刚操作,反则是历史记录而已.age越少就代表是刚刚建立的连接,越大则是历史连接.而有些时候个别使用者用了scan或keys命令,会对数据量大的redis造成很大的负载压力,所以需要特别关注.


特大key的统计:

在redis的单线程处理方式下,一些数据量比较大的key的操作明显是会影响性能,所以必要时,我们要统计出来,交给开发来优化

#统计生产上比较大的key
redis-cli -h* -a* -p* --bigkeys
#查看某个key的持续时间
127.0.0.1:6379> OBJECT IDLETIME key名字

--bigkeys信息解析:

1.该命令使用scan方式对key进行统计,所以使用时无需担心对redis造成阻塞。
2.输出大概分为两部分,summary之上的部分,只是显示了扫描的过程。summary部分给出了每种数据结构中最大的Key,所以下面部分更重要些。
3.统计出的最大key只有string类型是以字节长度为衡量标准的。list,set,zset等都是以元素个数作为衡量标准,不能说明其占的内存就一定多,需要另外计算。

得出最大key名字后,就去看看粗略大小,

#查看某个key序列化后的长度
debug object key

输出的项的说明:

  • Value at:key的内存地址

  • refcount:引用次数

  • encoding:编码类型

  • serializedlength:经过压缩后的序列化长度,单位是 B, 也就是 Byte(字节),因为压缩效果要看编码类型,不一定反应内存中的大小,只是有参考价值.

  • lru_seconds_idle:空闲时间

终上所述,我们要关注的大key信息正是serializedlength的长度了.

另外还有一个工具[rdbtools],可以全面分析redis里面的key信息,但是要额外安装,对于内网用户不可谓不麻烦,因为不是系统自带的功能,这里不详细说明,请等待另一篇文章另外介绍.


数据持久化引发的延迟

Redis的数据持久化工作本身就会带来延迟,需要根据数据的安全级别和性能要求制定合理的持久化策略:

1.AOF + fsync always的设置虽然能够绝对确保数据安全,但每个操作都会触发一次fsync,会对Redis的性能有比较明显的影响
2.AOF + fsync every second是比较好的折中方案,每秒fsync一次
3.AOF + fsync never会提供AOF持久化方案下的最优性能,使用RDB持久化通常会提供比使用AOF更高的性能,但需要注意RDB的策略配置
4.每一次RDB快照和AOF Rewrite都需要Redis主进程进行fork操作。fork操作本身可能会产生较高的耗时,与CPU和Redis占用的内存大小有关。根据具体的情况合理配置RDB快照和AOF Rewrite时机,避免过于频繁的fork带来的延迟.
例如:Redis在fork子进程时需要将内存分页表拷贝至子进程,以占用了24GB内存的Redis实例为例,共需要拷贝24GB / 4kB * 8 = 48MB的数据。在使用单Xeon 2.27Ghz的物理机上,这一fork操作耗时216ms。

可以通过INFO命令返回的latest_fork_usec字段查看上一次fork操作的耗时(微秒)。


Swap引发的延迟

当Linux将Redis所用的内存分页移至swap空间时,将会阻塞Redis进程,导致Redis出现不正常的延迟。Swap通常在物理内存不足或一些进程在进行大量I/O操作时发生,应尽可能避免上述两种情况的出现。

在/proc/redis进程号/smaps文件中会保存进程的swap记录,通过查看这个文件,能够判断Redis的延迟是否由Swap产生。如果这个文件中记录了较大的Swap size,则说明延迟很有可能是Swap造成的。

例子如下,可以看到当前swap的状态时0KB,也就是没用到swap,

#/proc/pid/smaps显示了进程运行时的内存影响,系统的运行时库(so),堆,栈信息均可在其中看到。
cat /proc/`ps aux |grep redis |grep -v grep |awk '{print $2}'`/smaps
00400000-00531000 r-xp 00000000 fc:02 805438521 /usr/local/bin/redis-server
Size:               1220 kB
Rss:                 924 kB
Pss:                 924 kB
Shared_Clean:          0 kB
Shared_Dirty:          0 kB
Private_Clean:       924 kB
Private_Dirty:         0 kB
Referenced:          924 kB
Anonymous:             0 kB
AnonHugePages:         0 kB
Shared_Hugetlb:        0 kB
Private_Hugetlb:       0 kB
Swap:                  0 kB
SwapPss:               0 kB
KernelPageSize:        4 kB
MMUPageSize:           4 kB
Locked:                0 kB


内存满了怎么办:
redis内存满了,那确实是很麻烦的事,但是再麻烦也得处理啊,这就得从redis架构原理说起了.
首先我们要了解,redis的内存满,并不代表是用了系统的100%内存,为什么这么说呢?我们都知道redis做持久化是save和bgsave,而常用的bgsave(也是默认)是fork一个进程,把内存copy一份再压缩后存到硬盘成*.rdb文件.这里就涉及一个问题,你内存必须保证有和数据一样大的空间才能做bgsave,那严格来说,只要你的内存超过系统内存的50%,那就可以被称为redis内存满了.
redis的持久化策略,上面只说了持久化会阻塞操作导致延时,而如果内存满了,数据量的增加会让持久化造成的延时会更严重,而且持久化失败后是默认每分钟重试一遍.
那么问题就来了,因为内存满了,持久化失败,然后一分钟后再持久化,就造成了恶性循环,redis的性能直线下降.那怎么办好呢?
更改持久化策略是个临时解决方案,就是直接把rdb持久化关闭掉:

config set save ""

为什么能解决,答案也很明显,关闭了持久化,那就不会阻塞操作,那你redis的性能还是保证到了.但是又会引入新问题,没了持久化,内存数据如果redis-server程序重启或关闭就没了,还是比较危险的.而且内存满的问题还在,如果内存用到了系统内存100%,甚至触发了系统的OOM,那就坑大了,因为内存被彻底清空,数据也都没有了.也就是所谓的临时解决办法.
所以正确的做法是,在不阻塞操作之后,删掉可以删除的数据,再重新拉起持久化,然后准备扩容的工作.
占用内存看哪里,上面已经说了,但是内存满了的定义,并不一定只是实际内存占用,碎片也是要包含在内的,例如:

#这种情况,肯定就是内存满
used_memory_human:4.2G
maxmemory_human:4.00G
#但是这种情况,也是内存满
used_memory_human:792.30M
used_memory_rss_human:3.97G
used_memory_peak_human:4.10G
maxmemory_human:4.00G

因为内存碎片没有被释放,那它还是会占用内存空间,对于系统来说,碎片也是redis-server占用的内存,不是空闲的内存,那剩下的内存还是不足以用来bgsave.那怎么解决碎片呢?

在redis4.0之前,没其他更好办法,只能重启redis-server,之后的新版本则新加了一个碎片回收参数,杜绝了这个问题.

而碎片问题其实影响颇大,因为正常情况下,这些用不了又确实占着内存的数据,会让我们的redis浪费空间之余,还会额外造成内存满的风险.所以也正如上面说的那样,如果碎片率超过1.5之后,是该想想回收一下.

那确实需要保存内存数据怎么办?只能忍痛割爱,把不必要的数据删除掉,让内存降到能做bgsave之后再重启来回收碎片.要不,就是升级到4.0之后避免同类问题.


优化建议

系统优化

1.关闭Transparent huge pages

Transparent HugePages会让内核khugepaged线程在运行时动态分配内存。在大部分linux发行版本中默认是启用的,缺点是可能会造成内存在运行时的延迟分配,对于大内存应用并不友好,例如:oracle,redis等会占用大量内存的应用,所以建议关闭。

#关闭Transparent HugePages,默认状态是[always]
echo never > /sys/kernel/mm/transparent_hugepage/enabled

2.在物理机部署redis,这点不用多说了,虚拟机或者是docker都会有一定的延时,没有必要为了好管理二浪费这些性能。

3.多用连接池,而不是频繁断开再连接,效果我想不言而喻。

4.客户端进行的批量数据操作,应使用Pipeline特性在一次交互中完成。



行为优化

1.假如缓存数据小于4GB,可以选择使用32位的Redis实例。因为32位实例上的指针大小只有64位的一半,它的内存空间占用空间会更少些。Redis的dump文件在32位和64位之间是互相兼容的, 因此倘若有减少占用内存空间的需求,可以尝试先使用32位,后面再切换到64位上。

2.尽可能的使用Hash数据结构。因为Redis在储存小于100个字段的Hash结构上,其存储效率是非常高的。所以在不需要集合(set)操作或list的push/pop操作的时候,尽可能的使用Hash结构。Hash结构的操作命令是HSET(key, fields, value)和HGET(key, field),使用它可以存储或从Hash中取出指定的字段。

3.尽量设置key的过期时间。一个减少内存使用率的简单方法就是,每当存储对象时确保设置key的过期时间。倘若key在明确的时间周期内使用或者旧key不大可能被使用时,就可以用Redis过期时间命令(expire,expireat, pexpire, pexpireat)去设置过期时间,这样Redis会在key过期时自动删除key.用ttl命令可以查询过期时间,单位是秒,显示-2代表key不存在,显示-1代表没有设置超时时间(也就是永久的).

4.使用多参数命令:若是客户端在很短的时间内发送大量的命令过来,会发现响应时间明显变慢,这由于后面命令一直在等待队列中前面大量命令执行完毕。举例来说,循环使用LSET命令去添加1000个元素到list结构中,是性能比较差的一种方式,更好的做法是在客户端创建一个1000元素的列表,用单个命令LPUSH或RPUSH,通过多参数构造形式一次性把1000个元素发送的Redis服务上。

5.管道命令:另一个减少多命令的方法是使用管道(pipeline),把几个命令合并一起执行,从而减少因网络开销引起的延迟问题。因为10个命令单独发送到服务端会引起10次网络延迟开销,使用管道会一次性把执行结果返回,仅需要一次网络延迟开销。Redis本身支持管道命令,大多数客户端也支持,倘若当前实例延迟很明显,那么使用管道去降低延迟是非常有效的。

6.避免操作大集合的慢命令:如果命令处理频率过低导致延迟时间增加,这可能是因为使用了高时间复杂度的命令操作导致,这意味着每个命令从集合中获取数据的时间增大。 所以减少使用高时间复杂的命令,能显著的提高的Redis的性能。

7.限制客户端连接数:自Redis2.6以后,允许使用者在配置文件(Redis.conf)maxclients属性上修改客户端连接的最大数,也可以通过在Redis-cli工具上输入config set maxclients 去设置最大连接数。根据连接数负载的情况,这个数字应该设置为预期连接数峰值的110%到150之间,若是连接数超出这个数字后,Redis会拒绝并立刻关闭新来的连接。通过设置最大连接数来限制非预期数量的连接数增长,是非常重要的。另外,新连接尝试失败会返回一个错误消息,这可以让客户端知道,Redis此时有非预期数量的连接数,以便执行对应的处理措施。 上述二种做法对控制连接数的数量和持续保持Redis的性能最优是非常重要的