介绍
PHP 已经使用了多年的操作码缓存(APC,Turck MMCache,Zend OpCache)。它们通过 ALMOST 实现了显着的性能提升,完全消除了 PHP 代码重新编译的开销。使用操作码缓存,文件被编译一次(在第一个使用它们的请求上),然后存储在共享内存中。以下所有 HTTP 请求都使用共享内存中缓存的表示形式。
这个提议是关于上面提到的 ALMOST。虽然将文件存储在操作码缓存中可以消除编译开销,但仍然需要从缓存中获取文件和特定请求的上下文。我们仍然需要检查源文件是否被修改,将类和函数的某些部分从共享内存缓存复制到进程内存等。值得注意的是,由于每个 PHP 文件都是完全独立于任何其他文件进行编译和缓存的,因此我们可以当我们将文件存储在操作码缓存中时,不解决存储在不同文件中的类之间的依赖关系,并且必须在每个请求的运行时重新链接类依赖关系。
该提案的灵感来自为 Java HotSpot VM 设计的 “类数据共享” 技术。它旨在为用户提供交易传统 PHP 模型提供的一些灵活性的能力,以提高性能。在服务器启动时,在运行任何应用程序代码之前,我们可以将一组 PHP 文件加载到内存中,并使其内容 “永久可用” 到该服务器将服务的所有后续请求。这些文件中定义的所有函数和类将可用于开箱即用的请求,与内部实体完全相同(例如 strlen()
或 Exception
)。通过这种方式,我们可以预加载整个或部分框架,甚至整个应用程序类库。它还允许引入将用 PHP 编写的 “内置” 函数(类似于 HHVM 的 sytemlib)。交易灵活性包括一旦服务器启动就无法更新这些文件(更新文件系统上的这些文件将不会执行任何操作;将需要重新启动服务器以应用更改);而且,这种方法与托管多个应用程序的服务器或多个版本的应用程序不兼容,对于具有相同名称的某些类具有不同的实现,如果此类从一个应用程序的代码库预加载,则会发生冲突从其他应用程序加载不同的类实现。
提案
只需一个新的 php.ini 指令,opcache.preload 即可控制预加载。使用此指令,我们将指定一个 PHP 文件,它将执行预加载任务。加载后,该文件将完全执行,并可以通过包含它们或使用 opcache_compile_file()
函数预加载其他文件。以前,我尝试使用丰富的 DSL 来指定要加载哪些文件,使用模式匹配等来忽略哪些文件,但后来意识到在 PHP 中编写预加载方案本身更简单,更灵活。
例如,以下脚本引入了一个辅助函数,并使用它来预加载整个 Zend Framework。
<?php
function _preload($preload , string $pattern = "/\ .php $ /", array $ignore = []) {
if (is_array($preload)) {
foreach ($preload as $path) {_preload($path, $pattern, $ignore);
}
} elseif (is_string($preload) {
$path = $preload ;
if (!in_array($path, $ignore) {
if (is_dir($path) {
if ($dh = opendir($path)) {
while ($file = readdir($dh)) !== false) {
if ($file !== "." &&& $file !== ".." ) {
_preload($path . "/" . $file, $pattern, $ignore);
}
}
closedir($DH);
}
} elseif(is_file($path) && preg_match($pattern, $path)) {
if (!opcache_compile_file($path) {
trigger_error("预加载失败", E_USER_ERROR);
}
}
}
}
}
通过 set_include_path(get_include_path() 。 PATH_SEPARATOR。 真实路径(“/ 无功 / 网络 / ZendFramework / 库” )); _preload([ “/var/www / ZendFramework /library” ] );
如上所述,预加载的文件将永远缓存在 opcache 内存中。如果没有其他服务器重新启动,修改其相应的源文件将不起任何作用。这些文件中定义的所有函数和大多数类都将永久加载到 PHP 的函数和类表中,并在将来的任何请求的上下文中永久可用。在预加载期间,PHP 还解析了类依赖关系以及与父,接口和特征的链接。它还会删除不必要的包含并执行其他一些优化。
opcache_reset()
不会重新加载预加载的文件。使用当前的 opcache 设计是不可能的,因为在重启期间,某些进程可能会使用它们,任何修改都可能导致崩溃。
opcache_get_status
扩展为提供有关 “preload_statistics” 索引下的预加载函数,类和脚本的信息。
静态成员和静态变量
为了避免误解,很明显说预加载不会改变静态类成员和静态变量的行为。他们的价值观不会重新请求边界。
预加载限制
只能预加载没有未解析的父,接口,特征和常量值的类。如果一个类不满足这个条件,它将作为相应 PHP 脚本的一部分以与没有预加载相同的方式存储在 opcache SHM 中。此外,只有未嵌套在控制结构中的顶级实体(例如 if()…)可以被预加载。
在 Windows 上,也无法预加载从内部继承的类。Windows ASLR 和 fork()的缺失不允许保证不同进程中内部类的相同地址。
实施细节
预加载是作为 opcache 的一部分在另一个(已经提交的)补丁之上实现的,该补丁引入了 “不可变” 类和函数。他们假设不可变部分存储在共享内存中一次(对于所有进程)并且从不复制到进程内存,但变量部分是特定于每个进程的。该补丁引入了 MAP_PTR 指针数据结构,允许来自 SHM 的指针处理内存。
向后不兼容的变化
除非明确使用,否则预加载不会影响任何功能。但是,如果使用它,它可能会破坏某些应用程序行为,因为预加载的类和函数始终可用,并且 function_exists()或 class_exists()检查将返回 TRUE,从而阻止执行预期的代码路径。如上所述,具有多个应用程序的服务器上的错误使用也可能导致失败。由于不同的应用程序(或同一应用程序的不同版本)可能在不同的文件中具有相同的类 / 函数名称,如果预先加载了该类的一个版本 – 它将阻止加载在不同文件中定义的该类的任何其他版本。
建议的 PHP 版本
PHP 7.4
RFC 影响
对 Opcache
预加载是作为 opcache 的一部分实现的。
php.ini 默认值
opcache.preload – 指定将在服务器启动时编译和执行的 PHP 脚本。
性能
在没有任何代码修改的情况下使用预加载,我在 ZF1_HelloWorld(3620 req /sec vs 2650 req /sec)上获得 30%的加速,在 ZF2Test(1300 req /sec vs 670 req /sec)参考应用中获得 50%。但是,实际收益将取决于代码的引导开销与代码运行时之间的比率,并且可能会更低。对于具有较短运行时间的请求(例如微服务),这可能会提供最明显的收益。
未来范围
- 预加载可以在 HHVM 中用作 systemlib,以在 PHP 中定义 “标准” 函数 / 类
- 可以预编译预加载脚本并使用二进制格式(甚至可以是原生的 .so 或 .dll )来加速服务器启动。
- 与 ext / FFI(危险扩展)一起使用时,我们可能仅在预加载的 PHP 文件中允许 FFI 功能,但在常规的 PHP 文件中不允许
- 可以执行更积极的优化并为预加载的函数和类生成更好的 JIT 代码(类似于 HHVM 中的 HHVM Repo 权威模式)
- 使用某种部署机制扩展预加载,更新预加载的 bundle 而不重启服务器会很棒。
必须 注册 为本站用户, 登录 后才可以发表评论!