Redis|常见线上问题&性能问题分析(上)

579 阅读14分钟

1 主从数据不一致

1.1 主从数据不一致原因及处理

原因

主从传输网络延迟,或者从库正在执行复杂度高的命令阻塞

测试延迟了多少毫秒

redis-cli --latency -h `host` -p `port`

解决

(1)主从网络状况好,避免在不同机房,或和其他网络通信密集的应用部署在一起

(2)redis.conf文件中的slave-serve-stale-data 设置为 no

slave-serve-stale-data表示当一个从库主库失去联系时,或者复制正在进行的时候,从库应对请求的行为。

  • slave-serve-stale-data = yes(默认值),从库 仍然会应答客户端请求,但返回的数据可能是过时,或者数据可能是空的在第一次同步的时候
  • slave-serve-stale-data= no ,在执行除了info salveof之外的其他命令时,从库都将返回一个 "SYNC with master in progress" 的错误

(3)可以监控从库的复制进度,当从库的复制进度赶上主库时,才允许客户端再次跟这些从库连接。

INFO replication命令可以查看主库接收写命令的进度信息master_repl_offset和 从库复制写命令的进度信息slave_repl_offset:
从库和主库间的复制进度差 = master_repl_offset - slave_repl_offset

1.2 info replication讲解

主库上执行

127.0.0.1:6379> info replication
# Replication
# 角色
role:master
# 从节点的连接数
connected_slaves:2
# 从节点详细信息 IP PORT 状态 命令(单位:字节长度)偏移量 延迟秒数
# 主节点每次处理完写操作,会把命令的字节长度累加到master_repl_offset中。
# 从节点在接收到主节点发送的命令后,会累加记录子什么偏移量信息slave_repl_offset,同时,也会每秒钟上报自身的复制偏移量到主节点,以供主节点记录存储。
# 在实际应用中,可以通过对比主从复制偏移量信息来监控主从复制健康状况。
slave0:ip=192.168.10.102,port=6379,state=online,offset=23866,lag=0
slave1:ip=192.168.10.103,port=6379,state=online,offset=23866,lag=0
# master启动时生成的40位16进制的随机字符串,用来标识master节点
master_replid:acc2aaa1f0bb0fd79d7d3302f16bddcbe4add423
master_replid2:0000000000000000000000000000000000000000
# master 命令(单位:字节长度)已写入的偏移量
master_repl_offset:23866
second_repl_offset:-1
# 0/1:关闭/开启复制积压缓冲区标志(2.8+),主要用于增量复制及丢失命令补救
repl_backlog_active:1
# 缓冲区最大长度,默认 1M
repl_backlog_size:1048576
# 缓冲区起始偏移量
repl_backlog_first_byte_offset:1
# 缓冲区已存储的数据长度
repl_backlog_histlen:23866

从库上执行

127.0.0.1:6379> info replication
# Replication
# 角色
role:slave
# 主节点详细信息
master_host:192.168.10.101
master_port:6379
# slave端可查看它与master之间同步状态,当复制断开后表示down
master_link_status:up
# 主库多少秒未发送数据到从库
master_last_io_seconds_ago:1
# 从服务器是否在与主服务器进行同步 0否/1是
master_sync_in_progress:0
# slave复制命令(单位:字节长度)偏移量
slave_repl_offset:24076
# 选举时,成为主节点的优先级,数字越大优先级越高,0 永远不会成为主节点
slave_priority:100
# 从库是否设置只读,0读写/1只读
slave_read_only:1
# 连接的slave实例个数
connected_slaves:0
# master启动时生成的40位16进制的随机字符串,用来标识master节点
master_replid:acc2aaa1f0bb0fd79d7d3302f16bddcbe4add423
# slave切换master之后,会生成了自己的master标识,之前的master节点的标识存到了master_replid2的位置
master_replid2:0000000000000000000000000000000000000000
# master 命令(单位:字节长度)已写入的偏移量
master_repl_offset:24076
# 主从切换时记录主节点的命令偏移量+1,为了避免全量复制
second_repl_offset:-1
# 0/1:关闭/开启复制积压缓冲区标志(2.8+),主要用于增量复制及丢失命令补救
repl_backlog_active:1
# 缓冲区最大长度,默认 1M
repl_backlog_size:1048576
# 缓冲区起始偏移量
repl_backlog_first_byte_offset:1
# 缓冲区已存储的数据长度
repl_backlog_histlen:24076

