在生产环境中,有时会出现 Redis command timed out 异常,甚至程序端设置超时时间为 10s 都不能避免。本文探讨下产生该异常的原因及解决办法。
一、Redis 使用不当
这里指的是 Redis 的确处理不过来导致的 Redis command timed out。
如果的确请求量太大,Redis 处理不过来的,可以考虑的方案有:
- 升级到 Redis 6.0 以上版本,设置多个 IO 线程,性能会有明显提升;
- 配置多套 Redis。
当然,Redis 虽然是单线程(指的是执行命令的主线程,哪怕是 6.0 版本以上也只是 IO 线程可以多个,主线程还是一个),但是性能并不低,处理不过来的原因,大部分都是因为客户端使用不当。
我们主要讨论客户端使用不当有哪些原因。
1、slowlog
当出现 Redis command timed out 异常时,首先要考虑是否有慢查询。可以通过 slowlog get 20
命令查询,默认的配置是记录超过 10ms 以上的命令(slowlog-log-slower-than 参数),且只记录最近的 128 条(slowlog-max-len 参数)。
如果客户端程序设置的 redis 超时时间为 1s 或者更长,想通过 slowlog 直接找到对应的超时命令是很难的。因为 slowlog 记录的耗时,是 Redis 主线程的实际执行命令的耗时,不包括请求的排队时间、请求的解析、回复结果给客户端等时间。
我们关注 slowlog,不仅是关注慢查询命令本身,还要和频率结合起来看。当短时间内有大量的 slowlog 时,很容易积少成多,导致大量请求排队,从而触发命令超时。
这里分享一个案例。
有个项目新增了一个定时任务,每天固定时间点刷新所有产品的信息到缓存中,大概需要执行 1 分钟刷完。代码中用到了 keys、set、hset 等各种命令,其中仅 keys 命令就达到了每秒钟 2000 次左右。从 slowlog 看,只有极少数的 keys 命令超过 10ms,但是程序出现大量的命令超时报错。这里简单计算下,哪怕 keys 命令的平均耗时只有 1ms,redis 主线程也就每秒能处理 1000 个请求,所以会导致请求持续积压。在这种情况下,把 keys 命令改成 scan 并不能解决问题,虽然减少了单个命令的耗时,但是大幅增加了命令的数量;把 Redis 单节点改成 Redis 集群也不行,keys 命令需要在每个节点执行。简单的处理方法是控制请求的速度,延长定时任务的执行时间;更好的方法是明确需要刷新的 key 有哪些,避免使用 keys/scan 等命令。
为了减少慢查询,在平时的开发过程中,尽量要少用O(n)的命令,这里建议如下:
- 针对 hkeys、hmget 等命令,严格控制元素的个数;
- 针对 keys 等命令,存在潜在威胁的,最好直接禁止。虽然现在总的 key 可能不多,但很可能随着业务功能的开发,导致 key 的总数不停累加,然后突然某一天就发现性能有问题了。
2、bigKey
Redis 提供了扫描 bigKey 的命令。如果是 String 类型,指的是实际占用的内存;如果是 Hash、List、Set 等类型,指的是 subKey 的数量。
针对 String 类型,如果值设置的很大,不管是分配内存还是释放内存,都会比较耗时。经常有人会问:String 类型实际占用内存超过多少才算 bigKey 呢?Hash 类型实际超过多少个元素数量才算 bigKey 呢?
没有统一的答案,基本的判断依据是:是否影响正常使用。比如有个 String 类型的 key1,占用内存 1MB;另有一个 key2,占用内存 100KB。key1 的调用频率大概是每秒钟 1 次,key2 的调用频率大概是每秒 1000 次。很显然,key2 更需要优化,因为会产生大量的网络 IO,一旦带宽跑满,会影响整个 Redis 的可用性。又比如 Hash 类型有一百万个元素,并且需要执行 hgetall 等 O(n) 的命令,则需要考虑设计的是否合理,因为很容易造成慢查询。
针对 bigKey 问题,这里也给出几个建议:
- 首先要考虑的是尽量避免,因为它很容易引起性能问题;
- 对于某些隐式操作,尽量不占用主执行线程。比如设置
lazyfree-lazy-expire yes
,清理过期 key 时延迟删除(把key释放操作放在bio(Background I/O)单独的子线程处理中);设置lazyfree-lazy-server-del yes
,针对隐式的 DEL 操作,延迟删除,比如 RENAME 命令;del 等命令改为 unlink 等。
3、其他原因
其他需要关注的原因主要有:
(1) 内存使用,是否快达到上限,是否禁用了 Swap;
(2) fork 的时间需要多久,一般内存占用越大就越久;
(3) 网络流量/磁盘 IO 等是否异常;
(4) 是否有大量的 key 集中过期,尤其是有定时任务刷新缓存等情况时需要重点关注;
为及时发现异常,这里提供的建议是:
部署 Redis 监控组件,比如通过 redis_exporter + prometheus + grafana 等,实时监控内存使用量、客户端连接数、每秒执行命令数、网络 IO、key 的总数量、集中过期 key 的数量等各种指标。
二、连接失效或者集群拓扑变更
这里指的是 Redis 本身没有异常,或者 Redis 集群有节点挂了,但通过主从切换很快就恢复了,但是客户端一直在报错的情况。
1、连接失效
在某些时候,有可能会出现 Redis 服务端关闭了连接,但客户端连接还存在的情况。此时,如果客户端没有配置连接校验,新的请求还会走该异常连接,从而得不到服务端响应,报 Redis command timed out。针对该种情况,可以考虑的方案有:
- 每次执行请求前,客户端先校验连接的可用性。需要注意的是,该方案会严重影响 Redis 的性能,相当于每次发业务请求前,先发送了 PING 请求,所以实际的请求量会翻倍,导致性能差不多降低了一半;
- 当出现 Redis command timed out 异常时,校验连接的可用性。这里的建议是,要控制好频率,以 Lettuce 为例,默认采用的是 share connection,一旦报错,会出现大量的请求超时。
2、集群拓扑变更
当客户端配置了自适应刷新时,Redis 集群发生主从切换,一般来说客户端会及时知道,从而刷新拓扑结构,之后请求正常。
客户端配置自适应刷新的原理是,如果发生主从切换,客户端发起请求时,服务端会返回 ASK/MOVED 等命令,客户端收到这些命令时,即认为服务端拓扑结构有变化,通过 cluster nodes 命令重新获取最新拓扑即可。注:服务端不会主动告知集群拓扑有变更。
但有些时候,比如某个节点硬件故障时,服务端不会返回任何命令,此时客户端就不知道了。可以参考 Lettuce中RedisCommandTimeoutException异常分析。针对改种情况,可以考虑的方案有:
- 客户端配置定时刷新拓扑,比如每 30s 刷新一次;
- 当出现 Redis command timed out 异常时,主动获取最新的 Redis 集群拓扑。同样的,这里的建议是,要控制好频率,以 Lettuce 为例,默认采用的是 share connection,一旦报错,会出现大量的请求超时。