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

2020-11-19 11:04:56 +08:00
 reyleon
我手上维护着两台 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 程序员)。
5309 次点击
所在节点    程序员
63 条回复
hbolive
2020-11-19 11:14:22 +08:00
没研究过,只能凭感觉说下。
其实说法二和三并不冲突,PHP 确实会自动释放内存。说法二的意思估计是:PHP 并不会立刻释放不需要的资源,而如果频繁的申请内存,导致原先的无用的资源还没来得及释放,所以 PHP 只能去申请新的内存。。
水平有限,以上纯属猜测。。
wei745359223
2020-11-19 11:17:19 +08:00
这种情况也有可能是代码上出了问题。
lbp0200
2020-11-19 11:17:26 +08:00
PHP 就是这样的异步处理模型,一个请求一个进程,1000 个并发就是 1000 个 PHP 进程,请求结束,进程关闭。

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

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

首先内存确实是 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
2020-11-19 13:56:22 +08:00
@liuxu 如果楼主设置的进程数后,发现并发变低,cpu 负载消耗也低,可以适当调大数值。大概就是 1s 内,100ms 是 cpu 运行,900ms 是 io 等待的话,可以把 1 个进程调整到 5-10 个进程。至于为什么不是直接 10 个进程,是因为考虑进程数多了 cpu 上下文切换带来的消耗,有时候 10 个进程还没有 8 个进程并发高。

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

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

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

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

© 2021 V2EX