你应该知道的 OpCode 缓存

本文介绍OpCode缓存的概念及其对PHP性能的影响。通过使用ZendOpCache或APC等工具,开发者能在生产环境中显著提升PHP应用的速度。文章还讨论了如何安装配置这些工具,并介绍了填充缓存的方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

什么是 OpCode 缓存

OpCode 缓存是PHP性能增强的扩展,它们通过将自己注入PHP的执行生命周期,并缓存编译阶段的结果,以便以后重用。近通过启用 OpCode 缓存即可看到3倍的性能提升并不罕见。

什么时候该使用OpCode缓存

鉴于 OpCode 缓存几乎没有额外的内存使用(存储缓存)的副作用,它们应该始终在生产环境中使用。要担心的主要副作用是初始缓存造成的一些开销和缓存丢失(新服务器、重启 Apache / php-fpm、重启机器) 以及可能导致所谓的 cache stampede 效应。当然,这可以通过启动高速缓存来缓解。

哪个 OpCode 缓存

在整个 PHP 生命周期中,都有一些 OpCode 缓存;第一个是来自 Zend,然后它是专有的。因此,在过去的几年中,主要是使用 APC 替代 PHP 缓存。虽然 APC 很优秀,但是它缺乏 Zend 的一些功能。另外还缺少维护人员,对其进行升级。

随着 PHP 5.5发布,Zend 开源了自己的缓存产品, 以 Zend OpCache 来命名。并将其包含在 PHP 中。

Zend OpCache 性能比 APC 更高,功能更加全面,更可靠。 然而,Zend OpCache 不包含由 APC 提供的辅助功能 - 用户变量缓存。为了缓解这个问题,发布了一个新的扩展 apcu,它只提供用户变量缓存功能,并且 100% 兼容 APC 的实现。

虽然 Zend 开源了自己的缓存方案,但是 APC 仍然广泛的使用者,并且对于 OpCode 缓存仍然是一个不错的选择。

安装

安装任何以下扩展后,你都必须要重启 PHP 才行,无论是通过重启 Apache 还是 PHP-FPM。

Zend OpCache

对于 PHP 5.5 及以上,Zend OpCache 默认都被编译为一个共享的扩展,除非你编译 PHP 时指定 --disable-all 。要启用它,你必须在 编译 PHP 时加上 --enable-opcache。

对于 PHP 5.4 或者更早的版本(> = 5.2),则可以使用 PECL 来安装:

$ pecl install zendopcache-beta

PECL 命令将尝试自动更新你的 php.ini 配置文件。你可以使用下面命令查看 pecl 将尝试更新的文件:

$ pecl config-get php_ini

它只是简单的将新的配置添加到指定文件顶部(如果有的话)。你也可以自己将它们移动到更合适的位置。

[zendopcache]
zend_extension=/full/path/to/opcache.so        ; 可能由 PECL 添加  
opcache.memory_consumption=128  
opcache.interned_strings_buffer=8  
opcache.max_accelerated_files=4000  
opcache.revalidate_freq=60  
opcache.fast_shutdown=1  
opcache.enable_cli=1  

如果你不知道扩展完整的路径,你可以看看 php.ini 中指定的 extension_dir 路径。另外,如果你通过 PECL 安装,它会输出一行(非常接近它的输出结尾),如下所示:/usr/local/php/lib/php/extensions/no-debug-non-zts-20100525/opcache.so

APCu - APC 的用户变量缓存(可选)

如果想使用 APC 的用户变量缓存,你还得安装 APCu。APCu 可以通过 PECL 安装。APCu 完全向后兼容 APC。APCu 不应该和 APC 一起安装。

$ pecl install apcu-beta

然后安装时,会提出两个问题,你可以直接使用这两个问题的默认值就好了。

和 Zend OpCache 一样,pecl 安装时可能已经为你在配置文件中增加了相应的行。当然,你可以将它们移到合适的位置。

[apcu]
extension=apcu.so                        ; 可能由 PECL 添加  
apc.serializer=php                        ; 有关详细信息,请参阅错误报告:http://ey.io/1aJhcOY  

配置好后,你现在可以使用apc_用户变量缓存功能了。

APC - 替代 PHP 缓存

要安装 APC,你将在此使用 PECL:

$ pecl install apc-beta

安装的时候,会有以下选项,你可以直接选默认值就好。

一旦安装成功,它将自动在 php.ini 中添加配置:

[APC]
extension=apc.so  
apc.enable=1  
Engine Yard的PHP性能工具

要使用 Composer 安装,需要将以下内容添加到composer.json中:

"repositories":[
  {
    "type": "vcs",
    "url": "http://github.com/engineyard/ey-php-performance-tools"
  }
],
"require": {
  "php": ">=5.3.3",
  "engineyard/php-performance-tools": "dev-master"
}

然后在终端中执行下列命令:

$ composer update engineyard/php-performance-tools

这就将最新版本的 Engine Yard 的PHP性能工具安装好了。

使用

Zend OpCache 和 APC 在操作中都是透明的 - 当执行一个 PHP 文件时,OpCode 将被缓存,以备重用。

然后,正如我们所提到的,可能会引发 cache stampede效应。出现这种情况大多是系统负载过高,且缓存不可用的情况下 - 因为尚未构建或由于某些原因,缓存已经被清除。

为了解决这个问题,你可以预先填充缓存,这被称呼为填充缓存

填充缓存(Zend OpCache)

不幸的是,还没有(当然,可能是我不知道)一个很好的方式来填充 Zend OpCache。Zend OpCache 自己提供了一些方法来做到这些,不过我觉得不是很好用。

当然,你也可以使用一些笨办法来达到这一目的:

  1. 发送多个 Web 请求到服务器,从而导致 Zend OpCache 缓存生成的 OpCode。 
    1. 只适合用于当前未处于负载状态的服务器
    2. 这可能是很多的请求,且需要一些时间来缓存所需的代码
  2. 在部署代码时,使用多个文档根目录来保存尽可能多的缓存。
HTTP 缓存

实现我们的第一选择的一种方法,就是使用我们的集成测试的测试套件(或其子套件)。如果没有的话,我们可以写个简单的爬虫脚本来完成这些请求。

Engine Yard的PHP性能工具库中,提供了一个简单的 HTTP Cache Primer 工具。这个简单的脚本使用 pecl_http 扩展并行地执行多个请求,在 MacBook Air 上,大约 4 秒执行超过 100 个请求。

如果没有安装 pecl_http,脚本会一个个顺序执行,会比并行执行慢很多

要使用它,请在当前目录中创建一个基于 config.php-dist的config.php,其中包含你希望执行的 URL 列表,如下:

return [  
  /* URLs to cache */
  'urls' => [
    'http://xxx.com/',
    'http://xxx.com/user/login',
    'http://xxx.com/user/register',
    'http://xxx.com/privary'
  ],
  /* 需要多少并发, 需要 pecl_http 扩展的支持 */
  'threads' => 3
]

然后我们运行它:

$ /path/to/vendor/bin/cache-primer

这将产生如下输出:

=== Cache Primer ===
Attempting to cache 4 URLs:  
Running in parallel with 3 concurrent requests  
...!
Cached 3 URLs in 0.6162059307098 seconds  
Encountered 1 errors  

或者使用更多的 URL:

=== Cache Primer ===
Attempting to cache 104 URLs:  
Running in parallel with 10 concurrent requests  
............................................................................!...........................
Cached 103 URLs in 4.6162059307098 seconds  
Encountered 1 errors  

以这种填充方式的一个优点是,几乎可以填充任何 HTTP 高速缓存,例如 Varnish 或 Squid

Zend OpCache 自带的方式

新的 Zend OpCache 中 添加了 opcode_compile_file(),只需要使用 zend-primer 就可以填充当前启用的缓存。

填充缓存 (APC)

你可以想 Zend OpCache 一样,使用 HTTP 请求来填充缓存,你也可以直接使用 APC 提供的直接填充缓存的方法。而我们仍然需要向 Web 服务器发送请求,我们可以用单个请求缓存整改代码库。

我们可以使用 APC 提供的 apc_compile_file()函数做到这一点,该函数将编译指定文件,并将其添加到缓存中。这意味着,我们可以编译任何有效的 PHP 文件,类,模版等。

类似于 HTTP 填充缓存,我们需要创建一个基于 config.php-dist 的 config.php 文件,只是这次它包含的不再是 URL 列表,而是编译文件的目录列表,以及 HTTP Basic 身份 (可选) 配置。

HTTP Basic 身份验证为此脚本提供了最少的保护,更好的方式是禁用此身份验证,并将脚本包含在系统管理界面中。

if (!defined('DS')) {  
  define('DS', DIRECTORY_SEPARATOR);
}

return [  
  /* 身份验证 */
  'auth' => [
    'enable' => true,
    'username' => 'admin',
    'password' => 'admin',
  ],

  /* 缓存路径(递归) */
  'paths' => [
    __DIR__ . DS . '..' . DS . '..' . DS . 'config',
    __DIR__ . DS . '..' . DS . '..' . DS . 'module',
    __DIR__ . DS . '..' . DS . '..' . DS . 'vendor',
    __DIR__ . DS . '..' . DS . '..' . DS . 'public'
  ]
];

