折磨人的 OPCache

背景

一切源于同一份代码在开发机(云主机)中和测试环境(k8s容器)中运行的结果相关很大。如下图:左边是云主机,右边是优化前的 k8s 容器。

image-20220413162904064

是何原因导致?刚开始找我查这个问题时也是一脸的懞。先说下我的怀疑路径:

  • 代码有问题(新开发的框架);
  • 环境配置有问题;
  • 引用的包有问题;
  • OPCache 配置有问题;
  • ......

怀疑的过程就是一一验证的过程,本文将一点一点的介绍我使用哪些工具,如何验证等等!

小心求证

框架已较优秀

本司的框架是基于 Lumen8 进行迭代的,优化的点是:

  1. 废弃了 .env 文件,因为每次请求会读取 .env 内容然后解析,这操作过程是有 I/O 过程的,此优化是本人在之前推进完成(后续会推出相关内容);
  2. 路由的优化。

刚开始听说框架是新开发的,自然也就怀疑相关调整是否有误?于是本人就在开发环境搭建了性能分析的平台 tideway_xhprof + xhgui 来一探究竟。起初的结果也是一个乌龙。因为分析后出现了大量的 Composer\Autoload\includeFile@1 及相关错误,怀疑了下是不是因为框架的规范不完善导致的 OPCache 失效。耽误了一点时间才想起,本人开发环境是为了适配新的框架对 php 版本的要求,重新装的新版本,OPCache 就没打开,开启后结果就达到上图左侧效果了。(不够严谨的作者,该批!)

环境已成模板

框架不是问题,接下来的怀疑点就是环境上了。通过 php -i and php -m 等命令对环境配置做了很多确认,毫无收获。甚至把新、老版本的代码放一起进行压测比较。老版本的代码是没有任何问题的。说明不是环境问题了。

接下的时间又进行了很多其它的验证,均无收获!

锁定 OPCache

因为在测试环境的结果就与上图右侧结果一致(不好在测试环境搭建性能分析平台),所以大胆的猜测了下是 OPCache 未生效。所以开始时用了入侵式的方案,在入口处增加了 var_dump(opcache_get_status());die; 代码进行对比,发现测试环境的结果中 scripts 一项未出现任何开源的包,只出现了业务代码与内部包。多次调整代码,发现入侵式的 OPCache 信息查阅很不友好。幸亏马上就查到 CacheTool 这一非入侵式的工具,为后续的排查带来质的飞越。

image-20220413180420381

通过在两个环境运行如下命令非常确定的是非私有包都未被加载到 cache 中。

1
2
3
4
5
# cvm 
php cachetool.phar opcache:status:scripts --fcgi=127.0.0.1:9000 | grep illuminate

# pod container
php cachetool.phar opcache:status:scripts --fcgi=127.0.0.1:9000

image-20220413180942219

那么,到此为止,一切的问题就变成了为什么 OPCache 在容器中对开源包不生效的问题了

拨开云雾见真经

在确定了 OPCache 不生效只针对开源项目的包,还折腾了很久......

后来一个无意间的小操作发现了问题所在,在怀疑是否文件有问题时 ls -al 了下文件,发现居然是 2022 年 9 月 22 一个未来时间。虽然没有第一时间去查阅为啥都以 OPCache 对未来时间不生效,但是为了快速确定是否有影响,本人修改文件的时间再次验证:

1
touch ./vendor/illuminate/database/DatabaseServiceProvider.php

修改完时间后再次执行代码,发现已经能通过 cachetool 看到相应的缓存脚本了。那接下来就是验证结论的时刻。

1
find ./* -type f -exec touch {} \; # 修改完所有文件的时间

再次运行压测,然后就一切正常了。那接下来问题就变成了两个:运维确定因啥时间会变成未来时间? & OPCache 使用哪个时间及怎么判断时间了?

针对时间问题,本人翻阅了下 php 源码中 OPCache 相关代码,可以确定是使用了 st_mtime。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 调用
accel_time_t zend_get_file_handle_timestamp(zend_file_handle *file_handle, size_t *size)
{
// ...
zend_fstat(fileno(file_handle->handle.fp), &statbuf)
// ...
return statbuf.st_mtime;
}
// 依赖
# define zend_fstat fstat // 非 win 系统
struct stat {
// ...
time_t st_mtime; /* time of last modification */
};
// 验证
int validate_timestamp_and_record(zend_persistent_script *persistent_script, zend_file_handle *file_handle)
{
if (persistent_script->timestamp == 0) {
return SUCCESS; /* Don't check timestamps of preloaded scripts */
} else if (ZCG(accel_directives).revalidate_freq &&
// 验证时间与请求时间对比,理解上此处一直是成功状态,所以不会进行缓存
persistent_script->dynamic_members.revalidate >= ZCG(request_time)) {
return SUCCESS;
} else if (do_validate_timestamps(persistent_script, file_handle) == FAILURE) {
return FAILURE;
} else {
// 验证时间是上一次验证时间 + 间隔时间
persistent_script->dynamic_members.revalidate = ZCG(request_time) + ZCG(accel_directives).revalidate_freq;
return SUCCESS;
}
}

参考


折磨人的 OPCache
https://blog.isnap.cn/posts/ca12726f/
作者
三岁于辛
发布于
2022年4月13日
许可协议