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

现代高效 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。

ini 复制代码
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 默认是禁用的,必须通过配置用于生成机器代码的额外内存缓冲区来启用。

ini 复制代码
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 进程的数量。

ini 复制代码
pm = static
pm.max_children = 20

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

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

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

php 复制代码
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 趋势的了解是值得的。

相关推荐
Victor3562 小时前
Redis(164)如何使用Redis实现排行榜?
后端
Victor3562 小时前
Redis(165)如何使用Redis实现推荐系统?
后端
百万蹄蹄向前冲6 小时前
Trae Genimi3跟着官网学实时通信 Socket.io框架
前端·后端·websocket
狂炫冰美式7 小时前
TRAE SOLO 驱动:重构AI模拟面试产品的复盘
前端·后端·面试
x***38169 小时前
springboot和springframework版本依赖关系
java·spring boot·后端
Xudde.10 小时前
friendly2靶机渗透
笔记·学习·安全·web安全·php
韩立学长10 小时前
基于Springboot课堂教学辅助系统08922bq1(程序、源码、数据库、调试部署方案及开发环境)系统界面展示及获取方式置于文档末尾,可供参考。
数据库·spring boot·后端
盖世英雄酱5813610 小时前
java深度调试技术【第六七八章:宽字节与多字节】
java·后端