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

PHP -fpm 服务器内存占用陡坡式上涨,请问如何彻底解决?

  •  1
     
  •   reyleon · 2020-11-19 11:04:56 +08:00 · 5334 次点击
    这是一个创建于 1473 天前的主题,其中的信息可能已经有所发展或是发生改变。
    我手上维护着两台 Web 服务器,后端运行的是 PHP 接口服务,服务器配置为 8 核 16G,PHP 版本为 5.6.40 ,运行在 PHP-FPM 模式下,php-fpm 进程的内存使用情况随着请求数的增加而不断上涨,系统可用内存呈陡坡式下降,一不留神就会有内存告警但我们又处理不够及时而导致业务奔溃的可能。

    这个问题让我黯然伤神,不知该如何是好?

    我查了一下,几乎全网都在告诉你调整 php-fpm 运行模式,如 pm 改为 dynamic 或 static 以及调整 php-fpm 子进程数量,或者就是让你调低 pm.max_requests 的值,让它达到请求数时自动销毁进程以释放内存。

    我们就是这么做的,虽然有点用,但这明显不是治本的办法,这只是一种妥协。

    让我疑惑的是,对于内存的释放,我也查到不同的说法:

    说法一(来自 Swoole 官方微信公众号)

    FPM 自带黑魔法,传统的跑在 FPM 下的 PHP 代码是没有“内存泄漏”一说的。因为 PHP 内核有一个关键函数叫做 php_request_shutdown,此函数会在请求结束后,把请求期间申请的所有内存都释放掉,这从根本上杜绝了内存泄漏。


    说法二(来自 PHP 开发组核心成员博客)

    PHP 之所以会在请求结束后正确的释放掉所有的资源,内存,这是因为当我们在脚本中使用新的内存的时候,PHP 会向 OS 申请一大块内存(ZEND_MM_SEG_SIZE 大小),然后分给你你需要的合适的一块小内存。

    当你不使用这块小内存的时候,PHP 也不会返还给 OS,而是保留下来给后续的处理使用。所以,如果你使用完了资源不及时释放,那么后续的逻辑如果请求内存,PHP 发现之前申请的一大块内存已经分光了,它就只好再次向 OS 发起 malloc 调用,得到一块新的大内存。 并且它还需要对这个大内存做一些标记处理。

    而如果你使用完资源,及时释放的话,那么下次脚本申请内存的时候,你之前归还的内存块就可以被重复利用,那么也许你的整个脚本只需要和 OS 申请一次内存。

    如果你买了一本 PHP 的书,它告诉你: “不用在 PHP 主动释放资源,因为 PHP 会帮你释放”的话,我建议你,烧了它。


    说法三(来自 PHP 官方手册)

    引用计数系统是 Zend 引擎的一部分,可以自动检测到一个资源不再被引用了(和 Java 一样)。这种情况下此资源使用的所有外部资源都会被垃圾回收系统释放。因此,很少需要手工释放内存。


    以上说法,不知道哪个是正确的。但事实是存在的,php-fpm 进程的内存使用就是会上涨。

    有没有大神指点一下,如何深入分析内存上涨的原因?假设要深入 PHP 代码,有没有行之有效的分析工具?

    (注:我不是 php 程序员)。
    63 条回复    2021-02-09 01:45:14 +08:00
    hbolive
        1
    hbolive  
       2020-11-19 11:14:22 +08:00
    没研究过,只能凭感觉说下。
    其实说法二和三并不冲突,PHP 确实会自动释放内存。说法二的意思估计是:PHP 并不会立刻释放不需要的资源,而如果频繁的申请内存,导致原先的无用的资源还没来得及释放,所以 PHP 只能去申请新的内存。。
    水平有限,以上纯属猜测。。
    wei745359223
        2
    wei745359223  
       2020-11-19 11:17:19 +08:00   ❤️ 1
    这种情况也有可能是代码上出了问题。
    lbp0200
        3
    lbp0200  
       2020-11-19 11:17:26 +08:00
    PHP 就是这样的异步处理模型,一个请求一个进程,1000 个并发就是 1000 个 PHP 进程,请求结束,进程关闭。

    所以,NGINX 后面使用多个 PHP 服务器,就是加机器,这样业务峰值的时候,就不会崩溃了。

    1 台 PHP 机器不够,就 2 台,没有一万台服务器解决不了的问题。
    dawniii
        4
    dawniii  
       2020-11-19 11:25:30 +08:00
    第三方扩展是可能存在内存泄露的,之前遇到过 curl 某个版本有问题,升级就好了。
    zpfhbyx
        5
    zpfhbyx  
       2020-11-19 11:25:52 +08:00   ❤️ 3
    @lbp0200 你这别误导人。。。
    young
        6
    young  
       2020-11-19 11:31:16 +08:00
    大概率代码问题, 之前用 xhprof 分析过代码
    https://www.php.net/manual/en/book.xhprof.php
    dawniii
        7
    dawniii  
       2020-11-19 11:47:12 +08:00
    @dawniii 之前 curl 是 cpu 涨,还不是内存。内存出问题,基本没遇到。可以看看数据统计是统计的真实占用的物理内存,还是带 buffer 的?
    sgq1128
        8
    sgq1128  
       2020-11-19 11:49:48 +08:00
    每天凌晨定时重启下呗
    buaacss
        9
    buaacss  
       2020-11-19 11:58:39 +08:00
    php 很多 c 的扩展都有内存泄漏的 bug,可以用 valgrind 试试,如果内核支持的话 epbf 也有相应的工具来看内存泄漏
    nuk
        10
    nuk  
       2020-11-19 11:59:21 +08:00
    一般情况,每个 php 的 process 会加载很多库,可以试试 preload 一些内存占用比较多的库
    这样就可以共享内存使用了,可以少很多内存,不过仅限 7.4 以上
    我之前在 php5 上做过类似的东西,我们自己的服务器大概一个 php 进程能少 100M 左右。
    ben1024
        11
    ben1024  
       2020-11-19 12:12:40 +08:00
    @nuk
    “一个 php 进程能少 100M 左右” , 好奇你们一个进程平均是多少内存
    ben1024
        12
    ben1024  
       2020-11-19 12:13:27 +08:00
    排查下代码中是否有长连接没有释放,例如 mongodb 一类
    sagaxu
        13
    sagaxu  
       2020-11-19 12:26:56 +08:00 via Android   ❤️ 1
    php 扩展良莠不齐,内存泄露和 coredump 是家常便饭,pm.max_requests 调到 100 保平安
    nuk
        14
    nuk  
       2020-11-19 12:31:20 +08:00
    @ben1024 如果啥事都不干的话大概 100M 左右,处理请求的话大概多加 10~20M 左右吧,如果 php5 把该加载的提前加载好,一个 100ms 的请求可以优化到 10ms 左右。
    不过现在我们换 php7 了。。。
    ben1024
        15
    ben1024  
       2020-11-19 12:36:34 +08:00
    @nuk
    没有逻辑处理时应用进程达到 100M 感觉有点大,
    php7 的预加载是不错,使用案例太少
    nuk
        16
    nuk  
       2020-11-19 12:41:38 +08:00
    @ben1024 很久以前的系统了,所有的业务 php 第一句都是 include 所有的模块,不过我们现在已经差不多重构完了
    nuk
        17
    nuk  
       2020-11-19 12:47:03 +08:00
    @ben1024 我们 48 核心 64G 的服务器,1000 并发就炸你敢信。。。
    ben1024
        18
    ben1024  
       2020-11-19 12:50:02 +08:00
    @nuk
    震惊,include 是挺老的项目的,命名空间都没用
    并发数对不起这硬件。。。
    liuxu
        19
    liuxu  
       2020-11-19 13:49:57 +08:00   ❤️ 4
    都是对的

    首先内存确实是 zend 一次申请一块大内存,而不是系统调用,因为系统调用代价很高
    php 脚本代码释放掉的内存也是给了 zend 内核,而不是还给系统
    引用计数释放掉的内存和 php_request_shutdown 释放掉的内存都是还给 zend,zend 不还给系统

    fpm 的运行原理是:
    a. fpm 是多进程的,是同步 io,也就是 php 脚本代码调用 io 请求会阻塞,例如 http 请求,mysql 请求,文件读写
    b. fpm 的每个进程有自己的 zend 内核在运行,每个进程维护自己的内存块
    c. fpm 可以设置最大进程数,避免内存使用过高,例如 20 个,不用设置太大,因为 cpu 上下文切换在高并发时返而会消耗大量 cpu,具体根据业务请求阻塞 io 调整
    d. fpm 可以设置每个进程可以接受的请求数,超过这个请求数就结束进程重新再起一个,避免内存泄露

    根据以上可知,想调整 fpm,需要关注内存和接收并发的能力,文档: https://www.php.net/manual/zh/install.fpm.configuration.php

    下面给出例子:
    1. 楼主为了避免内存一直占用,需要限制 php 进程数。根据楼主的硬件配置,目前不知道楼主的业务,这里给出假设。
    2. 设置 pm 为 dynamic,然后设置 max_children 为 64,这样可以限制 fpm 最大启用 64 个进程。
    3. 设置 start_servers 为 16,为 fpm 启动时为 16 个进程,这样 fpm 可同时处理 16 个请求。
    4. 设置 min_spare_servers 为 16,这样空闲时最小为 16 个进程,max_spare_servers 为 32,空闲时最大为 32 个进程。至于 fpm 空闲时到底会因为什么原因在这个区间伸缩,等我有时间看了相关内核源码再说。。
    5. 设置 max_requests 为 10240,为每个进程处理 10240 个请求进程就结束进程重启起一个新的,避免内存泄露。


    注:可以根据 xhprof ( php7 可以使用 Tideways )做分析
    liuxu
        20
    liuxu  
       2020-11-19 13:56:22 +08:00
    @liuxu 如果楼主设置的进程数后,发现并发变低,cpu 负载消耗也低,可以适当调大数值。大概就是 1s 内,100ms 是 cpu 运行,900ms 是 io 等待的话,可以把 1 个进程调整到 5-10 个进程。至于为什么不是直接 10 个进程,是因为考虑进程数多了 cpu 上下文切换带来的消耗,有时候 10 个进程还没有 8 个进程并发高。
    wangritian
        21
    wangritian  
       2020-11-19 14:01:40 +08:00
    max_requests 我觉得并不是妥协,而是针对大量不可控代码的最有效方案
    如果你想深究,建议分析业务代码,通过压力测试定位到泄露内存的函数,如果是第三方类引起的,仔细读一下他的文档,是否忘记释放对象或某些关键方法
    Evilk
        22
    Evilk  
       2020-11-19 14:04:14 +08:00
    @lbp0200 你的" Reply 3
    lbp0200 2 小时 43 分钟前
    PHP 就是这样的异步处理模型,一个请求一个进程,1000 个并发就是 1000 个 PHP 进程,请求结束,进程关闭"
    看得出来,你对 PHP 的认识,还是比较老旧,建议更新下
    fenglangjuxu
        23
    fenglangjuxu  
       2020-11-19 14:04:46 +08:00
    我觉得升级 php 到 7 可能比较简单点.当然可能还得修改一些代码不兼容的地方.
    debug 工具 可以试下这个 phptrace 360 出的
    sunznx
        24
    sunznx  
       2020-11-19 14:18:05 +08:00
    招个 php 的维护不行吗
    pigfly123
        25
    pigfly123  
       2020-11-19 14:20:22 +08:00
    1. 检查 fpm 配置是否合理,看看处理每个请求占用的内存来设置一个合理的值;
    2. 利用 xhprof 去线上开启内存分析采样,基本能定位到内存占用高的具体位置,然后优化代码。
    lbp0200
        26
    lbp0200  
       2020-11-19 14:27:15 +08:00
    @Evilk 一个免费的解决方案,您还想要多详细?加个进程池,就自行车变特斯拉了?
    reyleon
        27
    reyleon  
    OP
       2020-11-19 14:27:34 +08:00
    @liuxu 还是不能理解 php-fpm 进程内存使用会慢慢上涨的原因。

    最初启动时一个 php-fpm 进程占用大概 20M 内存,随着接受请求数的增加,内存慢慢会往上涨到 120M 左右,这个时候达到了我们设置 pm.max_requests 的值,然后进程销毁,如何周而复始。

    如你所说 “引用计数释放掉的内存和 php_request_shutdown 释放掉的内存都是还给 zend,zend 不还给系统”。

    那我这个内存一直往上涨,说明是 Zend 一直在向 OS 申请内存,这没错吧。
    那这是不是可以说 “引用计数” 和 “php_request_shutdown” 根本就没有释放过内存?因为如果释放了内存给 Zend,那就说明 Zend 手里有空闲内存,那就不用向操作系统申请啦。

    另:我现在正在研究如何使用 xhprof 分析业务代码。
    我也是操碎了心。
    reyleon
        28
    reyleon  
    OP
       2020-11-19 14:30:52 +08:00
    @wangritian 可能如你所说,因为同样的 php 环境,只是不同业务的另外一台服务器就没有出现过内存上涨的情况,内存使用稳如狗。
    reyleon
        29
    reyleon  
    OP
       2020-11-19 14:32:38 +08:00
    @pigfly123 第一步的配置应该是没问题的。我现在在研究使用 xhprof 去分析代码,希望能有结果
    reyleon
        30
    reyleon  
    OP
       2020-11-19 14:34:48 +08:00
    @sunznx 并不是没有 php 人员呀,之前写这套代码的人已经离职了;
    不过不是我说,就算是自己写的代码,也不一定能轻易找出原因吧,这个问题在他在职的时候也不是没有提过
    然而没卵用
    C603H6r18Q1mSP9N
        31
    C603H6r18Q1mSP9N  
       2020-11-19 14:35:54 +08:00
    php-fpm 我们接触下来 无解!!!
    并发高了,会吃内存和数据库链接不释放,导致整套系统垮掉,解决是高并发 上 java
    reyleon
        32
    reyleon  
    OP
       2020-11-19 14:42:51 +08:00
    @Evilk 一个请求对应一个 PHP 进程,请求处理结束,这个进程才能处理下一个请求。1000 个并发如果没有那么多 php 进程处理,那么那些请求就在队列里排队等待处理。

    我也是这么理解的。这有问题吗?需要更新对 PHP 哪方面的认识?
    liuxu
        33
    liuxu  
       2020-11-19 14:48:32 +08:00
    @reyleon 没错,你得看看你的进程是不是结束从新起了新的
    lovecy
        34
    lovecy  
       2020-11-19 14:49:12 +08:00
    @reyleon 一个 php-fpm 进程就是一个 cgi 进程咯,也就是对应一个 zend 。那些第三方库、扩展啥的,都是自己的内存池吧。如果调用一次 php 代码,运行一次扩展,扩展申请一次 zend 的内存而不释放,代码调用次数增加,慢慢 zend 内存就不够用了。这时候销毁 cgi 进程重启,就能释放掉这些内存。

    如果没法改代码,就按 19 楼的说法调优 fpm 咯,反正能用就行,#21 楼说的"max_requests 我觉得并不是妥协,而是针对大量不可控代码的最有效方案",挺有道理。

    另外想分析的话可以列一下 php 安装的库、扩展,还有对应版本,看看有无懂哥知道某个库出现过内存泄漏的
    reyleon
        35
    reyleon  
    OP
       2020-11-19 15:00:42 +08:00
    @lovecy 代码是可以改的,问题是得先找到内存上涨的原因才改的动呀。

    #21 楼说的"max_requests 我觉得并不是妥协,而是针对大量不可控代码的最有效方案",挺有道理。当前我也是这么做的,但是感觉不爽。反正能用就行,这要求也忒低了点吧?

    # php -m
    [PHP Modules]
    bcmath
    Core
    ctype
    curl
    date
    dom
    ereg
    fileinfo
    filter
    gd
    hash
    iconv
    json
    libxml
    mbstring
    mcrypt
    mongo
    mongodb
    mysql
    mysqli
    mysqlnd
    openssl
    pcntl
    pcre
    PDO
    pdo_mysql
    pdo_sqlite
    Phar
    posix
    redis
    Reflection
    session
    SimpleXML
    sockets
    SPL
    sqlite3
    standard
    tokenizer
    xml
    xmlreader
    xmlwriter
    zip
    zlib

    版本就不知道了。
    keepeye
        36
    keepeye  
       2020-11-19 15:05:26 +08:00   ❤️ 1
    可能是代码、第三方模块导致内存泄漏,你也无法控制代码质量,只能通过一些措施减少影响吧
    比如:
    php.ini 设置 max_memory 小一些
    pm.max_requests 设小一些
    夜深人静的时候重启一下 php-fpm
    wangritian
        37
    wangritian  
       2020-11-19 16:04:42 +08:00
    @reyleon 确实是野路子,但完美解决方案成本不可控啊,而且这个野路子也没什么副作用。举个极端例子,假如 bug 扩展的所有版本都有泄露,而你又不得不依赖它,怎么办?哪怕现在解决了 bug,为了防止后面更新再次泄露,也推荐设置 max_requests
    liuyibao
        38
    liuyibao  
       2020-11-19 16:29:02 +08:00
    假如是偏业务的接口,大部分是业务代码写的不好,比如数据慢查询等等。导致进程等待,瞬时进程数过大。所以检查下你的接口响应时间是多少,太慢的话肯定是业务代码的问题。
    yc8332
        39
    yc8332  
       2020-11-19 17:11:23 +08:00
    请求量大了占用内存不是很正常的吗?配置好相应的最大进程数就好了。。如果是随着时间推移内存占用多,那就是你的 php 代码内存泄露,正常是不会。
    Evilk
        40
    Evilk  
       2020-11-19 17:27:18 +08:00
    @reyleon 怎么我之前看到的你的回答,不一样,奇怪
    我针对的是你之前说的"请求结束,进程关闭"
    老的 cgi 模式,请求结束后,进程会关闭
    php-fpm 模式下,请求结束,当前进程并不会退出
    仅此而已
    joyqi
        41
    joyqi  
       2020-11-19 17:40:05 +08:00
    php 的性能没有很多人想象得那么烂,这个帖子里很多人的服务器资源称得上奢侈了。楼主提供的信息有限,你的服务器并发量现在是多少,平均响应时间是多少,是否存在慢查询,服务器的内存占用监控图是否可以提供下。
    JasperYanky
        42
    JasperYanky  
       2020-11-19 17:45:13 +08:00
    加机器!

    业务跑的好:这么赚钱的业务,加点机器怎么了?
    业务跑的不好:业务这么差还让人投入精力改代码改配置?加机器就完事了!
    ladypxy
        43
    ladypxy  
       2020-11-19 17:53:19 +08:00 via iPhone
    不需要的模块不要加载
    换 php 7.3
    php 效率其实很高,一般都是你设置问题
    reyleon
        44
    reyleon  
    OP
       2020-11-19 18:30:28 +08:00
    @Evilk 你可能看错了。“请求结束,进程关闭” 这是 3 楼说的,不是我说的,你搞错对象了。哈哈哈
    reyleon
        45
    reyleon  
    OP
       2020-11-19 18:39:16 +08:00
    @joyqi 并发量其实很低,目前日 PV 90 万不到,我之前自己做过压测,我们这个接口服务器性能其实很差,估计并发撑不过 40,但性能问题目前并不是我急需解决的。

    主要是 php-fpm 进程吃内存,会慢慢往上涨,这才是我想快点解决的。
    如果不设置 pm.max_requests, 它可以吃完机器所有的内存。

    另:V2EX 貌似无法上图?
    zhenhuaYang
        46
    zhenhuaYang  
       2020-11-19 19:18:44 +08:00
    @liuxu 666666 啊
    MeteorCat
        47
    MeteorCat  
       2020-11-19 19:23:24 +08:00 via Android
    ulimit 多少
    seth19960929
        48
    seth19960929  
       2020-11-19 19:34:06 +08:00 via Android
    如果 php-fpm 有内存泄漏,不太可能有这么低级的错误
    我从优先级给你排查
    你的代码有调用系统命令吗?或者守护进城服务,比如 kafka

    是否有定时任务执行常驻内存的 PHP 脚本任务

    你在内存占用高的时候 top 一下看有几个 work 进程
    liuxu
        49
    liuxu  
       2020-11-19 19:47:47 +08:00
    @zhenhuaYang 又被你发现了
    CODEWEA
        50
    CODEWEA  
       2020-11-19 20:18:38 +08:00
    又来黑 php,你的 pv 才 90 万,就算是设置成 static=30 都没问题,要想找到解决办法,还得分析具体业务
    ben1024
        51
    ben1024  
       2020-11-19 20:49:20 +08:00
    @shanghai1998
    不太赞同换语言就能彻底解决并发问题,其他语言优势是有,但是开发者本身水平才是关键
    everyx
        52
    everyx  
       2020-11-19 21:47:48 +08:00
    @ben1024 之前用 https://github.com/DarkGhostHunter/Preloader 这个给项目上了 preloading,你可以试试
    anerevol
        53
    anerevol  
       2020-11-19 22:40:44 +08:00
    可能是下面说的这个问题么
    你这个基本属于能必现的问题 理论上用二分法很快能定位问题的
    https://bugs.php.net/bug.php?id=76436

    [2019-08-14 21:50 UTC] phpbug at ethaniel dot com
    I have this problem in PHP 5.6.40 on Centos 7.6.

    This simple code triggers it. I just read around 1 million rows from the table and my memory usage is just growing higher and higher.

    $cnt = 0;
    $result = mysqli_query($conn,"SELECT `text`,`sms_date`,`to` FROM `sms_data`.`sms_201933`;");
    while ($row = mysqli_fetch_object($result)) {

    if ($cnt%1000) {
    echo memory_get_usage()." *** \n\n";
    }

    $cnt++;

    }
    huangsen365
        54
    huangsen365  
       2020-11-19 23:57:43 +08:00
    用 remi php 7.x
    用 docker 构建镜像
    上多节点上负载均衡
    数据库使用读写分离
    IDAEngine
        55
    IDAEngine  
       2020-11-20 01:35:54 +08:00 via iPhone
    没遇到过,内存上涨不释放很大原因是代码写的有问题
    reyleon
        56
    reyleon  
    OP
       2020-11-20 09:24:44 +08:00
    @anerevol 感谢!不过我看了下,应该不是这个问题,我搜索了一下,代码中并没有涉及 mysqli 的代码,事实上都没有连接 MySQL 服务。有连接的只有 redis
    oneend
        57
    oneend  
       2020-11-20 09:48:18 +08:00
    @reyleon
    试试加上 opcache
    rqrq
        58
    rqrq  
       2020-11-20 10:24:23 +08:00
    就是代码问题,自己一点点的去掉代码查吧。
    之前用 swoole 弄了个 http server,按照文档 “捕获 Server 运行期致命错误” 加了 register_shutdown_function 放到 onRequest 里面,结果就是内存泄漏。
    onion83
        59
    onion83  
       2020-11-20 10:26:09 +08:00
    安利一下 Swoole tracker https://business.swoole.com/tracker/index
    litujin1123
        60
    litujin1123  
       2020-11-20 13:41:09 +08:00
    @reyleon 那不是也应该排查一下 redis 部分?
    Varobjs
        61
    Varobjs  
       2020-11-20 17:53:47 +08:00
    可能 99%是业务代码写的问题

    重构吧
    liuxu
        62
    liuxu  
       2021-02-09 01:37:04 +08:00
    @liuxu #19 刚刚看了 min_spare_servers 和 max_spare_servers 相关源码,php-fpm 有一个定时器每秒检测一下

    如果进程状态为 FPM_REQUEST_ACCEPTING,也就是没有处理请求,处于闲置状态,而且这些闲置进程的数量 idle 超过了 max_spare_servers,就会每秒 kill 掉一个闲置进程

    如果闲置进程的数量 idle 小于 min_spare_servers,就会美妙创建一个进程,直到 min_spare_servers 个,但这有个前提是目前正在运行的进程数量小于 pm_max_children,否则不会创建
    liuxu
        63
    liuxu  
       2021-02-09 01:45:14 +08:00
    @liuxu #19 s/就会美妙创建一个进程 /就会每秒创建一个进程 /g
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2689 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 09:19 · PVG 17:19 · LAX 01:19 · JFK 04:19
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.