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

- Name
- Terry
引子:书里的框架与浏览器里的证据
翻完《系统之美》,我关掉 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 Time和Waterfall,记录顺序。要特别留意灰色的Queueing与Stalled段,往往对应前端或服务端的串行依赖。 - 渲染策略:在 Next.js 里区分
SSR、SSG、ISR。如果使用 App Router,应复查fetch的缓存策略,确认没有额外阻塞。 - 状态快照:借助 React DevTools 查看组件树,判断是否存在等待外部数据的组件,结合网络时序即可复原依赖关系。
2. 服务入口:Java/Spring Boot 的接口边界
- 请求上下文:在网关或 Spring Boot 层统一打印
trace_id、user_id、_request_path,用结构化日志把进入系统的流量串联。 - 线程池与限流:关注
Tomcat或Undertow的线程使用情况,配合micrometer指标,判断线程池耗尽是否造成排队。 - 依赖调用拓扑:使用
Spring Cloud Sleuth或 OpenTelemetry 做分布式追踪,把外部调用的起止时间串联,再与浏览器的瀑布图对照。
3. 业务服务:代码里的流量调度
- 阻塞点:排查
@Transactional方法里是否包含远程调用,把数据库事务与外部 IO 混在一起往往会放大锁时间。 - 缓存策略:确认本地缓存、Redis、消息队列等是否形成新的存量,避免让原本并行的流量在某个环节重新串联。
- 背景任务:检查调度器与批处理任务,不少「莫名」的延迟其实是任务撞上流量尖峰触发的竞争。
4. 数据库:PostgreSQL 的存量真实状态
- 事务视角:查询
pg_stat_activity,关注活动事务的state与wait_event,长时间的idle in transaction会拖慢整条链路。 - 锁和索引:用
pg_locks、pg_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)])
真正的价值不在这两行代码,而在观察顺序:先看系统行为,再回推实现,去掉多余的特殊情况。随后把这两个接口放进一个带缓存的聚合层,保证后端不会因为前端并发放大压力。链路保持并行、接口保持幂等,系统才能稳定输出。
通用的观测流程
- 定义目的:明确页面或流程的目标指标,例如「用户进入 3 秒内看到订单列表」。没有目的,所有信号都是噪音。
- 描摹流量图:从用户动作开始画箭头,直到数据库或其他终点,把所有请求、任务、缓存标在图上。
- 收集信号:浏览器瀑布图、后端追踪、数据库视图、日志。把同一个
trace_id在各层的时间对齐,得到完整时间线。 - 识别约束:判断瓶颈是在 CPU、IO、锁、队列还是错误重试。约束不一定表现为性能,也可能是功能完整性或一致性。
- 提出假设,小步验证:只改一处,再观察同样的信号,看是否达成目标。需要上线多个改动时,就用特性开关逐步放量,兑现「Never break userspace」的承诺。
工具与信号清单
- 浏览器:Chrome DevTools(Network、Performance、Coverage)、Lighthouse、WebPageTest(验证端到端体验)。
- 服务端:结构化日志(最好 JSON)、OpenTelemetry Trace、Grafana + Prometheus 指标、线程/堆转储。
- 数据库:
pg_stat_activity、pg_locks、pg_stat_statements、Auto Explain,必要时导出关键 SQL 的执行计划。 - 协作:借助共享文档记录排障时间线,让系统知识沉淀,避免依赖个别人的记忆。
常见陷阱与应对
- 只看平均值:平均响应时间参考价值有限,要关注 P95/P99,并回到用户路径,看是否出现重定向或阻塞。
- 调试环境与生产割裂:观测必须基于生产或接近生产的数据,配合限流与灰度,否则结论无法落地。
- 忽略反馈环:系统存在是为了用户。如果优化让客服或运营流程复杂,那就是破坏用户空间,应立即回滚。
结语:系统观测是一项习惯
系统思维不是写在幻灯片上的哲学,它要求把每一次观测当成体系化的学习。Next.js 把页面送到用户眼前,Java 服务决定业务约束,PostgreSQL 保存承诺过的数据。只有在每一层都找到可验证的信号,系统才能持续兑现目标,用户才会信任它。要做的事很直接:把信号接在一起,让系统指出错误,然后用最简单的手段修复,而不是用华丽的理论掩盖问题。