Redis
是典型的单线程架构,所有的读写操作都是在一条主线程中完成的。当Redis
用于高并发场景时,这条线程就变成了它的生命线。如果出现阻塞,哪怕是很短时间,对于我们的应用来说都是噩梦。导致阻塞问题的场景大致分为内在原因和外在原因:
- 内在原因包括:不合理地使用API或数据结构、CPU饱和、持久化阻塞等。
- 外在原因包括:CPU竞争、内存交换、网络问题等。
1 发现阻塞
当
Redis
阻塞时,线上应用服务应该最先感知到,这时应用方会收到大量Redis
超时异常,比如
Jedis
客户端会抛
JedisConnectionException
异常。常见的做法是在应用方加入异常统计并通过邮件/
短信
/
微信报警,以便及时发现通知问题。开发人员需要处理如何统计异常以及触发报警的时机。何时触发报警一般根据应用的并发量决定,如1
分钟内超过
10
个异常触发报警。在
实现异常统计时要注意,由于
Redis
调用
API
会分散在项目的多个地方,每个地方都监听异常并加入监控代码必然难以维护。这时可以借助于日志系统,如Java
语言可以使用
logback
或
log4j
。当异常发生时,异常信息最终会被日志系统收集到Appender
(输出目的地),默认的
Appender
一般是具体的日志文件,开发人员可以自定义一个Appender
,用于专门统计异常和触发报警逻辑,如下图
所示。
2 内在原因
定位到具体的
Redis
节点异常后,首先应该排查是否是
Redis
自身原因导致,围绕以下几个方面排查:
- API或数据结构使用不合理。
- CPU饱和的问题。
- 持久化相关的阻塞。
2.1 API或数据结构使用不合理
通常
Redis
执行命令速度非常快,但也存在例外,如对一个包含上万个元素的hash
结构执行
hgetall
操作,由于数据量比较大且命令算法复杂度是 O(
n
),这条命令执行速度必然很慢。这个问题就是典型的不合理使用
API和数据结构。对于高并发的场景我们应该尽量避免在大对象上执行算法复杂度超过O
(
n
)的命令。
1>.
如何发现慢查询
Redis
原生提供慢查询统计功能,执行
slowlog get{n}
命令可以获取最近的n
条慢查询命令,默认对于执行超过
10
毫秒的命令都会记录到一个定长队列中,线上实例建议设置为1
毫秒便于及时发现毫秒级以上的命令。如果命令执行时间在毫秒级,则实例实际OPS
只有
1000
左右。慢查询队列长度默认128,可适当调大。慢查询更多细节见第
3
章。慢查询本身只记录了命令执行 时间,不包括数据网络传输时间和命令排队时间,因此客户端发生阻塞异常后,可能不是当前命令缓慢,而是在等待其他命令执行。需要重点比对异常和慢查询发生的时间点,确认是否有慢查询造成的命令阻塞排队。
发现慢查询后,开发人员需要作出及时调整。可以按照以下两个方向去调整:
1
)修改为低算法度的命令,如
hgetall
改为
hmget
等,禁用
keys
、
sort
等命令;
2)调整大对象:缩减大对象数据或把大对象拆分为多个小对象,防止一次命令操作过多的数据。大对象拆分过程需要视具体的业务决定,如用户好友集合存储在Redis中,有些热点用户会关注大量好友,这时可以按时间或其他维度拆分到多个集合中。
2>.
如何发现大对象
Redis
本身提供发现大对象的工具,对应命令:
redis-cli-h{ip}- p{port}bigkeys。内部原理采用分段进行
scan
操作,把历史扫描过的最大对象统计出来便于分析优化,运行效果如下:
根据结果汇总信息能非常方便地获取到大对象的键,以及不同类型数据结构的使用情况。
2.2 CPU
饱和
单线程的
Redis
处理命令时只能使用一个
CPU
。而
CPU
饱和是指
Redis
把单核CPU
使用率跑到接近
100%
。使用
top
命令很容易识别出对应
Redis
进程的CPU使用率。
CPU
饱和是非常危险的,将导致
Redis
无法处理更多的命令,严重影响吞吐量和应用方的稳定性。对于这种情况,首先判断当前Redis
的并发量是否达到极限,建议使用统计命令redis-cli-h{ip}-p{port}--stat
获取当前Redis使用情况,该命令每秒输出一行统计信息,运行效果如下:
以上输出是一个接近饱和的
Redis
实例的统计信息,它每秒平均处理
6
万+的请求。对于这种情况,垂直层面的命令优化很难达到效果,这时就需要做集群化水平扩展来分摊OPS
压力。如果只有几百或几千
OPS
的
Redis
实例就接近CPU饱和是很不正常的,有可能使用了高算法复杂度的命令。还有一种情况是过度的内存优化,这种情况有些隐蔽,需要我们根据info commandstats
统计信息分析出命令不合理开销时间,例如下面的耗时统计:
查看这个统计可以发现一个问题,
hset
命令算法复杂度只有
O
(
1
)但平均耗时却达到135
微秒,显然不合理,正常情况耗时应该在
10微秒以下。这是因为上面的Redis实例为了追求低内存使用量,过度放宽ziplist使用条件(修改了hash-max-ziplist-entries和hash-max-ziplist-value配置)。进程内的hash对象平均存储着上万个元素,而针对ziplist的操作算法复杂度在O(n)到O(n2)之间。虽然采用ziplist编码后hash结构内存占用会变小,但是操作变得更慢且更消耗CPU。ziplist压缩编码是Redis用来平衡空间和效率的优化手段,不可过度使用。关于ziplist编码细节见第8章的8.3节“内存优化
2.3
持久化阻塞
对于开启了持久化功能的
Redis
节点,需要排查是否是持久化导致的阻塞。持久化引起主线程阻塞的操作主要有:fork
阻塞、
AOF
刷盘阻塞、 HugePage写操作阻塞。
1>.fork
阻塞
fork
操作发生在
RDB
和
AOF
重写时,
Redis
主线程调用
fork
操作产生共享内存的子进程,由子进程完成持久化文件重写工作。如果fork
操作本身耗时过长,必然会导致主线程的阻塞。
可以执行info stats
命令获取到
latest_fork_usec
指标,表示
Redis
最近一次fork操作耗时,如果耗时很大,比如超过
1
秒,则需要做出优化调整,如避免使用过大的内存实例和规避fork缓慢的操作系统等。
2>.AOF
刷盘阻塞
当我们开启
AOF
持久化功能时,文件刷盘的方式一般采用每秒一次,后台线程每秒对AOF
文件做
fsync
操作。当硬盘压力过大时,
fsync
操作需要等 待,直到写入完成。如果主线程发现距离上一次的fsync
成功超过
2
秒,为了数据安全性它会阻塞直到后台线程执行fsync
操作完成。这种阻塞行为主要是硬盘压力引起,可以查看Redis
日志识别出这种情况,当发生这种阻塞行为时,会打印如下日志:
也可以查看info persistence统计中的aof_delayed_fsync指标,每次发生
fdatasync阻塞主线程时会累加。
3>.HugePage
写操作阻塞
子进程在执行重写期间利用
Linux
写时复制技术降低内存开销,因此只有写操作时Redis
才复制要修改的内存页。对于开启
Transparent HugePages
的操作系统,每次写命令引起的复制内存页单位由4K
变为
2MB
,放大了
512倍,会拖慢写操作的执行时间,导致大量写操作慢查询。例如简单的incr
命令也会出现在慢查询中。
3
外在原因
排查
Redis
自身原因引起的阻塞原因之后,如果还没有定位问题,需要排查是否由外部原因引起。围绕以下三个方面进行排查:
- CPU竞争
- 内存交换
- 网络问题
3.1
CPU
竞争
CPU
竞争问题如下:
- 进程竞争:Redis是典型的CPU密集型应用,不建议和其他多核CPU密集型服务部署在一起。当其他进程过度消耗CPU时,将严重影响Redis吞吐量。可以通过top、sar等命令定位到CPU消耗的时间点和具体进程,这个问题比较容易发现,需要调整服务之间部署结构。
- 绑定CPU:部署Redis时为了充分利用多核CPU,通常一台机器部署多个实例。常见的一种优化是把Redis进程绑定到CPU上,用于降低CPU频繁上下文切换的开销。这个优化技巧正常情况下没有问题,但是存在例外情况,如图7-2所示。
当
Redis
父进程创建子进程进行
RDB/AOF
重写时,如果做了
CPU
绑定,会与父进程共享使用一个CPU
。子进程重写时对单核
CPU
使用率通常在
90%以上,父进程与子进程将产生激烈CPU
竞争,极大影响
Redis
稳定性。因此对于开启了持久化或参与复制的主节点不建议绑定CPU
。
3.2
内存交换
内存交换(
swap
)对于
Redis
来说是非常致命的,
Redis
保证高性能的一个重要前提是所有的数据在内存中。如果操作系统把Redis
使用的部分内存换出到硬盘,由于内存与硬盘读写速度差几个数量级,会导致发生交换后的Redis性能急剧下降。识别
Redis
内存交换的检查方法如下:
1
)查询
Redis
进程号:
2
)根据进程号查询内存交换信息:
如果交换量都是
0KB
或者个别的是
4KB
,则是正常现象,说明
Redis
进程内存没有被交换。预防内存交换的方法有:
- 保证机器充足的可用内存。
- 确保所有Redis实例设置最大可用内存(maxmemory),防止极端情况下Redis内存不可控的增长.
-
降低系统使用 swap 优先级,如 echo10>/proc/sys/vm/swappiness。
3.3
网络问题
网络问题经常是引起
Redis
阻塞的问题点。常见的网络问题主要有:连接拒绝、网络延迟、网卡软中断等。
1>.
连接拒绝
当出现网络闪断或者连接数溢出时,客户端会出现无法连接
Redis
的情况。我们需要区分这三种情况:网络闪断、Redis
连接拒绝、连接溢出。
第一种情况:网络闪断
。一般发生在网络割接或者带宽耗尽的情况,对于网络闪断的识别比较困难,常见的做法可以通过sar-nDEV
查看本机历史流量是否正常,或者借助外部系统监控工具(如Ganglia
)进行识别。具体问题定位需要更上层的运维支持,对于重要的Redis
服务需要充分考虑部署架构的优化,尽量避免客户端与Redis
之间异地跨机房调用。
第二种情况:
Redis
连接拒绝
。
Redis
通过
maxclients
参数控制客户端最大连接数,默认10000
。当
Redis
连接数大于
maxclients
时会拒绝新的连接进入,infostats的
rejected_connections
统计指标记录所有被拒绝连接的数量:
Redis
使用多路复用
IO
模型可支撑大量连接,但是不代表可以无限连接。客户端访问Redis
时尽量采用
NIO
长连接或者连接池的方式。
第三种情况:连接溢出
。这是指操作系统或者
Redis
客户端在连接时的问题。这个问题的原因比较多,下面就分别介绍两种原因:进程限制、backlog队列溢出。
(
1
)进程限制
客户端想成功连接上
Redis
服务需要操作系统和
Redis
的限制都通过才可以,如图7-3
所示。
操作系统一般会对进程使用的资源做限制,其中一项是对进程可打开最大文件数控制,通过ulimit-n
查看,通常默认
1024
。由于
Linux
系统对
TCP
连接也定义为一个文件句柄,因此对于支撑大量连接的Redis
来说需要增大这个值,如设置ulimit-n65535
,防止
Toomanyopenfiles
错误。
(
2
)
backlog
队列溢出
系统对于特定端口的
TCP
连接使用
backlog
队列保存。
Redis
默认的长度为511
,通过
tcp-backlog
参数设置。如果
Redis
用于高并发场景为了防止缓慢连接占用,可适当增大这个设置,但必须大于操作系统允许值才能生效。当Redis启动时如果
tcp-backlog
设置大于系统允许值将以系统值为准,
Redis
打印如下警告日志:
系统的
backlog
默认值为
128
,使用
echo511>/proc/sys/net/core/somaxconn命令进行修改。可以通过netstat-s
命令获取因
backlog
队列溢出造成的连接拒绝统计,如下:
2>.
网络延迟
网络延迟取决于客户端到
Redis
服务器之间的网络环境。主要包括它们之间的物理拓扑和带宽占用情况。常见的物理拓扑按网络延迟由快到慢可分为:同物理机>
同机架
>
跨机架
>
同机房
>
同城机房
>
异地机房。但它们容灾性正好相反,同物理机容灾性最低而异地机房容灾性最高。Redis
提供了测量机器之间网络延迟的工具,在redis-cli-h{host}-p{port}命令后面加入如下参数进行延迟测试:
- --latency:持续进行延迟测试,分别统计:最小值、最大值、平均值、采样次数。
- --latency-history:统计结果同--latency,但默认每15秒完成一行统计,可通过-i参数控制采样时间。
- --latency-dist:使用统计图的形式展示延迟统计,每1秒采样一次。
网络延迟问题经常出现在跨机房的部署结构上,对于机房之间延迟比较严重的场景需要调整拓扑结构,如把客户端和Redis
部署在同机房或同城机房等。
带宽瓶颈通常出现在以下几个方面:
- 机器网卡带宽。
- 机架交换机带宽。
- 机房之间专线带宽。
带宽占用主要根据当时使用率是否达到瓶颈有关,如频繁操作
Redis
的大对象对于千兆网卡的机器很容易达到网卡瓶颈,因此需要重点监控机器流量,及时发现网卡打满产生的网络延迟或通信中断等情况,而机房专线和交换机带宽一般由上层运维监控支持,通常出现瓶颈的概率较小。
3>.
网卡软中断
网卡软中断是指由于单个网卡队列只能使用一个
CPU,高并发下网卡数据交互都集中在同一个CPU,导致无法充分利用多核CPU的情况。网卡软中断瓶颈一般出现在网络高流量吞吐的场景,如下使用“top+数字1”命令可以很明显看到CPU1的软中断指(si)过高: