太戏剧性了。画重点:
2011 年,Google 推出「 Panda 」 机制动摇了很多老的 SEO 手段,digg 流量被腰斩。推出 DiggV4 作战计划。经过紧张的开发发布,不过访客页面没问题,已登录用户打不开 MyNews 页面。开发不得不用临时手段把登录用户的默认页面改成 TopNews
MyNews 只能通过不断重启进程才能短暂修复。初期以为是 cassandra 的缓存击穿了 memcache,后来加紧用 redis 重写了,还是得几个小时重启一次
(折腾了一个月之后)
终于发现原因了:API 服务器是 tornado 写的名字叫 Bobtail。里面最常用的函数是:
def get_user_by_ids(ids=[])
然后这个 ids 就一直被 append
直到撑爆内存
所以这个 MyNews 功能也渐渐用的人少,因为没法定制化看新闻,后来,大家都不去 diggv4 而去 reddit 了。。
后来,digg 以 50w 美金被别人收了。。
作为这次 digg v4 事件的受害者,觉得太神奇了。。
1
owenliang 2018-07-03 16:19:36 +08:00 via Android
这种 bug 有点意思😁
|
2
hahastudio 2018-07-03 16:23:43 +08:00 1
证明没开 Pylint
|
3
liuxu 2018-07-03 16:25:26 +08:00
python 就没有一个 xhprof 这样的工具监控内存么。。。
|
4
Livid MOD 无论哪个行业,基本上能活下来的公司都是能够搞定性能和安全问题的。
|
6
zynlp 2018-07-03 16:30:10 +08:00 via iPhone 1
这 bug 要找一个月?😂
|
7
pynix 2018-07-03 16:32:38 +08:00
我一个四五年没用过 python 的人都知道不宜用 mutable 做默认参数。
|
8
cszhiyue 2018-07-03 16:35:22 +08:00
同样排查过这个坑
|
9
deadEgg 2018-07-03 16:51:01 +08:00
有点惨,这个 Bug 知道原理的都不会犯,恰巧他写这么一个方法返回的数据不一定是错的(有可能他只取返回的部分)。
|
10
Hstar 2018-07-03 16:51:20 +08:00
这问题只在面试时见过, 我觉得这问题测试的时候很容易暴露出来啊
|
13
liuxey 2018-07-03 17:04:16 +08:00
不了解 Python,能解释下原因吗?这个 ids=[] 为什么会被撑爆
|
14
lxy 2018-07-03 17:09:52 +08:00 4
嗯,以前被这个坑过……
def f(l=[]): l.append(1) print(l) f() f() f() 输出 [1] [1, 1] [1, 1, 1] |
15
zsdroid 2018-07-03 17:11:27 +08:00 9
|
16
doubleflower 2018-07-03 17:17:59 +08:00
哪怕有这个问题一下子找不出来还可以多搞几台机子定时重启程序清内存吧
|
17
monsterxx03 2018-07-03 17:24:03 +08:00 2
https://mg.pov.lt/objgraph/ 调试内存泄漏挺有用的, 前阵子升级碰到 celery 4.2 的一个内存泄漏问题, 光看代码真看不出来
|
19
megachweng 2018-07-03 17:43:15 +08:00 via iPhone
不用可变参数作为函数的默认参数不是 Python 最基础的吗...😂
|
21
tabris17 2018-07-03 17:46:14 +08:00
问题是 wsgi 容器都有『处理 N 个请求后重启 python 进程』的功能,就算有内存泄露也不会致命呀
|
22
est OP @glasslion 原文大概意思是 MyNews 页面卡,初步原因估计 cassandra 太卡。就重写了缓存层。
> this time with the goal of rewriting our MyNews implementation from scratch. The current version wrote into Cassandra, and its load was crushing the clusters, breaking the social functionality, and degrading all other functionality around it. We decided to rewrite to store the data in Redis 然后上线了发现还是得 4 个小时重启一次进程。 |
23
tabris17 2018-07-03 17:48:40 +08:00
|
24
est OP @glasslion 估计有个逻辑是去 cassandra 里取用户名和 id。然后那个默认参数的 ids 就会越来越长,直到把 cassandra 也查挂。而且这个逻辑上是没问题的。传入 ids 长,得到的返回是个 dict,你还是能获取到正确的值。只不过会附加很多没用的 key。
> This took so long to catch because we returned the values as a dictionary, and the dictionary always included the necessary values, it just happened to also include tens of thousands of extraneous values too |
25
ofooo 2018-07-03 17:49:59 +08:00
技术不行,给你啥语言你都可以把系统搞崩溃。
用这事赖 python,有点搞笑~~~ 不过这帖子让我对数组参数的重要性有了清晰的认识,蛮好的~~~ |
26
ManjusakaL 2018-07-03 17:52:28 +08:00
这个太惨
|
27
bomb77 2018-07-03 17:54:34 +08:00
看提到了 reddit,然后不由自主地多看了楼主头像几眼。。。
|
28
Wichna 2018-07-03 17:55:40 +08:00
戏剧到难以置信
|
29
glasslion 2018-07-03 18:05:34 +08:00 9
@tabris17 显然不在一台机器上, 他们 redis 做了集群的
@est 被你起的函数名 get_user_by_ids 误导了 看了帖子主要有两个疑问: 1. Python 的内存泄露是比较容易发现的,digg 为什么用了那么久? 2. get_user_by_ids 这个函数如果 id 列表不断膨胀的话, 返回出来的数据都是错的, 为什么业务调用方一直没发现? 认真读了一遍原文, 大概明白这个 bug 为什么难查了。有问题的那个函数不是叫 get_user_by_ids (@est 你误导我), 而是一个更新用户数据缓存的函数。 这个函数的数据会被写入到缓存里, 所以 Python 内存泄露还没明显时, 就先把缓存压爆了, 这也 digg 前期一直在优化 memcache , Redis 的原因。 因此重启 Python 不起作用。 至于调用方没发现返回的数据异常,是因为缓存是批量写, 但单条读。 读到的数据是正常的。 |
30
jjx 2018-07-03 18:10:00 +08:00
fluent python 中专门有一节 是 不要把可变类型作为参数的默认值
例子用的就是[], 因为 python 在函数对象的__defaults__放默认值, 如果是可变类型的话, 就是这样 >>> def test(a=[]): ... a.append("test") ... >>> test.__defaults__ ([],) >>> test() >>> test.__defaults__ (['test'],) >>> test() >>> test.__defaults__ (['test', 'test'],) 几天后, 内存爆了 |
31
Cbdy 2018-07-03 18:12:17 +08:00 via Android
之前我跟别人吐槽这个特性,还被说是“特性”
|
32
zhuangzhuang1988 2018-07-03 18:20:09 +08:00
所以 python 没啥用的。
|
33
monsterxx03 2018-07-03 18:49:38 +08:00
再给个 python 内存泄漏的例子, A 只要被实例化就永远不会被回收 :)
class A(object): def __init__(self): self.callback = self.cb def cb(self): pass def __del__(self): pass |
34
qsnow6 2018-07-03 19:09:56 +08:00 via iPhone
基础不扎实
|
37
tao1991123 2018-07-03 19:52:41 +08:00
明显是 Python 的锅好么 设计缺陷
JS 就不会的 function f (a = [], b = 1) { a.push(b); return a;} f() // [1] f() // [1] f() // [1] |
38
est OP @tao1991123
@HaoC12 其实 python 这里就等于: var v=[]; function f (a = v, b = 1) { a.push(b); return a;} 解释器一次性扫了默认参数之后不会再次清空。 |
39
lrz0lrz 2018-07-03 20:12:22 +08:00
@tao1991123 #35 我居然看到了一个 JS 的语法优点!
|
40
huijiewei 2018-07-03 20:16:12 +08:00 via iPhone
python 这么奇葩的?
|
41
PythonAnswer 2018-07-03 21:04:30 +08:00 via iPhone
迪哥用户报道。原来经常过去挖土豆
|
42
Ehco1996 2018-07-03 21:12:20 +08:00
@monsterxx03 我最近也遇到了 celery 内存泄露的问题,也还在排查,我怀疑是用了 pyopenssl 导致的,今天晚上实验一下。话说你的问题排查出来了么?
|
43
monsterxx03 2018-07-03 21:25:53 +08:00 via iPhone
@Ehco1996 你用的什么版本? 我的是 4.2,问题查出来了,可以看下我这个 pull request https://github.com/celery/celery/pull/4839,我这个只能算临时 fix, 那段代码本来就不会被执行,官方 master 上还没修,但确认了这个问题,估计会在 4.3 里修,我碰到的这个问题,只会在插入 task 的进程里发生泄漏。4.x 问题都蛮多的,如果你用 redis 做 broker 的话,还会碰到其他问题
|
45
wwqgtxx 2018-07-03 22:06:01 +08:00
@monsterxx03 没看出来你#33 的代码为什么会导致内存泄漏,能否解释一下
|
46
yangqi 2018-07-03 22:07:17 +08:00
有点标题党,最后总结是他们发布新版本太仓促了,没有经过详细全面的测试就发布,而且没有回滚计划,所以出了问题只能硬着头皮上,这才是最大的经验教训。至于 bug, 任何软件都会有的。
|
49
swulling 2018-07-03 22:23:01 +08:00
这种超大 Object,随便就分析出来了啊,Python 的内存分析工具不要太多
|
50
monsterxx03 2018-07-03 22:32:23 +08:00 via iPhone
@wwqgtxx 循环引用是不会造成内存泄漏,前提是没有同时重载__del__, 这会导致 mark and sweep 机制在回收对象的时候,不知道以什么顺序去执行对象的__del__, https://docs.python.org/2/library/gc.html#gc.garbage
这里如果尝试想用 weak.ref 去修的话还会碰到另一个坑,weak.ref 对 instance method 不起作用,解引用永远是 None, 需要 python3.4 里的 WeakMethod |
51
falcon05 2018-07-03 22:59:50 +08:00 via iPhone
老实说 Python 这个特征挺反直觉的.
|
52
joyqi 2018-07-03 23:11:52 +08:00
居然只卖了 50w 刀。。。
|
53
wwqgtxx 2018-07-04 00:51:07 +08:00 via iPhone
@monsterxx03 那么为什么不看看 py3 的文档呢
Changed in version 3.4: Following PEP 442, objects with a __del__() method don ’ t end up in gc.garbage anymore. |
54
ofooo 2018-07-04 06:38:09 +08:00 via iPhone
3/2=1 你们觉得反直觉不?
数组从 0 开始查,你们觉得反直觉不? |
55
cf472436288 2018-07-04 09:12:26 +08:00
广州天河东圃诚聘 python 工程师,主要负责公司后端服务系统的开发工作,12-20K,双休。欢迎加我微信:cf472436288.谢谢!
|
56
Marmot 2018-07-04 09:26:52 +08:00
可变参数做默认值本来大多时候都是错误,很早就明白这个开始避免了
|
57
Ghayn 2018-07-04 09:29:10 +08:00
ruby 没有这样的问题
# test.rb def test(args=[]) args.push(1) p args end test() test() test() -> ruby test.rb [1] [1] [1] |
58
deepreader 2018-07-04 09:29:11 +08:00
我就得题主就是断章取义。读了下原文,作者发现了这个 bug,然后很快就修好了,然后成功地 launch 了。
|
59
est OP @deepreader
> It really was limping though, requiring manual restarts of every process each four hours. It took a month to track this bug down, and by the end only three people were left trying. 断章取义? |
60
deepreader 2018-07-04 09:44:36 +08:00
@est Sorry,我理解错了。整体这篇文章还是相当棒的。
|
61
monsterxx03 2018-07-04 09:48:23 +08:00
@wwqgtxx 对, 忘记说了,3.4 开始不受这个影响了,生产环境还是 2.7
|
62
misaka19000 2018-07-04 10:02:33 +08:00
我一直不知道原来不能用可变参数作为默认变量,不过每次当我想这样尝试的时候 Pycharm 都会有警告,所以我从来没有真正的这样干过~~现在总算知道是什么原因了
|
63
jimi2018 2018-07-04 10:08:28 +08:00
哎,程序员很重要啊。
|
64
susucoolsama 2018-07-04 10:17:39 +08:00
这些坑真的是语言的设计缺陷吧,我觉得,开发者只有避免了。
|
65
hubqin 2018-07-04 10:21:01 +08:00
默认参数应该改成不变对象`None`:
def add_end(L=None): if L is None: L = [] L.append('END') return L |
66
ihipop 2018-07-04 10:35:36 +08:00
用好的 IDE 这种坑都会被 IDE 标出来的。非得用什么 VIM 不装任何语法分析插件什么的,就得保证自己有足够好的基本功底。
|
67
fwee 2018-07-04 11:09:59 +08:00
闭嘴!这是一个 feature !!
|
68
Linxing 2018-07-04 14:43:29 +08:00
@cf472436288 要求啥经验
|
69
noNOno 2018-07-04 14:59:36 +08:00
可真是值钱的 bug
|
70
risent 2018-07-04 15:37:23 +08:00
我去, 这锅当年是算在 cassandra 头上的,同一时期很多公司包括 Twitter 也在准备迁移到 cassandra,出了 digg 这档子事后都赶紧拉倒了。
cassandra 背锅这么多年啊!! |
71
standin000 2018-07-04 16:20:50 +08:00
reddit 也是 python 写的,不过最开始是 lisp 的
|
72
yongzhong 2018-07-04 16:29:01 +08:00
@monsterxx03 看过这个工具,但一直不了解在生产环境怎么使用,hardcode 到代码中然后灰度到集群上进行观察?
|
74
monsterxx03 2018-07-04 16:44:58 +08:00
@yongzhong 最好能先定位有问题的大概代码段, 写个脚本离线 benchmark 下, 完全没头绪就只能 hardcode 到代码里线上测了, 要小心点(比如 10% 的采样执行), 性能影响没测过.
|
76
reus 2018-07-05 09:05:28 +08:00
坑是小坑,但是如果要你在一大堆代码里定位出这个,估计楼上一些冷嘲热讽的人,一个月都不行吧。
这是语言设计上的问题,这个行为是和直觉相悖的。因为每次函数调用,参数都是独立的,大部分人都会有这个直觉,但谁知道默认参数居然是每次调用都共用的呢? js、ruby 的行为都不是这样,估计 python 这样的,仅此一家了。 |
77
ericgui 2018-07-05 09:35:12 +08:00
由于一颗钉子输了一场战争,最后覆灭一个王国,
这个事,似乎有点夸大。 |
78
shyangs 2018-07-05 11:18:38 +08:00
https://www.v2ex.com/t/163431
我在這裡吐槽過, 還被 Pythoneer 說這是特性, 不是坑. |
79
Ehco1996 2018-07-08 10:52:29 +08:00
@monsterxx03 我的问题也排查出来了,就是 pyopenssl 的锅
|