记一次redis读取超时的排查过程(SADD惹的祸)
问题背景
在业务使用redis过程中,出现了read timeout 的异常。
问题排查
直接原因
运维查询redis慢查询日志,发现在异常时间节点,有redis慢查询日志,执行sadd 命令花费了1秒钟。但由于redis是单线程应用,执行单条命令的阻塞,会造成其他命令的排队等候,导致read timeout。
深入排查-为什么sadd这么慢呢
为什么sadd这么慢呢?查阅redis文档看到,sadd操作的复杂度是O(1)的,实际使用本机docker搭建redis进行测试,使用脚本进行sadd,直到800W以上的量级才偶尔出现100毫秒以上的情况。(测试过程详见后面)
搭建redis环境
偷懒在本机就行测试,使用docker跑起了redis应用,过程如下:docker pull redis # 使用redis3.x版本docker run -itv ~/redis.conf:/redis.conf -p 32768:6379 --name myredis5 -d redis redis-server /redis.conf
测试脚本#coding=utf-8import timeimport redisimport random
r = redis.Redis(host='x.x.x.x', port=xxxx, decode_responses=True)
k = 'key4'tarr = []
st = time.clock()
st2 = time.clock()
r.sadd(k, 1) # 创建连接也会有耗时for i in range(1, 1600000):
t1 = time.clock() * 1000
rn = random.randint(100000000000, 20000000000000)
r.sadd(k, rn)
t2 = time.clock() * 1000
c = t2 - t1
tarr.append(str(c)) if c > 100: print i, cprint time.clock()
s = "\n".join(tarr)with open('./result.txt', 'w') as f:
f.write(s)
测试结果
到达800W的时候开始偶尔出现sadd需要100ms的现象。
问题分析
查询了很多资料,无意中看到redis del操作复杂度为O(n),这里补充一下超时的更多背景,举例如下:
慢查询日志时间:16号00点00分01秒,命令为sadd prefix_20180215, 且key有过期时间。
看到这里答案已经呼之欲出,是不是sadd触发了redis是过期删除操作,同时由于del命令的复杂度为O(n),时间花在了删除过期数据上。
测试重现for i in range(1, 1000000):
t1 = time.clock() * 1000
rn = random.randint(100000000000, 20000000000000)
r.sadd(k, rn)
t2 = time.clock() * 1000
c = t2 - t1
tarr.append(str(c)) if c > 100: print i, c
x = int(time.time())
x += 10 #延时10每秒过期r.expire(k, 10)while True:
y = time.time()
t1 = time.clock() * 1000
rn = random.randint(1, 1000000000)
r.sadd(k, rn)
t2 = time.clock() * 1000
tarr.append(str(c)) if c > 100:#复现sadd慢查询的情况
print i, c if y > x + 5: # 超时时间就break
breakprint time.clock()
重现的步骤很简单,给某个key sadd上足够的数据(百万级)
给key设置一个相对过期时间。
持续调用sadd命令,记录调用时间。
最后观察redis的慢查询日志。
如猜想一样,慢查询日志中出现了SADD命令,耗时1秒。
解决方案与总结
由于redis 对于集合键的del操作复杂度均为O(n),所以对于集合键,最好设置通过分片,避免单个key的值过大。另外,redis4.0已经通过配置支持延时删除,可以通过lazyfree_lazy_expire/azyfree_lazy_eviction/lazyfree_lazy_server_del 来实现异步删除的操作,避免异步阻塞
延伸阅读
最后,让我们来看看redis3.x和4.x处理删除key的源码吧。
redis 有三种淘汰key的机制,分别为del命令被动淘汰(当请求命令对应的键过期时进行删除)
主动删除(redis主动对键进行淘汰,回收内存)
我们先看看redis3.x版本中上面三种淘汰机制的入口代码。
del命令 - delCommandvoid delCommand(client *c) {
int deleted = 0, j; for (j = 1; j argc; j++) {
expireIfNeeded(c->db,c->argv[j]); if (dbDelete(c->db,c->argv[j])) {
signalModifiedKey(c->db,c->argv[j]);
notifyKeyspaceEvent(NOTIFY_GENERIC, "del",c->argv[j],c->db->id);
server.dirty++;
deleted++;
}
}
addReplyLongLong(c,deleted);
<