Skip to content

Commit 69ae986

Browse files
fix: 撤回省 CPU hack,恢复 best practice(dashboard 数据修订诊断)
用户 review 后指出"丢西瓜捡芝麻"风险,复盘 Vercel 30 天 dashboard 后修订: ## 真实根因(不是 /_not-found) dashboard 30 天曲线显示 5/11 CPU 峰值 80-90min(基线 5-15min/day),完美 对应 SEO PR 落地时间: - 5/11 15:01-15:39 UTC: PR #341 (253 MDX descriptions + 32 新 EN 翻译) - 5/11 16:02-18:41 UTC: PR #342 (remark heading shift + leetcode dedup) - 5/11 19:01-19:27 UTC: PR #343 + #340,4 小时 4 次 deploy 清空 ISR 加上 deploy.yml 里 IndexNow 主动告诉 Bing 重抓 → 5/10-5/12 crawler 风暴。 **这是 SEO 工作 successful 的代价,不是 bug。** 真实流量。 /_not-found 静态化 + bot blocklist 是真实 waste 清理(保留),但不能独立 解释 4× 激增。 ## 撤回的两条 hack 1. Sentry tracesSampleRate 0.1 → 0.02:撤回,保持 10% observability 不能为这点 CPU 让步,10% 是行业标准,client/server/edge 三处必须一致才能跨 runtime 串联 trace。 2. fetchEvents 失败一律返空:改成只在 NEXT_PHASE === phase-production-build 时返空,运行时仍 throw 让 Sentry 抓真故障。否则 prod backend 挂了会被 误显示成"暂无活动",掩盖故障。 ## 保留的修复(best practice,不是 hack) - /_not-found ƒ → ○:根 404 本就不需要 i18n - proxy.ts bot blocklist:扫描器不该烧 Fluid - /[locale]/docs /events /login 缺 setRequestLocale → 补:SSG/ISR 本就该工作 - /editor /share cascade ●:纯 client component,安全 ## Build 验证 pnpm build 重跑: - /[locale]/events 仍是 ● ISR 5m 1y - [events] fetch failed at build, rendering empty shell(NEXT_PHASE guard 工作)
1 parent 7828000 commit 69ae986

4 files changed

Lines changed: 85 additions & 31 deletions

File tree

app/[locale]/events/page.tsx

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,24 @@ interface ApiResponse<T> {
3030
message?: string;
3131
}
3232

33+
/**
34+
* 在 build 阶段才允许"后端不可达就降级返空"。Next 16 用 NEXT_PHASE 标记
35+
* phase-production-build,build 时返空让 generateStaticParams 能跑完不挂;
36+
* 运行时仍然 throw,Sentry / 错误页才能感知真故障,不至于把 prod backend
37+
* 挂了误显示成"暂无活动"。
38+
*/
39+
const IS_BUILD = process.env.NEXT_PHASE === "phase-production-build";
40+
3341
async function fetchEvents(): Promise<EventView[]> {
3442
const backendUrl = process.env.BACKEND_URL;
35-
// 改成"失败降级返回空"而非 throw:build 时 SSG(generateStaticParams 触发预渲染)
36-
// 如果后端不可达,throw 会让整次 build 失败。返回空数组让页面 build 出"暂无活动"
37-
// 的静态壳,等 revalidate=300 到点后台刷新拿到真数据。
38-
// 同步好处:Vercel CF 偶发挡 build IP / 后端临时挂时不再 break deploy。
3943
if (!backendUrl) {
40-
console.warn("[events] BACKEND_URL not set, rendering empty list");
41-
return [];
44+
if (IS_BUILD) {
45+
console.warn(
46+
"[events] BACKEND_URL not set at build, rendering empty shell; ISR will fetch real data after deploy",
47+
);
48+
return [];
49+
}
50+
throw new Error("BACKEND_URL is not configured");
4251
}
4352
try {
4453
const res = await fetch(`${backendUrl}/api/events`, {
@@ -49,16 +58,26 @@ async function fetchEvents(): Promise<EventView[]> {
4958
},
5059
});
5160
if (!res.ok) {
52-
console.warn(
53-
`[events] backend ${res.status} ${res.statusText}, rendering empty list`,
54-
);
55-
return [];
61+
if (IS_BUILD) {
62+
console.warn(
63+
`[events] backend ${res.status} at build, rendering empty shell`,
64+
);
65+
return [];
66+
}
67+
throw new Error(`/api/events backend ${res.status} ${res.statusText}`);
5668
}
5769
const json = (await res.json()) as ApiResponse<EventView[]>;
5870
return json.success && json.data ? json.data : [];
5971
} catch (err) {
60-
console.warn("[events] fetch failed, rendering empty list:", err);
61-
return [];
72+
if (IS_BUILD) {
73+
console.warn(
74+
"[events] fetch failed at build, rendering empty shell:",
75+
err,
76+
);
77+
return [];
78+
}
79+
// 运行时失败仍然 throw —— Sentry 抓到,错误页正常显示,不掩盖故障
80+
throw err;
6281
}
6382
}
6483

