PHP的apc扩展导致引入文件错误

48次阅读

共计 4268 个字符,预计需要花费 11 分钟才能阅读完成。

最近遇到一个非常奇怪的 bug,在主机 PHP 代码版本回退的过程中,导致备机服务不可用。经过各种复现和文档查询,发现是 PHP 的 apc 扩展在和 rsync 同时使用时,会导致无法正确的处理缓存文件,最终影响服务。解决方案官方也有提供,加上一行配置:
# php.ini
[apc]
apc.stat_ctime=1
下面我们来说明下这个问题出现的机制。
关键点:使用了 PHP+apc 扩展 +rsync 主从同步机制
故障表现:引入时找不到文件
平台服务上线更新后,访问平台服务时报错信息:
Warning: include(Yii.php): failed to open stream: No such file or directory in /home/disk4/htdocs/oss_debug/protected/lib/Yii/framework/YiiBase.php on line 421

Warning: include(): Failed opening ‘Yii.php’ for inclusion (include_path=’.:/home/work/lnmp/weblib/phplib:/home/work/lnmp/lib/php’) in /home/disk4/htdocs/oss_debug/protected/lib/Yii/framework/YiiBase.php on line 421

Fatal error: Class ‘Yii’ not found in /home/disk4/htdocs/oss_debug/index.php on line 42
这里的提示信息表明,问题出现在 YiiBase.php 文件中,在 421 行引入 Yii.php 时找不到该文件,而这里的 include 为相对路径,当前的引入路径为.:/home/work/lnmp/weblib/phplib:/home/work/lnmp/lib/php,多个引入路径以: 分割,所以这里会在./,/home/work/lnmp/weblib/phplib,/home/work/lnmp/lib/php 三个目录下查找该文件,分别检索了一下,发现确实均不存在该文件。
但是在正常的服务下,却并不会查找该文件。具体为什么会去查找该文件,我猜测是先判断 Yii 类是否存在,不存在就去引入 Yii.php,而 Yii 类在 yii.php 文件中有定义,因此猜测是没有正确引入 yii.php 导致。
# yii.php
<?php
require(dirname(__FILE__).’/YiiBase.php’);
class Yii extends YiiBase
{
}
这个问题没有深究,因为最后发现故障跟这个点无关。
复现一个小问题:改变目录后无法服务
你只需要将你的服务目录换个名字即可复现,如你当前的服务目录是 /home/work/lnmp/htdocs/oss/,你将它重名为 /home/work/lnmp/htdocs/oss2,这个时候你就会发现服务受到了影响:
# 访问 domain.com/oss2/index.php
Warning: file_get_contents(/home/work/lnmp/htdocs/oss/version): failed to open stream: No such file or directory in /home/work/lnmp/htdocs/oss/index.php on line 26
Warning: require_once(/home/work/lnmp/htdocs/oss/protected/lib/Yii/framework/yii.php): failed to open stream: No such file or directory in /home/work/lnmp/htdocs/oss/index.php on line 38
Fatal error: require_once(): Failed opening required ‘/home/work/lnmp/htdocs/oss6/protected/lib/Yii/framework/yii.php’ (include_path=’.:/home/work/lnmp/weblib/phplib:/home/work/lnmp/lib/php’) in /home/work/lnmp/htdocs/oss6/index.php on line 38
可以看到当我们访问 oss2 目录时,程序却依然在尝试读取 oss 目录下的文件,这时文件自然不存在,因此报错。那么这是为什么呢?
原因是我们使用了 PHP 的 apc 扩展。

PHP 的服务过程
学习过计算机原理的同学,都了解语言分为编译型语言和解释型语言,由于语言是人来编写的,而机器无法直接执行,因此,在代码被执行前需要经历一个编译成机器可以识别的操作码的过程。
编译型语言在执行前提前编译好,然后发布;解释型语言先发布,在执行时即时编译。因此我们常说编译型语言的性能好,主要就是快在这个地方。
PHP 属于解释型语言,常规的执行流程是:

Nginx 转发请求给 PHP 主进程
主进程引入代码文件

PHP 解释器会先将代码切分为 Token

生成抽象语法树
生成机器可以直接执行的操作码

PHP 虚拟机执行操作码
如果文件有引入其他文件,循环执行上述 2 - 6 步骤
执行完成,返回结果

可以看到每次请求过来,都会对文件做一次编译和缓存,那么这样会非常影响效率,为了保证 PHP 的灵活性,同时提升效率,我们需要对编译好的操作码进行缓存。这就是 apc 扩展做的事情:

