为什么 Laravel 会占用这么多 CPU?

即使是最简单的 Laravel “hello world” 应用,大部分 CPU 时间也消耗在应用代码之外。通过 OpenResty XRay 进行 Laravel CPU 分析可以发现,Service Provider 的启动和注册才是头号 CPU 消耗者——而不是你的路由处理函数。具体来说:

  • 最热 PHP 代码路径 #1(36.4% CPU)bootProvider —— Laravel 在每次请求时都会启动所有已注册的 Service Provider(Carbon 日期库、Ignition 等)。
  • 最热 PHP 代码路径 #2(14.5% CPU)register —— Service Provider 的注册和解析过程(resolveProviderDatabaseServiceProvider::register 等)同样在每次新请求时执行。
  • 最热 PHP 代码路径 #3(12.8% CPU):实际的 “hello world” 响应——只有一小部分 CPU 用在了你自己的代码上。

换句话说,在典型的 Laravel 高 CPU 使用率场景中,框架引导开销占据了主导地位。下面的教程将详细介绍如何使用 OpenResty XRay CPU 火焰图来识别这些热路径,帮助你了解优化精力应该集中在哪里。

如果你正在排查更广泛的 PHP 应用高 CPU 问题,许多相同的分析技术同样适用。


下面的演练展示了我们是如何得出这些结论的——从选择目标 PHP 进程到解读 PHP 层的 CPU 火焰图——以便你在自己的 Laravel 应用上重复同样的分析。

测试环境:一个跑满 CPU 的 Hello World 应用

我们用 PHP 的 Laravel 框架搭建了一个简单的 “hello world” Web 应用。

使用 PHP Laravel 框架搭建的 hello world Web 应用

在这里定义了一个请求处理函数,它会返回一个 “Hello, world” 的响应。

返回 Hello, world 响应的 Laravel 路由处理函数

使用 curl 命令访问 Laravel 的 HTTP 接口。响应体确实是 “hello world”。

curl 命令访问 Laravel HTTP 接口并返回 hello world

运行 top 命令来检查目标 PHP 进程的 CPU 使用情况。

这是之前展示过的 PHP “hello world” 服务进程。

top 命令显示 Laravel PHP hello world 服务进程

为了获得清晰的 CPU 分析数据,我们事先使用客户端压测工具将 CPU 使用率压满到 100%。

top 显示压测下 Laravel PHP 进程 CPU 占满 100%

运行 ps 命令确认该进程使用的是 Linux 发行版自带的标准 php 二进制可执行文件。

ps 命令确认 Laravel 应用使用标准 php 二进制文件

Laravel CPU 分析:三条最热代码路径

接下来,我们使用 OpenResty XRay 来查看 CPU 时间是如何分布在 PHP 进程内部的各个代码路径上的。在 OpenResty XRay Web 控制台中,确认正在监控正确的机器,打开 “Guided Analysis” 页面,选择 “High CPU usage” 作为要诊断的问题类型。

OpenResty XRay 引导式分析中选择 High CPU usage 问题类型

接下来,选择 PHP 应用并选取占用近 100% CPU 的工作进程——与之前在 top 中看到的同一个进程(此处为 PID 2092,CPU 占用 93%)。

选择占用 93% CPU 的 PHP 工作进程 PID 2092 进行分析

OpenResty XRay 自动检测应用类型,可以同时分析多个语言级别,因此保持 PHP 和 C/C++ 同时选中,最长分析时间保持默认的 300 秒。启动后,系统进行多轮分析,交替采样 C 层和 PHP 层的 CPU 火焰图。两轮即可满足本例需求,因此在此停止分析。

语言级别设为 PHP 和 C/C++,最大分析时间 300 秒

OpenResty XRay 随后自动生成了一份分析报告。

OpenResty XRay 自动生成的 Laravel CPU 分析报告

报告标记了我们诊断的问题类型:CPU。

报告标记的问题类型为 CPU

这是 CPU 资源消耗最高的 C 代码路径,占用了 96.2% 的 CPU 时间。

占用 96.2% CPU 的最热 C 代码路径

第一个 zend_execute 函数用于解释和执行 PHP 操作码。

火焰图中解释执行 PHP 操作码的 zend_execute 函数

当服务器收到一个新的 HTTP 请求时,php_cli_server_dispatch_router 函数会被调用来读取和解析请求数据。

读取并解析 HTTP 请求的 php_cli_server_dispatch_router 函数

main 函数帧表明本演示运行在 PHP CLI 启动的内置 Web 服务器上。生产环境部署在 php-fpm 上时,这一层 C 级别的服务器分派帧会有所不同,但下面的 Laravel 框架层 PHP 热点才是重点关注对象。

