导致阻塞问题的场景大致分为内在原因和外在原因:

  • 内在原因包括: 不合理地使用 API 或数据结构、CPU 饱和、持久化阻塞等
  • 外在原因包括: CPU竞争、内存交换、网络问题等

7.1 发现阻塞

常见的做法是在应用方加入异常统计并通过邮件/短信/微信报警, 以便及时发现通知问题。

  • 可以借助于日志系统

image.png
图7-1 自定义 Appender 收集 Redis 异常

还可以借助 Redis 监控系统发现阻塞问题, 当监控系统检测到 Redis 运行期的一些关键指标出现不正常时会触发报警。

7.2 内在原因

7.2.1 API 或数据结构使用不合理

1. 如何发现慢查询

  1. slowlog get {n}

也有可能是网络原因.

解决方案:

  • 修改为低算法度的命令, 如 hgetall 改为 hmget 等, 禁用 keys、sort 等命令
  • 调整大对象: 缩减大对象数据或把大对象拆分为多个小对象, 防止一次命令操作过多的数据

2. 如何发现大对象

内部原理采用分段进行 scan 操作, 把历史扫描过的最大对象统计出来便于分析优化:

redis-cli -h {ip} -p {port} bigkeys

7.2.2 CPU 饱和

单线程的 Redis 处理命令时只能使用一个 CPU。而 CPU 饱和是指 Redis 把单核 CPU 使用率跑到接近100%。

建议使用统计命令 redis-cli -h {ip} -p {port} --stat 获取当前 Redis 使用情况,该命令每秒输出一行统计信息,运行效果如下:

image.png

还有一种情况是过度的内存优化, 这种情况有些隐蔽, 需要我们根据 info commandstats 统计信息分析出命令不合理开销时间, 例如下面的耗时统计:

cmdstat_hset:calls=198757512,usec=27021957243,usec_per_call=135.95

7.2.3 持久化阻塞

1. fork 阻塞

fork 操作发生在 RDB 和 AOF 重写时, Redis 主线程调用 fork 操作产生共享内存的子进程, 由子进程完成持久化文件重写工作。如果 fork 操作本身耗时过长,必然会导致主线程的阻塞。

info stats 命令中的 latest_fork_usec 指标:

  • 表示 Redis 最近一次 fork 操作耗时

2. 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 阻塞主线程时会累加。

硬盘压力可能是 Redis 进程引起的, 也可能是其他进程引起的, 可以使用 iotop 查看具体是哪个进程消耗过多的硬盘资源。

3. HugePage 写操作阻塞

子进程在执行重写期间利用 Linux 写时复制技术降低内存开销, 因此只有写操作时 Redis 才复制要修改的内存页。

对于开启 Transparent HugePages 的操作系统, 每次写命令引起的复制内存页单位由 4K 变为 2MB, 放大了512倍, 会拖慢写操作的执行时间, 导致大量写操作慢查询。

7.3 外在原因

7.3.1 CPU 竞争

  • 进程竞争: Redis 是典型的 CPU 密集型应用, 不建议和其他多核 CPU 密集型服务部署在一起
    • top
    • sar
  • 绑定CPU: 部署 Redis 时为了充分利用多核 CPU, 通常一台机器部署多个实例。常见的一种优化是把 Redis 进程绑定到 CPU 上, 用于降低 CPU 频繁上下文切换的开销。这个优化技巧正常情况下没有问题, 但是存在例外情况

image.png

当 Redis 父进程创建子进程进行 RDB/AOF 重写时, 如果做了 CPU 绑定, 会与父进程共享使用一个CPU。子进程重写时对单核 CPU 使用率通常在90%以上, 父进程与子进程将产生激烈 CPU 竞争, 极大影响 Redis 稳定性。因此对于开启了持久化或参与复制的主节点不建议绑定CPU。

7.3.2 内存交换

内存交换 (swap) 对于 Redis 来说是非常致命的, Redis 保证高性能的一个重要前提是所有的数据在内存中。

识别 Redis 内存交换的检查方法如下:

  1. 查询 Redis 进程号
# redis-cli -p 6383 info server | grep process_id
process_id:4476
  1. 根据进程号查询内存交换信息