判断文件是否有更新
如果更新,重新编译并缓存
否则,直接读取缓存的操作码

apc 扩展
apc 扩展文档

Alternative PHP Cache (APC 可选 PHP 缓存) 是一个开放自由的 PHP opcode 缓存。它的目标是提供一个自由、开放,和健全的框架,用于缓存、优化 PHP 中间代码。
该扩展也提供了一些内置的方法,可以用于手动设置或清空缓存。清空缓存的方法:apc_clear_cache()。调用这个方法后可以解决因 apc 缓存过期文件导致的 bug。
另外,我们需要关注的几个配置项:

apc.stat integer 是否启用脚本更新检查。改变这个指令值要非常小心。默认值 On 表示 APC 在每次请求脚本时都检查脚本是否被更新,如果被更新则自动重新编译和缓存编译后的内容。但这样做对性能有不利影响。如果设为 Off 则表示不进行检查,从而使性能得到大幅提高。但是为了使更新的内容生效,你必须重启 Web 服务器(译者注:如果采用 cgi/fcgi 类似的,需重启 cgi/fcgi 进程)。生产服务器上脚本文件很少更改, 可以通过禁用本选项获得显著的性能提升。这个指令对于 include/require 的文件同样有效。但是需要注意的是,如果你使用的是相对路径,APC 就必须在每一次 include/require 时都进行检查以定位文件。而使用绝对路径则可以跳过检查,所以鼓励你使用绝对路径进行 include/require 操作。
apc.stat_ctime integer 验证 ctime(创建时间)可以避免 SVN 或者 rsync 带来的问题,确保自上次缓存统计 inode 没有改变。APC 通常只检查 mtime(修改时间)。
apc.file_update_protection integer 当你在一个运行中的服务器上修改文件时,你应当执行原子操作。也就是先写进一个临时文件,然后将该文件重命名 (mv) 到最终的名字。文本编辑器以及 cp, tar 等程序却并不是这样操作的,从而导致有可能缓冲了残缺的文件。默认值 2 表示在访问文件时如果发现修改时间距离访问时间小于 2 秒则不做缓冲。那个不幸的访问者可能得到残缺的内容,但是这种坏影响却不会通过缓存扩大化。如果你能确保所有的更新操作都是原子操作,那么可以用 0 关闭此特性。如果你的系统由于大量的 IO 操作导致更新缓慢,你就需要增大此值。

可以看到,apc 扩展可能会导致两个问题:

rsync/svn 配合使用时存在无法正确处理文件缓存的问题
可能读到残缺文件,导致影响部分人的请求

针对这两个问题,也分别提供了解决方案:
# php.ini
[apc]
# 启动 ctime 检查
stat_ctime=1
# 默认值为 2,变大这个值
file_update_protection=5
虽然文档中有说明,但还是有很多人会遇到这种问题,可以参考:

stack overflow 问题:Problems with APC on publish

官方 Issue:apc.include_once_override turn on issue

在遇到这个问题时,除了上面的配置解决问题,还可以:

PHP 代码中执行 apc_clear_cache()

重启 php-fpm 进程

另外,我们可以将 apc 扩展安装时包含的 apc.php 文件放到 web 服务目录下,就可以可视化的观察 apc 扩展的缓存情况。

服务使用了 rsync 同步
这次故障的一个关键因素是使用了 rsync 同步,我的服务架构是:
导致这个问题的原因探究
具体为什么在 apc 扩展跟 rsync 同时使用会产生这个 bug,我没有看源码,不太了解,但我做了一些大胆的猜测,下面的内容不够清楚和正确,希望大家能给我更精确的指导:这里可以看出文件是怎么检查是否有更新的,而问题也就出现在这一部分,没有办法判断文件是否被更新,同时正确读取到缓存的文件。
参考资料

PHP 手册 – APC 运行时配置:https://www.php.net/manual/zh…

stack overflow – Problems with APC on publish:https://stackoverflow.com/que…

PHP 官方 issue – apc.include_once_override turn on issue:https://bugs.php.net/bug.php?…

php 可选缓存 APC:https://www.cnblogs.com/hf805…

关于上线系统的一些想法 (for php):http://bikong0411.github.io/2…

如何刷新 APC 类加载器缓存?:http://cn.voidcc.com/question…

rsync 文件同步服务:https://xdays.me/rsync%E6%96%…

APC’s Include Once Override breaks install:https://www.drupal.org/projec…

《PHP 7 底层设计和源码实现》

正文完
 0