显示 PHP CLI 内置 Web 服务器的 main 函数帧

展开该条目可以查看完整的 C 层调用路径,从 _start 进程入口点,经过服务器事件循环,一直到 PHP 操作码解释器。

从 _start 到 PHP 操作码解释器的完整 C 层调用路径

这条热代码路径是由这个 C 语言级别的 CPU 火焰图自动推导出来的。

OpenResty XRay 生成的 C 层 CPU 火焰图

下面是对当前问题更详细的解释和建议。

其中提到了我们之前看到的 zend_execute 函数。

报告解释中提及 zend_execute 函数

现在来看 PHP 层的结果。最热 PHP 代码路径 #1 单独就消耗了 36.4% 的 CPU 时间。

占用 36.4% CPU 的最热 PHP 代码路径

bootProvider 函数是 Laravel 框架的一部分,该函数负责启动应用注册的 Service Provider。

启动已注册 Service Provider 的 Laravel bootProvider 函数

完整路径显示请求从 public/index.php 和 HTTP 内核进入,然后通过 array_walk 遍历所有已注册的 Provider,展开到 Service Provider 的启动过程。

从 public/index.php 经 HTTP 内核进入 Service Provider 启动的调用路径

报告还包含对该代码路径的自动生成解释:bootProvider 启动应用注册的每个 Service Provider,而 Service Provider 是 Laravel 应用配置的核心位置。

bootProvider 启动 Service Provider 路径的自动生成解释

放大 PHP 层 CPU 火焰图,可以看到 bootProvider 帧以及其下方正在启动的各个 Service Provider。

放大到 bootProvider 帧及其下方 Service Provider 的 PHP 层 CPU 火焰图

Laravel 的 ServiceProvider 程序中,ServiceProvider::boot 方法用于为 Carbon 日期库注册宏和配置。相应的 boot 方法用于初始化时区等设置。

为 Carbon 日期库初始化时区设置的 ServiceProvider boot 方法

IgnitionServiceProvider::boot 方法负责启动所有的 Service Provider。

启动所有 Service Provider 的 IgnitionServiceProvider boot 方法

再来看第二热 PHP 代码路径 #2,它消耗了 14.5% 的 CPU 时间。

占用 14.5% CPU 的第二热 PHP 代码路径

这个 register 函数在应用引导过程中运行,负责解析和注册 Service Provider。由于 Laravel 在每次请求时都会创建新的应用实例(除非使用 Laravel Octane 等常驻内存方案),这一开销会反复产生。

在应用引导中解析并注册 Service Provider 的 Laravel register 函数

完整路径与上面的启动路径类似:请求从 public/index.php 和 HTTP 内核进入,然后深入到 registerConfiguredProviders

从 public/index.php 深入 registerConfiguredProviders 的调用路径

自动生成的解释分解了同样的调用序列,从 index.php 中的应用入口点开始。

从 index.php 入口开始的 register 路径自动生成解释

在放大的 PHP 层 CPU 火焰图中,Application::register 帧在周围的引导帧中被高亮显示。

PHP 层 CPU 火焰图中高亮的 Application::register 帧

resolveProvider 是 Laravel 框架中的一个方法,用于解析和注册 Service Provider。

解析并注册 Service Provider 的 Laravel resolveProvider 方法

DatabaseServiceProvider::register 方法在 Laravel 中负责向服务容器注册数据库服务及其相关组件。

注册 Laravel 数据库服务的 DatabaseServiceProvider register 方法

第三热的代码路径消耗了 12.8% 的 CPU 时间。

占用 12.8% CPU 的第三热 PHP 代码路径

其调用链经过 Laravel 的路由管道——Router::dispatch、中间件栈和 ControllerDispatcher——最终到达控制器。

经过 Router::dispatch、中间件栈和 ControllerDispatcher 的调用链

这就是实际实现 “hello world” 响应的路径:我们自己的处理函数代码,加上围绕它的路由和响应机制。

实现 Laravel hello world 响应的代码路径

注意,#1 和 #2 路径都属于同一个请求级别的引导过程:一个负责启动 Service Provider,另一个负责注册它们。两者合计占了超过一半的 CPU 时间——还未执行任何应用逻辑。

Service Provider 启动与注册路径合计超过一半 CPU 时间

作为参考,这里提供了 Laravel “hello world” 应用与等效 OpenResty 处理函数的吞吐量对比:371 请求/秒 vs 28,000 请求/秒——大约 75 倍的差距。这一对比并非完全对等,因为 Laravel 是一个全栈框架,而 OpenResty 的每请求抽象层要轻量得多,但它说明了上面测量到的框架开销在实际中的代价。如果你的 Laravel 应用还存在高内存消耗问题,OpenResty XRay 同样可以进行分析。

