七、优化以获得高性能

多年来,PHP 已经发展成为我们用来构建 web 应用程序的一种出色的语言。大量的语言特性,加上无数的库和框架,使我们的工作变得更加容易。我们经常编写包含多个堆栈层的代码,而不考虑它。这使得人们很容易忽略每个应用程序最重要的一个方面——性能。

虽然开发人员需要注意性能的几个方面,但最终用户只对其中一个方面感兴趣——加载网页所需的时间。归根结底,这就是一切。如今,用户期望他们的页面在不到 2 秒钟的时间内加载。再多的话,我们就面临着转化率下降的问题,这通常会转化为大型电子商务零售商的严重财务损失:

"A 1 second delay in page response can result in a 7% reduction in conversions."

"If an e-commerce site is making $100,000 per day, a 1 second page delay could potentially cost you $2.5 million in lost sales every year."                                                                                                                  - kissmetrics.com

在本章中,我们将讨论 PHP 中直接或间接影响应用程序性能和行为的一些方面:

  • 最大执行时间
  • 内存管理
  • 文件上传
  • 会话处理
  • 输出缓冲
  • 禁用调试消息
  • Zend OPcache
  • 并发性

最大执行时间

最大执行时间是开发人员遇到的最常见错误之一。默认情况下,在浏览器中执行的 PHP 脚本的最大执行时间为 30 秒,除非我们在 CLI 环境中执行脚本,而 CLI 环境中没有此类限制。

我们可以通过一个简单的例子,通过index.phpscript.php文件,很容易地进行测试,如下所示:

<?php
// index.php
require_once 'script.php';
error_reporting(E_ALL);
ini_set('display_errors', 'On');
sleep(10);
echo 'Test#1';
?php
// script.php
sleep(25);
echo 'Test#2';

从浏览器中执行,将返回以下错误:

Test#2
Fatal error: Maximum execution time of 30 seconds exceeded in /var/www/html/index.php on line 5

从 CLI 环境中执行,将返回以下输出:

Test#2Test#1

幸运的是,PHP 提供了两种控制超时值的方法:

  • 使用max_execution_time配置指令(php.ini文件,ini_set()功能)
  • 使用set_time_limit()功能

set_time_limit()函数的使用有一个有趣的含义。让我们来看看下面的例子:

<?php
// index.php
error_reporting(E_ALL);
ini_set('display_errors', 'On');
echo 'Test#1';
sleep(5);
set_time_limit(10);
sleep(15);
echo 'Test#2';

上述示例将导致以下错误:

Test#1
Fatal error: Maximum execution time of 10 seconds exceeded in /var/www/html/index.php on line 9

有趣的是,set_time_limit()函数在调用超时计数器的点从零重新启动超时计数器。这实际上意味着,在一个相当复杂的系统中,在整个代码中使用set_time_limit()函数,我们可以显著地将总超时时间延长到最初设想的界限之外。这是相当危险的,因为当将最终网页交付给用户的浏览器时,PHP 超时并不是唯一的超时。

Web 服务器自带各种超时配置,可能会中断 PHP 执行:

  • 阿帕奇:
    • TimeOut指令,默认为 60 秒
  • Nginx:
    • client_header_timeout指令,默认为 60 秒
    • client_body_timeout指令,默认为 60 秒
    • fastcgi_read_timeout指令,默认为 60 秒

虽然我们当然可以在浏览器上下文中控制脚本超时,但重要的问题是我们为什么要这样做?超时通常是资源密集型操作的结果,例如各种非优化循环、数据导出、导入、PDF 文件生成等。当涉及到所有资源密集型作业时,CLI 环境或理想情况下的专用服务应该是我们的首选。而浏览器环境的主要重点应该是在尽可能短的时间内向用户提供页面。

内存管理

PHP 开发人员经常需要处理大量数据。虽然“大”是一个相对术语,但“记忆”不是。如果不负责任地使用函数和语言结构的某些组合,可能会在几秒钟内阻塞服务器内存

可能最臭名昭著的函数是file_get_contents()。这个易于使用的函数从字面上抓取整个文件的内容并将其放入内存。为了更好地理解这个问题,我们来看看下面的例子:

<?php

$content = file_get_contents('users.csv');
$lines = explode("\r\n", $content);

foreach ($lines as $line) {
  $user = str_getcsv($line);
  // Do something with data from $user...
}

虽然这段代码是完全有效和有效的,但它是一个潜在的性能瓶颈。$content变量将把整个users.csv文件的内容拉入内存。虽然这可能适用于较小的文件大小,比如说几兆字节,但代码的性能并没有得到优化。当users.csv开始增长时,我们将开始经历记忆问题。

我们能做些什么来缓解这个问题?我们可以重新思考解决问题的方法。当我们将注意力转移到必须优化性能模式时,其他解决方案就变得清晰了。我们可以通过以下方式解析文件行,而不是将整个文件的内容读入变量:

<?php

if (($users = fopen('users.csv', 'r')) !== false) {
  while (($user = fgetcsv($users)) !== false) {
    // Do something with data from $user...
  }
  fclose($users);
}

我们不再使用file_get_contents()str_getcsv(),而是重点使用另一组函数fopen()fgetcsv()。最终的结果是完全一样的,还有一个额外的好处就是性能友好。在这个特定的例子中,使用带句柄的函数,我们有效地保证了内存限制对我们的脚本来说不是问题。

不负责任地使用循环是内存的另一个常见原因:

<?php

$conn = new PDO('mysql:host=localhost;dbname=eelgar_live_magento',
'root', 'mysql');

$stmt = $conn->query('SELECT * FROM customer_entity');
$users = $stmt->fetchAll();

foreach ($users as $user) {
    if (strstr($user['email'], 'test')) {
        // $user['entity_id']
        // $user['email']
        // Do something with data from $user...
    }
}

现在,让我们继续看一个修改过的、内存友好的示例,其效果相同:

<?php

$conn = new PDO('mysql:host=localhost;dbname=eelgar_live_magento', 
  'root', 'mysql');

$stmt = $conn->prepare('SELECT entity_id, email FROM customer_entity WHERE email LIKE :email');
$stmt->bindValue(':email', '%test%');
$stmt->execute();

while ($user = $stmt->fetch(PDO::FETCH_ASSOC)) {
  // $user['entity_id']
  // $user['email']
  // Do something with data from $user...
}

fetchAll()方法比fetch()稍快,但需要更多内存。

当 PHP 达到内存限制时,它会停止脚本执行并抛出以下错误:

Fatal error: Allowed memory size of 33554432 bytes exhausted (tried to 
allocate 2348617 bytes) ...

幸运的是,memory_limit指令使我们能够控制可用内存量。默认的memory_limit值为128M,这意味着 128 兆字节的内存。指令是PHP_INI_ALL可变的,这意味着除了通过php.ini文件进行设置外,我们还可以在运行时使用ini_set('memory_limit', '512M');进行设置。

除了调整memory_limit指令外,PHP 还提供以下两个函数返回内存使用信息:

  • memory_get_usage():返回 PHP 脚本当前分配的内存量
  • memory_get_peak_usage():返回 PHP 脚本分配的最大内存量

虽然我们可能想增加这个值,但我们应该三思而后行。内存限制是每个进程,而不是每个服务器。Web 服务器本身可以启动多个进程。因此,使用较大的内存限制值可能会阻塞服务器。除此之外,任何可能实际消耗大量内存的脚本都很容易成为性能优化的候选。将简单、经过深思熟虑的技术应用到我们的代码中可以大大减少内存使用。

当涉及到实际的内存管理时,这里的事情是相当自动化的。与我们自己管理内存的 C 语言不同,PHP 结合使用垃圾收集和引用计数机制。不必深入到机制本身的内部和外部,只要说变量不再使用时会自动释放就足够了。

For more details on garbage collection, check out http://php.net/manual/en/features.gc.php.

文件上传

上传文件是许多 PHP 应用程序的常见功能。这种功能非常常见,以至于 PHP 提供了一个方便的全局$_FILES变量,我们可以使用它来访问上传的文件,或者访问文件上传尝试背后的错误。

