21CTO 导读:本文为使用Laravel 框架时发生性能问题的实用解决方案。
我们在笔记本上开发Laravel 应用时可能非常流畅,但在生产环境中运行几个月后却会变得异常缓慢。
但是Laravel框架本身很少是真正的问题所在。更多时候,问题出在数据库规模扩大、循环加载关联数据、报表提取的数据量远超预期,或者第三方 API 悄然成为了请求的关键路径。
如果你想掌握如何解决Laravel在生产环境中的性能问题,请一定先克制住想要使用 Redis、Octane 或其他大型服务器的冲动。
第一步的动作,找出请求中不应该执行的操作。移除这些无用的操作通常比添加基础设施更能带来更大、更安全的性能提升。
以下,是我将采用的以生产环境为中心的优化流程。
1. 在修改代码之前,先测量慢请求
“仪表盘(Dashboard)运行缓慢”并非真正诊断结果。请选择一条路由,记录其响应时间、数据库查询次数、累计 SQL 执行时间、内存使用情况以及任何外部调用。然后使用类似生产环境的数据重现该问题。
Laravel Telescope 和 Pulse 可以帮助进行受控式调查。当请求涉及 PHP、Redis、MySQL、队列和第三方服务时,APM 跟踪就显得更加有用。对于更具体的数据库检查,可以监听异常缓慢的查询:
use Illuminate\Database\Events\QueryExecuted;use Illuminate\Support\Facades\DB;DB::listen(function (QueryExecuted $query) {if ($query->time >= 200) {logger()->warning('Slow database query', ['sql' => $query->toRawSql(),'time_ms' => $query->time,]);}});
请务必保持针对性。在繁忙的服务器上记录每个查询会产生噪音,消耗磁盘 I/O,并且可能会记录一些不应该出现在应用程序日志中的值。
还有一点很重要:每次更改前后都要比较同一个请求。否则,缓存温度升高或数据库活动减少可能会使无效的修复看起来像是成功了。
2. 在 N+1 次查询数量相乘之前停止它们
N+1 查询很容易被忽略,因为页面仍然会返回正确的数据。这段看似无害的代码实际上会对每个订单执行一次订单查询,然后再执行一次客户查询:
$orders = Order::latest()->take(100)->get();foreach ($orders as $order) {echo $order->customer->name;}
换为刻意加载这种明确的表关系:
$orders = Order::query()->select(['id', 'customer_id', 'total', 'created_at'])->with('customer:id,name')->latest()->take(100)->get();
这样通常可以将 101 个查询简化为 2 个。它还能避免检索响应中从未使用的列。请记住,Eloquent 需要保持主键和外键与模型匹配。
withCount()对于涉及 Blade 框架、API 资源、访问器和回归测试的极端情况,请使用这篇完整的 Laravel N+1 问题检测和修复指南。Laravel 官方的Eloquent 关系文档对于约束和嵌套的预加载也是非常有用的参考资料。
在开发和持续集成过程中,要让意外的延迟加载行为可见:
use Illuminate\Database\Eloquent\Model;public function boot(): void{Model::preventLazyLoading(! app()->isProduction());}
请不要急于求成,一次性加载所有数据。将成千上万个相关模型加载到内存中,可能会将查询问题变成数据填充问题。
3. 修复查询计划,而不仅仅是 Eloquent 语法
一旦查询次数得到控制,就应该检查慢的 SQL 语句本身。即使是简洁的 Eloquent 语句链,也可能导致数据库扫描大型表。
假设一份生产报告反复按日期筛选已付款订单:
Order::query()->where('status', 'paid')->whereBetween('created_at', [$from, $to])->orderByDesc('created_at')->get();
复合索引可能符合这种访问模式:
Schema::table('orders', function (Blueprint $table) {$table->index(['status', 'created_at']);});
EXPLAIN使用实际值运行,并检查所选索引、预估行数、连接顺序和排序工作。此外,MySQL查询执行计划指南解释了优化器显示的内容。
不要为了保险起见而建立索引。每个不必要的索引都会占用存储空间,并增加插入和更新操作的开销。
4. 停止将整个表加载到 PHP 内存中
导出和维护命令在测试期间经常有效,但之后会失败,因为get()会加载完整的结果集。
分批稳定处理大型数据表:
Order::query()->where('status', 'pending')->chunkById(500, function ($orders) {foreach ($orders as $order) {ProcessPendingOrder::dispatch($order->id);}});
paginate()适用于普通屏幕、cursorPaginate()适合大型有序数据源、lazyById()或chunkById()适合后台处理。游标占用内存较少,但如果每条记录都需要庞大的关系图,它也不是万能的解决方案了。
5. 使用失效计划缓存高价值结果
当底层结果开销较大且可以安全重用时,缓存就很有帮助。但它并不能把糟糕的查询变成好的查询。
$stats = Cache::remember("organization:{$organizationId}:dashboard-stats",now()->addMinutes(10),fn () => $this->buildDashboardStats($organizationId));
在发布此内容之前,请你回答三个问题:什么事件会导致值失效?谁有权查看该值?当多个请求同时重建该值时会发生什么情况?
在缓存键中包含租户、权限、语言环境和其他相关维度。当缓存键的源数据发生变化时,应使用遗忘机制或版本控制机制来更新该键。对于在高流量下容易过期的值,应使用锁或“过期时重新验证”策略来防止缓存数据溢出。
Laravel 的缓存文档中涵盖了原子锁和可用的存储方式。
在多服务器应用中,请使用共享存储,例如 Redis。本地文件缓存会导致每个服务器获得不同的结果。
6. 将慢速任务从 Web 请求中移除
在邮件发送、PDF 生成、视频处理、大文件导入、Webhook 导出和搜索索引等操作通常都放在队列中处理。控制器应该验证请求、提交所需状态、分发任务并返回。
GenerateInvoicePdf::dispatch($invoice->id)->afterCommit();afterCommit()防止快速工作进程在周围事务提交之前读取记录。
队列不会移除工作,只会移动工作。要监控队列深度、等待时间、运行时间、失败作业数、重试次数和工作进程可用性。即使是只需一秒钟就能完成的作业,如果因为队列资源不足而等待 15 分钟,仍然会被视为延迟。
当重试行为异常时,请检查原始连接、队列名称、工作进程配置、超时时间、退避策略和retry_after相关值。这篇Laravel 队列重试和失败任务故障排除指南将逐步讲解这些生产环境中的案例,而不会将本文变成队列使用手册。
7. 为外部 API 设置边界
即使你自己的代码效率很高,支付、客户关系管理 (CRM)、物流API或人工智能 (AI) 服务也可能导致 Laravel 路由变得缓慢。请在代码中设置明确的连接和响应超时时间:
$response = Http::connectTimeout(2)->timeout(5)->retry(2, 200)->get($url);
仅重试可以安全重复的操作。对于可选信息,可以缓存上次成功获取的值或异步更新它。可控的回退机制比让一个不健康的供应商占用所有 PHP 工作进程要好得多。
8. 应用 Laravel 和 PHP 生产环境优化
部署不应导致 Laravel 在实际请求中重新构建框架元数据。请安装生产环境依赖项并缓存应用程序引导文件:
composer install --no-dev --prefer-dist --optimize-autoloaderphp artisan optimize
该命令会缓存配置、事件、路由和视图。Laravel 在其官方部署优化指南optimize中详细记录了各个命令及其环境注意事项。
将env()调用保存在配置文件中。配置一旦被缓存,.env应用程序其他位置的直接调用就不会再以通常的方式加载该文件。
生产环境还应包括配置文件的参数:
APP_ENV=productionAPP_DEBUG=false
接下来,请启用 PHP OPcache 并根据已部署的代码库调整其大小。
OPcache 会重用已编译的 PHP 字节码,但缓存过小会导致缓存频繁更新,从而失去大部分优势。相关设置请各位参阅PHP OPcache 配置参考文档。
最后,部署后重新加载长时间运行的进程:
php artisan queue:restartphp artisan octane:reload
仅当应用程序使用 Octane 时才运行 Octane 命令。应通过 Horizon 终止由 Horizon 管理的 worker,以便进程监视器使用新版本启动它们。
9. 减少日志记录、有效载荷和序列化浪费
在循环内记录调试日志可能会非常耗费资源。保持日志结构清晰、易于操作,定期轮换日志,并避免记录过大的请求或响应正文,除非调查确实需要。
还要查看响应。API 资源如果附加未使用的属性、序列化深层关系或返回数千条记录,会一次性浪费数据库时间、PHP 内存、网络带宽和客户端渲染时间。
在整个请求过程中,较小的响应通常是成本最低的性能提升方式。
快速生产诊断订单
当你需要修复运行缓慢的 Laravel 应用程序,而又不想进行随意更改时,可以按以下顺序操作:
使用真实数据追踪一条慢速路线。
统计查询次数并找出重复的 SQL 语句。
移除 N+1 查询和不必要的列。
继续EXPLAIN执行剩余的慢语句。
对过大的结果进行分页、流式传输或分块显示。
仅缓存稳定且昂贵的值。
将用户暂时不需要的工作加入队列。
为外部调用添加超时和回退机制。
缓存 Laravel 引导文件并启用 OPcache。
重复上述测量并比较结果。
结语
解决 Laravel 生产环境性能问题的可靠方法是在增加容量之前先移除不必要的工作。控制查询次数,优化数据库的索引,控制进入 PHP 的数据量,并防止缓慢的依赖项无限期地占用请求工作进程。
每次只解决一个经过评估的瓶颈问题。这样可以保证变更可审查,证明哪些优化措施有效,并防止仓促的“性能优化”演变成一系列永久性的权宜之计。
作者:场长
本篇文章为 @ 场长 创作并授权 21CTO 发布,未经许可,请勿转载。
内容授权事宜请您联系 webmaster@21cto.com或关注 21CTO 微信公众号。
该文观点仅代表作者本人,21CTO 平台仅提供信息存储空间服务。
请扫描二维码,使用微信支付哦。