Livid
403.84D
565.5D
V2EX  ›  Redis

关于 V2EX 在 2014 年 6 月初遇到的性能问题

  Livid ·
PRO
· Jun 10, 2014 · 15390 views
This topic created in 4532 days ago, the information mentioned may be changed or developed.
大概是从 5 月中开始,页面的生成时间就越来越长,但是我一直没有找到原因。因为同样的代码,在开发环境里跑是 8ms,到了生产环境就变成 180ms,这太奇怪了。

而且这个性能问题在过去的几个星期里呈现恶化趋势。终于在 2014 年 6 月 10 号这天让网站彻底打不开了。

彻底打不开的那段时间,我看到 CPU 占用率最高的一个进程始终是 redis-server。所以这种情况下,需要的是一个能够 profiling redis 的工具,我在 GitHub 上找到了 Instagram 开源的这个工具:

https://github.com/Instagram/redis-faina

然后,在经历了之前半个月的各种难受和猜测之后,终于找到了原因:

我在代码里滥用了 KEYS 这条指令。

http://redis.io/commands/KEYS

找到问题根源之后,修复就很容易了。

所以几点总结就是:

* KEYS 的性能随着数据库尺寸的增大而越来越慢
* 在一个足够大的数据库上,连续的 KEYS 指令足以让 Redis 彻底堵住

74 replies  •  2014-06-30 15:11:11 +08:00
twoconk
twoconk
Jun 10, 2014
1
学习了:)
yangyang
yangyang
Jun 10, 2014 via Android
2
現在頁面生成很快,這個頁面只要 33ms。Good job!
jedyu
jedyu
Jun 10, 2014
3
修复就好
zuroyu
zuroyu
Jun 10, 2014
4
是的,我们已经禁止使用keys操作了,每次的keys都会带来许多超时...
KennyZJ
KennyZJ
Jun 10, 2014
5
原来因为这个。
我记得很早想用keys做模糊查询的时候就因为看到了作者这条警告只好作罢,一直没做过测试。
感谢分享这次经验。
新的v2.8.9里面的zrangebylex倒是可以值得考虑发挥一下作用。
homever
homever
Jun 10, 2014
6
back
shao
shao
Jun 10, 2014
7
请问,这个配图是为了卖萌嘛?
xuc
xuc
Jun 10, 2014
8
30ms
Livid
Livid
MOD
OP
PRO
Jun 10, 2014❤️2
9
@shao 是
wwqgtxx
wwqgtxx
Jun 10, 2014 via Android
10
现在好快,赞一个
canesten
canesten
Jun 10, 2014
12
大意失荆州
Reids官方自己在指令列表里清楚的写明了Warning

Warning: consider KEYS as a command that should only be used in production environments with extreme care. It may ruin performance when it is executed against large databases. This command is intended for debugging and special operations, such as changing your keyspace layout. Don't use KEYS in your regular application code. If you're looking for a way to find keys in a subset of your keyspace, consider using sets.

