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-bytes
和active-defrag-threshold-lower
两个参数只有全部满足才会开始清理active-defrag-cycle-min
保证清理能正常开展active-defrag-cycle-max
避免在清理时,大量的内存拷贝阻塞 Redis,导致响应延迟升高active-defrag-cycle-min
和active-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
备份,推荐在低峰期执行 - 而对于丢失数据不敏感的业务,可以关闭
AOF
和AOF 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 使用情况