2 内存问题

2.1 内存占用

Redis的内存占用主要可以划分为以下几个部分:

数据

作为数据库,数据是最主要的部分,这部分占用的内存会统计在used_memory中。

进程本身运行需要的内存

Redis主进程本身运行肯定需要占用内存,如代码、常量池等等。这部分内存大约几兆,在大多数生产环境中与Redis数据占用的内存相比可以忽略。这部分内存不是由jemalloc分配,因此不会统计在used_memory中

缓冲内存

缓冲内存包括:

  • 客户端缓冲区:存储客户端连接的输入输出缓冲;
  • 复制积压缓冲区:用于部分复制功能;
  • AOF缓冲区:用于在进行AOF重写时,保存最近的写入命令。

这部分内存由jemalloc分配,因此会统计在used_memory中

内存碎片

内存碎片是Redis在分配、回收物理内存过程中产生的。例如,如果对数据更改频繁,而且数据之间的大小相差很大,可能导致Redis释放的空间在物理内存中并没有释放,但Redis又无法有效利用,这就形成了内存碎片。内存碎片不会统计在used_memory中

2.1 内存碎片问题

2.1.1 判断内存碎片

INFO memory命令可以判断内存碎片

>info memory
# Memory
used_memory:810575104 //数据占用了多少内存(字节)

used_memory_human:773.02M //数据占用了多少内存(带单位的,可读性好)

used_memory_rss:885465088 //redis占用了多少内存

used_memory_rss_human:844.45M //redis占用了多少内存(带单位的,可读性好)

used_memory_peak:2001274696 //占用内存的峰值(字节)

used_memory_peak_human:1.86G //占用内存的峰值(带单位的,可读性好)

mem_fragmentation_ratio:1.09 //内存碎片率

其中:
used_memory: 即Redis分配器分配的内存总量(单位是字节),包括使用的虚拟内存(即swap) used_memory_rss: 即Redis进程占据操作系统的内存(单位是字节),与top及ps命令看到的值是一致的;除了分配器分配的内存之外,还包括进程运行本身需要的内存、内存碎片等,但是不包括虚拟内存

used_memory和used_memory_rss,前者是从Redis角度得到的量,后者是从操作系统角度得到的量。二者之所以有所不同,一方面是因为内存碎片和Redis进程运行需要占用内存,使得前者可能比后者小,另一方面虚拟内存的存在,使得前者可能比后者大.

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

  • 范围通常在1 - 1.5
  • 如果大于1.5说明碎片过多,必须要清理了。
  • 小于1,表明实际分配的内存小于申请的内存了,很显然内存不足了,导致部分数据写入到 Swap 中,Redis访问Swap中的数据时,延迟会变大,性能会降低。

2.1.2 清理内存碎片

  • redis4.0以前: 只能关闭redis重启后才能生效。
  • redis4.0以后: 新增了配置项activedefrag

当需要清理碎片的时候,使用命令将activedefrag的配置设置为开启状态。则redis会自动清理碎片,回收内存。

config set activedefrag yes //默认为no

相关参数配置说明

内存清理相关参数如下,可以使用config get的方式查看对应的值

//碎片整理总开关 
activedefrag yes

active-defrag-ignore-bytes 400mb // 如果内存碎片达到了 400mb ,开始清理
active-defrag-threshold-lower 20 // 碎片率达到百分之20% 时,开始清理