让我们看看下面的简单文件上传表单:

<form method="post" enctype="multipart/form-data">
  <input type="file" name="photo" />
  <input type="file" name="article" />
  <input type="submit" name="submit" value="Upload" />
</form>

为了让 PHP 拾取文件,我们需要将form method值设置为post,将enctype设置为multipart/form-data。一旦提交,PHP 将选择并适当填写$_FILES变量:

array(2) { 
  ["photo"] => array(5) { 
    ["name"] => string(9) "photo.jpg" 
    ["type"] => string(10) "img/jpeg" 
    ["tmp_name"] => string(14) "/tmp/phpGutI91" 
    ["error"] => int(0) 
    ["size"] => int(42497) 
  } 
  ["article"] => array(5) { 
    ["name"] => string(11) "article.pdf" 
    ["type"] => string(15) "application/pdf" 
    ["tmp_name"] => string(14) "/tmp/phpxsnx1e" 
    ["error"] => int(0) 
    ["size"] => int(433176)
  } 
}

在不深入实际上传后文件管理的细节的情况下,只需说$_FILES包含了足够的信息,我们可以选择并进一步管理文件,或者在上传过程中指出可能的错误代码。可以返回以下八个错误代码:

  • UPLOAD_ERR_OK
  • UPLOAD_ERR_INI_SIZE
  • UPLOAD_ERR_FORM_SIZE
  • UPLOAD_ERR_PARTIAL
  • UPLOAD_ERR_NO_FILE
  • UPLOAD_ERR_NO_TMP_DIR
  • UPLOAD_ERR_CANT_WRITE
  • UPLOAD_ERR_EXTENSION

