首页   注册   登录
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐学习书目
Learn Python the Hard Way
Python 学习手册
Python Cookbook
Python 基础教程
Python Sites
PyPI - Python Package Index
http://www.simple-is-better.com/
http://diveintopython.org/toc/index.html
Pocoo
值得关注的项目
PyPy
Celery
Jinja2
Read the Docs
gevent
pyenv
virtualenv
Stackless Python
Beautiful Soup
结巴中文分词
Green Unicorn
Sentry
Shovel
Pyflakes
pytest
Python 编程
pep8 Checker
Styles
PEP 8
Google Python Style Guide
Code Style from The Hitchhiker's Guide
华为云
V2EX  ›  Python

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

  •  4
     
  •   est · 137 天前 · 13899 次点击
    这是一个创建于 137 天前的主题,其中的信息可能已经有所发展或是发生改变。

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

    80 回复  |  直到 2018-07-13 15:45:03 +08:00
        1
    owenliang   137 天前 via Android
    这种 bug 有点意思😁
        2
    hahastudio   137 天前   ♥ 1
    证明没开 Pylint
        3
    liuxu   137 天前
    python 就没有一个 xhprof 这样的工具监控内存么。。。
        4
    Livid   V2EX Moderator   137 天前   ♥ 6
    无论哪个行业,基本上能活下来的公司都是能够搞定性能和安全问题的。
        5
    est   137 天前
    @liuxu 有类似。不过估计 digg 当时的开发水平太挫。。。。这个问题基本是 python 实习生都会必面的问题。。。
        6
    zynlp   137 天前 via iPhone   ♥ 1
    这 bug 要找一个月?😂
        7
    pynix   137 天前
    我一个四五年没用过 python 的人都知道不宜用 mutable 做默认参数。
        8
    cszhiyue   137 天前
    同样排查过这个坑
        9
    deadEgg   137 天前
    有点惨,这个 Bug 知道原理的都不会犯,恰巧他写这么一个方法返回的数据不一定是错的(有可能他只取返回的部分)。
        10
    Hstar   137 天前
    这问题只在面试时见过, 我觉得这问题测试的时候很容易暴露出来啊
        11
    est   137 天前
    @Hstar 测试怎么暴露呃。。测内存泄露?
        12
    xomix   137 天前
    @est 压力测试的时候跟内存消耗。
    我们之前就这样跟出来一个可能出现的 bug。
    当然要歌颂 vs 的诊断工具,确实太有用了。
        13
    liuxey   137 天前
    不了解 Python,能解释下原因吗?这个 ids=[] 为什么会被撑爆
        14
    lxy   137 天前   ♥ 2
    嗯,以前被这个坑过……
    def f(l=[]):
    l.append(1)
    print(l)

    f()
    f()
    f()
    输出
    [1]
    [1, 1]
    [1, 1, 1]
        15
    zsdroid   137 天前   ♥ 9
        16
    doubleflower   137 天前
    哪怕有这个问题一下子找不出来还可以多搞几台机子定时重启程序清内存吧
        17
    monsterxx03   137 天前   ♥ 1
    https://mg.pov.lt/objgraph/ 调试内存泄漏挺有用的, 前阵子升级碰到 celery 4.2 的一个内存泄漏问题, 光看代码真看不出来
        18
    glasslion   137 天前
    @est 内存泄露不可怕, 这种 bug 要 1 个多月才发现 才可怕吧。再说内存泄露是怎样想到缓存穿透上去的
        19
    megachweng   137 天前 via iPhone
    不用可变参数作为函数的默认参数不是 Python 最基础的吗...😂
        20
    tabris17   137 天前
    @glasslion redis 和 python 跑在一个服务器上
        21
    tabris17   137 天前
    问题是 wsgi 容器都有『处理 N 个请求后重启 python 进程』的功能,就算有内存泄露也不会致命呀
        22
    est   137 天前
    @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   137 天前
    @liuxey 代码相当于

    ids = []

    get_user_by_ids(ids)

    ids 相当于是个静态变量,永远不会被回收
        24
    est   137 天前
    @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   137 天前
    技术不行,给你啥语言你都可以把系统搞崩溃。
    用这事赖 python,有点搞笑~~~

    不过这帖子让我对数组参数的重要性有了清晰的认识,蛮好的~~~
        26
    ManjusakaL   137 天前
    这个太惨
        27
    bomb77   137 天前
    看提到了 reddit,然后不由自主地多看了楼主头像几眼。。。
        28
    Wichna   137 天前
    戏剧到难以置信
        29
    glasslion   137 天前   ♥ 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   137 天前
    fluent python 中专门有一节 是 不要把可变类型作为参数的默认值

    例子用的就是[], 因为 python 在函数对象的__defaults__放默认值, 如果是可变类型的话, 就是这样

    >>> def test(a=[]):
    ... a.append("test")
    ...
    >>> test.__defaults__
    ([],)

    >>> test()
    >>> test.__defaults__
    (['test'],)

    >>> test()
    >>> test.__defaults__
    (['test', 'test'],)


    几天后, 内存爆了
        31
    Cbdy   137 天前 via Android
    之前我跟别人吐槽这个特性,还被说是“特性”
        32
    zhuangzhuang1988   137 天前
    所以 python 没啥用的。
        33
    monsterxx03   137 天前
    再给个 python 内存泄漏的例子, A 只要被实例化就永远不会被回收 :)

    class A(object):
    def __init__(self):
    self.callback = self.cb

    def cb(self):
    pass

    def __del__(self):
    pass
        34
    qsnow6   137 天前 via iPhone
    基础不扎实
        35
    xor   137 天前 via iPhone
    @ofooo 好的设计就是避免这种凭直觉就会犯的错误
        36
    HaoC12   137 天前
    @lxy #14 为啥这样啊?
        37
    tao1991123   137 天前
    明显是 Python 的锅好么 设计缺陷

    JS 就不会的
    function f (a = [], b = 1) { a.push(b); return a;}

    f() // [1]
    f() // [1]
    f() // [1]
        38
    est   137 天前
    @tao1991123
    @HaoC12

    其实 python 这里就等于:

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


    解释器一次性扫了默认参数之后不会再次清空。
        39
    lrz0lrz   137 天前
    @tao1991123 #35 我居然看到了一个 JS 的语法优点!
        40
    huijiewei   137 天前 via iPhone
    python 这么奇葩的?
        41
    PythonAnswer   137 天前 via iPhone
    迪哥用户报道。原来经常过去挖土豆
        42
    Ehco1996   137 天前
    @monsterxx03 我最近也遇到了 celery 内存泄露的问题,也还在排查,我怀疑是用了 pyopenssl 导致的,今天晚上实验一下。话说你的问题排查出来了么?
        43
    monsterxx03   137 天前 via iPhone
    @Ehco1996 你用的什么版本? 我的是 4.2,问题查出来了,可以看下我这个 pull request https://github.com/celery/celery/pull/4839,我这个只能算临时 fix, 那段代码本来就不会被执行,官方 master 上还没修,但确认了这个问题,估计会在 4.3 里修,我碰到的这个问题,只会在插入 task 的进程里发生泄漏。4.x 问题都蛮多的,如果你用 redis 做 broker 的话,还会碰到其他问题
        44
    liuxey   137 天前 via Android
    @tabris17 谢谢,你这么一说就非常清楚了
        45
    wwqgtxx   137 天前
    @monsterxx03 没看出来你#33 的代码为什么会导致内存泄漏,能否解释一下
        46
    yangqi   137 天前
    有点标题党,最后总结是他们发布新版本太仓促了,没有经过详细全面的测试就发布,而且没有回滚计划,所以出了问题只能硬着头皮上,这才是最大的经验教训。至于 bug, 任何软件都会有的。
        47
    menc   137 天前
    @wwqgtxx 循环引用,counter 不可能为 0,就不可能被 GC 掉,造成内存泄漏
        48
    wwqgtxx   137 天前
    @menc cpython 的 GC 早就可以解决循环引用的问题了,都什么年代了
        49
    swulling   137 天前
    这种超大 Object,随便就分析出来了啊,Python 的内存分析工具不要太多
        50
    monsterxx03   137 天前 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   137 天前 via iPhone
    老实说 Python 这个特征挺反直觉的.
        52
    joyqi   137 天前
    居然只卖了 50w 刀。。。
        53
    wwqgtxx   137 天前 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   137 天前 via iPhone
    3/2=1 你们觉得反直觉不?
    数组从 0 开始查,你们觉得反直觉不?
        55
    cf472436288   137 天前
    广州天河东圃诚聘 python 工程师,主要负责公司后端服务系统的开发工作,12-20K,双休。欢迎加我微信:cf472436288.谢谢!
        56
    Marmot   137 天前
    可变参数做默认值本来大多时候都是错误,很早就明白这个开始避免了
        57
    Ghayn   137 天前
    ruby 没有这样的问题
    # test.rb
    def test(args=[])
    args.push(1)
    p args
    end

    test()
    test()
    test()

    -> ruby test.rb
    [1]
    [1]
    [1]
        58
    deepreader   137 天前
    我就得题主就是断章取义。读了下原文,作者发现了这个 bug,然后很快就修好了,然后成功地 launch 了。
        59
    est   137 天前
    @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   137 天前
    @est Sorry,我理解错了。整体这篇文章还是相当棒的。
        61
    monsterxx03   137 天前
    @wwqgtxx 对, 忘记说了,3.4 开始不受这个影响了,生产环境还是 2.7
        62
    misaka19000   137 天前
    我一直不知道原来不能用可变参数作为默认变量,不过每次当我想这样尝试的时候 Pycharm 都会有警告,所以我从来没有真正的这样干过~~现在总算知道是什么原因了
        63
    jimi2018   137 天前
    哎,程序员很重要啊。
        64
    susucoolsama   137 天前
    这些坑真的是语言的设计缺陷吧,我觉得,开发者只有避免了。
        65
    hubqin   137 天前
    默认参数应该改成不变对象`None`:
    def add_end(L=None):
    if L is None:
    L = []
    L.append('END')
    return L
        66
    ihipop   137 天前
    用好的 IDE 这种坑都会被 IDE 标出来的。非得用什么 VIM 不装任何语法分析插件什么的,就得保证自己有足够好的基本功底。
        67
    fwee   137 天前
    闭嘴!这是一个 feature !!
        68
    Linxing   137 天前
    @cf472436288 要求啥经验
        69
    noNOno   137 天前
    可真是值钱的 bug
        70
    risent   137 天前
    我去, 这锅当年是算在 cassandra 头上的,同一时期很多公司包括 Twitter 也在准备迁移到 cassandra,出了 digg 这档子事后都赶紧拉倒了。
    cassandra 背锅这么多年啊!!
        71
    standin000   136 天前
    reddit 也是 python 写的,不过最开始是 lisp 的
        72
    yongzhong   136 天前
    @monsterxx03 看过这个工具,但一直不了解在生产环境怎么使用,hardcode 到代码中然后灰度到集群上进行观察?
        73
    kappa   136 天前
    @risent 貌似当时的新闻说是 CTO 因为选型 Cassandra 引咎辞职?
        74
    monsterxx03   136 天前
    @yongzhong 最好能先定位有问题的大概代码段, 写个脚本离线 benchmark 下, 完全没头绪就只能 hardcode 到代码里线上测了, 要小心点(比如 10% 的采样执行), 性能影响没测过.
        75
    est   136 天前
    @kappa reddit 也是 cassandra。。。
        76
    reus   136 天前
    坑是小坑,但是如果要你在一大堆代码里定位出这个,估计楼上一些冷嘲热讽的人,一个月都不行吧。
    这是语言设计上的问题,这个行为是和直觉相悖的。因为每次函数调用,参数都是独立的,大部分人都会有这个直觉,但谁知道默认参数居然是每次调用都共用的呢?
    js、ruby 的行为都不是这样,估计 python 这样的,仅此一家了。
        77
    ericgui   136 天前
    由于一颗钉子输了一场战争,最后覆灭一个王国,

    这个事,似乎有点夸大。
        78
    shyangs   136 天前
    https://www.v2ex.com/t/163431
    我在這裡吐槽過, 還被 Pythoneer 說這是特性, 不是坑.
        79
    Ehco1996   133 天前
    @monsterxx03 我的问题也排查出来了,就是 pyopenssl 的锅
        80
    XuAaron   128 天前
    @tabris17 这点还好,关键是列表可变。
    关于   ·   FAQ   ·   API   ·   我们的愿景   ·   广告投放   ·   感谢   ·   实用小工具   ·   1984 人在线   最高记录 3821   ·  
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.1 · 24ms · UTC 07:48 · PVG 15:48 · LAX 23:48 · JFK 02:48
    ♥ Do have faith in what you're doing.
    沪ICP备16043287号-1