image.png

如果交换量都是 0KB 或者个别的是 4KB, 则是正常现象, 说明 Redis 进程内存没有被交换。

预防内存交换的方法有:

  • 保证机器充足的可用内存
  • 确保所有 Redis 实例设置最大可用内存 (maxmemory), 防止极端情况下 Redis 内存不可控的增长
  • 降低系统使用 swap 优先级, 如 echo 10 > /proc/sys/vm/swappiness

7.3.3 网络问题

1. 连接拒绝

  • 网络闪断。一般发生在网络割接或者带宽耗尽的情况, 对于网络闪断的识别比较困难, 常见的做法可以通过 sar-n DEV 查看本机历史流量是否正常,或者借助外部系统监控工具 (如 Ganglia) 进行识别
  • Redis连接拒绝。Redis 通过 maxclients 参数控制客户端最大连接数, 默认10000。当 Redis 连接数大于maxclients 时会拒绝新的连接进入, info stats 的 rejected_connections 统计指标记录所有被拒绝连接的数量:
# redis-cli -p 6384 info Stats | grep rejected_connections
rejected_connections:0

Redis 使用多路复用 IO 模型可支撑大量连接, 但是不代表可以无限连接。客户端访问 Redis 时尽量采用 NIO 长连接或者连接池的方式。

设置 tcp-keepalive 和 timeout 参数让 Redis 主动检查和关闭无效连接.

  • 连接溢出。这是指操作系统或者 Redis 客户端在连接时的问题。这个问题的原因比较多,下面就分别介绍两种原因: 进程限制、backlog 队列溢出。
  1. 进程限制

客户端想成功连接上 Redis 服务需要操作系统和 Redis 的限制都通过才可以:

image.png

操作系统的限制:

ulimit -n
  1. backlog 队列溢出

系统对于特定端口的 TCP 连接使用 backlog 队列保存。Redis 默认的长度为511, 通过 tcp-backlog 参数设置。

系统的 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

2. 网络延迟

网络延迟取决于客户端到 Redis 服务器之间的网络环境。主要包括它们之间的物理拓扑和带宽占用情况。常见的物理拓扑按网络延迟由快到慢可分为: 同物理机>同机架>跨机架>同机房>同城机房>异地机房。但它们容灾性正好相反, 同物理机容灾性最低而异地机房容灾性最高。

Redis 提供了测量机器之间网络延迟的工具, 在 redis-cli-h {host} -p {port} 命令后面加入如下参数进行延迟测试:

  • —latency: 持续进行延迟测试, 分别统计: 最小值、最大值、平均值、采样次数
  • —latency-history: 统计结果同 —latency, 但默认每15秒完成一行统计, 可通过 -i 参数控制采样时间
  • —latency-dist: 使用统计图的形式展示延迟统计, 每1秒采样一次

带宽瓶颈通常出现在以下几个方面:

  • 机器网卡带宽。
  • 机架交换机带宽。
  • 机房之间专线带宽。

3. 网卡软中断

网卡软中断是指由于单个网卡队列只能使用一个 CPU, 高并发下网卡数据交互都集中在同一个CPU, 导致无法充分利用多核 CPU 的情况。网卡软中断瓶颈一般出现在网络高流量吞吐的场景, 如下使用 “top + 数字1” 命令可以很明显看到 CPU1 的软中断指标 (si) 过高:

image.png

Linux 在内核2.6.35以后支持 Receive Packet Steering (RPS).

7.4 本章重点回顾

  1. 客户端最先感知阻塞等 Redis 超时行为, 加入日志监控报警工具可快速定位阻塞问题, 同时需要对 Redis 进程和机器做全面监控。
  2. 阻塞的内在原因: 确认主线程是否存在阻塞, 检查慢查询等信息,发现不合理使用 API 或数据结构的情况, 如keys、sort、hgetall 等。关注 CPU 使用率防止单核跑满。当硬盘 IO 资源紧张时, AOF 追加也会阻塞主线程。
  3. 阻塞的外在原因: 从 CPU 竞争、内存交换、网络问题等方面入手排查是否因为系统层面问题引起阻塞。