背景
产品发布上线了一个后台功能后,published 服务器内存占用爆表,回滚代码后线上恢复正常。
表现
400个PHP进程,每个进程都占用了32MB,需要12G内存,但 40机器 只有8G内存。
单个PHP进程占用内存超过了配置文件 memory_limit 的32MB
PHP Fatal error: Allowed memory size of 33554432 bytes exhausted (tried to allocate 72 bytes) in /data/wwwroot/pregnancy.mama.cn/ThinkPHP/Common/functions.php on line 614
原因分析
ThinkPHP3.2 框架生成 runtime 文件会调用 token_get_all 方法去清除核心代码和配置文件中的空白和注释,然后写入runtime文件。
这次上线在配置文件里加入了1000个广告点位的请求转换,导致 token_get_all 系统函数返回的数组占用从 20MB 上升到40MB,导致以上报错,无法生成runtime文件。
广告上线前,内存峰值9M,调用12572次
广告上线后,内存峰值29M,调用43008次
配置优化后,内存峰值9M,调用12583次
可以明显看到移除点位后的效果,token_get_all导致is_string这个函数调用次数太多,消耗的内存还是性能的主要问题,不过好在它是初始化的。
解决方案
将1000个广告点位配置存到数据库里,不再存放在配置文件中。
防范措施
1、开发环境、测试环境 PHP 配置要跟线上保持一致,如单进程大小限制成 32MB,将问题提前发现在测试阶段
2、线上 PHP进程数设置要考虑当前机器内存大小,不出现像这次一样占用满了内存的情况
3、首次生成 runtime.php 文件考虑可以从 用户触发生成 变成 系统脚本触发生成
4、升级 published 服务器项目为 thinkphp5 。
其他参考
1、token_get_all 函数介绍
2、报错代码上下文
tp5.0 与 tp3.2 的本地问题重现 和 通过Xdebug对比
tp3.2源码,在application/common/home/conf/config.php写入1000个array。
tp5.0源码,在application/config.phpp写入1000个array。
运行环境:docker-compose + nginx1.12 + php56 已托管在Git
xdebug可视化工具:webGrind
使用方式参考我之前写的文章 xdebug 远程调试代码 和 代码性能分析
可以看到,tp3和tp5同样运行index.php,tp3因为要压缩的生成common~runtime.php文件,导致要读很多数据到内存。
tp3初始化时大量的占用在strip_whitespace函数上,占比91%,函数内的is_string功能调用40902次。
再看看tp5
tp5调用最多的也才35次,占比都很均匀,最多的include引入文件功能占9%,看来tp5已经放弃了tp3的生成common~runtime.php的方式,改用了依赖注入了。