编程

生产环境:使用 Laravel Nightwatch 调试真实环境

16 2026-03-31 00:51:00

本系列中,我介绍了开发中的调试——dd()、Ray、Xdebug。当你在本地计算机上构建功能和清除错误时,这些工具非常棒。但这是一个令人不安的事实:

本地环境欺骗你

性能会欺骗你,因为你是唯一的用户。数据会欺骗你,因为你的测试数据是干净和可预测的。它是关于边缘情况的,因为你不可能想象真实用户会做的所有奇怪的事情。

测试也会撒谎。它更接近现实,但仍然不是现实。

只有生产告诉真相。生产是事情变得有趣的地方。

只在生产中出现的问题

让我描述一下我遇到的一些情况,无论进行多少本地调试都无法捕捉到:

仅在大量账户中出现的 N+1 查询。在开发过程中,每个用户有 5 个订单。在生产中,一个客户有 47000 个订单。突然间,仪表板页面需要 30 秒才能加载。

缓存未命中级联。你的缓存策略在中等流量下运行良好。但在高峰时段,缓存失效会导致数据库崩溃。

第三方 API 超时。测试期间,支付网关在 200 毫秒内响应。但每周一次,在看似随机的时间,这需要 8 秒。用户认为收银台坏了。

静默失败的作业。排队作业会为用户数据中的特定边缘情况抛出异常。用户永远不会收到他们的电子邮件。直到支持票堆积起来,才有人注意到。

隐藏在多态关联中的慢速查询。对于大多数模型来说,它运行良好,但对于一种恰好有更多记录的特定类型来说,这是一场灾难。

这些问题有一个共同点:你不能在本地复制它们。你需要看看生产中实际发生了什么,有真实的流量、真实的数据和真实的用户行为。

引入 Laravel Nightwatch

Laravel Nightwatch 是 Laravel 的官方生产监控平台。它不是 Telescope(用于本地调试),也不是 Pulse(显示聚合指标)。Nightwatch 为你提供了生产中发生的事情的全貌——每个请求、每个查询、每个作业、每个异常——以及实际解决问题所需的上下文。

Laravel 团队在意识到现有的 APM 工具不是为 Laravel 设计的之后构建了它。他们使用通用的概念和术语。Nightwatch 讲 Laravel 的”语言“。它理解请求、中间件、Eloquent 查询、排队作业、计划命令。它向你展示了你对应用的看法。

当我第一次在生产应用上安装 Nightwatch 时,我在第一个小时内发现了我不知道存在的问题。不是因为我是一个糟糕的开发人员,而是因为生产有一种方法来展示测试无法展示的东西。

开始

安装几乎很简单:

composer require laravel/nightwatch

然后添加 token 到 .env 文件中:

NIGHTWATCH_TOKEN=your-token-here

部署后,启动 agent:

php artisan nightwatch:agent

就这样,Nightwatch 立马开始收集数据。无需复杂的配置,没有自定义组件,无需学习查询语言。

Nightwatch 实际展示了哪些内容

让我来介绍一下在调查生产问题时如何使用 Nightwatch。

仪表盘:应用概览

打开 Nightwatch 时,我看到的第一件事是健康概述。我一眼就能看出:

  • 我的应用正在处理多少请求
  • 随时间变化的错误率
  • P95 响应时间(第 95 百分位——比平均值更有用)
  • 失败的作业及其频率
  • 可能需要注意的慢速路由

这不是空洞的指标。当事情出错时,这是我首先注意到的地方。错误率飙升,响应时间突然增加——这些都是需要调查的信号。

请求跟踪:

当用户报告“页面速度慢”或“我遇到错误”时,我做的第一件事就是在 Nightwatch 中找到该特定请求。

我可以按路由、用户、时间窗口和状态代码进行搜索。一旦我找到请求,我就会看到一个时间线视图,显示发生的一切:

中间件执行实际

  • 控制器方法执行
  • 每个数据库查询及其持续时间
  • 缓存命中或者未命中
  • 向外部 API 发送 HTTP 请求
  • 事件发送
  • 任务排队

这就是问题变得明显的地方。如果一个请求需要 3 秒,我可以确切地看到原因。也许有 150 个数据库查询(你好,N+1)。也许支付 API 需要 2.8 秒才能响应。也许缓存丢失触发了昂贵的开销。

我不用再猜测了。我看到了到底发生了什么。

生产环境中的 N+1 问题 (即使有保护措施)

自 Lavael 8.43,我们有了内置的 N+1 监测。在 AppServiceProvider 中,我加入了这段代码:

// app/Providers/AzppServiceProvider.php
public function boot(): void
{
    Model::preventLazyLoading(!app()->isProduction());
    Model::preventSilentlyDiscardingAttributes(!app()->isProduction());
    Model::preventAccessingMissingAttributes(!app()->isProduction());
}

这对于开发环境捕获 N+1 问题非常有用。如果我忘记立即加载关联,Laravel 会立刻抛出异常。我会在 comit 之前修复。

不过需要注意条件是:!app()->isProduction()

