当年 1.6 亿美金估值的公司—— Digg 是如何被一句 Python 函数可变默参 毁掉的

2018-07-03 16:18:14 +08:00
 est

https://lethain.com/digg-v4/

太戏剧性了。画重点:

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 事件的受害者,觉得太神奇了。。

20665 次点击
所在节点    Python
80 条回复
tabris17
2018-07-03 17:46:14 +08:00
问题是 wsgi 容器都有『处理 N 个请求后重启 python 进程』的功能,就算有内存泄露也不会致命呀
est
2018-07-03 17:46:19 +08:00
@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 个小时重启一次进程。
tabris17
2018-07-03 17:48:40 +08:00
@liuxey 代码相当于

ids = []

get_user_by_ids(ids)

ids 相当于是个静态变量,永远不会被回收
est
2018-07-03 17:48:40 +08:00
@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
ofooo
2018-07-03 17:49:59 +08:00
技术不行,给你啥语言你都可以把系统搞崩溃。
用这事赖 python,有点搞笑~~~

不过这帖子让我对数组参数的重要性有了清晰的认识,蛮好的~~~
ManjusakaL
2018-07-03 17:52:28 +08:00
这个太惨
bomb77
2018-07-03 17:54:34 +08:00
看提到了 reddit,然后不由自主地多看了楼主头像几眼。。。
Wichna
2018-07-03 17:55:40 +08:00
戏剧到难以置信
glasslion
2018-07-03 18:05:34 +08:00
@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 不起作用。

至于调用方没发现返回的数据异常,是因为缓存是批量写, 但单条读。 读到的数据是正常的。
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'],)


几天后, 内存爆了
Cbdy
2018-07-03 18:12:17 +08:00
之前我跟别人吐槽这个特性,还被说是“特性”
zhuangzhuang1988
2018-07-03 18:20:09 +08:00
所以 python 没啥用的。
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
qsnow6
2018-07-03 19:09:56 +08:00
基础不扎实
xor
2018-07-03 19:11:59 +08:00
@ofooo 好的设计就是避免这种凭直觉就会犯的错误
HaoC12
2018-07-03 19:40:07 +08:00
@lxy #14 为啥这样啊?
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]
est
2018-07-03 19:55:16 +08:00
@tao1991123
@HaoC12

其实 python 这里就等于:

var v=[];
function f (a = v, b = 1) { a.push(b); return a;}


解释器一次性扫了默认参数之后不会再次清空。
lrz0lrz
2018-07-03 20:12:22 +08:00
@tao1991123 #35 我居然看到了一个 JS 的语法优点!
huijiewei
2018-07-03 20:16:12 +08:00
python 这么奇葩的?

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/467817

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX