全链路性能监控体系
优化做了,但效果好不好?哪些页面需要优化?线上真实用户体验如何?没有监控,优化就是盲人摸象。本章构建一套从浏览器到服务端到数据库的全链路性能监控体系,让每一次请求的耗时都可追踪、可归因、可告警。
1. 监控分层架构
1.1 三层监控模型
┌─────────────────────────────────────────────────┐
│ 前端层:Web Vitals + 自定义埋点 + 错误监控 │
│ (用户真实体验 = RUM,Real User Monitoring) │
├─────────────────────────────────────────────────┤
│ 服务端层:API 耗时 + SSR 渲染时间 + 链路追踪 │
│ (OpenTelemetry + 结构化日志) │
├─────────────────────────────────────────────────┤
│ 基础设施层:数据库查询 + 缓存命中率 + CPU/内存 │
│ (数据库监控 + 资源监控) │
└─────────────────────────────────────────────────┘
每一层回答不同的问题:
- 前端层:用户体验好不好?哪些页面慢?
- 服务端层:哪个 API 慢?慢在哪个环节?
- 基础设施层:是数据库慢还是缓存没命中?服务器资源够不够?
1.2 关键指标体系
| 层级 | 指标 | 目标值 | 告警阈值 |
|---|---|---|---|
| 前端 | LCP | < 2.5s | > 4s |
| 前端 | CLS | < 0.1 | > 0.25 |
| 前端 | INP | < 200ms | > 500ms |
| 前端 | JS 错误率 | < 0.1% | > 1% |
| 服务端 | API P95 | < 200ms | > 1s |
| 服务端 | SSR TTFB P95 | < 500ms | > 2s |
| 服务端 | 错误率 | < 0.1% | > 1% |
| 基础设施 | DB 查询 P95 | < 50ms | > 200ms |
| 基础设施 | 缓存命中率 | > 90% | < 70% |
2. 前端 Web Vitals 自动上报
2.1 web-vitals 库集成
// plugins/web-vitals.client.ts
export default defineNuxtPlugin(() => {
onNuxtReady(async () => {
const { onLCP, onCLS, onINP, onFCP, onTTFB } = await import('web-vitals')
function reportMetric(metric: any) {
const body = {
name: metric.name,
value: metric.value,
rating: metric.rating, // 'good' | 'needs-improvement' | 'poor'
delta: metric.delta,
id: metric.id,
navigationType: metric.navigationType,
url: window.location.href,
userAgent: navigator.userAgent,
timestamp: Date.now(),
}
// 使用 sendBeacon 确保页面关闭前发送
if (navigator.sendBeacon) {
navigator.sendBeacon('/api/analytics/vitals', JSON.stringify(body))
} else {
fetch('/api/analytics/vitals', {
method: 'POST',
body: JSON.stringify(body),
keepalive: true,
})
}
}
onLCP(reportMetric)
onCLS(reportMetric)
onINP(reportMetric)
onFCP(reportMetric)
onTTFB(reportMetric)
})
})2.2 自定义性能指标
除了 Web Vitals 标准指标,还需要业务相关的性能指标:
// app/composables/usePerformanceMark.ts
export function usePerformanceMark(name: string) {
const startTime = ref(0)
function start() {
startTime.value = performance.now()
performance.mark(`${name}:start`)
}
function end() {
performance.mark(`${name}:end`)
const measure = performance.measure(name, `${name}:start`, `${name}:end`)
// 上报自定义指标
reportCustomMetric(name, measure.duration)
}
return { start, end }
}
// 使用示例
const { start, end } = usePerformanceMark('video-player-init')
start()
await initPlayer(container, options)
end() // 上报播放器初始化耗时2.3 前端错误监控
// plugins/error-monitor.client.ts
export default defineNuxtPlugin((nuxtApp) => {
// Vue 组件渲染错误
nuxtApp.hook('vue:error', (error, instance, info) => {
reportError({
type: 'vue-error',
message: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
componentName: instance?.$options?.name,
info,
url: window.location.href,
})
})
// 未捕获的 Promise 拒绝
window.addEventListener('unhandledrejection', (event) => {
reportError({
type: 'unhandled-rejection',
message: event.reason?.message || String(event.reason),
stack: event.reason?.stack,
url: window.location.href,
})
})
// 资源加载失败(图片、脚本、样式)
window.addEventListener('error', (event) => {
if (event.target instanceof HTMLElement) {
reportError({
type: 'resource-error',
tagName: event.target.tagName,
src: (event.target as any).src || (event.target as any).href,
url: window.location.href,
})
}
}, true)
})3. 服务端性能监控
3.1 API 耗时中间件
// server/middleware/performance.ts
export default defineEventHandler((event) => {
const start = performance.now()
const method = event.method
const path = event.path
// 响应完成后记录
event.node.res.on('finish', () => {
const duration = performance.now() - start
const statusCode = event.node.res.statusCode
// 结构化日志
console.log(JSON.stringify({
type: 'api-performance',
method,
path,
statusCode,
duration: Math.round(duration),
timestamp: new Date().toISOString(),
}))
// 慢请求告警
if (duration > 1000) {
console.warn(`[SLOW API] ${method} ${path} took ${Math.round(duration)}ms`)
}
})
})3.2 SSR 渲染时间监控
// plugins/ssr-timing.server.ts
export default defineNuxtPlugin((nuxtApp) => {
const start = performance.now()
nuxtApp.hook('app:rendered', () => {
const duration = performance.now() - start
const event = useRequestEvent()
// 添加 Server-Timing 头(可在 DevTools 的 Network 面板看到)
if (event) {
setResponseHeader(event, 'Server-Timing', `ssr;dur=${Math.round(duration)}`)
}
console.log(JSON.stringify({
type: 'ssr-performance',
path: event?.path,
duration: Math.round(duration),
timestamp: new Date().toISOString(),
}))
})
})Server-Timing 头会在浏览器 DevTools 的 Network 面板中显示,开发调试时非常方便。
3.3 OpenTelemetry 链路追踪
OpenTelemetry(OTel)是可观测性的行业标准,可以追踪一个请求从浏览器到服务端到数据库的完整链路:
// server/plugins/otel.ts
import { NodeSDK } from '@opentelemetry/sdk-node'
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
export default defineNitroPlugin(() => {
const sdk = new NodeSDK({
serviceName: 'ai-video-platform',
traceExporter: new OTLPTraceExporter({
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
}),
instrumentations: [getNodeAutoInstrumentations()],
})
sdk.start()
})OTel 的自动 instrumentation 会自动追踪:
- HTTP 请求(入站和出站)
- 数据库查询(支持 PostgreSQL、MySQL、Redis)
- 外部 API 调用
一个完整的 Trace 看起来像:
[Trace: 视频详情页请求]
├── SSR 渲染 (450ms)
│ ├── useFetch /api/videos/123 (50ms)
│ │ └── DB: SELECT * FROM videos WHERE id=123 (8ms)
│ ├── useFetch /api/videos/123/comments (300ms)
│ │ └── DB: SELECT * FROM comments WHERE video_id=123 (250ms) ← 慢查询!
│ └── Vue 渲染 (100ms)
└── 响应发送 (2ms)
一眼就能看出瓶颈在评论查询——需要加索引或缓存。
4. 数据库查询性能监控
4.1 Drizzle ORM 查询日志
// server/utils/db.ts
import { drizzle } from 'drizzle-orm/node-postgres'
export const db = drizzle(pool, {
logger: {
logQuery(query, params) {
const start = performance.now()
// Drizzle 的 logger 在查询前调用
// 需要配合 OTel 或自定义 wrapper 获取耗时
console.log(JSON.stringify({
type: 'db-query',
query: query.substring(0, 200), // 截断,避免日志过长
timestamp: new Date().toISOString(),
}))
},
},
})4.2 慢查询发现与优化
常见慢查询模式及解决方案:
| 模式 | 症状 | 解决 |
|---|---|---|
| 缺少索引 | SELECT 全表扫描 | CREATE INDEX 添加索引 |
| N+1 查询 | 列表页每个 item 一次查询 | 改用 JOIN 或批量查询 |
| 大偏移分页 | OFFSET 10000 很慢 | 改用游标分页(WHERE id > lastId) |
| **SELECT *** | 返回不需要的列 | 只 SELECT 需要的字段 |
4.3 缓存命中率监控
// server/utils/cache-monitor.ts
const stats = { hits: 0, misses: 0 }
export async function monitoredCache<T>(
key: string,
queryFn: () => Promise<T>,
ttl = 60
): Promise<T> {
const storage = useStorage('cache')
const cached = await storage.getItem<T>(key)
if (cached) {
stats.hits++
return cached
}
stats.misses++
const result = await queryFn()
await storage.setItem(key, result, { ttl })
// 定期输出命中率(每 100 次请求)
if ((stats.hits + stats.misses) % 100 === 0) {
const hitRate = stats.hits / (stats.hits + stats.misses) * 100
console.log(JSON.stringify({
type: 'cache-stats',
hits: stats.hits,
misses: stats.misses,
hitRate: `${hitRate.toFixed(1)}%`,
}))
}
return result
}5. AI 视频生成耗时追踪
5.1 异步任务监控
AI 视频生成是一个长耗时异步任务(几十秒到几分钟),需要专门的监控:
// server/api/ai/generate.post.ts
export default defineEventHandler(async (event) => {
const body = await readBody(event)
const taskId = generateId()
const startTime = Date.now()
// 记录任务开始
console.log(JSON.stringify({
type: 'ai-task-start',
taskId,
model: body.model,
promptLength: body.prompt.length,
timestamp: new Date().toISOString(),
}))
try {
const result = await generateVideo(body)
// 记录任务完成
console.log(JSON.stringify({
type: 'ai-task-complete',
taskId,
duration: Date.now() - startTime,
model: body.model,
outputSize: result.fileSize,
timestamp: new Date().toISOString(),
}))
return result
} catch (error) {
// 记录任务失败
console.error(JSON.stringify({
type: 'ai-task-error',
taskId,
duration: Date.now() - startTime,
model: body.model,
error: error instanceof Error ? error.message : String(error),
timestamp: new Date().toISOString(),
}))
throw error
}
})5.2 任务状态看板
汇总 AI 任务数据可以构建运营看板:
| 指标 | 计算方式 | 用途 |
|---|---|---|
| 平均生成时间 | AVG(duration) | 用户体验基线 |
| P95 生成时间 | PERCENTILE(duration, 0.95) | 尾部延迟 |
| 成功率 | complete / (complete + error) | 服务稳定性 |
| 模型耗时分布 | GROUP BY model | 不同模型的性能对比 |
| 队列等待时间 | 入队到开始执行的时间 | 容量规划 |
6. 监控数据可视化
6.1 日志平台选择
| 平台 | 特点 | 适用 |
|---|---|---|
| Grafana + Loki | 开源,日志 + 指标 + 追踪一体 | 自建监控 |
| Datadog | 全功能 SaaS,开箱即用 | 预算充足的团队 |
| Sentry | 专注错误监控 + 性能 | 错误追踪 |
| Vercel Analytics | Vercel 部署自带 | Vercel 用户 |
| 腾讯云 CLS / 阿里云 SLS | 国内云厂商方案 | 国内部署 |
6.2 告警策略
| 级别 | 触发条件 | 通知方式 |
|---|---|---|
| P0 紧急 | 错误率 > 5% 或服务不可用 | 电话 + 短信 |
| P1 重要 | API P95 > 2s 或 Web Vitals Poor > 20% | 企业微信/钉钉 |
| P2 一般 | 缓存命中率 < 70% 或慢查询增多 | 邮件 |
| P3 信息 | 日常性能波动 | 看板展示 |
6.3 Server-Timing 开发调试
Server-Timing 头在开发时特别有用——无需打开日志,在 DevTools Network 面板就能看到服务端耗时拆解:
// server/middleware/server-timing.ts
export default defineEventHandler((event) => {
const timings: string[] = []
const start = performance.now()
// 记录各阶段耗时
event.context.timing = {
mark(name: string) {
timings.push(`${name};dur=${Math.round(performance.now() - start)}`)
},
}
event.node.res.on('finish', () => {
timings.push(`total;dur=${Math.round(performance.now() - start)}`)
setResponseHeader(event, 'Server-Timing', timings.join(', '))
})
})在 DevTools 中看到的效果:
Server-Timing: db;dur=12, cache;dur=2, render;dur=45, total;dur=60
7. 监控最佳实践
7.1 采样策略
不需要采集 100% 的数据——对于高流量站点,采样可以大幅降低成本:
| 数据类型 | 采样率 | 原因 |
|---|---|---|
| Web Vitals | 100% | 数据量小,全量采集 |
| API 性能日志 | 10-100% | 视流量而定 |
| OTel Traces | 1-10% | Trace 数据量大 |
| 错误日志 | 100% | 每个错误都重要 |
7.2 日志规范
统一结构化日志格式,方便后续查询和分析:
// 统一的日志接口
interface PerformanceLog {
type: 'api-performance' | 'ssr-performance' | 'db-query' | 'cache-stats' | 'ai-task-start' | 'ai-task-complete'
timestamp: string
duration?: number
[key: string]: any
}7.3 避免监控本身成为瓶颈
- 前端上报用
sendBeacon(非阻塞) - 服务端日志异步写入(不
await) - 采样率动态调整(流量高峰时降低)
- 监控数据和业务数据分开存储
本章小结
- 三层监控:前端(Web Vitals + 错误)→ 服务端(API + SSR + Trace)→ 基础设施(DB + 缓存)
- Web Vitals 上报:
web-vitals库 +sendBeacon+ 自定义业务指标 - API 监控:中间件记录耗时 + 结构化日志 + 慢请求告警
- OpenTelemetry:自动 instrumentation 追踪 HTTP → DB 的完整链路,一眼定位瓶颈
- Server-Timing:开发调试利器,DevTools 直接看服务端耗时拆解
- AI 任务监控:taskId 串联开始/完成/失败,构建运营看板
- 告警分级:P0 电话/P1 即时通讯/P2 邮件/P3 看板