我们在生产环境中禁用这些保护,因为为延迟加载抛出异常会破坏用户的应用。因此,安全网络恰恰在最重要的地方消失了。

就是这样——无论如何,一些 N+1 问题都会被忽略:

来自包的问题。可能是安装的管理面板软件包?它可能有自己的延迟加载问题,这些问题在测试过程中不会触发,但会随着真实数据的爆炸而爆发。

小数据集隐藏的问题。你的数据工厂创建了 3 条相关记录。N+1 运行得如此之快,以至于你在测试输出中没有注意到异常。在生产中,用户有 500 条相关记录,现在这很重要。

边缘案例代码路径中的问题。你测试了一些顺畅的路径。只有当第三方 API 失败时才运行的错误处理路径?这是你从未见过的 N+1。

条件关联加载。你的代码在一个条件内执行 $user->orders,这个条件在本地很少为真,但在生产中经常为真。

让我给你举一个真实的例子。我的仪表板路由被客户报告为缓慢。在本地,它在 200 毫秒内加载。我启用了 preventLazyLoading。没有异常。一切看起来都很好。

在生产中,对于一些用户来说,这需要 8-10 秒。

在 Nightwatch 中,我发现了一个缓慢的请求,并查看了查询日志。共有 847 个查询。这是单页加载。 
时间线向我展示了到底发生了什么:

QUERY 1.2ms  SELECT * FROM projects WHERE user_id = 42
QUERY 0.8ms  SELECT * FROM tasks WHERE project_id = 1
QUERY 0.9ms  SELECT * FROM tasks WHERE project_id = 2
QUERY 0.7ms  SELECT * FROM tasks WHERE project_id = 3
...
(840 more queries)

启用 preventLazyLoading 后是如何发生这种事的的?关联被热加载了——但在一个迭代项目的 Blade 组件中,有一个对 $project->tasks->where('status','pending') 的调用。任务已加载,但随后另一个查询正在运行,该查询访问了另一个我错过的关联。

本地数据集有 5 个项目,每个项目有 2-3 个任务。总开销为毫秒,低于我注意到或调查的阈值。在生产中,一个高级用户有 200 个项目,每个项目有几十个任务。问题扩大了。

Nightwatch 不仅向我展示了有太多的查询,还向我准确地展示了哪个代码负责,哪个路由触发了它,它发生的频率,以及哪些用户受到了影响。

修复方法是添加几个 with() 调用并重新构造该计算属性。已部署。问题解决了。总调试时间:约 10 分钟。
这里的教训不是 preventLazyLoading 毫无用处——它非常有价值,可以在触发前发现大多数问题。但这是一个开发时的安全门,而不是生产监控解决方案。你两者都需要。

查找缓存问题 
众所周知,缓存问题很难调试,因为它们取决于时间、流量模式和数据状态。Nightwatch 跟踪每个缓存命中和未命中。

在一个项目中,我注意到响应时间以可预测的间隔激增。查看缓存指标,我看到了一个模式:缓存命中率为 95%,然后突然降至 10%,然后慢慢回升。

在这些高峰期间深入研究请求,我发现了问题。计划命令每小时运行一次,导致大部分缓存无效。然后,下一波请求同时撞击数据库,造成了雷鸣般的混乱。

修复方法是在调度命令中加热缓存,而不仅仅是使其无效。但如果没有看到与请求时间相关的缓存指标,我永远不会连接这些点。

调试失败的作业(Job)

Job 以静默的方式失败了。用户看不到错误页面——他们只是没有收到电子邮件,或者他们的导出没有出现,又或者付款没有处理。

Nightwatch 跟踪每一项作业的执行情况。当工作失败时,我看到:

  • 异常和堆栈跟踪
  • 作业负载 (传入了什么数据)
  • 失败之前运行了多久
  • 尝试了多少次
  • 哪个队列 woker  处理的该任务 
    比如有一个应用,我发现一个 JOB 大约有 2% 的次数失败。异常情况是数据库死锁。从时间上看,这些失败总是发生在多个 worker 处理类似工作的高峰时段。
  • 如果没有 Nightwatch,我可能最终会从工单中注意到。使用 Nightwatch,我在第一周就发现了它,并修复了潜在的并发问题。

跟踪第三方 API 问题

现代应用程序依赖于外部服务。支付网关、电子邮件提供商、地理编码 API、社交登录。当这些服务出现问题时,你的应用也会出现问题。

Nightwatch 发出的外部的 HTTP 请求。我可以看到:

我的应用调用了哪个外部 API

  • 响应时间(平均值、P95、P99)
  • 每个端点的错误率
  • 超时频率

在一个项目中,我注意到结账流程的响应时间不一致。一些请求在 500 毫秒内完成,其他请求在 6 秒内完成。查看传出的请求日志,我发现了罪魁祸首:支付网关存在周期性的延迟峰值。

有了这些数据,我:

  1. 缩短超时时间以快速失败,而不是让用户等待
  2. 为瞬态故障实现了重试逻辑
  3. 增加了一个断路器,延长中断时间
  4. 与支付提供商就他们的 SLA 进行了交谈

Nightwatch 的数据为我提供了诊断和对话的依据。

