Redis是典型的单线程架构,所有的读写操作都是在一条主线程中完成的。当Redis用于高并发场景时,这条线程就变成了它的生命线。如果出现阻塞,哪怕是很短时间,对于应用来说都是噩梦。导致阻塞问题的场 景大致分为内在原因和外在原因:
- 内在原因包括:不合理地使用API或数据结构、CPU饱和、持久化阻塞 等。
-
阻塞的内在原因
API或数据结构使用不合理
例如对一个包含上万个元素的hash结构执行hgetall操作,由于数据量比较大且命令算法复杂度是 O(n),这条命令执行速度必然很慢。对于高并发的场景应该尽量避免在大对象上执行算法复杂 度超过O(n)的命令。
如何发现慢查询?
执行slowlog get {n}
命令可以获取最近的n条慢查询命令,默认对于执行超过10ms
命令都会记录到一个定长队列中,线上实例建议设置为1ms
便于及时发现毫秒级以上的命令。慢查询队列长度默认 128,可适当调大。
慢查询本身只记录了命令执行 时间,不包括数据网络传输时间和命令排队时间,因此客户端发生阻塞异常 后,可能不是当前命令缓慢,而是在等待其他命令执行。需要重点比对异常 和慢查询发生的时间点,确认是否有慢查询造成的命令阻塞排队。
发现慢查询后可以按照两个方向去调整: 修改为低算法度的命令,如hgetall改为hmget等,禁用keys、sort等命令;
- 调整大对象:缩减大对象数据或把大对象拆分为多个小对象,防止一次命令操作过多的数据。
如何发现大对象?
Redis提供了redis-cli -h {ip} -p {port} bigkeys
用于发现大对象。内部原理采用分段进行scan操作,把历史扫描过的最大对象统计出来便于分析优化。
CPU饱和问题
单线程的Redis处理命令时只能使用一个CPU。而CPU饱和是指Redis把单核CPU使用率跑到接近100%。使用top命令很容易识别出对应Redis进程的 CPU使用率。CPU饱和是非常危险的,将导致Redis无法处理更多的命令,严重影响吞吐量和应用方的稳定性。
对于这种情况,首先判断当前Redis的并 发量是否达到极限,建议使用统计命令redis-cli -h {ip} -p {port} --stat
获取当前 Redis使用情况,该命令每秒输出一行统计信息,运行效果如下:
# redis-cli --stat
------- data ------ --------------------- load -------------------- - child -
keys mem clients blocked requests connections
3789785 3.20G 507 0 8867955607(+0) 555894
3789813 3.20G 507 0 8867959511(+63904) 555894
3789822 3.20G 507 0 8867961602(+62091) 555894
3789831 3.20G 507 0 8867965049(+63447) 555894
3789842 3.20G 507 0 8867969520(+62675) 555894
3789845 3.20G 507 0 8867971943(+62423) 555894
上面是一个接近饱和的Redis实例的统计信息,它每秒平均处理6万+的请求。对于这种情况,垂直层面的命令优化很难达到效果,这时就需要做集群化水平扩展来分摊OPS压力。
如果只有几百或几千OPS的Redis实例就接近CPU饱和是很不正常的,有可能使用了高算法复杂度的命令。还有一种情况是过度的内存优化,这种情况有些隐蔽,需要根据info commandstats
统计信息分析出命令不合理开销时间。
持久化相关阻塞
对于开启了持久化功能的Redis节点,需要排查是否是持久化导致的阻 塞。持久化引起主线程阻塞的操作主要有:fork阻塞、AOF刷盘阻塞、 HugePage写操作阻塞。
fork
阻塞
fork操作发生在RDB和AOF重写时,Redis主线程调用fork操作产生共享内存的子进程,由子进程完成持久化文件重写工作。如果fork操作本身耗时过长,必然会导致主线程的阻塞。 可以执行info stats
命令获取到latest_fork_usec
指标,表示Redis最近一次 fork操作耗时,如果耗时很大,比如超过1秒,则需要做出优化调整,如避免使用过大的内存实例和规避fork缓慢的操作系统等,更多细节见第5章5.3 节中fork优化部分。
- AOF刷盘阻塞
开启AOF持久化功能时,文件刷盘的方式一般采用每秒一次,后台线程每秒对AOF文件做fsync操作。当硬盘压力过大时,fsync操作需要等待,直到写入完成。如果主线程发现距离上一次的fsync成功超过2秒,为了数据安全性它会阻塞直到后台线程执行fsync操作完成。这种阻塞行为主要 是硬盘压力引起,可以查看Redis日志识别出这种情况,当发生这种阻塞行 为时,会打印如下日志:
Asynchronous AOF fsync is taking too long (disk is busy). Writing the AOF
buffer without waiting for fsync to complete, this may slow down Redis.
也可以查看info persistence
统计中的aof_delayed_fsync
指标,每次发生 fdatasync阻塞主线程时会累加。定位阻塞问题后具体优化方法见第5.3节的 AOF追加阻塞部分。
硬盘压力可能是Redis进程引起的,也可能是其他进程引起的,可以使用iotop
查看具体是哪个进程消耗过多的硬盘资源。
- HugePage阻塞
子进程在执行重写期间利用Linux写时复制技术降低内存开销,因此只有写操作时Redis才复制要修改的内存页。对于开启Transparent HugePages的操作系统,每次写命令引起的复制内存页单位由4K变为2MB,放大了512 倍,会拖慢写操作的执行时间,导致大量写操作慢查询。例如简单的incr命令也会出现在慢查询中。
阻塞的外在原因
CPU竞争
- 进程竞争:Redis是典型的CPU密集型应用,不建议和其他多核CPU密 集型服务部署在一起。当其他进程过度消耗CPU时,将严重影响Redis吞吐 量。可以通过
top
、sar
等命令定位到CPU消耗的时间点和具体进程,这个问题比较容易发现,需要调整服务之间部署结构。 - 绑定CPU:部署Redis时为了充分利用多核CPU,通常一台机器部署多个实例。常见的一种优化是把Redis进程绑定到CPU上,用于降低CPU频繁上下文切换的开销。
- 例外情况:当Redis父进程创建子进程进行RDB/AOF重写时,如果做了CPU绑定, 会与父进程共享使用一个CPU。子进程重写时对单核CPU使用率通常在90% 以上,父进程与子进程将产生激烈CPU竞争,极大影响Redis稳定性。因此 对于开启了持久化或参与复制的主节点不建议绑定CPU。
内存交换
Redis保证高性能的一个重要前提是所有的数据在内存中。如果操作系统把Redis使用的部分内存换出到硬盘,由于内存与硬盘读写速度差几个数量级,会导致发生交换后的 Redis性能急剧下降。识别内存交换的检查方法如下: ```bash查询Redis进程号:
redis-cli -p 6383 info server | grep process_id process_id:4476
- 例外情况:当Redis父进程创建子进程进行RDB/AOF重写时,如果做了CPU绑定, 会与父进程共享使用一个CPU。子进程重写时对单核CPU使用率通常在90% 以上,父进程与子进程将产生激烈CPU竞争,极大影响Redis稳定性。因此 对于开启了持久化或参与复制的主节点不建议绑定CPU。
根据进程号查询内存交换信息:
cat /proc/4476/smaps | grep Swap
如果交换量都是0KB或者个别的是4KB,则是正常现象,说明Redis进程内存没有被交换。
预防交换的方法:
- 保证机器充足的可用内存;
- 确保所有Redis实例设置最大可用内存(maxmemory),防止极端情况下Redis内存不可控的增长;
- 降低系统使用swap优先级,如echo10>/proc/sys/vm/swappiness。
<a name="qwfLL"></a>
## 网络问题
1. **连接拒绝**
当出现网络闪断或者连接数溢出时,客户端会出现无法连接Redis的情况。需要区分这三种情况:网络闪断、Redis连接拒绝、连接溢出。
- 网络闪断:一般发生在网络割接或者带宽耗尽的情况,对于网络闪断的识别比较困难,常见的做法可以通过sar-n DEV查看本机历史流量是否正常。
- Redis连接拒绝。Redis通过maxclients参数控制客户端最大连接数,默认10000。当Redis连接数大于maxclients时会拒绝新的连接进入, `info stats`的`rejected_connections`统计指标记录所有被拒绝连接的数量。
- 连接溢出:这是指操作系统或者Redis客户端在连接时的问题。两种主要原因如下:
- 进程限制:客户端想成功连接上Redis服务需要操作系统和Redis的限制都通过才可以。操作系统一般会对进程使用的资源做限制,其中一项是对进程可打开最 大文件数控制,通过`ulimit-n`查看,通常默认1024。由于Linux系统对TCP连接也定义为一个文件句柄,因此对于支撑大量连接的Redis来说需要增大这个值,如设置ulimit-n 65535,防止Too many open files错误。
- backlog队列溢出:系统对于特定端口的TCP连接使用backlog队列保存。Redis默认的长度为511,通过tcp-backlog参数设置。如果Redis用于高并发场景为了防止缓慢连接占用,可适当增大这个设置,但必须大于操作系统允许值才能生效。当 Redis启动时如果tcp-backlog设置大于系统允许值将以系统值为准,Redis打印如下警告日志:
```bash
# WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/
net/core/somaxconn is set to the lower value of 128.
系统的backlog默认值为128,使用echo 511>/proc/sys/net/core/somaxconn 命令进行修改。可以通过netstat-s命令获取因backlog队列溢出造成的连接拒绝统计,如下:
# netstat -s | grep overflowed
663 times the listen queue of a socket overflowed
如果怀疑是backlog队列溢出,线上可以使用cron
定时执行netstat -s|grep overflowed
统计,查看是否有持续增长的连接拒绝情况。
- 网络延迟
网络延迟取决于客户端到Redis服务器之间的网络环境。常见的物理拓扑按网络延迟由快到慢可分为:同物理机>同机架>跨机架>同机房>同城机房>异地机房。(容灾性相反)
Redis提供了测量机器之间网络延迟的工具,在redis-cli -h {host} -p {port}
命令后面加入如下参数进行延迟测试:
--latency
: 持续进行延迟测试,分别统计:最小值、最大值、平均值、采样次数。--latency-history
:统计结果同—latency,但默认每15秒完成一行统计, 可通过-i
参数控制采样时间。--latency-dist
:使用统计图的形式展示延迟统计,每1秒采样一次。
网络延迟问题经常出现在跨机房的部署结构上,对于机房之间延迟比较严重的场景需要调整拓扑结构,如把客户端和Redis部署在同机房或同城机 房等。
带宽瓶颈通常出现在以下几个方面:
- 机器网卡带宽。
- 机架交换机带宽。
- 机房之间专线带宽。
- 网卡软中断
网卡软中断是指由于单个网卡队列只能使用一个CPU,高并发下网卡数据交互都集中在同一个CPU,导致无法充分利用多核CPU的情况。网卡软中 断瓶颈一般出现在网络高流量吞吐的场景。