active-defrag-cycle-min 25 // 表示自动清理过程所用 CPU 时间的比例不低于 25% ,保证清理能正常开展
active-defrag-cycle-max 75 // 表示自动清理过程所用 CPU 时间的比例不高于 75% ,一旦超过,就停止清理

  • active-defrag-ignore-bytesactive-defrag-threshold-lower 两个参数只有全部满足才会开始清理
  • active-defrag-cycle-min 保证清理能正常开展
  • active-defrag-cycle-max 避免在清理时,大量的内存拷贝阻塞 Redis,导致响应延迟升高
  • active-defrag-cycle-minactive-defrag-cycle-max 两个参数控制了清理过程中的CPU时间占比,保证了正常处理请求不受影响

手动清理

127.0.0.1:6379> memory purge 
OK

2.1 内存飙升案例

2.1.1 redis-cluster某个分片内存飙升案例

可能原因

客户端的hash(key)有问题,造成分配不均。(redis使用的是crc16, 不会出现这么不均的情况) 存在个别大的key-value: 例如一个包含了几百万数据set数据结构(这个有可能)

分析定位

使用info命令,发现client_longes_output_list有些异常。

> info
   ...
  # Clients
  connected_clients:8
  client_longest_output_list:621058
  client_biggest_input_buf:0
  blocked_clients:0

服务端和客户端交互时,分别为每个客户端设置了输入缓冲区输出缓冲区,也会占用Redis服务器的内存。 使用client list命令,来查询输出(omem)或者输入(qbuf)缓冲区不为0的客户端连接.

# grep -v "qbuf=0" 表示查询输入缓冲区
$ redis-cli -h 127.0.0.1 -c -p 6379 client list | grep -v "omem=0"
id=140028229 addr=10.20.0.82:59788 fd=11 name= age=3192 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=32768 obl=0 oll=380887 omem=6218527258 events=rw cmd=lrange

紧急处理

找到输出(omem)或者输入(qbuf)缓冲区占用内大的客户端addr,将其kill掉

> client kill 10.20.0.82:59788
OK
(1.65s)

预防办法: ) 添加command-rename配置,将一些危险的命令(flushall, monitor, keys * , flushdb)做rename,如果有需要的话,找到redis的运维人员处理

2.1.2 Redis client list 详解

id: 唯一的64位的客户端ID(Redis 2.8.12加入)。
addr : 客户端的地址和端口
fd : 套接字所使用的文件描述符
age : 以秒计算的已连接时长
idle : 以秒计算的空闲时长
flags : 客户端 flag
db : 该客户端正在使用的数据库 ID
sub : 已订阅频道的数量
psub : 已订阅模式的数量
multi : 在事务中被执行的命令数量
qbuf : 查询缓冲区的长度(字节为单位, 0 表示没有分配查询缓冲区)
qbuf-free : 查询缓冲区剩余空间的长度(字节为单位, 0 表示没有剩余空间)
obl : 输出缓冲区的长度(字节为单位, 0 表示没有分配输出缓冲区)
oll : 输出列表包含的对象数量(当输出缓冲区没有剩余空间时,命令回复会以字符串对象的形式被入队到这个队列里)
omem : 输出缓冲区和输出列表占用的内存总量
events : 文件描述符事件
cmd : 最近一次执行的命令
 
客户端 flag 可以由以下部分组成:
O : 客户端是 MONITOR 模式下的附属节点(slave)
S : 客户端是一般模式下(normal)的附属节点
M : 客户端是主节点(master)
x : 客户端正在执行事务
b : 客户端正在等待阻塞事件
i : 客户端正在等待 VM I/O 操作(已废弃)
d : 一个受监视(watched)的键已被修改, EXEC 命令将失败
c : 在将回复完整地写出之后,关闭链接
u : 客户端未被阻塞(unblocked)
A : 尽可能快地关闭连接
N : 未设置任何 flag
 
文件描述符事件可以是:
r : 客户端套接字(在事件 loop 中)是可读的(readable)
w : 客户端套接字(在事件 loop 中)是可写的(writeable)

3 变慢

3.1 慢查询的配置

慢查询的配置可以通过redis.conf设置,也可以config set方式动态设置

配置

慢查询队列

 slowlog-max-len  //这个慢查询队列的长度,这个队列放在内存中不会被持久化(需要定期持久化慢查询)

慢查询阈值

slowlog-log-slower-than(微秒) //默认10000微妙,也就是10ms

`= 0` 会记录所有的命令
`< 0` 对于任何命令都不会进行记录

如果要Redis将配置持久化到本地配置文件, 需要执行命令

config rewrite

查看

查看slowlog总条数: slowlog len

获取慢查询队列,获取前n条慢查询的数据 slowlog get [n]

127.0.0.1:6379> SLOWLOG GET 1
1) 1) (integer) 26            // slowlog唯一编号id
 2) (integer) 1440057815    // 查询的时间戳
 3) (integer) 47            // 查询的耗时(微妙),如表示本条命令查询耗时47微秒
 4) 1) "SLOWLOG"            // 查询命令,完整命令为 SLOWLOG GET,slowlog最多保存前面的31个key和128字符
    2) "GET"

清空慢查询队列 : slowlog reset

实践

  • slowlog-log-slower-than配置建议

需要根据Redis并发量调整该值。由于Redis采用单线程响应命令,对于高流量的场景,如果命令执行时间在1毫秒以上,那么Redis最多可支撑OPS不到1000。因此对于高OPS场景的Redis建议设置为1毫秒

  • 慢查询只记录命令执行时间,并不包括命令排队和网络传输时间

  • 慢查询日志可能会丢失

 由于慢查询日志是一个先进先出的队列,也就是说如果慢查询比较多的情况下,可能会丢失部分慢查询命令。为了防止这种情况发生,可以定期执行slowget命令将慢查询日志持久化到其他存储中(例如MySQL),然后可以制作可视化界面进行查询

3.2 基准测试

Redis在不同的软硬件环境下性能各不相同。只有了解 Redis在生产环境服务器上的基准性能,才能进一步评估 Redis是否变慢了。以下命令,可以测试出这个实例 60 秒内的最大响应延迟

$ redis-cli -h 127.0.0.1 -p 6379 --intrinsic-latency 60
Max latency so far: 1 microseconds.
Max latency so far: 15 microseconds.
Max latency so far: 17 microseconds.
Max latency so far: 18 microseconds.
Max latency so far: 31 microseconds.
Max latency so far: 32 microseconds.
Max latency so far: 59 microseconds.
Max latency so far: 72 microseconds.

1428669267 total runs (avg latency: 0.0420 microseconds / 42.00 nanoseconds per run).
Worst run took 1429x longer than the average latency.

从输出结果可以看到,这 60 秒内的最大响应延迟为 72 微秒(0.072毫秒)。

你还可以使用以下命令,查看一段时间内 Redis 的最小、最大、平均访问延迟:

$ redis-cli -h 127.0.0.1 -p 6379 --latency-history -i 1
min: 0, max: 1, avg: 0.13 (100 samples) -- 1.01 seconds range
min: 0, max: 1, avg: 0.12 (99 samples) -- 1.01 seconds range
min: 0, max: 1, avg: 0.13 (99 samples) -- 1.01 seconds range
min: 0, max: 1, avg: 0.10 (99 samples) -- 1.01 seconds range
min: 0, max: 1, avg: 0.13 (98 samples) -- 1.00 seconds range
min: 0, max: 1, avg: 0.08 (99 samples) -- 1.01 seconds range
...

以上输出结果是,每间隔 1 秒,采样 Redis 的平均操作耗时,其结果分布在 0.08 ~ 0.13 毫秒之间。