http://redis.io/commands/keys
tonghuashuai
tonghuashuai
Jun 10, 2014
13
记得当初开始学 redis 的时候,说完 keys * 就提到了在生产环境慎用 keys,没想到 @Livid 给亲身示范了下……
lovelotuslonely
lovelotuslonely
Jun 10, 2014
14
跟著警長的那隻是甚麼。。。(我好弱)
DearMark
DearMark
Jun 10, 2014 via Android
15
33ms 这性能终于爆棚了
Tink
Tink
Jun 10, 2014
16
29MS
cj1324
cj1324
Jun 10, 2014
17
memcached不提供类似keys命令。 看来也是有益处的。
mxi1
mxi1
Jun 10, 2014
18
这张图竟然有3m大,怪不得一直卡啊卡啊,还以为gif图就这样呢~~~~
mxi1
mxi1
Jun 10, 2014
19
@lovelotuslonely 大兔子🐰
hitigon
hitigon
Jun 10, 2014
20
Livid卖萌都卖得这么正经……(倒地
qloog
qloog
Jun 10, 2014
21
怪不得早上打不开呢,原来确实是故障,以为我的网络问题呢~
cctvsmg
cctvsmg
Jun 10, 2014❤️1
22
这么快,不太适应
感觉刷v2ex的一大乐趣没了
nobita
nobita
Jun 10, 2014
23
good job
lm902
lm902
Jun 10, 2014
24
lazyphp
lazyphp
Jun 10, 2014
25
难怪刚才访问 出504了。
lm902
lm902
Jun 10, 2014
26
好不容易才打开一个页面
yhf
yhf
Jun 10, 2014 via iPad
27
@Livid 能否透露一下KEYS现在大约是多少量级?
kslr
kslr
Jun 10, 2014
28
昨晚上4点左右504了
lovelotuslonely
lovelotuslonely
Jun 10, 2014 via iPhone
29
@mxi1 這是啥兔,耳朵那麼大
soli
soli
Jun 10, 2014
30
请问 @Livid 能不能给点数据?比如,KEY 到多大数据量的时候开始出现明显的性能下降?

还有,具体的解决办法能透露一下么?
Yuansir
Yuansir
Jun 10, 2014
31
也被KEYS坑过
karvinchen
karvinchen
Jun 10, 2014
32
学习了
snowhs
snowhs
Jun 10, 2014
33
我就是想问下配图想表达啥...
slixurd
slixurd
Jun 10, 2014
34
KEY这条指令我记得还在阿里还是腾讯的笔试题上出过
让我们阅读英文的manual,然后说出用法和注意事项以及猜测实现来着
iptux
iptux
Jun 10, 2014
35
@snowhs 配图文件大小约3M
Livid
Livid
MOD
OP
PRO
Jun 10, 2014❤️1
36
@soli 100 万左右。

这次的解决方法是,需要用到 KEYS 的地方,其实是我们目前的在线人数统计,现在这个地方已经加上了缓存,所以对 KEYS 的调用就大大减少了。
Kabie
Kabie
Jun 10, 2014
37
。。。我说怎么之前经常打不开呢……
PotatoBrother
PotatoBrother
Jun 10, 2014 via iPhone
38
现在我这已经40ms 以内了,终于可以顺畅的逛 V2 了
cutehalo
cutehalo
Jun 10, 2014
39
Livid会卖萌 谁也挡不住 XD
decken
decken
Jun 10, 2014
40
soli
soli
Jun 10, 2014
41
@Livid 非常感谢。我也回去找找我代码里的 KEYS 。。。
missdeer
missdeer
Jun 10, 2014
42
75ms
Numbcoder
Numbcoder
Jun 10, 2014
43
keys 相当于 select * ,在产品环境几乎是要禁用的。
还有 Instagram 那个脚本也会对性能有很大影响,在产品环境还是少用!
Los
Los
Jun 10, 2014❤️1
44
@Livid 在线人数统计为什么不用 Sorted Set 呢?Sorted Set 天然适合干这事
r.zadd("v2ex:online", int(time.time()), member.id)
r.zrangebyscore("v2ex:online", int(time.time()-60*10), "+inf")
Livid
Livid
MOD
OP
PRO
Jun 10, 2014
45
@Los 明白了。谢谢。
Los
Los
Jun 10, 2014
46
@Livid 汗,对 Python 不熟悉,第二段代码应该对取 Unix timestamp 的函数搞错了。
yueyoum
yueyoum
Jun 10, 2014 via Android
47
这要是别人来说这个问题,估计早就被你们喷死了吧……
9
9
Jun 10, 2014
48
@yueyoum 应该不会吧, v2ex 气氛还是挺好的,不过 Livid 比较特殊, 自带主角光环, 所以你懂的.
Livid
Livid
MOD
OP
PRO
Jun 10, 2014
49
@yueyoum
@9

我每天各种压力也很大。
Tinet
Tinet
Jun 10, 2014
50
看来在做开发的时候还是要抽空多读读官方的文档啊
westup
westup
Jun 10, 2014
51
速度好快啊现在
est
est
Jun 10, 2014
52
@Livid 可以改用SCAN
jevonszmx
jevonszmx
Jun 10, 2014
53
哈,记得一定要看redis手册中的时间复杂度啊,比如ltrim、hgetall之类的,就是陷阱啊,分分钟卡死redis。
Los
Los
Jun 10, 2014❤️2
54
#44
总在线人数统计使用 ZCOUNT
r.zcount("v2ex:online", int(time.time())-60*10, "+inf")
某用户是否在线使用 ZSCORE 取得最后活动时间进行判断
r.zscore("v2ex:online", member.id)
wenbinwu
wenbinwu
Jun 10, 2014
55
昨天我也碰到一个django的性能问题
ModelFormSet没有指定queryset就会默认使用all()
在production直接100%cpu然后死在那了
ipconfiger
ipconfiger
Jun 10, 2014
56
keys 是 O(n)级别的操作啊,绝对不能直接暴露出去调用的
ngn999
ngn999
Jun 10, 2014
57
不适应这么快的速度。=͟͟͞͞ʕ•̫͡•ʔ
cloudzhou
cloudzhou
Jun 10, 2014
58
@Livid
"这次的解决方法是,需要用到 KEYS 的地方,其实是我们目前的在线人数统计,现在这个地方已经加上了缓存,所以对 KEYS 的调用就大大减少了。"
这么看起来,你只是减少了 KEYS 的调用次数,可能就是加入缓存,每3,5分钟 keys() 一次。

我认为这样的做法还不够优雅,对你现在说的这个需求,以下是我的做法:
引入 Sorted sets,创建一个名字叫: user:online
当用户 user(id: user_id),进行一次页面操作的时候,timestamp_now 就是当前时间戳:
> ZADD user:online timestamp_now user_id
对于每一个用户的页面操作都是做这样的操作

* 最新的用户在线列表(精确列出在线用户,以下统计前 1000 个在线用户,按照时间戳逆序)*:
> ZREVRANGE 0 1000
1) user_id_1
2) user_id_2
...

