Skip to content

阻塞

Redis 是典型的单线程架构,所有的读写操作都是在一条主线程中完成的。
当 Redis 用于高并发场景时,这条线程就变成了它的生命线。如果出现阻塞,哪怕是很短时间,对于我们的应用来说都是噩梦。
导致阻塞问题的场景大致分为内在原因和外在原因

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

发现阻塞

当 Redis 阻塞时,线上应用服务应该最先感知到,这时应用方会收到大量 Redis 超时异常,比如 Jedis 客户端会抛出 JedisConnectionException 异常。
常见的做法是在应用方加入异常统计并通知邮件/短信/微信报警,以便及时发现通知问题。
开发人员需要处理如何统计异常以及触发报警的时机。
何时触发报警一般根据应用的并发量决定,如 1 分钟内超过 10 个异常触发报警。
在实现异常统计时要注意,由于 Redis 调用 API 会分散在项目的多个地方,每个地方都监听异常并加入监控代码必然难以维护。
这时可以借助于日志系统,如 Java 语言可以使用 logback 或 log4j。
当异常发生时,异常信息最终会被日志系统收集到 Appender(输出目的地),默认的 Appender 一般是具体的日志文件,开发人员可以自定义一个 Appender,用于专门统计异常和触发报警逻辑

出了在应用方加入统计报警逻辑之外,还可以借助 Redis 监控系统发现阻塞问题,当监控系统监测到 Redis 运行期的一些关键指标出现不正常时会触发报警。
Redis 相关的监控系统开源的方案有很多,一些公司内部也会自己开发监控系统。
一个可靠的 Redis 监控系统首先需要做到对关键指标全方位监控和异常识别,辅助开发运维人员发现定位问题。
如果 Redis 服务没有引入监控系统做辅助支撑,对于线上服务是非常不负责任和危险的。
这里推荐笔者团队开源的 CacheCloud 系统,它内部的统计监控模块能够很好地辅助工程师发现定位问题。

监控系统所监控的关键指标有很多,如命令耗时、慢查询、持久化阻塞、连接拒绝、CPU/内存/网络/磁盘使用过载等。
当出现阻塞时如果相关人员不能深刻理解这些关键指标的含义和背后的原理,会严重影响解决问题的速度。

内在原因

定位到具体的 Redis 节点异常后,首先应该排查是否是 Redis 自身原因导致,围绕以下几个方面排查:

  • API 或数据结构使用不合理
  • CPU 饱和的问题
  • 持久化相关的阻塞

API 或数据结构使用不合理

通常 Redis 执行命令速度非常快,但也存在例外,如对一个包含上万个元素的 hash 结构执行 hgetall 操作,
由于数据量比较大且命令算法复杂度是 O(n),这条命令执行速度必然很慢。
这个问题就是典型的不合理使用 API 和数据结构。
对于高并发的场景我们应该尽量避免在大对象上执行算法复杂度超过 O(n) 的命令

如何发现慢查询

Redis 原生提供慢查询统计功能,执行 slowlog get{n}命令可以获取最近的 n 条慢查询命令,
默认对于执行超过 10 毫秒的命令都会记录到一个定长队列中,线上实例建议设置为 1 毫秒便于及时发现毫秒级以上的命令。
如果命令执行时间在毫秒级,则实例实际 OPS 只有 1000 左右。
慢查询队列长度默认 128,可适当调大。
慢查询本身只记录了命令执行时间,不包括数据网络传输时间和命令排队时间,
因此客户端发生阻塞异常后,可能不是当前命令缓慢,而是在等待其他命令执行。
需要重点对比异常和慢查询发生的时间点,确认是否有慢查询造成的命令阻塞排队。

发现慢查询后,开发人员需要做出及时调整。可以按照以下两个方向去调整:

(1) 修改为低算法度的命令,如 hgetall 改为 hmget 等,禁用 keys、sort 等命令
(2) 调整大对象: 缩减大对象数据或把大对象拆分为多个小对象,防止一次命令操作过多的数据。大对象拆分过程需要视具体的的业务觉醒,如用户好友集合存储在 Redis 中,有些热点用户会关注大量好友,这时可以按时间或其他维度拆分到多个集合中