异常分组问题

原始异常日志有噪声。同样的错误可能会在数千个请求中发生数千次。Nightwatch 智能地将相关异常分组为“问题”。

我没有看到 5000 个单独的 ModelNotFoundException 条目,而是看到了一个问题:

  • 出现了多少次
  • 第一次和最后一次发生
  • 影响了哪些用户
  • 哪个路由触发了该问题
  • 趋势(变坏还是变好?)

我可以处理一个问题,将其标记为已解决,并在问题再次出现时收到通知。这将错误跟踪从“扫描日志并希望你注意到模式”转变为“以下是你需要修复的 5 个问题,按影响排序”。

用户旅程跟踪

有时,最有价值的视图不是单个请求,而是用户在应用中的旅程。

Nightwatch 允许我查看特定用户的所有请求。如果客户来信说“我试图结账,但出了点问题”,我可以:

  1. 查看用户 ID
  2. 查看上一次他发出的每个请求
  3. 定位那一刻出现错误
  4. 查看在那之前和之后发生了什么

这将”我出现错误“的工单转换成”到底发生了什么情况“的调查。

有哪些问题在开发环境中隐藏而在生产环境中显现

在多个项目中使用 Nightwatch 后,我发现了一些仅在生产环境中稳定出现的问题:

数据量问题:50 条记录测试数据库无法暴露出在 500 万条记录时变慢的查询。

并发问题:在本地环境中,你是唯一用户。但在生产环境中,100 名用户可能同时访问同一端点,从而暴露出竞态条件和死锁问题。

缓存时序问题:缓存过期与再生模式仅在真实流量模式下显现。

外部服务问题:第三方 API 在负载下表现各异,存在自身故障及延迟现象。

用户数据中的边缘情况:真实用户拥有带特殊字符的姓名、来自小众供应商的电子邮件地址、2015 年版本的浏览器,还会以你从未预料到的方式使用你的应用。
内存与资源限制:你的笔记本电脑配备 32 GB 内存,而生产容器仅有 512 MB。这种差异至关重要。

Nightwatch 并不能解决这些问题。但它能让问题显而易见,而发现问题正是解决问题的第一步。

性能阈值和警报

我特别欣赏的一个功能是自定义性能阈值。我可以定义如下规则:

  • 如果任何请求需要超过 5 秒,请提醒我
  • 如果 /api/checkout 端点超过 2 秒,请提醒我
  • 如果作业失败率超过 1%,请提醒我
  • 如果错误率比正常值高出 50%,请提醒我

这些警报可以转到 Slack,这样我就可以在用户开始抱怨之前发现问题。
这就是主动调试和被动调试之间的区别。我没有等待工单,而是在用户注意到之前进行调查。

高流量应用采样

如果应用每天处理数百万个请求,你不一定需要捕获每一个请求。Nightwatch 支持采样:

NIGHTWATCH_REQUEST_SAMPLE_RATE=0.1

这个设置捕获了 10% 的请求,足以查看模式并发现问题,而不会超出的你事件配额。

你还可以对不同的路由应用不同的采样率。也许你采样了 100% 的结账流程(关键),但只采样了 5% 的健康检查端点(噪音)。

Route::get('/checkout', [CheckoutController::class, 'show'])
    ->middleware(Sample::rate(1.0));
Route::get('/health', fn () => 'OK')
    ->middleware(Sample::rate(0.05));

真正的好处:信心

进行适当的生产监控最好的事情不是捕捉错误,而是它给你带来的信心。

在 Nightwatch 之前,部署到生产环境就像把代码发送到虚空中,并希望最好的结果。现在我部署并监测。我看到流量流动,响应时间保持稳定,没有新的异常出现。几分钟内我就知道有什么不对劲了。

这种信心改变了我的工作方式。我更频繁地部署,因为我相信我会很快发现问题。我做出更大胆的改变,因为我能看到它们的影响。我睡得更好,因为我知道如果凌晨 3 点有东西坏了,我会得到提醒,而不是在早上 9 点从愤怒的用户那里发现。

调试工作流程的演变

本系列从最简单的调试工具 dd() 开始。我们逐步学习了 Log 语句、RayXdebug,每种语句都在开发过程中为我们提供了更多的可见性和控制力。
Nightwatch 完成了全景。这是代码离开机器进入现实世界后发生的事情。

以下是我现在对所有调试工具的看法:

解析工具效用
快速检测dd()立即检测值
开发流Ray不破坏可见性
深度调查Xdebug逐步控制
生产真相Nightwatch真实世界的可见性

每个工具都有其作用。它们共同覆盖了代码的整个生命周期,从你写的第一行到生产中的第一百万个请求。

开始使用

如果你没有使用专门构建的工具来监控你的生产 Laravel 应用,那你就是在瞎飞。你可能很幸运,什么都不会坏。但是,当某些东西确实坏了,你想从你的监控仪表板上找到答案,而不是从你的用户那里。
Nightwatch 可以免费开始。在一个应用上安装它,观察数据流,看看你发现了什么。我觉得,你有可能和我一样,你会在第一时间发现你不知道存在的问题。