虽然所有的错误都应该得到同样的解决,但其中两个(UPLOAD_ERR_FORM_SIZEUPLOAD_ERR_PARTIAL提出了关键的性能问题:我们可以上传多大的文件在上传过程中有没有超时

这两个问题的答案可以在配置指令中找到,其中一些指令与文件上载直接相关,而另一些指令与更通用的 PHP 选项相关:

  • session.gc_maxlifetime:数据被视为垃圾并被清理的秒数;默认值为 1440 秒
  • session.cookie_lifetime:这是 cookie 的生存期,以秒为单位;默认情况下,cookie 在浏览器关闭之前有效
  • max_input_time:这是允许脚本解析输入数据(如 POST)的最长时间(秒);默认情况下,此选项处于禁用状态
  • max_execution_time:脚本终止前允许运行的最长时间;默认为 30 秒
  • upload_max_filesize:上传文件的最大大小;默认为 2 兆字节(2M)
  • max_file_uploads:这是单个请求中允许上载的最大文件数
  • post_max_size:这是允许的 post 数据的最大大小;它默认为 8 兆字节(8M)

调整这些选项可以确保我们避免超时和计划大小限制。为了确保我们能够在流程早期避免最大文件大小限制,MAX_FILE_SIZE可以用作隐藏表单字段:

<form method="post" enctype="multipart/form-data">
  <input type="hidden" name="MAX_FILE_SIZE" value="100"/>
  <input type="file" name="photo"/>
  <input type="file" name="article"/>
  <input type="submit" name="submit" value="Upload"/>
</form>

MAX_FILE_SIZE字段必须位于表单可能具有的任何其他文件字段之前。它的值代表 PHP 接受的最大文件大小。

尝试上载一个大于MAX_FILE_SIZE隐藏字段定义的文件将导致一个$_FILES变量,类似于此处所示的变量:

array(2) {
  ["photo"] => array(5) {
    ["name"] => string(9) "photo.jpg"
    ["type"] => string(0) ""
    ["tmp_name"] => string(0) ""
    ["error"] => int(2)
    ["size"] => int(0)
  }
  ["article"] => array(5) {
    ["name"] => string(11) "article.pdf"
    ["type"] => string(0) ""
    ["tmp_name"] => string(0) ""
    ["error"] => int(2)
    ["size"] => int(0)
  }
}

我们可以看到误差现在变成了值2,它等于UPLOAD_ERR_FORM_SIZE常数。

虽然通常我们会通过代码优化来解决默认配置的限制,但文件上传是特定的;在这方面,我们确实需要确保在需要时可以上传大文件。

会话处理

会话在 PHP 中是一种有趣的机制,允许我们在总体上是无状态通信的情况下维护状态。我们可以将它们可视化为保存到文件中的每个用户的序列化信息数组。我们使用它们在不同的页面上存储特定于用户的信息。默认情况下,会话依赖于 cookie,但可以将其配置为在浏览器中使用SID参数。

PHP 会话的 cookie 版本大致如下所示:

  1. 从 cookie 中读取会话令牌。
  2. 在磁盘上创建或打开现有文件。
  3. 锁定文件以便写入。
  4. 读取文件的内容。
  5. 将文件数据放入全局$_SESSION变量中。
  6. 设置缓存头。
  7. 将 cookie 返回到客户端。
  8. 在每个页面请求上,重复步骤 1-7。

除了 cookie 部分,PHP 会话的SID 版本的工作方式基本相同。这里的 cookie 被我们通过 URL 推送的 SID 值替换。

会话机制可以用于各种用途,其中一些包括用户登录机制、存储次要数据缓存、部分模板等。根据使用情况,这可能会提出最大会话大小的问题。

默认情况下,当脚本执行时,会话从文件读取到内存中。因此,会话文件的最大大小不能超过memory_limit指令,该指令默认为 128 MB。通过定义自定义会话处理程序,我们可以绕过这个默认会话行为。session_set_save_handler()函数允许我们注册一个自定义会话处理程序,它必须符合SessionHandlerInterface接口。使用自定义会话处理程序,我们可以从文件机制转移到在数据库中存储会话数据。这样做的附加好处是更高的性能效率,因为我们现在能够在负载平衡器后面创建可伸缩的 PHP 环境,所有应用程序节点都连接到中心会话服务器。

Redis and memcached are two data stores that are quite popular among PHP developers. The Magento 2 e-commerce platform supports both Redis and memcached for external session storage.

虽然会话存储在性能方面起着关键作用,但有几个配置指令值得注意:

  • session.gc_probability:默认为 1
  • session.gc_divisor:默认为 100
  • gc_maxlifetime:默认为 1440 秒(24 分钟)

gc_probabilitygc_divisor指令协同工作。它们的比率(gc_ 概率/gc_ 除数=>1/100=>1%定义了垃圾收集器在每个session_start()调用上运行的概率。一旦垃圾收集器运行,gc_maxlifetime的值指令告诉它某个东西是否应该被视为垃圾,并可能被清除。

对于高性能站点,会话很容易成为瓶颈。经过深思熟虑的调优和会话存储选择可以带来恰到好处的性能差异

输出缓冲

输出缓冲是一种控制脚本输出的 PHP 机制。假设我们在 PHP 脚本中写下echo 'test';,但在屏幕上看不到任何东西。这怎么可能?答案是o输出缓冲

以下代码是输出缓冲的一个简单示例:

<?php

ob_start();
sleep(2);
echo 'Chunk#1' . PHP_EOL;
sleep(3);
ob_end_flush();

ob_start();
echo 'Chunk#2' . PHP_EOL;
sleep(5);
ob_end_clean();

ob_start();
echo 'Chunk#3' . PHP_EOL;
ob_end_flush();

ob_start();
sleep(5);
echo 'Chunk#4' . PHP_EOL;

//Chunk#1
//Chunk#3
//Chunk#4

在 CLI 环境中执行时,我们首先会看到几秒钟后Chunk#1出来,然后几秒钟后,我们会看到Chunk#3出来,最后,再过几秒钟,我们会看到Chunk#4出来。Chunk#2永远不会被输出。这是一个相当不错的概念,因为我们习惯于在调用echo构造之后输出内容。

有几个与输出缓冲相关的函数,其中以下五个是最有趣的函数:

  • ob_start():如果在另一个非关闭缓冲区之后调用,则触发一个新缓冲区并创建堆叠缓冲区
  • ob_end_flush():输出最上面的缓冲区并关闭该输出缓冲区
  • ob_end_clean():这会清理输出缓冲区并关闭输出缓冲
  • ob_get_contents():返回输出缓冲区的内容
  • ob_gzhandler():这是与ob_start()一起使用的回调函数,用于 GZIP 输出缓冲区

以下示例演示了堆叠缓冲区:

<?php

ob_start(); // BUFFER#1
sleep(2);
echo 'Chunk #1' . PHP_EOL;

 ob_start(); // BUFFER#2
 sleep(2);
 echo 'Chunk #2' . PHP_EOL;
 ob_start(); // BUFFER#3
 sleep(2);
 echo 'Chunk #3' . PHP_EOL;
 ob_end_flush();
 ob_end_flush();

sleep(2);
echo 'Chunk #4' . PHP_EOL;
ob_end_flush();

//Chunk #1
//Chunk #2
//Chunk #3
//Chunk #4

这里的整个输出被保留了大约 8 秒,之后所有四个Chunk#...字符串都被同时输出。这是因为ob_end_flush()函数是唯一将输出发送到控制台的函数,而ob_end_flush()函数只是关闭缓冲区,将其传递给代码中存在的父缓冲区。

使用ob_get_contents()功能可以进一步增加输出缓冲的动态性,如下例所示:

<?php

$users = ['John', 'Marcy', 'Alice', 'Jack'];

ob_start();
foreach ($users as $user) {
  echo 'User: ' . $user . PHP_EOL;
}
$report = ob_get_contents();
ob_end_clean();

ob_start();
echo 'Listing users:' . PHP_EOL;
ob_end_flush();

echo $report;

echo 'Total of ' . count($users) . ' users listed' . PHP_EOL;

//Listing users:
//User: John
//User: Marcy
//User: Alice
//User: Jack
//Total of 4 users listed

ob_get_content()函数允许我们获取存储在缓冲区中的内容的字符串表示形式。我们可以选择是进一步修改内容、输出内容,还是将内容传递给其他构件。

所有这些如何应用于网页?毕竟,我们对脚本的性能感兴趣,主要是在网页上下文中。在没有输出缓冲的情况下,当 PHP 通过我们的脚本时,HTML 被分块发送到浏览器。通过输出缓冲,HTML 作为脚本末尾的一个字符串发送到浏览器

记住ob_start()函数接受回调函数,我们可以使用回调函数进一步修改输出。这种修改可以是任何形式的过滤,甚至是压缩。

以下示例演示了输出筛选的使用:

<?php

ob_start('strip_away');
echo '<h1>', 'Bummer', '</h1>';
echo '<p>', 'I felt foolish and angry about it!', '</p>';
ob_end_flush();

function strip_away($buffer)
{
  $keywords = ['bummer', 'foolish', 'angry'];
  foreach ($keywords as $keyword) {
    $buffer = str_ireplace(
      $keyword,
      str_repeat('X', strlen($keyword)),
      $buffer
    );
  }
  return $buffer;
}

// Outputs:
// <h1>XXXXXX</h1><p>I felt XXXXXXX and XXXXX about it!</p>

然而,如今,我们不太可能自己编写这种结构,因为框架抽象为我们掩饰了它。

禁用调试消息

Ubuntu 服务器是一个流行的、免费的、开源的 Linux 发行版,我们可以使用它快速建立一个LAMPLinux、Apache、MySQL、PHP堆栈。Ubuntu 服务器的易安装性和长期支持使其成为 PHP 开发人员的热门选择。使用干净的服务器安装,我们只需执行以下命令即可启动并运行 LAMP 堆栈:

sudo apt-get update && sudo apt-get upgrade
sudo apt-get install lamp-server^ 

完成这些操作后,访问外部服务器 IP 地址,我们将看到一个 Apache 页面,如以下屏幕截图所示:

我们在浏览器中看到的 HTML 源于/var/www/html/index.html文件。在将index.html替换为index.php之后,我们可以很好地使用 PHP 代码。

这个类似 Ubuntu 服务器的介绍是为了强调某些服务器默认设置。在所有配置指令中,我们绝不应该盲目接受错误记录错误显示的默认设置在开发环境和生产环境之间的不断切换使得在浏览器中暴露机密信息变得太容易,或者忽略记录正确的错误。

考虑到这一点,我们假设在新安装的 Ubuntu 服务器 LAMP 堆栈上有以下损坏的index.php文件:

<?php

echo 'Test;

尝试在浏览器中打开时,Apache 将发送回HTTP 500 Internal Server Error,最终用户可能会看到该HTTP 500 Internal Server Error,具体取决于浏览器,如以下屏幕截图所示:

理想情况下,我们会让我们的 web 服务器配置一个样式良好的通用错误页面,以使其更加用户友好。虽然浏览器响应可能会让最终用户满意,但在这种情况下,它肯定不会让开发人员满意。返回的信息没有显示任何有关错误性质的信息,因此很难修复错误。幸运的是,对于我们来说,这种情况下的默认灯堆栈配置包括将错误记录到一个/var/log/apache2/error.log文件:

[Thu Feb 02 19:23:26.026521 2017] [:error] [pid 5481] [client 93.140.71.25:55229] PHP Parse error: syntax error, unexpected ''Test;' (T_ENCAPSED_AND_WHITESPACE) in /var/www/html/index.php on line 3

虽然这种行为对于生产来说是完美的,但是对于开发环境来说却很麻烦。在开发时,我们真的希望我们的错误显示在浏览器中,只是为了加快速度。PHP 允许我们通过几个配置指令控制错误报告和日志记录行为,以下是最重要的:

  • error_reporting:这是我们希望监控的错误级别;我们可以使用管道(|操作符)列出几个错误级别常量。其默认值为E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED
  • display_errors:指定是将错误发送到浏览器/CLI 还是对用户隐藏错误。
  • error_log:这是我们要记录 PHP 错误的文件。
  • log_errors:这告诉我们是否应该将错误记录到error_log文件中。

可用的错误级别常量定义如下:

  • E_ERROR (1)
  • E_WARNING (2)
  • E_PARSE (4)
  • E_NOTICE (8)
  • E_CORE_ERROR (16)
  • E_CORE_WARNING (32)
  • E_COMPILE_ERROR (64)
  • E_COMPILE_WARNING (128)
  • E_USER_ERROR (256)
  • E_USER_WARNING (512)
  • E_USER_NOTICE (1024)
  • E_STRICT (2048)
  • E_RECOVERABLE_ERROR (4096)
  • E_DEPRECATED (8192)
  • E_USER_DEPRECATED (16384)
  • E_ALL (32767)

使用error_reporting()ini_set()函数,我们可以使用其中一些指令来配置运行时的日志记录和显示:

<?php

error_reporting(E_ALL);
ini_set('display_errors', 'On');

careful using ini_set() for display_errors as it won't have any effect if the script has fatal errors, simply because runtime does not get executed.

错误显示和错误日志记录是两种不同的机制,它们彼此协同工作。虽然我们可能会从开发环境中的错误显示中受益更多,但错误日志记录是生产环境的一种方式。

Zend OPcache

PHP 的一个主要缺点是它会在每个请求上加载和解析 PHP 脚本。PHP 代码是用纯文本编写的,首先编译成操作码,然后执行操作码。虽然这种性能影响在总共只有一个或几个脚本的小型应用程序中可能不明显,但它与较大的平台(如 Magento、Drupal 等)有很大区别。

从 PHP5.5 开始,这个问题有一个现成的解决方案。Zend OPcache 扩展通过将编译后的操作码存储在共享内存(RAM)中来解决重复编译问题。打开或关闭它只是更改配置指令的问题。

有相当多的配置指令,其中一些将帮助我们开始:

  • opcache.enable:默认为 1,可通过PHP_INI_ALL修改。
  • opcache.enable_cli:默认为 0,可通过PHP_INI_SYSTEM修改。
  • opcache.memory_consumption:默认为 64,可通过PHP_INI_SYSTEM修改,定义 OPcache 使用的共享内存大小。
  • opcache.max_accelerated_files:默认为 2000,可通过PHP_INI_SYSTEM修改,定义 OPcache 哈希表中的最大键/脚本数。其最大值为 1000000。
  • opcache.max_wasted_percentage:默认为 5,可通过PHP_INI_SYSTEM进行更改,该选项定义了在计划重新启动之前允许的最大内存浪费百分比。

虽然opcache.enable被标记为PHP_INI_ALL,但在运行时使用ini_set()启用它将不起作用。只有使用ini_set()禁用它才能工作。

尽管 Zend OPcache 完全自动化,但它也提供了一些功能供我们使用:

  • opcache_compile_file():编译并缓存脚本,而不执行脚本
  • opcache_get_configuration():获取 OPcache 配置信息
  • opcache_get_status():取 OPcache 信息
  • opcache_invalidate():这将使 OPcache 无效
  • opcache_is_script_cached():这告诉我们脚本是否通过 OPcache 缓存
  • opcache_reset():这将重置 OPcache 缓存

虽然我们不太可能单独使用这些方法,但它们对于为我们处理 OPcache 的实用工具来说确实很有用。

The opcache-gui tool shows OPcache statistics, settings, and cached files whilst providing a real-time update. This tool is available for download at https://github.com/amnuts/opcache-gui.

OPcache 需要注意的一点是它潜在的缓存猛击问题。使用memory_consumptionmax_accelerated_filesmax_wasted_percentage配置指令,OPcache 确定何时刷新缓存。发生这种情况时,具有大量流量的服务器可能会遇到缓存 slam 问题,大量请求同时生成相同的缓存项。因此,我们应该尽量避免频繁的缓存刷新。要做到这一点,我们可以使用缓存监控工具并调整三个配置指令以适应我们的应用程序大小。

并发性

虽然并发性是一个适用于多层堆栈的主题,但是每个开发人员都应该熟悉一些关于 web 服务器的配置指令。并发是指在 web 服务器内处理多个连接。PHP、Apache 和 Nginx 这两个最流行的 web 服务器都允许处理多个连接的一些基本配置。

虽然对于哪种服务器更快,但 Apache 与 MPM 事件模块几乎相当于 NGNX 性能。

以下指令规定了 Apache MPM 事件并发性,因此值得注意:

  • ThreadsPerChild:这是每个子进程创建的线程数
  • ServerLimit:这是对可配置进程数的限制
  • MaxRequestWorkers:这是同时处理的最大连接数
  • AsyncRequestWorkerFactor:这是每个进程并发连接的限制

可以使用以下公式计算可能并发连接的绝对最大数量:

max_connections = (AsyncRequestWorkerFactor + 1) * MaxRequestWorkers

公式相当简单;然而,改变AsyncRequestWorkerFactor不仅仅是冲入更高的配置值。我们需要对影响 web 服务器的流量有扎实的了解,这意味着需要进行广泛的测试和数据收集。

以下指令规定了 Nginx 并发性,因此值得关注:

  • worker_processes:工人进程数;它默认为 1
  • worker_connections:工作进程可以同时打开的最大连接数;默认为 512

Nginx 可以服务的理想用户总数可以归结为以下公式:

max_connections = worker_processes * worker_connections

虽然我们几乎没有触及 web 服务器并发性的表面以及这两个 web 服务器的总体配置指令,但前面的信息应该作为起点。虽然开发人员通常不调优 web 服务器,但他们应该知道何时标记可能影响其 PHP 应用程序性能的错误配置。

总结

在本章中,我们讨论了 PHP 性能优化的一些方面。虽然这些只是总体性能主题的皮毛,但它们概括了每个 PHP 开发人员都应该非常熟悉的最常见领域。广泛的配置指令允许我们调整通常与 web 服务器本身协同工作的应用程序行为。然而,最佳性能的关键在于在整个堆栈中充分利用资源,正如我们通过简单的 SQL 查询示例所观察到的那样。

接下来,我们将研究无服务器体系结构,这是标准开发环境的一种新兴抽象。