V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐学习书目
Learn Python the Hard Way
Python Sites
PyPI - Python Package Index
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
est
V2EX  ›  Python

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

  •  4
     
  •   est ·
    est · 2018-07-03 16:18:14 +08:00 · 20948 次点击
    这是一个创建于 2360 天前的主题,其中的信息可能已经有所发展或是发生改变。

    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
    owenliang
        1
    owenliang  
       2018-07-03 16:19:36 +08:00 via Android
    这种 bug 有点意思😁
    hahastudio
        2
    hahastudio  
       2018-07-03 16:23:43 +08:00   ❤️ 1
    证明没开 Pylint
    liuxu
        3
    liuxu  
       2018-07-03 16:25:26 +08:00
    python 就没有一个 xhprof 这样的工具监控内存么。。。
    Livid
        4
    Livid  
    MOD
       2018-07-03 16:25:40 +08:00   ❤️ 6
    无论哪个行业,基本上能活下来的公司都是能够搞定性能和安全问题的。
    est
        5
    est  
    OP
       2018-07-03 16:27:03 +08:00
    @liuxu 有类似。不过估计 digg 当时的开发水平太挫。。。。这个问题基本是 python 实习生都会必面的问题。。。
    zynlp
        6
    zynlp  
       2018-07-03 16:30:10 +08:00 via iPhone   ❤️ 1
    这 bug 要找一个月?😂
    pynix
        7
    pynix  
       2018-07-03 16:32:38 +08:00
    我一个四五年没用过 python 的人都知道不宜用 mutable 做默认参数。
    cszhiyue
        8
    cszhiyue  
       2018-07-03 16:35:22 +08:00
    同样排查过这个坑
    deadEgg
        9
    deadEgg  
       2018-07-03 16:51:01 +08:00
    有点惨,这个 Bug 知道原理的都不会犯,恰巧他写这么一个方法返回的数据不一定是错的(有可能他只取返回的部分)。
    Hstar
        10
    Hstar  
       2018-07-03 16:51:20 +08:00
    这问题只在面试时见过, 我觉得这问题测试的时候很容易暴露出来啊
    est
        11
    est  
    OP
       2018-07-03 16:51:46 +08:00
    @Hstar 测试怎么暴露呃。。测内存泄露?
    xomix
        12
    xomix  
       2018-07-03 17:00:52 +08:00
    @est 压力测试的时候跟内存消耗。
    我们之前就这样跟出来一个可能出现的 bug。
    当然要歌颂 vs 的诊断工具,确实太有用了。
    liuxey
        13
    liuxey  
       2018-07-03 17:04:16 +08:00
    不了解 Python,能解释下原因吗?这个 ids=[] 为什么会被撑爆
    lxy
        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]
    zsdroid
        15
    zsdroid  
       2018-07-03 17:11:27 +08:00   ❤️ 9
    doubleflower
        16
    doubleflower  
       2018-07-03 17:17:59 +08:00
    哪怕有这个问题一下子找不出来还可以多搞几台机子定时重启程序清内存吧
    monsterxx03
        17
    monsterxx03  
       2018-07-03 17:24:03 +08:00   ❤️ 2
    https://mg.pov.lt/objgraph/ 调试内存泄漏挺有用的, 前阵子升级碰到 celery 4.2 的一个内存泄漏问题, 光看代码真看不出来
    glasslion
        18
    glasslion  
       2018-07-03 17:39:37 +08:00
    @est 内存泄露不可怕, 这种 bug 要 1 个多月才发现 才可怕吧。再说内存泄露是怎样想到缓存穿透上去的
    megachweng
        19
    megachweng  
       2018-07-03 17:43:15 +08:00 via iPhone
    不用可变参数作为函数的默认参数不是 Python 最基础的吗...😂
    tabris17
        20
    tabris17  
       2018-07-03 17:44:19 +08:00
    @glasslion redis 和 python 跑在一个服务器上
    tabris17
        21
    tabris17  
       2018-07-03 17:46:14 +08:00
    问题是 wsgi 容器都有『处理 N 个请求后重启 python 进程』的功能,就算有内存泄露也不会致命呀
    est
        22
    est  
    OP
       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
        23
    tabris17  
       2018-07-03 17:48:40 +08:00
    @liuxey 代码相当于

    ids = []

    get_user_by_ids(ids)

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

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

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


    几天后, 内存爆了
    Cbdy
        31
    Cbdy  
       2018-07-03 18:12:17 +08:00 via Android
    之前我跟别人吐槽这个特性,还被说是“特性”
    zhuangzhuang1988
        32
    zhuangzhuang1988  
       2018-07-03 18:20:09 +08:00
    所以 python 没啥用的。
    monsterxx03
        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
    qsnow6
        34
    qsnow6  
       2018-07-03 19:09:56 +08:00 via iPhone
    基础不扎实
    xor
        35
    xor  
       2018-07-03 19:11:59 +08:00 via iPhone
    @ofooo 好的设计就是避免这种凭直觉就会犯的错误
    HaoC12
        36
    HaoC12  
       2018-07-03 19:40:07 +08:00
    @lxy #14 为啥这样啊?
    tao1991123
        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]
    est
        38
    est  
    OP
       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
        39
    lrz0lrz  
       2018-07-03 20:12:22 +08:00
    @tao1991123 #35 我居然看到了一个 JS 的语法优点!
    huijiewei
        40
    huijiewei  
       2018-07-03 20:16:12 +08:00 via iPhone
    python 这么奇葩的?
    PythonAnswer
        41
    PythonAnswer  
       2018-07-03 21:04:30 +08:00 via iPhone
    迪哥用户报道。原来经常过去挖土豆
    Ehco1996
        42
    Ehco1996  
       2018-07-03 21:12:20 +08:00
    @monsterxx03 我最近也遇到了 celery 内存泄露的问题,也还在排查,我怀疑是用了 pyopenssl 导致的,今天晚上实验一下。话说你的问题排查出来了么?
    monsterxx03
        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 的话,还会碰到其他问题
    liuxey
        44
    liuxey  
       2018-07-03 21:40:22 +08:00 via Android
    @tabris17 谢谢,你这么一说就非常清楚了
    wwqgtxx
        45
    wwqgtxx  
       2018-07-03 22:06:01 +08:00
    @monsterxx03 没看出来你#33 的代码为什么会导致内存泄漏,能否解释一下
    yangqi
        46
    yangqi  
       2018-07-03 22:07:17 +08:00
    有点标题党,最后总结是他们发布新版本太仓促了,没有经过详细全面的测试就发布,而且没有回滚计划,所以出了问题只能硬着头皮上,这才是最大的经验教训。至于 bug, 任何软件都会有的。
    menc
        47
    menc  
       2018-07-03 22:11:25 +08:00
    @wwqgtxx 循环引用,counter 不可能为 0,就不可能被 GC 掉,造成内存泄漏
    wwqgtxx
        48
    wwqgtxx  
       2018-07-03 22:15:13 +08:00
    @menc cpython 的 GC 早就可以解决循环引用的问题了,都什么年代了
    swulling
        49
    swulling  
       2018-07-03 22:23:01 +08:00
    这种超大 Object,随便就分析出来了啊,Python 的内存分析工具不要太多
    monsterxx03
        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
    falcon05
        51
    falcon05  
       2018-07-03 22:59:50 +08:00 via iPhone
    老实说 Python 这个特征挺反直觉的.
    joyqi
        52
    joyqi  
       2018-07-03 23:11:52 +08:00
    居然只卖了 50w 刀。。。
    wwqgtxx
        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.
    ofooo
        54
    ofooo  
       2018-07-04 06:38:09 +08:00 via iPhone
    3/2=1 你们觉得反直觉不?
    数组从 0 开始查,你们觉得反直觉不?
    cf472436288
        55
    cf472436288  
       2018-07-04 09:12:26 +08:00
    广州天河东圃诚聘 python 工程师,主要负责公司后端服务系统的开发工作,12-20K,双休。欢迎加我微信:cf472436288.谢谢!
    Marmot
        56
    Marmot  
       2018-07-04 09:26:52 +08:00
    可变参数做默认值本来大多时候都是错误,很早就明白这个开始避免了
    Ghayn
        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]
    deepreader
        58
    deepreader  
       2018-07-04 09:29:11 +08:00
    我就得题主就是断章取义。读了下原文,作者发现了这个 bug,然后很快就修好了,然后成功地 launch 了。
    est
        59
    est  
    OP
       2018-07-04 09:37:00 +08:00
    @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.

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

    这个事,似乎有点夸大。
    shyangs
        78
    shyangs  
       2018-07-05 11:18:38 +08:00
    https://www.v2ex.com/t/163431
    我在這裡吐槽過, 還被 Pythoneer 說這是特性, 不是坑.
    Ehco1996
        79
    Ehco1996  
       2018-07-08 10:52:29 +08:00
    @monsterxx03 我的问题也排查出来了,就是 pyopenssl 的锅
    XuAaron
        80
    XuAaron  
       2018-07-13 15:45:03 +08:00
    @tabris17 这点还好,关键是列表可变。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   5921 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 43ms · UTC 02:08 · PVG 10:08 · LAX 18:08 · JFK 21:08
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.