dev_docs/vercel-cpu-overage-2026-05.md

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,30 @@
44
> Fast Origin Transfer 12.04 GB / 10 GB(120%)。用户报告"之前做过 SSR
55
> 优化但情况更糟"。本文档记录调查方法、根因、修复、验证手段。
66
7-
## TL;DR
7+
## TL;DR(看完 Vercel dashboard 30 天图表后的修订诊断)
88

9-
之前的 SSG 优化(2026-05-06 commit `8517332`**只翻转了 1 条路由**(/[locale]
10-
首页)。`next build` 输出显示**还剩 18 条 ƒ Dynamic**,其中最致命的是
11-
`/_not-found`:所有漏洞扫描器(.env / wp-admin / php-info / graphql)和真实
12-
404 全落到这条 dynamic 路由。
9+
**真正的元凶**:5/11 一天里 PR #341(253 MDX descriptions backfill + 32 新 EN
10+
翻译页)+ PR #342(remark h1 plugin)+ PR #343(escape-angles)连续 4 次
11+
deploy。每次 deploy 自动 ping IndexNow → Bing/Google 5/10-5/12 大规模重抓 +
12+
索引 32 个新 URL。Dashboard 30 天曲线显示 5/11 CPU 峰值 80-90min(pre-spike
13+
基线 5-15min/day),完美对应 SEO PR 落地时间。
14+
15+
**这是 SEO 工作 successful 的代价,不是 bug**。流量是真实的搜索引擎 + 真实
16+
用户增长,付费 Pro plan 阈值就是这么到的。
17+
18+
**本 PR 做的事是真实 waste 的清理**(不是 hack):
19+
20+
- `/_not-found` 之前是 ƒ Dynamic(每条 scanner 都烧 Fluid)→ ○ Static
21+
- `proxy.ts` 加 bot path 早返 404,scanner 不再进 Fluid
22+
- `/[locale]/docs` `/events` `/login` 缺 setRequestLocale 导致退回 dynamic →
23+
补上 + generateStaticParams 让 SSG/ISR 真正工作
24+
25+
**撤回的"省 CPU hack"(写完才意识到丢了西瓜)**
26+
27+
- ~~Sentry tracesSampleRate 0.1 → 0.02~~:保持 10%。observability 不能为这
28+
点 CPU 让步,10% 是行业标准
29+
- ~~fetchEvents 失败一律返空~~:改成只在 `NEXT_PHASE === "phase-production-build"`
30+
时返空,运行时仍然 throw 让 Sentry 抓到真故障
1331

1432
本次修复在 `next build` 输出层面把 6 条路由从 ƒ 翻成 ● / ○:
1533

