为什么我的Redis这么“慢”?
Redis 作为内存数据库,拥有非常高的性能,单个实例的 QPS 能够达到 10W 左右。
图片来自 Pexels
但我们在使用 Redis 时,经常时不时会出现访问延迟很大的情况,如果你不知道 Redis 的内部实现原理,在排查问题时就会一头雾水。
很多时候,Redis 出现访问延迟变大,都与我们的使用不当或运维不合理导致的。
这篇文章我们就来分析一下 Redis 在使用过程中,经常会遇到的延迟问题以及如何定位和分析。
使用复杂度高的命令
如果在使用 Redis 时,发现访问延迟突然增大,如何进行排查?
首先,第一步,建议你去查看一下 Redis 的慢日志。Redis 提供了慢日志命令的统计功能,我们通过以下设置,就可以查看有哪些命令在执行时延迟比较大。
首先设置 Redis 的慢日志阈值,只有超过阈值的命令才会被记录,这里的单位是微秒。
# 命令执行超过5毫秒记录慢日志
CONFIG SET slowlog-log-slower-than 5000
# 只保留最近1000条慢日志
CONFIG SET slowlog-max-len 1000
127.0.0.1:6379> SLOWLOG get 5
1) 1) (integer) 32693 # 慢日志ID
2) (integer) 1593763337 # 执行时间
3) (integer) 5299 # 执行耗时(微妙)
4) 1) "LRANGE" # 具体执行的命令和参数
2) "user_list_2000"
3) "0"
4) "-1"
2) 1) (integer) 32692
2) (integer) 1593763337
3) (integer) 5044
4) 1) "GET"
2) "book_price_1000"
...
通过查看慢日志记录,我们就可以知道在什么时间执行哪些命令比较耗时,如果你的业务经常使用 O(n) 以上复杂度的命令。
例如 sort、sunion、zunionstore,或者在执行 O(n) 命令时操作的数据量比较大,这些情况下 Redis 处理数据时就会很耗时。
如果你的服务请求量并不大,但 Redis 实例的 CPU 使用率很高,很有可能是使用了复杂度高的命令导致的。
解决方案就是,不使用这些复杂度较高的命令,并且一次不要获取太多的数据,每次尽量操作少量的数据,让 Redis 可以及时处理返回。
存储大 Key
如果查询慢日志发现,并不是复杂度较高的命令导致的,例如都是 SET、DELETE 操作出现在慢日志记录中,那么你就要怀疑是否存在 Redis 写入了大 Key 的情况。
Redis 在写入数据时,需要为新的数据分配内存,当从 Redis 中删除数据时,它会释放对应的内存空间。
如果一个 Key 写入的数据非常大,Redis 在分配内存时也会比较耗时。同样的,当删除这个 Key 的数据时,释放内存也会耗时比较久。
你需要检查你的业务代码,是否存在写入大 Key 的情况,需要评估写入数据量的大小,业务层应该避免一个 Key 存入过大的数据量。
那么有没有什么办法可以扫描现在 Redis 中是否存在大 Key 的数据吗?
redis-cli -h $host -p $port --bigkeys -i 0.01
使用上面的命令就可以扫描出整个实例 Key 大小的分布情况,它是以类型维度来展示的。
需要注意的是当我们在线上实例进行大 Key 扫描时,Redis 的 QPS 会突增,为了降低扫描过程中对 Redis 的影响,我们需要控制扫描的频率,使用 -i 参数控制即可,它表示扫描过程中每次扫描的时间间隔,单位是秒。
使用这个命令的原理,其实就是 Redis 在内部执行 Scan 命令,遍历所有 Key。
然后针对不同类型的 Key 执行 strlen、llen、hlen、scard、zcard 来获取字符串的长度以及容器类型(list/dict/set/zset)的元素个数。
而对于容器类型的 Key,只能扫描出元素最多的 Key,但元素最多的 Key 不一定占用内存最多,这一点需要我们注意下。
不过使用这个命令一般我们是可以对整个实例中 Key 的分布情况有比较清晰的了解。
针对大 Key 的问题,Redis 官方在 4.0 版本推出了 lazy-free 的机制,用于异步释放大 Key 的内存,降低对 Redis 性能的影响。
即使这样,我们也不建议使用大 Key,大 Key 在集群的迁移过程中,也会影响到迁移的性能,这个后面在介绍集群相关的文章时,会再详细介绍到。
集中过期
有时你会发现,平时在使用 Redis 时没有延时比较大的情况,但在某个时间点突然出现一波延时,而且报慢的时间点很有规律,例如某个整点,或者间隔多久就会发生一次。
如果出现这种情况,就需要考虑是否存在大量 Key 集中过期的情况。
如果有大量的 Key 在某个固定时间点集中过期,在这个时间点访问 Redis 时,就有可能导致延迟增加。
Redis 的过期策略采用主动过期+懒惰过期两种策略:
主动过期:Redis 内部维护一个定时任务,默认每隔 100 毫秒会从过期字典中随机取出 20 个 Key,删除过期的 Key。
如果过期 Key 的比例超过了 25%,则继续获取 20 个 Key,删除过期的 Key,循环往复,直到过期 Key 的比例下降到 25% 或者这次任务的执行耗时超过了 25 毫秒,才会退出循环。
懒惰过期:只有当访问某个 Key 时,才判断这个 Key 是否已过期,如果已经过期,则从实例中删除。
伪代码可以这么写:
# 在过期时间点之后的5分钟内随机过期掉
redis.expireat(key, expire_time + random(300))
实例内存达到上限
allkeys-lru:不管 Key 是否设置了过期,淘汰最近最少访问的 Key。
volatile-lru:只淘汰最近最少访问并设置过期的 Key。
allkeys-random:不管 Key 是否设置了过期,随机淘汰。
volatile-random:只随机淘汰有设置过期的 Key。
allkeys-ttl:不管 Key 是否设置了过期,淘汰即将过期的 Key。
noeviction:不淘汰任何 Key,满容后再写入直接报错。
allkeys-lfu:不管 Key 是否设置了过期,淘汰访问频率最低的 Key(4.0+支持)。
volatile-lfu:只淘汰访问频率最低的过期 Key(4.0+支持)。
Fork 耗时严重
绑定 CPU
开启 AOF
appendfsync always:每次写入都刷盘,对性能影响最大,占用磁盘 IO 比较高,数据安全性最高。
appendfsync everysec:1 秒刷一次盘,对性能影响相对较小,节点宕机时最多丢失 1 秒的数据。
appendfsync no:按照操作系统的机制刷盘,对性能影响最小,数据安全性低,节点宕机丢失数据取决于操作系统刷盘机制。
使用 Swap
网卡负载过高
Redis 最佳实践:业务层面和运维层面优化
业务层面
运维层面
业务层面
Key 的长度尽量要短,在数据量非常大时,过长的 Key 名会占用更多的内存。
一定避免存储过大的数据(大 Value),过大的数据在分配内存和释放内存时耗时严重,会阻塞主线程。
Redis 4.0 以上建议开启 lazy-free 机制,释放大 Value 时异步操作,不阻塞主线程。
建议设置过期时间,把 Redis 当做缓存使用,尤其在数量很大的时,不设置过期时间会导致内存的无限增长。
不使用复杂度过高的命令,例如 SORT、SINTER、SINTERSTORE、ZUNIONSTORE、ZINTERSTORE,使用这些命令耗时较久,会阻塞主线程。
查询数据时,一次尽量获取较少的数据,在不确定容器元素个数的情况下,避免使用 LRANGE key 0 -1,ZRANGE key 0 -1 这类操作,应该设置具体查询的元素个数,推荐一次查询 100 个以下元素。
写入数据时,一次尽量写入较少的数据,例如 HSET key value1 value2 value3...,控制一次写入元素的数量,推荐在 100 以下,大数据量分多个批次写入。
批量操作数据时,用 MGET/MSET 替换 GET/SET、HMGET/MHSET 替换 HGET/HSET,减少请求来回的网络 IO 次数,降低延迟,对于没有批量操作的命令,推荐使用 Pipeline,一次性发送多个命令到服务端。
禁止使用 KEYS 命令,需要扫描实例时,建议使用 SCAN,线上操作一定要控制扫描的频率,避免对 Redis 产生性能抖动。
避免某个时间点集中过期大量的 Key,集中过期时推荐增加一个随机时间,把过期时间打散,降低集中过期 Key 时 Redis 的压力,避免阻塞主线程。
根据业务场景,选择合适的淘汰策略,通常随机过期要比 LRU 过期淘汰数据更快。
使用连接池访问 Redis,并配置合理的连接池参数,避免短连接,TCP 三次握手和四次挥手的耗时也很高。
只使用 db0,不推荐使用多个 db,使用多个 db 会增加 Redis 的负担,每次访问不同的 db 都需要执行 SELECT 命令,如果业务线不同,建议拆分多个实例,还能提高单个实例的性能。
读的请求量很大时,推荐使用读写分离,前提是可以容忍从节数据更新不及时的问题。
写请求量很大时,推荐使用集群,部署多个实例分摊写压力。
运维层面
不同业务线部署不同的实例,各自独立,避免混用,推荐不同业务线使用不同的机器,根据业务重要程度划分不同的分组来部署,避免某一个业务线出现问题影响其他业务线。
保证机器有足够的 CPU、内存、带宽、磁盘资源,防止负载过高影响 Redis 性能。
以 master-slave 集群方式部署实例,并分布在不同机器上,避免单点,Slave 必须设置为 Readonly。
Master 和 Slave 节点所在机器,各自独立,不要交叉部署实例,通常备份工作会在 Slave 上做,做备份时会消耗机器资源,交叉部署会影响到 Master 的性能。
推荐部署哨兵节点增加可用性,节点数量至少 3 个,并分布在不同机器上,实现故障自动故障转移。
提前做好容量规划,一台机器部署实例的内存上限,最好是机器内存的一半,主从全量同步时会占用最多额外一倍的内存空间,防止网络大面积故障引发所有 master-slave 的全量同步导致机器内存被吃光。
做好机器的 CPU、内存、带宽、磁盘监控,在资源不足时及时报警处理,Redis 使用 Swap 后性能急剧下降,网络带宽负载过高访问延迟明显增大,磁盘 IO 过高时开启 AOF 会拖慢 Redis 的性能。
设置最大连接数上限,防止过多的客户端连接导致服务负载过高。
单个实例的使用内存建议控制在 20G 以下,过大的实例会导致备份时间久、资源消耗多,主从全量同步数据时间阻塞时间更长。
设置合理的 slowlog 阈值,推荐 10 毫秒,并对其进行监控,产生过多的慢日志需要及时报警。
设置合理的复制缓冲区 repl-backlog 大小,适当调大 repl-backlog 可以降低主从全量复制的概率。
设置合理的 Slave 节点 client-output-buffer-limit 大小,对于写入量很大的实例,适当调大可以避免主从复制中断问题。
备份时推荐在 Slave 节点上做,不影响 Master 性能。
不开启 AOF 或开启 AOF 配置为每秒刷盘,避免磁盘 IO 消耗降低 Redis 性能。
当实例设置了内存上限,需要调大内存上限时,先调整 Slave 再调整 Master,否则会导致主从节点数据不一致。
对 Redis 增加监控,监控采集 info 信息时,使用长连接,频繁的短连接也会影响 Redis 性能。
线上扫描整个实例数时,记得设置休眠时间,避免扫描时 QPS 突增对 Redis 产生性能抖动。
做好 Redis 的运行时监控,尤其是 expired_keys、evicted_keys、latest_fork_usec 指标,短时间内这些指标值突增可能会阻塞整个实例,引发性能问题。
总结
作者:Kaito
简介:90 后,坐标北京,6 年+工作经验,就职于一家移动互联网公司,目前从事基础架构和数据库中间件研发。
编辑:陶家龙
出处:http://kaito-kidd.com/
精彩文章推荐: