Published on

系统视角的观测:用 Next.js + Java + PostgreSQL 拆解一个 Web 应用

Authors
  • avatar
    Name
    Terry
    Twitter

引子:书里的框架与浏览器里的证据

翻完《系统之美》,我关掉 Kindle,把注意力转向 Chrome DevTools 的 Network 面板。那天在排查一个 Next.js 页面,首屏 HTML 返回得很快,可瀑布图里总有一段灰色停顿。连续几次记录之后才确认:服务端渲染阶段串行调用了两个后端接口,后续静态资源因此排队。这个细节在代码里藏得很深,却在系统行为层面无处躲藏。

书里那句「先看系统整体的目标与反馈」在这里有了现实对应。代码层面看到的是一次函数调用,系统层面要回答的是:用户动作如何引发状态变化,这个变化如何穿过浏览器、服务端、数据库,再返回给用户。理解一个 Web 应用,必须先站在系统的高度审视它,而不是盯着某一行 await 反复琢磨。

系统层级:目的、存量、流量

《系统之美》里讲的三个概念——目的(Purpose)、存量(Stocks)、流量(Flows)——在 Web 系统里可以直接落地:

层级Next.js + Java + PostgreSQL 场景里的对应物我们需要观测的信号
目的产品承诺的用户体验,例如「3 秒内看到完整的订单状态」用户路径、关键指标、AB 实验的用户反馈
存量浏览器缓存、服务端会话、数据库里的业务状态缓存命中率、请求上下文、数据库表里的状态快照
流量HTTP 请求、消息队列、数据库事务、后台任务请求时序、队列堆积、事务锁等待、资源使用曲线

把问题放进这三个层级,可以先回答「系统要达到什么目标」,再问「哪些状态控制结果」,最后追踪「流量在哪里阻塞」。这比立即跳进代码高效得多,因为观测面从一开始就被摊开。

四个观察窗口:从浏览器到数据库

1. 浏览器:Next.js 层的即时反馈

  • 用户动作 → 网络流量:在 Chrome DevTools 的 Network 面板里打开 Start TimeWaterfall,记录顺序。要特别留意灰色的 QueueingStalled 段,往往对应前端或服务端的串行依赖。
  • 渲染策略:在 Next.js 里区分 SSRSSGISR。如果使用 App Router,应复查 fetch 的缓存策略,确认没有额外阻塞。
  • 状态快照:借助 React DevTools 查看组件树,判断是否存在等待外部数据的组件,结合网络时序即可复原依赖关系。

2. 服务入口:Java/Spring Boot 的接口边界

  • 请求上下文:在网关或 Spring Boot 层统一打印 trace_iduser_id_request_path,用结构化日志把进入系统的流量串联。
  • 线程池与限流:关注 TomcatUndertow 的线程使用情况,配合 micrometer 指标,判断线程池耗尽是否造成排队。
  • 依赖调用拓扑:使用 Spring Cloud Sleuth 或 OpenTelemetry 做分布式追踪,把外部调用的起止时间串联,再与浏览器的瀑布图对照。

3. 业务服务:代码里的流量调度

  • 阻塞点:排查 @Transactional 方法里是否包含远程调用,把数据库事务与外部 IO 混在一起往往会放大锁时间。
  • 缓存策略:确认本地缓存、Redis、消息队列等是否形成新的存量,避免让原本并行的流量在某个环节重新串联。
  • 背景任务:检查调度器与批处理任务,不少「莫名」的延迟其实是任务撞上流量尖峰触发的竞争。

4. 数据库:PostgreSQL 的存量真实状态

  • 事务视角:查询 pg_stat_activity,关注活动事务的 statewait_event,长时间的 idle in transaction 会拖慢整条链路。
  • 锁和索引:用 pg_lockspg_stat_all_indexes 找到热点表,把行锁和索引退化纳入统一视图。
  • 复制延迟:如果采用读写分离,需要监控 pg_stat_replication 的延迟,防止读实例返回过期数据。

实战:一次被串联的瀑布图

回到那个案例。Network 面板显示首屏 HTML 大约在 280ms 内返回,但紧随其后的两个 API 请求明显排队,第二个请求的 Start Time 比第一个晚了约 500ms。追踪到服务端后,Next.js 的 Route Handler 写成了:

const user = await fetchUser(sessionId)
const orders = await fetchOrders(user.id)
return NextResponse.json({ user, orders })

这段代码把系统的「流量」在最上层就强行串联:必须先拿到用户对象,才能查询订单,而两个接口本来互不依赖。改成下面这样,瀑布图立刻收紧:

const [user, orders] = await Promise.all([fetchUser(sessionId), fetchOrders(sessionId)])

真正的价值不在这两行代码,而在观察顺序:先看系统行为,再回推实现,去掉多余的特殊情况。随后把这两个接口放进一个带缓存的聚合层,保证后端不会因为前端并发放大压力。链路保持并行、接口保持幂等,系统才能稳定输出。

通用的观测流程

  1. 定义目的:明确页面或流程的目标指标,例如「用户进入 3 秒内看到订单列表」。没有目的,所有信号都是噪音。
  2. 描摹流量图:从用户动作开始画箭头,直到数据库或其他终点,把所有请求、任务、缓存标在图上。
  3. 收集信号:浏览器瀑布图、后端追踪、数据库视图、日志。把同一个 trace_id 在各层的时间对齐,得到完整时间线。
  4. 识别约束:判断瓶颈是在 CPU、IO、锁、队列还是错误重试。约束不一定表现为性能,也可能是功能完整性或一致性。
  5. 提出假设,小步验证:只改一处,再观察同样的信号,看是否达成目标。需要上线多个改动时,就用特性开关逐步放量,兑现「Never break userspace」的承诺。

工具与信号清单

  • 浏览器:Chrome DevTools(Network、Performance、Coverage)、Lighthouse、WebPageTest(验证端到端体验)。
  • 服务端:结构化日志(最好 JSON)、OpenTelemetry Trace、Grafana + Prometheus 指标、线程/堆转储。
  • 数据库pg_stat_activitypg_lockspg_stat_statements、Auto Explain,必要时导出关键 SQL 的执行计划。
  • 协作:借助共享文档记录排障时间线,让系统知识沉淀,避免依赖个别人的记忆。

常见陷阱与应对

  • 只看平均值:平均响应时间参考价值有限,要关注 P95/P99,并回到用户路径,看是否出现重定向或阻塞。
  • 调试环境与生产割裂:观测必须基于生产或接近生产的数据,配合限流与灰度,否则结论无法落地。
  • 忽略反馈环:系统存在是为了用户。如果优化让客服或运营流程复杂,那就是破坏用户空间,应立即回滚。

结语:系统观测是一项习惯

系统思维不是写在幻灯片上的哲学,它要求把每一次观测当成体系化的学习。Next.js 把页面送到用户眼前,Java 服务决定业务约束,PostgreSQL 保存承诺过的数据。只有在每一层都找到可验证的信号,系统才能持续兑现目标,用户才会信任它。要做的事很直接:把信号接在一起,让系统指出错误,然后用最简单的手段修复,而不是用华丽的理论掩盖问题。