V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
Livid
V2EX  ›  Redis

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

  Livid · 2014-06-10 11:41:41 +08:00 · 13967 次点击
这是一个创建于 3819 天前的主题,其中的信息可能已经有所发展或是发生改变。
大概是从 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 条回复    2014-06-30 15:11:11 +08:00
twoconk
    1
twoconk  
   2014-06-10 11:43:25 +08:00
学习了:)
yangyang
    2
yangyang  
   2014-06-10 11:43:57 +08:00 via Android
現在頁面生成很快,這個頁面只要 33ms。Good job!
jedyu
    3
jedyu  
   2014-06-10 11:44:50 +08:00
修复就好
zuroyu
    4
zuroyu  
   2014-06-10 11:46:04 +08:00
是的,我们已经禁止使用keys操作了,每次的keys都会带来许多超时...
KennyZJ
    5
KennyZJ  
   2014-06-10 11:47:06 +08:00
原来因为这个。
我记得很早想用keys做模糊查询的时候就因为看到了作者这条警告只好作罢,一直没做过测试。
感谢分享这次经验。
新的v2.8.9里面的zrangebylex倒是可以值得考虑发挥一下作用。
homever
    6
homever  
   2014-06-10 11:47:27 +08:00
back
shao
    7
shao  
   2014-06-10 11:47:54 +08:00
请问,这个配图是为了卖萌嘛?
xuc
    8
xuc  
   2014-06-10 11:48:00 +08:00
30ms
Livid
    9
Livid  
MOD
OP
   2014-06-10 11:48:05 +08:00   ❤️ 2
@shao
wwqgtxx
    10
wwqgtxx  
   2014-06-10 11:48:11 +08:00 via Android
现在好快,赞一个
shao
    11
shao  
   2014-06-10 11:49:31 +08:00
canesten
    12
canesten  
   2014-06-10 11:49:51 +08:00
大意失荆州
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
    13
tonghuashuai  
   2014-06-10 11:50:45 +08:00
记得当初开始学 redis 的时候,说完 keys * 就提到了在生产环境慎用 keys,没想到 @Livid 给亲身示范了下……
lovelotuslonely
    14
lovelotuslonely  
   2014-06-10 11:54:08 +08:00
跟著警長的那隻是甚麼。。。(我好弱)
DearMark
    15
DearMark  
   2014-06-10 11:54:27 +08:00 via Android
33ms 这性能终于爆棚了
Tink
    16
Tink  
   2014-06-10 11:55:14 +08:00
29MS
cj1324
    17
cj1324  
   2014-06-10 11:56:52 +08:00
memcached不提供类似keys命令。 看来也是有益处的。
mxi1
    18
mxi1  
   2014-06-10 12:02:39 +08:00
这张图竟然有3m大,怪不得一直卡啊卡啊,还以为gif图就这样呢~~~~
mxi1
    19
mxi1  
   2014-06-10 12:03:16 +08:00
@lovelotuslonely 大兔子🐰
hitigon
    20
hitigon  
   2014-06-10 12:06:02 +08:00
