现代高效 PHP 开发的最佳实践

AI摘要
本文分享了现代高效 PHP 开发的关键运维实践。核心建议包括:优化 OPcache 配置以提升性能,谨慎评估 JIT 的适用场景,合理设置内存限制与超时时间以保障稳定性,并根据应用负载科学配置 PHP-FPM 进程。此外,文章推荐关注 FrankenPHP 运行时,并积极采用消息队列处理异步任务。

现代高效 PHP 开发的最佳实践

PHP 已经走过 30 年,是编程语言中的稳定常量——在不断变化的技术环境中可靠的锚。然而,即使对于 PHP 运维,你也应该始终质疑长期存在的假设。随着 Web 的持续演进,PHP 也必须适应与其他技术的交互,并满足稳定性和性能要求。在本文中,我们将探讨当前的最佳实践和现代工具,这些将在未来几年对 PHP 运维非常重要。

原文 现代高效 PHP 开发的最佳实践

配置 OPcache

OPcache 集成到 PHP 中已经带来了过去十年显著的性能提升。OPcache 负责缓存 PHP 脚本到字节码的转换并执行次要优化。这意味着脚本只需要通过编译器运行一次,显著加快了执行速度。

即使在今天,每个 PHP 版本对 OPcache 优化器的持续改进仍然确保进一步的(尽管较小的)性能提升。例如,自 PHP 8.4 起,sprintf 会自动转换为更快的字符串插值;在即将发布的 PHP 8.5 中,=== [] 的测试将在内部进行优化。

作为开发者,我们学会了依赖 OPcache 在我们不费任何力气的情况下提供显著效果。然而,随着框架和库的日益专业化,应用程序使用的 PHP 文件数量迅速增长——因此,php.ini 中 OPcache 的默认值很快就达到了极限。

即使 64 MB 的 opcache.memory_consumption 对于较小的应用程序足够,opcache.max_accelerated_files 设置通常是一个障碍,因为它将 OPcache 中的文件数量限制为 10,000。通常,这两个值都需要增加。

内部字符串缓存的设置也很重要,因为它决定了字符串或名称是从所有 PHP 进程的共享内存中重新分配,还是为每个进程单独分配。opcache.interned_strings_buffer 的默认值 8 MB 几乎总是太低,应该增加到最大值 32 MB。

opcache.memory_consumption=128
opcache.max_accelerated_files=100000
opcache.interned_strings_buffer=32

opcache_get_status() 函数可以检查使用的资源是否仍在设定的限制内。请注意,该函数必须在 Web 上下文中执行(Apache mod_php 或 PHP-FPM),因为 OPcache 在 CLI 中是单独运行的。

OPcache 预加载提供了更多的性能优化潜力,每个请求可以节省高达 10 毫秒。当 PHP 进程启动时,文件列表会被集中编译并直接加载到 OPcache 中。代码在启动时立即可用,不再需要自动加载。生成预加载文件可能有点棘手,你必须遵循正确的顺序。Symfony 为你完成这项工作并自动创建预加载文件。

JIT

自 PHP 8.0 以来,该语言包含了即时编译器(JIT),并在 PHP 8.4 中进行了根本性的重新设计。JIT 将 PHP 代码转换为机器代码,这应该比使用虚拟机和 OPcache 更快地执行。然而,JIT 默认是禁用的,必须通过配置用于生成机器代码的额外内存缓冲区来启用。

opcache.jit_buffer_size=100M
opcache.jit=tracing

通常,Web 应用程序尚未从 JIT 中受益。在某些情况下可以测量到微小的速度优势,而在其他情况下,甚至会有轻微的性能损失。在我看来,现在不值得花时间在 JIT 上来使你的 Web 应用程序更快。

然而,一些工具如 Psalm 和 PHPStan 正在命令行上尝试使用 JIT 来获得性能提升。在你自己的应用程序中,只有对于由 PHP 代码执行驱动的复杂任务,才值得测试 JIT。

但同样重要的是要注意,应该检查和监控执行的稳定性和正确性。JIT 执行还没有像常规 VM 那样经过充分测试。

内存限制

配置的最大 memory_limit 直接影响有多少 PHP 进程可以并行处理请求,而不需要操作系统介入管理短缺。

少数端点(批量导入、导出或报告)的高内存需求通常被用作将 memory_limit 的值从默认值(128 MB)设置为显著更高值的理由。

这允许低效的内存消耗者在核心、频繁使用的端点中不被注意地蔓延。这会导致性能问题,因为分配和释放内存也需要时间。最好降低 memory_limit——例如,降到 32 MB——然后在运行时或使用单独的 PHP-FPM 池为单个端点选择性地增加它。

这需要主动监控 PHP 的 error_log 以持续识别内存消耗增长的端点。

超时和执行时间

由于 PHP 的进程模型,同时请求的最大数量是有限的,不能任意高。这个值通常在两位数或低三位数范围内,取决于服务器的大小和数量。

由机器人或用户操作触发的长时间运行的请求可能会阻塞处理能力,并挤掉更重要的、业务关键的请求。这会导致 HTTP 503 错误(”服务不可用”),即使服务器没有完全利用。它只是没有可用的空闲 PHP 进程。