如何发现大对象

Redis 本身提供发现大对象的工具,对应命令: redis-cli -h {ip} -p {port} --bigkeys
内部原理采用分段进行 scan 操作,把历史扫描过的最大对象统计出来便于分析优化,运行效果如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
$ redis-cli --bigkeys

# Scanning the entire keyspace to find biggest keys as well as
# average sizes per key type.  You can use -i 0.1 to sleep 0.1 sec
# per 100 SCAN commands (not usually needed).

[00.00%] Biggest list   found so far 'ic_pct' with 5 items

-------- summary -------

Sampled 1 keys in the keyspace!
Total key length in bytes is 6 (avg len 6.00)

Biggest   list found 'ic_pct' has 5 items

0 strings with 0 bytes (00.00% of keys, avg size 0.00)
1 lists with 5 items (100.00% of keys, avg size 5.00)
0 sets with 0 members (00.00% of keys, avg size 0.00)
0 hashs with 0 fields (00.00% of keys, avg size 0.00)
0 zsets with 0 members (00.00% of keys, avg size 0.00)
0 streams with 0 entries (00.00% of keys, avg size 0.00)

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

# Scanning the entire keyspace to find biggest keys as well as
# average sizes per key type.  You can use -i 0.1 to sleep 0.1 sec
# per 100 SCAN commands (not usually needed).

[00.00%] Biggest list   found so far 'ic_pct' with 5 items

-------- summary -------

Sampled 1 keys in the keyspace!
Total key length in bytes is 6 (avg len 6.00)

Biggest   list found 'ic_pct' has 5 items

0 strings with 0 bytes (00.00% of keys, avg size 0.00)
1 lists with 5 items (100.00% of keys, avg size 5.00)
0 sets with 0 members (00.00% of keys, avg size 0.00)
0 hashs with 0 fields (00.00% of keys, avg size 0.00)
0 zsets with 0 members (00.00% of keys, avg size 0.00)
0 streams with 0 entries (00.00% of keys, avg size 0.00)

根据结果汇总信息能非常方便地获取到大对象的键,以及不同类型数据结构的使用情况

CPU 饱和

单线程的 Redis 处理命令时只能使用一个 CPU。
而 CPU 饱和是指 Redis 把单核 CPU 使用率跑到接近 100%。
使用 top 命令很容易识别出对应 Redis 进程的 CPU 使用率。
CPU 饱和是非常危险的,将导致 Redis 无法处理更多的命令,严重影响吞吐量的应用方的稳定性。
对于这种情况,首先判断当前 Redis 的并发量是否达到极限,建议使用统计命令 redis-cli -h {ip} -p {port} --bigkeys 获取当前 Redis 使用情况,该命令每秒输出一行统计信息,运行效果如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ redis-cli --stat
------- data ------ --------------------- load -------------------- - child -
keys       mem      clients blocked requests            connections
1          1.00M    1       0       8 (+0)              4
1          1.00M    1       0       9 (+1)              4
1          1.00M    1       0       10 (+1)             4
1          1.00M    1       0       11 (+1)             4
1          1.00M    1       0       12 (+1)             4
1          1.00M    1       0       13 (+1)             4
1          1.00M    1       0       14 (+1)             4
3789845    3.20G    507     0       8867971943(+62423)  555894

以上输出是一个接近饱和的 Redis 实例的统计信息,它每秒平均处理 6万+ 的请求。
对于这种情况,垂直层面的命令优化很难达到效果,这时就需要做集群化水平扩展来分摊 OPS 压力。

如果只有几百或几千 OPS 的 Redis 实例就接近 CPU 饱和是很不正常的,有可能使用了高算法复杂度的命令。
还有一种情况是过度的内存优化,这种情况有些隐蔽,需要我们根据 info commandstats 统计信息分析出命令不合理开销时间,例如下面的耗时统计:

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

查看这个统计可以发现一个问题,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 用来平衡空间和效率的优化手段。不可过度使用

持久化阻塞