创建此文件后,你可以将其打包成 phar 归档包,或者将其集成到任何其他已有的管理界面中。

当你请求此文件时,先进行身份验证,验证成功后,会递归遍历配置中的内容,查找具有 .php 和 .phtml 扩展名的所有文件; 然后过滤掉重复文件,最后将它们添加到缓存中。

当运行时,会得到类似下面的输出:

Cached 4841 files in 2.1302671432495 seconds  

这种方式只需要一次请求即可完成,为实际客户端留下更多的资源。

但是,缺点在于,我们不能像之前的方式那样可以填充 Varnish 或者其他缓存。

底层

无论你使用哪种 OpCache,它们的工作方式几乎是一样的。它们将自己注入到执行生命周期中,并在对时间内重复使用。Zend OpCache 还将在创建缓存时执行一些优化。

执行生命周期

PHP 时一种脚本语言,大多数人认为它不是编译执行的。虽然传统意义上,这确实是正确的,因为我们没有调用 GCC 或者 javac;事实上,每次请求 PHP 脚本时,都经过了编译操作。其实 PHP 和 JAVA 的生命周期时非常相似的,因为它们都编译为中间指令集(OpCode 或者字节码),然后在虚拟机 (Zend VM 或 JVM) 中运行。

解析和编译阶段很慢。当我们添加一个 opcache 时,我们通过存储解析和编译极端的结果来缩短这个过程,只需要执行一次就可以动态的运行。事实上,我们现在更接近于 JAVA 的生命周期;主要区别是我们保存到共享内存中,而不是文件中,并且如果脚本发生变更,可以自动重新编译。

Token 和 OpCodes

一旦获得了你的 PHP 代码,就会创建两个标识码。第一个是 Token,这是一种将代码分解为小块的方法,以便引擎将其编译成第二种码:OpCodes。OpCodes 是Zend VM 执行的实际指令。

用 Hello World 来看 Token 和 OpCode

你学编程写的第一个程序是不是 Hello World? 应该是吧! 那我们这里也用 Hello World 来演示下 Token 和 Opcodes。

<?php  
class Greeting {  
  public function sayHello($to) {
    echo "Hello $to";
  }
}

$greeter = new Greeting();
$greeter->sayHello("World");
?>
令牌化
Token 名
T_OPEN_TAG<?php
T_CLASSclass
T_WHITESPACE 
T_STRINGGreeting
T_WHITESPACE 
 {
T_WHITESPACE 
T_PUBLICpublic
T_WHITESPACE 
T_FUNCTIONfunction
T_WHITESPACE 
T_STRINGsayHello
 (
T_VARIABLE$to
 )
T_WHITESPACE 
 {
T_WHITESPACE 
T_ECHOecho
T_WHITESPACE 
 "
T_ENCAPSED_AND_WHITESPACEHello
T_VARIABLE$to
 "
 ;
T_WHITESPACE 
 }
T_WHITESPACE 
 }
T_WHITESPACE 
T_VARIABLE$greeter
T_WHITESPACE 
 =
T_WHITESPACE 
T_NEWnew
T_WHITESPACE 
T_STRINGGreeting
 (
 )
 ;
T_WHITESPACE 
T_VARIABLE$greeter
T_OBJECT_OPERATOR->
T_STRINGsayHello
 (
T_CONSTANT_ENCAPSED_STRING"World"
 )
 ;
T_WHITESPACE 
T_CLOSE_TAG?>

如上所示,大部分代码片段的名字都是以 T_ 开头然后是一个描述性的词。这是臭名昭着的TPAAMAYIMNEKUDOTAYIM错误来自:它代表双冒号。

有些令牌没有T_名称,这是因为它们是一个单一的字符,如果这里给它们分配一个更大的名字,显然没有意义而去很浪费。

上面看到变量的插值也很有意思("Hello $to"),采用两个字符串。第一个,将插值变量分为四个独特的操作码:

 "
T_ENCAPSED_AND_WHITESPACEHello
T_VARIABLE$to
 "

然后非插值字符串只是一个:

T_CONSTANT_ENCAPSED_STRING"Worl"

有了这个观点,我们可以看到我们如何以很小的方式开始影响性能。

用于生成 Token 名单的脚本:

#!/usr/bin/env php
<?php  
if (!isset($_SERVER['argv'][1])) {  
    echo "Usage: {$_SERVER['argv'][0]} <filename>" . PHP_EOL;
    exit;
}
$tokens = token_get_all(file_get_contents($_SERVER['argv'][1]));
foreach ($tokens as $token) {  
    if (is_integer($token[0])) {
        echo str_pad(token_name($token[0]), 28);
        echo trim($token[1]);
    } else {
        echo str_repeat(" ", 28);
        echo $token[0];
    }
    echo PHP_EOL;
}
?>
OpCodes