* 统计 5 分钟内的用户数(其实在线是个虚幻的概念,只能说 x 分钟内活跃认为在线)*
> ZCOUNT myzset (timestamp_5_minutes_ago timestamp_now

* 定期清除 x 分钟内没有活跃的用户,控制 Sorted sets 的长度 *
> ZREMRANGEBYSCORE myzset -inf (timestamp_10_minutes_ago

上面操作复杂度:O(log(N)+M) 这是可以控制的,并且数据非常及时和准确。

其实这是一个很好的面试题目。
广告:我需要前端工程师,设计师: http://v2ex.com/t/115602
holy_sin
holy_sin
Jun 10, 2014
59
这个兔子好大
RoCry
RoCry
Jun 10, 2014 via Android
60
早知道有这么大的图就不点开了... 我的流量...
Mac
Mac
Jun 10, 2014
61
现在的速度快的像飞一样啊
keakon
keakon
Jun 10, 2014
62
更简单的方案是放在一个单独的 db 里。
ShunYea
ShunYea
Jun 10, 2014 via Android
63
40MS,相当不错
spoonwep
spoonwep
Jun 10, 2014
64
原来如此,最近正在倒腾redis,学习了!
sdjl
sdjl
Jun 10, 2014
65
想知道v2ex每天pv有多少~~?
precisi0nux
precisi0nux
Jun 11, 2014
66
14ms, nice!
wikimore
wikimore
Jun 11, 2014
67
redis数据越多越觉得坑
nezhazheng
nezhazheng
Jun 12, 2014
68
scan就是被设计来解决这个问题的,生产环境keys肯定得禁用,redis单线程,keys之后,其他连接全部等待。
geew
geew
Jun 30, 2014
69
@cloudzhou
@Livid
@nezhazheng
@Yuansir
@KennyZJ
@est

keys = conn.keys(key + '*')
if keys:
conn.delete(*keys)

那使用keys的地方该用什么呢? 比如要删除一些缓存然后知道key的开头..如上代码. 该怎么优化呢??
Livid
Livid
MOD
OP
PRO
Jun 30, 2014
70
@geew 如果 keys 这样的指令是每 10 分钟运行一次,那不会有太大问题。但是如果是每几秒就需要运行一次的话,你就需要考虑其他替代方案了。
geew
geew
Jun 30, 2014
71
@Livid 目前主要用来删除某些相同前缀的缓存, 频率不是固定, 跟后台人员编辑新建数据有关, 问题我觉得倒不会有, 因为数据量不大而且更新也不频繁, 因此其实这条语句使用不多. 但看到了总觉得是个隐藏的坑, 因为这是个组件, 不排除以后会用在别的地方.
也看到了scan, 但不知道是不是我的使用方法有问题,一直报错: ResponseError: unknown command 'SCAN'

scan的原型不是这样的么
#con.scan(self, cursor=0, match=None, count=None)

这样用: con.scan(match=keys+'*')? 有问题?
cloudzhou
cloudzhou
Jun 30, 2014
72
@geew
@Livid
按照我的观点,那就是根本不要在线上使用 keys 这个指令,哪怕为了未来考虑,这是定时炸弹。
按照你上面的例子,解决方法其实很简单,两种策略:

1 外部引用,举个例子来说,就是添加值的时候做一次引用:
对 namespace.xx.yy 设值,同时把这个 namespace.xx.yy 放入 sets (以 namespace 划分的 sets),当要批量引用 namespace 开头的值时,从 sets 里面遍历,然后第二次访问。
同理,删除的时候对应删除。
缺点,有时候很难保证一致性,需要做一些补偿方案,内存使用会增加。

2 版本号的概念,对于 redis,我一直还是推荐持久化数据的,并且严格控制数据的动态产生,也就是没有删除数据这个操作,但是如果你是作为 cache 使用并且数据本来就可以丢失的,那么就可以利用版本号。使用 EXPIRE ,也就是 KEY 是有一定生存周期的,并且命名是这样的: namespace.version.xx.yy 其中 version 是一个 hashes 的对应值 {namespace : version},当你要丢弃整个版本号的时候, version = version + 1,之前的 namespace version 版本全部不再使用,在过了一段时间之后(EXPIRE)自然回收。
缺点,只使用易失性数据,cache 使用,内存使用量在丢弃频繁的时候浪费过多。

总之,根据你的需求,有很多种方法,但是尽量不要使用 keys.
nezhazheng
nezhazheng
Jun 30, 2014
73
@geew

@cloudzhou
说的是对的,绝对不要在线上使用keys,scan应该是2.8之后加上的指令,你看下你的版本。
geew
geew
Jun 30, 2014
74
@nezhazheng

In [2]: redis.VERSION
Out[2]: (2, 9, 1)

我的用法有问题么?
About   ·   Help   ·   Advertise   ·   Blog   ·   API   ·   FAQ   ·   Privacy   ·   Solana   ·   2162 Online   Highest 6679   ·     Select Language
创意工作者们的社区
World is powered by solitude
VERSION: 3.9.8.5 · 126ms · UTC 03:10 · PVG 11:10 · LAX 19:10 · JFK 22:10
♥ Do have faith in what you're doing.