如果观察到,这个实例的运行延迟是正常 Redis 基准性能的 2 倍以上,即可认为Redis实例确实变慢了。

3.3 变慢场景

3.3.1 慢查询命令

原因

第一种 Redis 在操作内存数据时,时间复杂度过高,要花费更多的 CPU 资源。
第二种 Redis 一次需要返回给客户端的数据过多,更多时间花费在数据协议的组装和网络传输过程中。

解决

  • 尽量不使用sort sunion/smembers等 O(N) 以上复杂度过高的命令 用其他高效的命令替代(sscan多次迭代返回)
  • 需要执行排序,交集,并集操作时,可以在客户端完成
  • keys一般不在生产环境使用

3.3.2 操作bigkey

如果发现 SET / DEL 这种简单命令出现在慢日志中,那么就要怀疑实例否写入了 bigkey。 bigkey在分配内存时就会比较耗时,同样的,当删除这个key时,释放内存也会比较耗时。 bigkey在很多场景下,如分片集群模式下数据的迁移,数据过期、数据淘汰、透明大页 都会产生性能问题。

扫描 bigkey

通过bigkeys命令可以扫描bigkey,看到每种数据类型所占用的最大内存 / 拥有最多元素的 key 是哪一个,以及每种数据类型在整个实例中的占比和平均大小 / 元素数量。

$ redis-cli -h 127.0.0.1 -p 6379 --bigkeys -i 0.01

...
-------- summary -------

Sampled 829675 keys in the keyspace!
Total key length in bytes is 10059825 (avg len 12.13)

Biggest string found 'key:291880' has 10 bytes
Biggest   list found 'mylist:004' has 40 items
Biggest    set found 'myset:2386' has 38 members
Biggest   hash found 'myhash:3574' has 37 fields
Biggest   zset found 'myzset:2704' has 42 members

36313 strings with 363130 bytes (04.38% of keys, avg size 10.00)
787393 lists with 896540 items (94.90% of keys, avg size 1.14)
1994 sets with 40052 members (00.24% of keys, avg size 20.09)
1990 hashs with 39632 fields (00.24% of keys, avg size 19.92)
1985 zsets with 39750 members (00.24% of keys, avg size 20.03)

注意

(1)对线上实例进行 bigkey 扫描时,Redis 的 OPS 会突增,指定 -i 参数可控制扫描的频率,表示每次扫描后休息的时间间隔,单位秒。
(2)扫描结果中,对于容器类型(List、Hash、Set、ZSet)的 key,只能扫描出元素最多的 key。但一个 key 的元素多,不一定表示占用内存也多。

优化

  • 业务应用尽量避免写入 bigkey
  • Redis4.0 以上版本,用 UNLINK 命令替代DEL可以把释放key内存的操作,放到后台线程中去执行
  • Redis6.0 以上版本,可以开启lazy-free机制(lazyfree-lazy-user-del = yes),在执行 DEL 命令时,释放内存也会放到后台线程中执行

3.3.3 key集中过期

现象表现

变慢的时间点很有规律,例如某个整点,或者每间隔多久就会发生一波延迟

Redis 的过期策略

  • 被动过期,即惰性删除:只有当访问某个 key 时,才判断这个 key 是否已过期,如果已过期,则从实例中删除
  • 主动过期,即定期删除:Redis 内部维护了一个定时任务,默认每100 毫秒从全局的过期哈希表中随机取出 20 个 key删除,重复此过程,直到过期 key 的比例下降到 25% 以下,或者这次任务的执行耗时超过了 25 毫秒

这个主动过期 key 的定时任务,是在 Redis 主线程中执行的。且这个操作延迟的命令并不会记录在慢日志中。

优化

(1) 设置 key 的过期时间时,增加一个随机时间
(2) Redis 4.0 以上版本,开启 lazy-free 机制

// 释放过期 key 的内存,放到后台线程执行
lazyfree-lazy-expire yes

