一、概述
本文主要对Redis使用过程中一些常见问题经验总结,包括Redis CPU持续升高和内存飙升的常见原因分析及对应排查方法、解决方案参考。
二、CPU过高
1、原因分析
引起Redis的CPU过高的原因通常包括以下几种:
- 同时执行的Redis操作命令太多
- 对一个相当大的 key 进行操作(如del/add/smembers/hmembers等)
- pipeline中的命令数量太多
- 开启了持久化RDB或者AOF
2、排查方法
- 通过监控信息,查看操作命令总量变化
- 若操作命令总体变化不大,再看是否有大key操作
- 若上面情况都没有,可以通过 redis-cli info | grep connected_clients 查看连接数量(根据经验 >2w连接数会对redis性能造成影响)
- 如果负载未到 50%,可以考虑用 redis-cli monitor 命令抓取一定时间的操作,然后分析看看比较频繁的命令和数据量较大的key (注意:monitor会导致redis进程的 cpu 升高10%-20%, 本来已经到80%+的情况下尽量不要使用,且尽量在从节点运行)
3、解决方案
- 批量更新大量的用户信息(如白名单,粉丝信息等>10w的列表),使用 get, set, hset, zadd 等一条一条的执行操作,都会造成cpu升高。
解决方法: 采用mget或者mset批量设置 - 不要直接操作大key (比如直接删除一个 1G 的key,很可能会卡死Redis)
解决方法: 尽量不要把数据维护在一个key里面,请分成多个key或者将hot key操作分散到其他节点,删除时请先rename再分批scan删除 - 大量并发key操作时,需要加上分布式锁,或者先返回一个默认值,避免造成缓存穿透,从而Redis的CPU会暴涨
解决方法: 避免缓存穿透,请加上分布式锁 - 开启了RDB或者AOF
解决方法: 关闭master的RDB或者AOF - 服务请求量太大,缓存设置不合理 (相同的数据存在不同的用户下,Redis命中率太低,查询太多的key等)
解决方法: 短期: 确认对业务影响不大情况下限流 长期: 优化代码,批量刷新缓存,Redis key复用,对热点数据进行预热和定时刷新 - 连接数过高引起或业务增长引起
解决方法: 若集群使用主从或者Sentinel模式,考虑迁移到Cluster并对Redis节点进行缩容,或根据业务分拆到其他Redis
三、内存过高
1、原因分析
引起Redis的内存过高的原因通常包括以下几种:
- 大量的key设置了过期时间
- 维护了大量的key,而这些key每个都占用较大的内存空间
- 缓存雪崩
- PUB/SUB,pipeline命令太多
- Redis本身占内存太多,输入输出,连接等 (>4.0版本可以通过 MEMORY STATS 命令分析)
- Redis默认移除算法, 对于过期后且未被访问的key会一直保留进行惰性删除, 故可能会存在占总量25%比例的已过期key
2、排查方法
- 通过 redis-cli --bigkeys 命令对Redis中的key进行采样,寻找较大的key。用的是scan方式,不用担心会阻塞Redis很长时间不能处理其他的请求,尽量在从节点运行命令
- 如果Redis CPU不高的话,可通过 redis-cli monitor 抓取一定时间的日志来进行分析, 能直观的看到key的操作,尽量在从节点运行命令
- 使用第三方工具如 rdbtools 分析
3、解决方案
- 大量的key过期后重新加载到内存时长较长,造成缓存雪崩
解决方法: 优化业务逻辑,改由一个任务异步刷新缓存 - 由于大key太多,造成输入缓存区过大,引起内存增加
解决方法: 避免bigkey返回大量结果,使用 client-output-buffer-limit 设置合理的缓冲区大小上限,或是缓冲区连续写入时间和写入量上限 - PUB/SUB,pipeline 命令太多
解决方法: 优化业务逻辑,合理设置操作命令,或者扩容Redis节点 - 使用 redis-cli monitor 后未关闭,引起内存持续增高
解决办法: 使用 redis-cli client list | grep -v "omem=0"命令来查询输出缓冲区不为0的客户端连接,关闭monitor - 惰性删除key
解决办法: 调整redis的过期策略, 增大主动删除key的频率(可能引起CPU上升)
四、扫描脚本
# !/usr/bin/env python
# -*- coding: utf-8 -*-
from itertools import islice
import datetime
from redis import StrictRedis
NUM = 200
HOST = ""
PORT = 6379
PASSWORD = "****"
KEY_FILE = "keys.%s_%s_%s" % (
HOST.replace(".", "_"), PORT,
datetime.datetime.now().strftime("%Y-%m-%d %H:%M"))
def scan_key():
rd = StrictRedis(host=HOST, password=PASSWORD)
total = 0
with open(KEY_FILE, "a") as f:
for keys in chunks(rd.scan_iter(match="*", count=NUM), n=NUM):
pipe = rd.pipeline(transaction=False)
total += len(keys)
for key in keys:
pipe.type(key)
rets = pipe.execute()
records = []
for key, _type in zip(keys, rets):
records.append("key=%s type=%s" % (key, _type))
f.write("\n".join(records))
if total % 100000 == 0:
print "now had key=%s." % (total,)
print "total key, len=%s." % (total,)
def chunks(seq, n=100, return_type=tuple):
it = iter(seq)
while True:
chunk = return_type(islice(it, n))
if not chunk:
return
yield chunk
if __name__ == "__main__":
scan_key()