生产环境:使用 Laravel Nightwatch 调试真实环境
本系列中,我介绍了开发中的调试——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 秒内完成。查看传出的请求日志,我发现了罪魁祸首:支付网关存在周期性的延迟峰值。
有了这些数据,我:
- 缩短超时时间以快速失败,而不是让用户等待
- 为瞬态故障实现了重试逻辑
- 增加了一个断路器,延长中断时间
- 与支付提供商就他们的 SLA 进行了交谈
Nightwatch 的数据为我提供了诊断和对话的依据。
异常分组问题
原始异常日志有噪声。同样的错误可能会在数千个请求中发生数千次。Nightwatch 智能地将相关异常分组为“问题”。
我没有看到 5000 个单独的 ModelNotFoundException 条目,而是看到了一个问题:
- 出现了多少次
- 第一次和最后一次发生
- 影响了哪些用户
- 哪个路由触发了该问题
- 趋势(变坏还是变好?)
我可以处理一个问题,将其标记为已解决,并在问题再次出现时收到通知。这将错误跟踪从“扫描日志并希望你注意到模式”转变为“以下是你需要修复的 5 个问题,按影响排序”。
用户旅程跟踪
有时,最有价值的视图不是单个请求,而是用户在应用中的旅程。
Nightwatch 允许我查看特定用户的所有请求。如果客户来信说“我试图结账,但出了点问题”,我可以:
- 查看用户 ID
- 查看上一次他发出的每个请求
- 定位那一刻出现错误
- 查看在那之前和之后发生了什么
这将”我出现错误“的工单转换成”到底发生了什么情况“的调查。
有哪些问题在开发环境中隐藏而在生产环境中显现
在多个项目中使用 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 语句、Ray 和 Xdebug,每种语句都在开发过程中为我们提供了更多的可见性和控制力。
Nightwatch 完成了全景。这是代码离开机器进入现实世界后发生的事情。
以下是我现在对所有调试工具的看法:
| 解析 | 工具 | 效用 |
|---|---|---|
| 快速检测 | dd() | 立即检测值 |
| 开发流 | Ray | 不破坏可见性 |
| 深度调查 | Xdebug | 逐步控制 |
| 生产真相 | Nightwatch | 真实世界的可见性 |
每个工具都有其作用。它们共同覆盖了代码的整个生命周期,从你写的第一行到生产中的第一百万个请求。
开始使用
如果你没有使用专门构建的工具来监控你的生产 Laravel 应用,那你就是在瞎飞。你可能很幸运,什么都不会坏。但是,当某些东西确实坏了,你想从你的监控仪表板上找到答案,而不是从你的用户那里。
Nightwatch 可以免费开始。在一个应用上安装它,观察数据流,看看你发现了什么。我觉得,你有可能和我一样,你会在第一时间发现你不知道存在的问题。