Livid卖萌都卖得这么正经……(倒地
qloog
    21
qloog  
   2014-06-10 12:07:22 +08:00
怪不得早上打不开呢,原来确实是故障,以为我的网络问题呢~
cctvsmg
    22
cctvsmg  
   2014-06-10 12:08:24 +08:00   ❤️ 1
这么快,不太适应
感觉刷v2ex的一大乐趣没了
nobita
    23
nobita  
   2014-06-10 12:15:26 +08:00
good job
lm902
    24
lm902  
   2014-06-10 12:15:39 +08:00
lazyphp
    25
lazyphp  
   2014-06-10 12:15:50 +08:00
难怪刚才访问 出504了。
lm902
    26
lm902  
   2014-06-10 12:16:17 +08:00
好不容易才打开一个页面
yhf
    27
yhf  
   2014-06-10 12:35:00 +08:00 via iPad
@Livid 能否透露一下KEYS现在大约是多少量级?
kslr
    28
kslr  
   2014-06-10 12:35:54 +08:00
昨晚上4点左右504了
lovelotuslonely
    29
lovelotuslonely  
   2014-06-10 12:36:45 +08:00 via iPhone
@mxi1 這是啥兔,耳朵那麼大
soli
    30
soli  
   2014-06-10 12:41:09 +08:00
请问 @Livid 能不能给点数据?比如,KEY 到多大数据量的时候开始出现明显的性能下降?

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

这次的解决方法是,需要用到 KEYS 的地方,其实是我们目前的在线人数统计,现在这个地方已经加上了缓存,所以对 KEYS 的调用就大大减少了。
Kabie
    37
Kabie  
   2014-06-10 13:14:13 +08:00
。。。我说怎么之前经常打不开呢……
PotatoBrother
    38
PotatoBrother  
   2014-06-10 13:15:48 +08:00 via iPhone
现在我这已经40ms 以内了,终于可以顺畅的逛 V2 了
cutehalo
    39
cutehalo  
   2014-06-10 13:18:22 +08:00
Livid会卖萌 谁也挡不住 XD
decken
    40
decken  
   2014-06-10 13:30:36 +08:00
soli
    41
soli  
   2014-06-10 13:35:48 +08:00
@Livid 非常感谢。我也回去找找我代码里的 KEYS 。。。
missdeer
    42
missdeer  
   2014-06-10 13:36:59 +08:00
75ms
Numbcoder
    43
Numbcoder  
   2014-06-10 13:41:55 +08:00
keys 相当于 select * ,在产品环境几乎是要禁用的。
还有 Instagram 那个脚本也会对性能有很大影响,在产品环境还是少用!
Los
    44
Los  
   2014-06-10 14:32:55 +08:00   ❤️ 1
@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
    45
Livid  
MOD
OP
   2014-06-10 14:39:00 +08:00
@Los 明白了。谢谢。
Los
    46
Los  
   2014-06-10 14:44:29 +08:00
@Livid 汗,对 Python 不熟悉,第二段代码应该对取 Unix timestamp 的函数搞错了。
yueyoum
    47
yueyoum  
   2014-06-10 14:51:26 +08:00 via Android
这要是别人来说这个问题,估计早就被你们喷死了吧……
9
    48
9  
   2014-06-10 14:57:23 +08:00
@yueyoum 应该不会吧, v2ex 气氛还是挺好的,不过 Livid 比较特殊, 自带主角光环, 所以你懂的.
Livid
    49
Livid  
MOD
OP
   2014-06-10 14:58:33 +08:00
@yueyoum
@9

我每天各种压力也很大。
Tinet
    50
Tinet  
   2014-06-10 15:05:39 +08:00
看来在做开发的时候还是要抽空多读读官方的文档啊
westup
    51
westup  
   2014-06-10 15:20:55 +08:00
速度好快啊现在
est
    52
est  
   2014-06-10 15:31:14 +08:00
@Livid 可以改用SCAN
jevonszmx
    53
jevonszmx  
   2014-06-10 15:47:51 +08:00
哈,记得一定要看redis手册中的时间复杂度啊,比如ltrim、hgetall之类的,就是陷阱啊,分分钟卡死redis。
Los
    54
Los  
   2014-06-10 16:09:24 +08:00   ❤️ 2
#44
总在线人数统计使用 ZCOUNT
r.zcount("v2ex:online", int(time.time())-60*10, "+inf")
某用户是否在线使用 ZSCORE 取得最后活动时间进行判断
r.zscore("v2ex:online", member.id)
wenbinwu
    55
wenbinwu  
   2014-06-10 16:13:46 +08:00
昨天我也碰到一个django的性能问题
ModelFormSet没有指定queryset就会默认使用all()
在production直接100%cpu然后死在那了
ipconfiger
    56
ipconfiger  
   2014-06-10 16:33:26 +08:00
keys 是 O(n)级别的操作啊,绝对不能直接暴露出去调用的
ngn999
    57
ngn999  
   2014-06-10 16:38:03 +08:00
不适应这么快的速度。=͟͟͞͞ʕ•̫͡•ʔ
cloudzhou
    58
cloudzhou  
   2014-06-10 16:52:52 +08:00
@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
    59
holy_sin  
   2014-06-10 16:55:28 +08:00
这个兔子好大
RoCry
    60
RoCry  
   2014-06-10 17:01:24 +08:00 via Android
早知道有这么大的图就不点开了... 我的流量...
Mac
    61
Mac  
   2014-06-10 17:18:08 +08:00
现在的速度快的像飞一样啊
keakon
    62
keakon  
   2014-06-10 17:20:33 +08:00
更简单的方案是放在一个单独的 db 里。
ShunYea
    63
ShunYea  
   2014-06-10 19:58:35 +08:00 via Android
40MS,相当不错
spoonwep
    64
spoonwep  
   2014-06-10 22:19:53 +08:00
原来如此,最近正在倒腾redis,学习了!
sdjl
    65
sdjl  
   2014-06-10 22:22:38 +08:00
想知道v2ex每天pv有多少~~?
precisi0nux
    66
precisi0nux  
   2014-06-11 09:50:57 +08:00
14ms, nice!
wikimore
    67
wikimore  
   2014-06-11 17:01:44 +08:00
redis数据越多越觉得坑
nezhazheng
    68
nezhazheng  
   2014-06-12 09:17:45 +08:00
scan就是被设计来解决这个问题的,生产环境keys肯定得禁用,redis单线程,keys之后,其他连接全部等待。
geew
    69
geew  
   2014-06-30 10:57:26 +08:00
@cloudzhou
@Livid
@nezhazheng
@Yuansir
@KennyZJ
@est

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

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

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

这样用: con.scan(match=keys+'*')? 有问题?
cloudzhou
    72
cloudzhou  
   2014-06-30 11:21:57 +08:00
@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
    73
nezhazheng  
   2014-06-30 13:46:12 +08:00
@geew

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

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

我的用法有问题么?
关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1589 人在线   最高记录 6679   ·     Select Language
创意工作者们的社区
World is powered by solitude
VERSION: 3.9.8.5 · 28ms · UTC 17:01 · PVG 01:01 · LAX 09:01 · JFK 12:01
Developed with CodeLauncher
♥ Do have faith in what you're doing.