吞吐量对比:Laravel 371 请求/秒 对比 OpenResty 28,000 请求/秒

自动 CPU 使用率分析与报告

OpenResty XRay 还可以自动监控在线进程,无需任何手动步骤。“Insights” 页面收集每个应用的日报和周报——包含上述相同的 CPU 分析结果和热代码路径分解——因此日常运行中无需使用 “Guided Analysis”。引导式分析在开发阶段和按需深入分析(如本教程)时仍然很实用。

Insights 页面显示 Laravel CPU 的自动日报和周报

常见问题:Laravel 高 CPU 使用率

Laravel 的 Service Provider 每次请求都会执行吗?

会。在标准的 Laravel 部署中,每次请求都会创建一个新的应用实例,因此所有 Service Provider 每次都会被注册和启动。在上面的分析中,register(14.5% CPU)和 bootProvider(36.4% CPU)都是逐请求执行的——两者合计超过一半的 CPU,而这些都发生在你的路由逻辑运行之前。这种逐请求的引导过程正是 Laravel 高 CPU 使用率场景中的主导开销。

Laravel Octane 能降低这种 CPU 开销吗?

本文测到的高开销来自每次请求都重复进行 Service Provider 的注册和启动。像 Laravel Octane 这样的常驻内存方案会让应用实例常驻内存,而不是每请求重建,因此这部分引导工作不会被反复执行。如果你的 Laravel 高 CPU 使用率主要由 registerbootProvider 路径主导,这正是此类方案要消除的开销。

如何定位 Laravel 应用中最热的代码路径?

使用 OpenResty XRay 的引导式分析剖析运行中的 PHP 进程。它会同时生成 C 层(zend_execute 和 PHP 虚拟机)和 PHP 层(Laravel 框架函数)的 CPU 火焰图,并按 CPU 时间占比对最热的路径排序——本例中为 bootProvider(36.4%)、register(14.5%)和路由响应(12.8%)。这样就能明确看出是哪些框架函数在主导 CPU,而不必靠猜测。

处理 hello world 响应时 OpenResty 比 Laravel 快多少?

在上面的吞吐量对比中,这个 Laravel “hello world” 应用为 371 请求/秒,而等效的 OpenResty 处理函数约为 28,000 请求/秒——大约 75 倍的差距。这一对比并非完全对等,因为 Laravel 是全栈框架,而 OpenResty 每请求的抽象层要轻量得多,但它说明了框架引导开销在实际中的代价。

关于 OpenResty XRay

OpenResty XRay 是一个动态追踪产品,它可以自动分析运行中的应用,以解决性能问题、行为问题和安全漏洞,并提供可行的建议。在底层实现上,OpenResty XRay 由我们的 Y 语言 驱动,可以在不同环境下支持多种不同的运行时,如 Stap+、eBPF+、GDB 和 ODB。

关于作者

章亦春是开源 OpenResty® 项目创始人兼 OpenResty Inc. 公司 CEO 和创始人。

章亦春(Github ID: agentzh),生于中国江苏,现定居美国湾区。他是中国早期开源技术和文化的倡导者和领军人物,曾供职于多家国际知名的高科技企业,如 Cloudflare、雅虎、阿里巴巴, 是 “边缘计算”、“动态追踪 “和 “机器编程 “的先驱,拥有超过 22 年的编程及 16 年的开源经验。作为拥有超过 4000 万全球域名用户的开源项目的领导者。他基于其 OpenResty® 开源项目打造的高科技企业 OpenResty Inc. 位于美国硅谷中心。其主打的两个产品 OpenResty XRay(利用动态追踪技术的非侵入式的故障剖析和排除工具)和 OpenResty Edge(最适合微服务和分布式流量的全能型网关软件),广受全球众多上市及大型企业青睐。在 OpenResty 以外,章亦春为多个开源项目贡献了累计超过百万行代码,其中包括,Linux 内核、Nginx、LuaJITGDBSystemTapLLVM、Perl 等,并编写过 60 多个开源软件库。

关注我们

如果您喜欢本文,欢迎关注我们 OpenResty Inc. 公司的博客网站 。也欢迎扫码关注我们的微信公众号:

我们的微信公众号

翻译

我们提供了英文版原文和中译版(本文)。我们也欢迎读者提供其他语言的翻译版本,只要是全文翻译不带省略,我们都将会考虑采用,非常感谢!