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

PHP 常驻任务不会释放内存的吗?

  •  1
     
  •   raysonlu · 2019-12-19 15:07:47 +08:00 · 10658 次点击
    这是一个创建于 1812 天前的主题,其中的信息可能已经有所发展或是发生改变。

    写了个处理 redis 阻塞队列的脚本任务常驻在系统里面,随着处理的任务数越多,进程占用内存慢慢增大。脚本大致内容如下:

    <?php 
    
    $redis = new Redis();
    $redis->setOption(\Redis::OPT_READ_TIMEOUT, -1)
    
    //连接 redis 省略
    
    while(1){
    	$data = $redis->brpop('queueName',60);
        
        if(!empty($data)){
        	$obj = new someObj();
            $obj->handleSomething($data);
            unset($obj);
        }else{
        	sleep(10);
        }
    }
    

    具体查看进程占用内存,我是参考这些参数: ps auxf命令的 RSS 列,以及 top命令的 RES 列

    为什么会这样,按我目前理解,每处理一个任务应该都释放部分内存啊,至少看不到会导致占用内存累加的部分,非常疑惑。请教一下各位 dalao 们。是我用的框架有问题?还是 php 的 redis 扩展问题?

    爆了设定的占用内存限制会自动结束进程,设置无限内存也有点实际,这问题有点头疼

    第 1 条附言  ·  2019-12-24 19:01:59 +08:00
    这几天抽空继续研究这个问题,参考各位 v 友提出的建议
    我目前能确切定位到是框架的 ORM 不释放内存(我使用的 phalcon 框架),看了框架相关的 issue 和搜了相关问题,暂未找到解决办法,不知道 Laravel、YII、TP 等其他家的 ORM 是否也出现这情况?有一点很肯定的是,这不是 PHP 的 PDO 的锅.....
    希望能最后找到解决办法~~( To be Continue
    第 2 条附言  ·  2019-12-25 23:30:26 +08:00

    终于可以结案了!最终原因是框架的问题。(当然前提是php的 zend.enable_gc 开启了或手动开启了gc gc_enable()

    最后还是用了分片注释代码,从框架入口的所有注册组件,到service层分析,找到了问题。

    一开始发现ORM不释放内存,只要用了ORM的查询,内存就一路上飙,但后面再三排查,发现是在测试环境下,使用了框架的一个Model Events引起的,这个是用来分析每一句SQL语句,把这个组件注释后,测试的DEMO不再长占内存了,但实际的业务代码问题依然。

    继续分片注释代码测试。。。

    最后定位到一个使用ORM的地方ActivityModel::findFirst($id),没错,这是这个框架(Phalcon)的一种写法,当然还有其他写法,比如 ActivityModel::finFirstById($id) 或者 ActivityModel::finFirstById(array(.....)) (数组里面是一些既定的参数),但只有第一种写法,会导致内存不断上飙,change it then OJBK.

    至于大家提供的建议中, new someObj() 应该放在循环外的问题,并不存在,unset后,该释放的还是被释放,同样函数结束后该释放的也会被释放。

    61 条回复    2019-12-26 10:36:07 +08:00
    junan0708
        1
    junan0708  
       2019-12-19 15:48:38 +08:00
    运行一段时间自动退出
    eason1874
        2
    eason1874  
       2019-12-19 15:53:38 +08:00
    PHP-FPM 就是爱占着内存不释放,你可以去修改配置来限制占用。
    php01
        3
    php01  
       2019-12-19 15:57:06 +08:00
    路子走歪了,在歪路上玩出花来也是错的
    ch3nz
        4
    ch3nz  
       2019-12-19 16:16:13 +08:00
    在 unset 之后 加个 gc_collect_cycles(); 强制内存回收试试看。
    如果没效果,我怀疑是 redis 扩展的问题。
    ksoeasyxiaosi
        5
    ksoeasyxiaosi  
       2019-12-19 16:46:46 +08:00
    @eason1874 #2 写的是脚本 跟 fpm 没关系、
    mclxly
        6
    mclxly  
       2019-12-19 18:14:24 +08:00
    我之前用 php 写过爬虫,结果脚本经常把内存吃光了,感觉 php 内存管理有问题,然后换别的语言了。
    zpfhbyx
        7
    zpfhbyx  
       2019-12-19 18:23:22 +08:00
    之前脚本也是内存爆了, 强制 gc 大数组 先赋值 null,在 unset 也会有增长, 后来改成处理完某批次任务,判断运行时间超过定长 就主动退出,然后靠守护进程拉起..
    xjmroot
        8
    xjmroot  
       2019-12-19 18:23:34 +08:00   ❤️ 1
    简单点: 用`memory_limit`参数限制最大内存,monitor 挂了自动拉起
    复杂点: 用`memory_get_usage`调试,看内存是啥时候起来的,排查原因
    areless
        9
    areless  
       2019-12-19 18:26:06 +08:00 via Android
    php 不会漏,是你句柄没有释放而已。常驻要用常驻的思路,都得 unset 赋空,redis->close()关闭句柄
    eason1874
        10
    eason1874  
       2019-12-19 18:42:48 +08:00
    @ksoeasyxiaosi #5 楼主说看进程占用内存啊,应该是 php-fpm 进程吧?我猜是说它,因为这玩意儿占用内存回收了也不会立即释放的,长时间占着,哪怕用不着。
    haiyang416
        11
    haiyang416  
       2019-12-19 18:45:26 +08:00
    @eason1874 说的是 CLI 吧。
    littleylv
        12
    littleylv  
       2019-12-19 18:48:28 +08:00
    @eason1874 #10 只是命令行 类似 nohup php a.php 这样的,不是 php-fpm。。。
    eason1874
        13
    eason1874  
       2019-12-19 18:49:32 +08:00
    @haiyang416 #11 这样,那是我想偏了。
    eason1874
        14
    eason1874  
       2019-12-19 18:50:27 +08:00
    @littleylv #12 对,是我想偏了,我没怎么用命令行,刚才没想到这方面。
    haiyang416
        15
    haiyang416  
       2019-12-19 18:52:40 +08:00   ❤️ 1
    PHP 进程有自己的内存管理,变量占用的内存在申请后不会直接释放返回系统,而是还给自己的内存池。并且 PHP 是基于计数器进行垃圾回收,你 unset 的数组或者对象如果存在循环引用,会一直保存在垃圾回收池中,默认大概是 10000 个 slot,只有所有 slots 都满了才会进行垃圾回收。
    raysonlu
        16
    raysonlu  
    OP
       2019-12-19 22:06:32 +08:00
    @haiyang416 有无办法强制召唤回收器? 如楼上说的“gc_collect_cycles();” ?
    cabing
        17
    cabing  
       2019-12-19 22:14:57 +08:00
    <?php

    set_ini("mem_limitxxxxx", 1024M);


    $redis = new Redis();
    $redis->setOption(\Redis::OPT_READ_TIMEOUT, 5)

    //连接 redis 省略

    $obj = new someObj();

    while(1){

    //重新连接下
    if(!$redis->ping()){
    重新连接下
    }
    $data = $redis->brpop('queueName',60);

    if(!empty($data)){
    $obj->handleSomething($data);
    }else{
    $redis->close();
    sleep(10);
    }
    }


    检查下正确性,使用 supervisor 监控下进程。
    haiyang416
        18
    haiyang416  
       2019-12-19 22:22:43 +08:00 via Android
    @raysonlu 这个函数就是手动进行垃圾回收的,不推荐每个队列任务都调用一次,你可以给个概率调用,不过前面也说了,回收了不一定还给系统。更通常的做法是设定条件重启进程,比如处理 1000 个队列任务或者请求后就自动退出,然后由守护进程重新拉起。
    GGGG430
        19
    GGGG430  
       2019-12-19 22:45:09 +08:00 via iPhone
    问题出在 new someObj ()这里,以及调用这个对象中方法中存在申请内存并且未释放,外部的 unset 是不管用的,建议你复用这个对象
    JaguarJack
        20
    JaguarJack  
       2019-12-19 23:22:10 +08:00 via iPhone
    new 应该在 while 外面
    ferock
        21
    ferock  
       2019-12-19 23:24:33 +08:00 via Android
    new 一次不就完了,干嘛循环 new
    shiny
        22
    shiny  
       2019-12-19 23:26:05 +08:00
    可以试试 xhprof 诊断
    iyaozhen
        23
    iyaozhen  
       2019-12-19 23:35:50 +08:00 via Android
    正常是不会内存泄露的,跑了好几个月的 cli
    楼上说了,你这个代码有问题,new 的放在死循环外面,然后 handleSomething 被你隐藏了很多信息呀
    shellic
        24
    shellic  
       2019-12-20 08:28:38 +08:00 via Android
    哈哈,PHP 表示这锅我不背
    askfilm
        25
    askfilm  
       2019-12-20 08:33:20 +08:00
    PHP 表示这锅我不背 +1
    DavidNineRoc
        26
    DavidNineRoc  
       2019-12-20 08:58:28 +08:00
    具体代码贴出来,即使释放对象,更正确的做法也应该是 $var = null; 这样没有栈变量指向对象,对象会自动释放
    namek
        27
    namek  
       2019-12-20 09:08:15 +08:00
    没有更多的细节 目前没办法帮助到楼主。不过希望楼主解决以后能结下帖子 :)
    flashrick
        28
    flashrick  
       2019-12-20 09:10:05 +08:00
    疯狂 new
    luoyou1014
        29
    luoyou1014  
       2019-12-20 09:13:07 +08:00
    首先, 确认下 enable_gc 有没有开, 如果更改 php.ini 里面的配置的话, 可以在代码前面加上一句 gc_enable();


    php 大量用于 web 应用, 开启 gc 对性能有轻微的影响, 很多版本默认不开 gc, 需要自己开下.
    z1154505909
        30
    z1154505909  
       2019-12-20 09:18:54 +08:00
    这锅 PHP 不背+2
    jimmzhou
        31
    jimmzhou  
       2019-12-20 09:23:04 +08:00 via iPhone
    楼上那些说在 while 外 new 的 有没有想过每次 new 的 是一个 job 是从 redis pop 出来的一个个 job 你怎么在 while 外面 new
    fancy111
        32
    fancy111  
       2019-12-20 09:32:28 +08:00
    你写的代码问题,$redis->brpop 自动阻塞 60 秒,后面又 sleep10。另外$redis->brpop 无返回值你确定 empty 会判断为空吗? brpop 超时后也是会返回一个列表的,这样你就是一直循环生成对象了。
    shadowsll
        33
    shadowsll  
       2019-12-20 09:41:38 +08:00
    这锅 PHP 不背的,注意一下使用规范,既然是常驻进程,考虑变量使用手动释放就好。听楼主使用的是框架,我说一个我遇到的也是内存持续增长的问题吧,看看对你是否有帮助。
    脚本中我使用了框架里面的日志记录,最终查到原因是这个日志记录首先记录在内存中,等着脚本结束后,在统一写入文件的。解决方案是在添加日志前面加一个判断,判断变量大小,大于多少写入文件,清空变量。
    fuxkcsdn
        34
    fuxkcsdn  
       2019-12-20 09:46:56 +08:00
    PHP 表示这锅我不背 +10086

    运行一段时间( idel 多少秒 或 执行了 N 次)建议退出让守护进程拉起

    至于守护进程,除了上面推荐的 supervisor,也可以尝试下我写的 https://github.com/consatan/pdaemon

    最开始我也是用 supervisor,但长时间没任务的时候,因为 idel 多次重启,结果 supervisor 认为是程序问题就不给我拉起了...(16 年左右,当时查了文档无解,所以才开始写自己的 守护进程)


    p.s. brpop 没获取到任务的时候,没必要 sleep(10),应该执行 idel 累加并马上再 brpop,不然在这 10 秒内有任务过来就会卡住了

    还有楼上一些纠结不要在循环里 new 的,po 主贴的是示例代码而已,这边是 new someObj(),不是 new 特定类,我估计 someObj 是从队列里获取到的,这个不在 循环里 new 还真没办法

    p.s. 再 p.s. 我自己写的守护进程最长运行时间近 2 年(之后上家公司就关了 /手动狗头),在新项目里也连续稳定执行 1 年多了
    liuxu
        35
    liuxu  
       2019-12-20 09:50:53 +08:00
    php 版本是多少? redis 模块版本呢?
    zjsxwc
        36
    zjsxwc  
       2019-12-20 09:58:31 +08:00
    php 的 gc 比较弱好像是基于引用计数的,并不能回收对象间存在循环引用导致的内存泄露(没真正确定过求反驳)
    简单粗暴点处理就定时重启吧
    zjsxwc
        37
    zjsxwc  
       2019-12-20 09:59:42 +08:00
    @zjsxwc

    php5.3 之后 gc 变聪明了,我收回上面的话

    https://www.php.net/manual/en/features.gc.collecting-cycles.php
    golden0125
        38
    golden0125  
       2019-12-20 10:00:34 +08:00
    在循环中 new 的对象内存能释放吗?
    sagaxu
        39
    sagaxu  
       2019-12-20 10:01:35 +08:00 via Android
    php 部分扩展存在内存泄露问题
    back0893
        40
    back0893  
       2019-12-20 10:01:58 +08:00
    没有遇到过表示,,
    平时写的 cli 命令一般是 cron 执行..
    PainfulJoe
        41
    PainfulJoe  
       2019-12-20 11:11:15 +08:00
    全部代码贴出来看看,我试验了半天都没出现内存泄漏
    widdy
        42
    widdy  
       2019-12-20 12:26:22 +08:00
    @jimmzhou 改代码,复用对象。
    realpg
        43
    realpg  
       2019-12-20 13:20:32 +08:00
    @raysonlu #16
    写常驻项目 就自己注意释放不用资源
    jimmzhou
        44
    jimmzhou  
       2019-12-20 14:20:37 +08:00
    @widdy 看 34 楼评论 如果 pop 出来的类都不一样 只能在循环里 new 了
    raysonlu
        45
    raysonlu  
    OP
       2019-12-20 17:41:32 +08:00
    感谢大家的回复,我会收纳大家的意见再尝试找出原因!


    @haiyang416 @fuxkcsdn @liuxu
    设定一定的条件自动重启整个脚本进程,这个方法肯定没毛病,但这么粗暴的做法有影响对 php 的信仰啊


    @GGGG430 @JaguarJack @ferock @flashrick @widdy
    目前在我的实际代码里,someObj()的确可以复用,我会考虑试试,但如果遇到无法复用的情况,那岂不是又要考究这个问题?


    @cabing
    你贴的码的主要目的是把 new 放在 while 外面来解决这个问题?我这边连接 redis 可以选择持续连接


    @iyaozhen
    我的理解是,handleSomething 是对象的函数,函数结束就得释放该函数所占用的内存


    @DavidNineRoc
    关于释放内存,是$var=null 还是 unset($var),我也有探究过,php 的内存管理准则是对象没有被引用,就得乖乖地准备被 gc 清走,我用过 xdebug 调试过两种方法都能使被引用的对象的引用次数变为 0,然后我个人觉得两者应该是一样


    @namek
    我会持续关注此问题,不会烂尾贴 hhhh


    @luoyou1014
    确认线上、本地环境都有开,phpinfo 中 zend.enable_gc 为 on


    @fancy111
    brpop()超时后的确是返回空数组,empty(array())的确是 true,这没毛病

    @shadowsll @realpg
    也有看到我释放变量吧(unset $obj),除了$obj 需要释放外,其他变量不需要释放,感谢你分享的经验,我试了一下,我这个框架的日志是实时写到日志文件,框架 phalcon


    @liuxu
    php 7.2.3, php-redis 4.1.0


    @sagaxu
    有这部分扩展的黑名单不?


    @back0893
    cron 执行的话,处理有些场景不实时,甚至要考虑上一个任务是否执行完毕


    @PainfulJoe
    跟你说个鬼故事,我本地模拟线上测试环境进行轻量测试也是正常,看到占用内存会在处理任务后自己降下来,我也是一脸懵逼才上来探讨探讨

    再次感谢关注此问题的各位以及回复的 v 友!
    realpg
        46
    realpg  
       2019-12-20 18:03:37 +08:00
    @raysonlu #45
    抱歉 你的代码是用 code 了的
    我的移动端之前出了问题 那部分全没看见 所以我并不知道你这里贴了代码……
    realpg
        47
    realpg  
       2019-12-20 18:05:18 +08:00
    new someObj();

    someObj 里面的东西如果有未释放内存,不一定会因为你 unset 了这个 obj 的实例而释放。尤其是比较复杂的类型成员变量以及方法。
    yoshiyuki
        48
    yoshiyuki  
       2019-12-20 18:06:12 +08:00
    应该主动退出,然后重启一下
    Varobjs
        49
    Varobjs  
       2019-12-20 19:53:25 +08:00 via Android
    昨天也遇到这个问题,现在已经解决

    php 垃圾回收 基本还是引用计数的方式,unset 或者 赋值 null 引用计数 = 0,但 php 并不会立即回收当前占用的内存空间,而是 按概率 好像默认百分之一的概率来回收,或者在脚本退出时 释放。

    fpm 模式一般很快就执行完,遇到问题的大多是脚本,执行很久,循环很多次的操作,所以内存会一直增加,直到达到默认 128 m 限制

    解决方法上面有人说了,就是脚本 先 gc-disable,循环内部结束后 unset 大的类或者数后,调用 gc-collect-cycles 会强制回收计数等于 0 的 内存空间

    可能 提高 ini 配置的 垃圾回收概率 也可能解决,不过还没有去验证过
    jenschen
        50
    jenschen  
       2019-12-20 19:58:52 +08:00 via iPhone
    我记得,php 在修改变量和函数返回的时候才会触发垃圾回收。
    Varobjs
        51
    Varobjs  
       2019-12-20 19:58:54 +08:00 via Android
    另外,php 是默认开启 垃圾回收的


    我是在同步 巨多数据,比如 上千次接口请求,然后 插入数据库 几十万条,但每次循环后,都 unset 了所以变量,或者 赋值 null 了
    Varobjs
        52
    Varobjs  
       2019-12-20 20:01:07 +08:00 via Android
    @jenschen 不是 ,是在引用计数 = 0 触发,但不是每次都触发,
    可以搜索 ini 配置 关于 gc 的,有个概率设置。
    Varobjs
        53
    Varobjs  
       2019-12-20 20:19:23 +08:00 via Android
    还有,加入你的代码,真的有循环引用,以上方法也救不了你的
    可以 memory-get-usage 写入到日志中,如果加入手动释放逻辑,内存还是单调递增,那就是有循环引用了
    jenschen
        54
    jenschen  
       2019-12-20 20:28:09 +08:00 via iPhone
    @Varobjs - -,当变量改变的时候,recount 是不是会成零。函数返回时,变量的引用计数会减 1。好像想把垃圾或者疑似垃圾添加到垃圾回收器。垃圾回收满了或者好像还有一定时间?。去执行垃圾回收。垃圾回收是将所有变量遍历,减一。
    Varobjs
        55
    Varobjs  
       2019-12-20 21:02:36 +08:00 via Android
    @jenschen。。
    具体细节我不太了解,php 源码没看过。
    最近脚本因为数据多了,频繁出现内存溢出问题,反复确认代码,在变量用完 unset,仍不解决问题后,怀疑垃圾回收的问题,然后在 stackoverflow 搜什么时候执行析构函数时,发现需要手动释放的,另外在内存够用的情况下,关闭 gc 可以很大提高效率,这个其实很好理解。
    runtu2019
        56
    runtu2019  
       2019-12-20 22:00:18 +08:00
    我用 dnspod 的 api 写了个 ddns 的脚本,里面也用了爬虫框架什么的,cli 运行,用 crontab 定时 10s 执行跑在树莓派里,也没有见内存问题,稳稳的。
    我觉得是你思路问题。我这里的业务也遇到过需要常驻刷新任务的需求,但也不会像你一样把一个请求执行卡着,解决方案是用 python 脚本定时执行请求 php 的 url 执行刷新操作,python 一直 while 循环半年了也没出什么问题。
    back0893
        57
    back0893  
       2019-12-21 01:40:12 +08:00
    要不 fork 子进程?
    子进程执行就退出,父进程只管子进程生成.
    jenschen
        58
    jenschen  
       2019-12-21 13:23:37 +08:00 via iPhone
    @Varobjs 就事论事吧。比方说,脚本一秒执行一次。可能这个脚本执行需要 1-2 分钟。这也会造成内存问题。关闭 gc 是会提高性能。不是说在内存足够下关闭 gc,是在确保不出现循环引用的情况下。
    fancy111
        59
    fancy111  
       2019-12-23 11:11:34 +08:00
    给你个建议吧,用 Swoole Tracker 调试一下看看,到底哪里内存泄露了。完了给帖子做个结论。
    raysonlu
        60
    raysonlu  
    OP
       2019-12-25 23:31:51 +08:00
    已结案,维护了一次 php 的信仰(狗头 具体看追加附言
    weirdo
        61
    weirdo  
       2019-12-26 10:36:07 +08:00
    马克学姿势
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   5709 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 34ms · UTC 02:58 · PVG 10:58 · LAX 18:58 · JFK 21:58
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.