PHP设置open_basedir导致的性能问题

某天收到报警,发现某台PHP Web机器的CPU比较高,压力比较大,登录到机器看了一下,发现,user的CPU还行,但是system的CPU比较高,导致了整个机器的负载比较高,于是就怀疑是不是系统某些地方存在性能瓶颈。

于是先用perf+FlameGraph生成了一下火焰图看一下:
火焰图

发现有很多 __lxstat64 调用占用了很多的CPU时间,这个调用是 stat 函数在64位Linux下的实现,正常情况下,PHP不应该会有这么多类似的调用,这是为什么呢?

>

于是就尝试用strace看一下httpd进程在做些什么:

...
getcwd("/data1/www/htdocs/41/ichenfu/1", 4096) = 35
lstat("/data1/www/htdocs/41/ichenfu/1/./APP/Lib/Model/Api/RoutemapModel.class.php", {st_mode=S_IFREG|0600, st_size=483, ...}) = 0
lstat("/data1/www/htdocs/41/ichenfu/1/./APP/Lib/Model/Api", {st_mode=S_IFDIR|0700, st_size=4096, ...}) = 0
lstat("/data1/www/htdocs/41/ichenfu/1/./APP/Lib/Model", {st_mode=S_IFDIR|0700, st_size=4096, ...}) = 0
lstat("/data1/www/htdocs/41/ichenfu/1/./APP/Lib", {st_mode=S_IFDIR|0700, st_size=4096, ...}) = 0
lstat("/data1/www/htdocs/41/ichenfu/1/./APP", {st_mode=S_IFDIR|0700, st_size=4096, ...}) = 0
lstat("/data1/www/htdocs/41/ichenfu/1", {st_mode=S_IFDIR|0700, st_size=4096, ...}) = 0
lstat("/data1/www/htdocs/41/ichenfu", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
lstat("/data1/www/htdocs/41/ichenfu/1/APP/Lib/Model/Api/RoutemapModel.class.php", {st_mode=S_IFREG|0600, st_size=483, ...}) = 0
lstat("/data1/www/htdocs/41/ichenfu/1/APP/Lib/Model/Api", {st_mode=S_IFDIR|0700, st_size=4096, ...}) = 0
lstat("/data1/www/htdocs/41/ichenfu/1/APP/Lib/Model", {st_mode=S_IFDIR|0700, st_size=4096, ...}) = 0
lstat("/data1/www/htdocs/41/ichenfu/1/APP/Lib", {st_mode=S_IFDIR|0700, st_size=4096, ...}) = 0
lstat("/data1/www/htdocs/41/ichenfu/1/APP", {st_mode=S_IFDIR|0700, st_size=4096, ...}) = 0
lstat("/data1/www/htdocs/41/ichenfu/1", {st_mode=S_IFDIR|0700, st_size=4096, ...}) = 0
lstat("/data1/www/htdocs/41/ichenfu", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
lstat("/dev/urandom", {st_mode=S_IFCHR|0666, st_rdev=makedev(1, 9), ...}) = 0
lstat("/dev", {st_mode=S_IFDIR|0755, st_size=4520, ...}) = 0
getcwd("/data1/www/htdocs/41/ichenfu/1", 4096) = 35
lstat("/data1/www/htdocs/41/ichenfu/1/./APP/Lib/Model/Api/RoutemapModel.class.php", {st_mode=S_IFREG|0600, st_size=483, ...}) = 0
lstat("/data1/www/htdocs/41/ichenfu/1/./APP/Lib/Model/Api", {st_mode=S_IFDIR|0700, st_size=4096, ...}) = 0
...

结果发现,确实有很多的lstat系统调用,看这个调用的样子,猜测可能是PHP的open_basedir相关,为什么呢,因为这个目录刚好在open_basedir里,而且看调用,应该是在代码中require一个文件,从而触发了open_basedir的检查。
于是尝试用关键字open_basedir和lstat搜索了一下,读到了 PHP safe_mode/open_basedir - lstat performance problem 这个PHP的bug提交,里面讨论了一大堆,简单来说,就是在PHP的safe_mode打开或者设置了open_basedir的时候,PHP是无法做realpath cache的,主要是因为安全因素,当cache住的某个路径变成了一个软链接时,可能会有潜在的安全问题。所以当设置了open_basedir之后,PHP会将realpath_cache关掉,这就导致了在检查open_basedir的时候,无法利用缓存,从而产生了很多的lstat调用,产生性能问题。

找到原因了,但是,目前的情况,设置open_basedir是必须的,如何在设置open_basedir的同时也开启realpath cache呢?最后找到了一个PHP扩展 realpath_turbo,利用这个扩展,可以在设置open_basedir的同时,开启realpath cache,他的原理也很简单,就是将php.ini中open_basedir配置,替换成realpath_turbo.open_basedir,然后在每个请求初始化的时候,设置open_basedir,这样绕过PHP初始化时的判断,使得realpath cache可以生效。当然,使用了这个扩展之后,需要讲PHP创建软链接的权限去掉,避免上面说的安全性问题。

接下来就简单了,在我们的扩展里,加入相关的逻辑,在请求初始化的时候,设置open_basedir,刚好平台的PHP也是禁止创建软链接的。所以也不会有类似的安全性顾虑。

升级之后,又生成了一张火焰图,和之前相比,要好很多:

火焰图

从系统监控上看,升级之前和升级之后,系统的负载,要下降接近一半左右,效果还是非常明显的。

火焰图