@@ -152,16 +170,33 @@ x-vercel-cache: HIT # ← CDN 命中
152170
age: 142
153171
```
154172

155-
### H3:Sentry tracesSampleRate 0.1(10%)叠在每次调用上
173+
### H3:~~Sentry tracesSampleRate 0.1 → 0.02~~(撤回)
174+
175+
最初打算改 10% → 2% 省 CPU。CR 后撤回——Sentry trace 在 30 天 CPU 占比远不及
176+
crawler 流量,2% 省的是芝麻丢的是西瓜(observability)。10% 是行业标准,
177+
client/server/edge 三处必须一致才能跨 runtime 串联请求链路。
178+
179+
**结论**:保持 0.1,不动 Sentry config。
180+
181+
### H4:dashboard 数据让根因更清晰(SEO 重抓风暴)
182+
183+
补观察后修订:
156184

157-
**证据**`sentry.server.config.ts:23``sentry.edge.config.ts:18` 都写
158-
`tracesSampleRate: 0.1`。在 368K 月度 invocations 下 = ~37K traces,每条
159-
trace 含 span 序列化 + 异步上报,叠加在每次 Fluid 调用 + 每次 edge middleware
160-
上。
185+
| 日期 | CPU | 主要 deploy |
186+
| -------------------- | ------------ | -------------------------------------------------------- |
187+
| 4/14 - 5/5 | 5-15min/day | 普通流量 + 周期扫描 baseline |
188+
| 5/11 15:01-15:39 UTC | 30 → 50min | PR #341:253 MDX descriptions backfill + 32 新 EN 翻译 |
189+
| 5/11 16:02-18:41 UTC | 50 → 80min | PR #342:remark heading shift + leetcode dedup |
190+
| 5/11 19:01-19:27 UTC | **90min 峰** | PR #343 + #340 deps,4 小时内 4 次 deploy ISR cache wipe |
191+
| 5/12 | ~45min | crawler 余波未消,但日益下降 |
161192

162-
**修复**:0.1 → 0.02。日均仍能采到几千条 trace 监控 P95 性能趋势。
193+
`.github/workflows/deploy.yml``INDEXNOW_API` 让每次 deploy 都**主动告诉**
194+
Bing / Google "URL 变了,快重抓"。PR #341 一次性改 253 个 MDX + 加 32 个新
195+
URL,触发的就是这一波重抓。
163196

164-
**验证**:Sentry dashboard `Performance` tab,Events / 24h 应该下降 5×。
197+
**结论**:5.11 之后激增是真实流量。本 PR 修的是**不该花的 CPU**(scanner /
198+
缺 SSG 的路由),让真实流量的边际成本最小化,但解决不了"SEO 太成功"这件事。
199+
长期路径:上 Pro plan 或 Cloudflare proxy 挡 crawler。
165200

166201
## 还没修但记录在案的问题
167202

sentry.edge.config.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ const dsn = process.env.SENTRY_DSN ?? process.env.NEXT_PUBLIC_SENTRY_DSN;
1515
Sentry.init({
1616
dsn,
1717
enabled: process.env.NODE_ENV === "production" && !!dsn,
18-
// 10% → 2%(dev_docs/vercel-cpu-overage-2026-05.md H3)。Edge middleware
19-
// 每条请求都过这里,10% trace 直接叠在 Fluid CPU 上。降到 2% 显著省 CPU
20-
tracesSampleRate: 0.02,
18+
// 10% 采样:与 server config 对齐,保证 client/server/edge 三处 trace 比例一致,
19+
// 跨 runtime 串联请求链路才完整。省 CPU 的 hack 撤回,observability 优先
20+
tracesSampleRate: 0.1,
2121
debug: false,
2222
beforeSend(event) {
2323
if (event.request?.headers) {

sentry.server.config.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@ const dsn = process.env.SENTRY_DSN ?? process.env.NEXT_PUBLIC_SENTRY_DSN;
2020
Sentry.init({
2121
dsn,
2222
enabled: process.env.NODE_ENV === "production" && !!dsn,
23-
// 10% → 2%(dev_docs/vercel-cpu-overage-2026-05.md H3)
24-
// 10% 在月百万级请求量下产生 10w+ traces,每条 trace 上报又叠加 Fluid CPU。
25-
// 2% 仍能日采几千条覆盖 P95 性能趋势,trace 配额也更宽裕。
26-
tracesSampleRate: 0.02,
23+
// 10% 采样:行业标准,足以发现 P95/P99 慢请求 + 偶发错误关联的请求链路
24+
// (曾试过 2% 想省 Vercel CPU,但 trace overhead 占比远小于 SEO 重抓带来的
25+
// crawler 流量,2% 是省芝麻丢西瓜的 hack —— observability 不能为这点 CPU 让步。)
26+
tracesSampleRate: 0.1,
2727
debug: false,
2828
beforeSend(event) {
2929
if (event.request?.headers) {

0 commit comments

Comments
 (0)