(3) 监控info 命令 expired_keys 指标是否出现了突增

> info stats
// key 过期事件的总数
expired_keys 
// 由于 maxmemory 限制,而被回收内存的 key 的总数
evicted_keys

3.3.4 AOF

AOF持久化保持数据磁盘,依赖文件系统,文件系统将数据写回磁盘的机制,会影响redis持久化的效率 AOF提供了三种写回策略:appendfsync = no,everysec,always

  • always写入内核缓冲区并同步到AOF文件
  • everysec写入内核缓冲区,如果距离上次同步时间超过1s则同步(默认)
  • no:写入内核缓冲区,但不同步,何时同步由操作系统决定

依赖两个系统调用:

write:日志记录到内核缓冲区
fsync:刷盘,时间较长

在AOF重写期间,如果AOF重写占用了大量的磁盘IO带宽,会导致fsync阻塞。 当主线程调用fsync,如果上一次fsync还没执行完,主线程也会阻塞。

优化

no-appendfsync-on-rewrite设置为yes, 表示在AOF重写时,不进行fsync操作(宕机数据丢失)。

使用 info persistence命令可以看到进行了多少个这样的命令。频繁发生的话代表硬盘负载过大。

> info persistence
...
# 主线程每次进行AOF会对比上次fsync成功的时间;
# 如果距上次不到2s,主线程直接返回;如果超过2s,则主线程阻塞直到fsync同步完成
aof_delayed_fsync:100

3.3.5 Fork

Redis的 RDB AOF rewrite 在后台在执行时,需要主进程创建出一个子进程进行数据的持久化。主进程创建子进程,会调用操作系统提供的 fork 函数

主进程需要拷贝自己的内存页表给子进程, 如果这个实例很大,那么这个拷贝的过程也会比较耗时。
fork过程会消耗大量的 CPU 资源,在完成 fork 之前,整个Redis实例会被阻塞住

查看fork耗时

> info stats
# 上一次 fork 耗时,单位微秒
latest_fork_usec:59477

优化

(1)Redis实例的内存尽量在10G以下,执行fork的耗时与实例大小有关,实例越大,耗时越久
(2)合理配置数据持久化策略

  • 在从节点执行RDB备份,推荐在低峰期执行
  • 而对于丢失数据不敏感的业务,可以关闭AOFAOF rewrite

(3) Redis 实例不要部署在虚拟机上:fork 的耗时也与系统也有关,虚拟机比物理机耗时更久
(4) 降低主从库全量同步的概率:适当调大 repl-backlog-size 参数,避免主从全量同步

3.3.5 Swap

触发swap的原因 redis实例自身使用了大量的内存,同一台机器上的其他进程在进行大量的文件读写操作,占用系统内存。

查看 Redis 进程是否使用到了 Swap

先找到 Redis 的 进程ID

redis-cli info|grep process_id  //5332

或者

$ ps -aux | grep redis-server  //5332

系统本身会在后台记录每个进程的swap


cd /proc/5332 //进程目录
cat smaps |egrep '^(swap|size)'

//输出
Size:               1256 kB
Swap:                  0 kB
Size:                  4 kB
Swap:                  0 kB
Size:                132 kB
Swap:                  0 kB
Size:              63488 kB
Swap:                  0 kB
Size:                132 kB
Swap:                  0 kB
Size:              65404 kB
Swap:                  0 kB
Size:            1921024 kB
Swap:                  0 kB
...

每一行 Size 表示 Redis 所用的一块内存大小,Swap 就表示这块 Size 大小的内存,有多少数据已经被换到磁盘上了。 如果是几百兆甚至上 GB 的内存被换到了磁盘上,需要警惕。

优化

(1)增加机器的内存
(2)整理内存空间,释放出足够的内存供 Redis 使用,然后释放 Redis 的 Swap
(3)监控Redis机器的内存和 Swap 使用情况