特别是在负载下,默认的 PHP 设置通常随机优先处理,而不是优先处理重要的页面功能。

首先,max_execution_time 通常应该从 30 秒设置为更低的值,并为重要的脚本和端点选择性地增加。

然而,这还不够,因为 max_execution_time 在 Linux 下是以 CPU 时间来衡量的。这意味着执行 sleep(30) 的脚本离最大执行时间一毫秒都没有接近。这同样适用于数据库查询、HTTP 以及任何其他 I/O 和系统调用时间。

这些 I/O 操作通常默认没有超时,例如 cURL 或数据库查询。或者它们使用 60 秒的高超时设置 default_socket_timeout 用于流,如通过 file_get_contents 或 Predis 库的 HTTP 查询。通常,你也可以分别为连接时间和传输时间配置超时。

如果超时配置没有被明确减少,可能会发生不必要的过载和多米诺效应。让我们看一个例子:一个高频、非常快的脚本由于 Redis 故障(远程字典服务器,一种将数据存储在 RAM 中的键值存储)而花费 2-5 秒而不是几毫秒,因为它在显示错误之前等待连接超时。然后,所有 PHP 进程都在忙于等待超时,而其他实际上不需要 Redis 的脚本被 Web 服务器拒绝。

终止脚本的可靠方法是 PHP-FPM 配置 request_terminate_timeout。然而,缺点是 PHP 进程不会被干净地终止。

PHP-FPM

在 80% 的情况下,你会使用 FPM 和 FastCGI 运行 PHP。对于在自己硬件上运行的应用程序,你应该始终静态定义池大小,并根据可用的 CPU 核心和 RAM 确定 PHP 进程的数量。

pm = static
pm.max_children = 20

确切的值可以使用计算器计算,主要取决于应用程序的 memory_limit 以及它主要是 I/O 受限还是 CPU 受限。

你可以通过测量来确定 CPU 或 RAM 是否是应用程序的瓶颈。Linux 的调度器统计提供了一种简单的方法来找出进程/请求等待 CPU 分配的时间。如果该数字在负载下增加,应用程序可能是 CPU 受限的;否则,它可能是 RAM 受限的。

输出可以在脚本的开始和结束时测量。差值可以被记录用于分析。

function current_runqueue_wait(): int {
    return (int) explode(" ", file_get_contents("/proc/self/schedstat"))[1];
}

$startRunQueueWait = current_runqueue_wait();
$application->run();

file_put_contents("/var/log/php/application_runqueue.log", sprintf("[%s] %s",
    date('c'),
    current_runqueue_wait() - $startRunQueueWait,
));

FrankenPHP

新运行时 FrankenPHP 作为 PHP-FPM 的替代品正在享受越来越高的人气。自 2025 年 5 月以来,它在 GitHub 上的 PHP 仓库中维护,并得到 PHP 基金会的支持。

FrankenPHP 相比 PHP-FPM 的一个主要优势是它可以作为一个大型单一二进制文件交付,不需要额外的 Web 服务器。这使得 Docker 中的部署更容易操作。

FrankenPHP 有两种模式:在经典模式下,它的行为类似于 PHP-FPM,使用最大线程数(而不是进程)工作。Worker 模式在同一进程中顺序处理多个 PHP 请求,放松了 shared nothing 架构,以便在多个请求之间共享框架引导开销,代价是内存安全性。

FrankenPHP 特别值得关注,尤其是对于每天超过一百万请求的大型应用程序。

消息队列

历史上,消息队列很少在 PHP 应用程序中使用,因为它们需要手动组装和监控许多组件。但今天不使用消息队列的人正在错过一个解决多个问题的强大工具:

  • 必须在 Web 服务器用户请求中处理的慢任务(例如,发送电子邮件)通常会对加载时间和用户体验产生负面影响
  • 如果任务不是由队列执行,而是由每分钟一次的 cron 作业在后台执行,执行可能会延迟多达一分钟
  • 组合许多任务的 cron 作业在出错时通常必须完全重新执行

消息队列的缺点是你必须担心队列中任务的并发性。这使得调试更加复杂。然而,后者可以通过队列中 worker 的规范日志来简化。

消息队列组件直接集成到 Laravel 和 Symfony 中,在最简单的情况下也可以使用 SQL 数据库或 Redis 运行。Shopware、Magento 和 Spryker 等应用程序也使用消息队列,易于访问。在这些框架或应用程序之外工作时,你也可以单独将 symfony/messenger 组件导入你的项目。

Worker 进程操作可以使用 SystemD 轻松集成到现有的 Linux 系统中。在基于容器的环境中,worker 只是作为与 Web 服务器分离的 PHP 进程在自己的容器中运行。

如果你很好地监控队列大小,worker 也可以在大型应用程序中有效使用,因为它们可以水平扩展。如果队列处理太慢,就会启动额外的 worker。

结论

即使在今天,运维 PHP 应用程序仍然令人兴奋且具有挑战性。为了获得最佳的稳定性和性能,保持对最新 PHP 趋势的了解是值得的。

本作品采用《CC 协议》,转载必须注明作者和本文链接
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!
开发 @ 家里蹲开发公司
文章
158
粉丝
87
喜欢
503
收藏
341
排名:18
访问:29.4 万
私信
所有博文
社区赞助商