接下来,让我们看看编译成 OpCodes 后的样子。

要获取 OpCodes,我们需要安装 VLD (Vulcan Logic Dumper) 扩展。

$ pecl install vld-beta

并确保以下内容已经成功添加到 php.ini 中:

extension=vld.so  

安装成功后,你可以在命令行中,使用以下命令 dump 任何代码的 OpCodes:

$ php -dvld.active=1 -dvld.execute=0 <file>

VLD dumps 全局代码(主脚本),全局函数和类函数。但是,我们先看看我们的类函数,以便遵循与代码本身相同的流程。

了解 VLD dumps

VLD dumps 通常是多个 dumps,一个用户主脚本,一个用户每个全局函数和类函数,每个 dump 结构相同。

首先列出(如果适用)该类和函数,然后列出文件名,接下来是函数的名称。

Class Greeting:  
Function sayhello:  
filename:      ./Greeting.php  
function name:  sayHello  

接下来,它列出了 dumps 中的 OpCodes 总数:

number of ops:  8  

再然后,列出编译的变量:

compiled vars:  !0 = $to  

最后,它列出了 OpCodes ,每一行,标题行下:

line      # *  op                     fetch        ext  return  operands  
------------------------------------------------------------------------

每个 OpCodes 有以下几点:

  • line: 在源文件中的行号
  • *:进入点(左对齐)和出口点(右对齐)
  • op:OpCodes 名
  • fetch:全局变量详细信息
  • ext:与 OpCodes 相关联的额外数据
  • return:返回数据的位置
  • operands:OpCodes 使用的操作数(例如两个变量的连接)

并非所有列都适用于所有 OpCodes。

变量

Zend Engine 中有多种类型的变量。所有变量都使用标识符。

  • 带有感叹号(!) 前缀的变量是编译变量(CVs) - 这些是用户空间变量的指针
  • 前缀为波浪号(~)的变量是用于进程内操作的临时存储(TMP_VAR)的临时变量
  • 以$($)为前缀的变量是另一种类型的临时变量(VAR),它们与用户空间变量(如CVs)相关联,因此需要重新计数。
  • 以冒号(:)作为前缀的变量是用于将查询结果存储在类哈希表中的临时变量
Dumping Hello World
Class Greeting:  
Function sayhello:  
number of ops:  8  
compiled vars:  !0 = $to

line      # *    op                      fetch          ext     return     operands  
----------------------------------------------------------------------------
   3      0  >   EXT_NOP
          1      RECV                                         !0
   5      2      EXT_STMT
          3      ADD_STRING                                   ~0    'Hello+'
          4      ADD_VAR                                      ~0    ~0, !0
          5      ECHO                                                 ~0
   6      6      EXT_STMT
          7    > RETURN                                               null

我们看到我们在类Greeting中的函数sayHello中,我们有一个编译变量$ to,它由!0标识。

接下来,按照操作码列表(忽略no-op),我们可以看到:

  • RECV:该函数接收分配给!0(表示$to)的值
  • ADD_STRING:接下来,我们创建一个由~,~0标识的临时变量,并分配静态字符串'Hello +',其中+表示空格
  • ADD_VAR:之后,我们将我们的变量!0的内容连接到我们的临时变量~0
  • ECHO:然后我们打印这个临时变量
  • RETURN:最后,功能结束时我们什么也不返回

接下来,看看主脚本:

这里我们看到一个编译变量$greeter,由!0标识。所以让我们来看看这一点,我们再次no-op的操作。

  • FETCH_CLASS:首先我们查找类Greeter; 我们将此参考存储在 :1
  • NEW:然后我们实例化一个类的实例(:1),并将其分配给一个VAR,$2
  • DO_FCALL_BY_NAME:调用构造函数
  • ASSIGN:接下来我们将生成的对象(VAR $2)分配给我们的CV(!0)
  • INIT_METHOD_CALL:我们开始调用sayHello方法
  • SEND_VAL:将"World"传递给该方法
  • DO_FCALL_BY_NAME:执行该方法
  • RETURN:脚本成功结束,隐式返回值为1
去对你的应用进行优化吧

使用 OpCode,它会给你的应用提升的性能,另外使用 Zend OpCache 你可以对你的项目进行大量的优化。

使用操作码缓存不应该是可选的,它将使你能够以很少的精力从硬件中获得更多的性能。如果你还没有使用,为什么不呢?

资源

写到凌晨 1:42 才写完,而且还是在公司写的。? 这就是我的五一假期。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值