From f94767fbc73578c062aaf1572808c90416df1d46 Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 23 Jul 2026 23:39:15 +0800 Subject: [PATCH] =?UTF-8?q?perf(cache):=20=E8=BE=B9=E7=BC=98=E7=BC=93?= =?UTF-8?q?=E5=AD=98=E5=AF=B9=E9=BD=90=20Cloudflare=20=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Makefile | 24 ++ docs/changelog/index.md | 1 + docs/design/edge-cache-design.md | 236 +++++++++++++----- docs/design/index.md | 2 +- docs/guide/index.md | 7 +- docs/guide/proxy-config.md | 31 +++ docs/guide/troubleshooting.md | 31 +++ docs/plan/20260723-edge-cache-cf-align.md | 85 +++++++ docs/plan/index.md | 1 + .../detail/components/cache-section.tsx | 85 ++++++- .../detail/components/section-shell.tsx | 7 +- pkg/render/openresty/render.go | 8 +- pkg/render/openresty/render_test.go | 37 ++- pkg/render/openresty/types.go | 4 +- 14 files changed, 465 insertions(+), 94 deletions(-) create mode 100644 docs/plan/20260723-edge-cache-cf-align.md diff --git a/Makefile b/Makefile index 7e1d2f52..f611433f 100644 --- a/Makefile +++ b/Makefile @@ -111,3 +111,27 @@ cross-build: . @echo "==> Done. Binaries written to ./bin/" @ls -lh bin/ + +dev-f: + @echo "==> Starting frontend development server..." + cd frontend && pnpm dev + +dev-b: + @echo "==> Starting backend development server..." + go run main.go all + +dev: + @echo "==> Starting frontend and backend development servers in parallel..." + @PIDS=""; \ + STATUS=0; \ + ( cd frontend && pnpm dev 2>&1 | sed 's/^/[frontend] /' ) & PIDS="$$PIDS $$!"; \ + ( go run main.go all 2>&1 | sed 's/^/[backend] /' ) & PIDS="$$PIDS $$!"; \ + for PID in $$PIDS; do \ + wait $$PID || STATUS=1; \ + done; \ + if [ $$STATUS -eq 0 ]; then \ + echo "==> All development servers exited successfully."; \ + else \ + echo "==> Development servers exited with errors." >&2; \ + exit 1; \ + fi diff --git a/docs/changelog/index.md b/docs/changelog/index.md index 216facf5..c4e461a7 100644 --- a/docs/changelog/index.md +++ b/docs/changelog/index.md @@ -28,6 +28,7 @@ sidebar: false ### 改进 +- 边缘缓存对齐 Cloudflare 默认模型:不再因请求会话 Cookie、Authorization 或客户端 Cache-Control 一律跳过缓存;响应带 Set-Cookie 时不写入边缘;无源站缓存头时按状态码使用默认 Edge TTL;标准静态扩展名默认不再包含 JSON。生效需重新发布节点配置。 - IP 组自动规则中的 `StatusCount` / `StatusRatio` 支持状态码类写法(如 `"2xx"`、`"4xx"`、`"5xx"`),便于按整类错误率匹配。 - IP 组同步间隔下限由 5 分钟调整为 1 分钟,便于更频繁同步自动/订阅名单。 - 自动 IP 组回看窗口字段由 `lookback_minutes` 调整为 `lookback`,支持 `60m`、`1h` 等时长写法,并移除最小 5 分钟限制(兼容旧字段)。 diff --git a/docs/design/edge-cache-design.md b/docs/design/edge-cache-design.md index 4ff2e89a..a57639b7 100644 --- a/docs/design/edge-cache-design.md +++ b/docs/design/edge-cache-design.md @@ -1,6 +1,6 @@ -# 边缘缓存策略设计(对标 Cloudflare 默认可缓存范围) +# 边缘缓存策略设计 -你会学到:OpenFlare 边缘 `proxy_cache` 的产品边界、默认可缓存范围如何对齐 Cloudflare「静态资源默认可缓存」、策略枚举与渲染规则、兼容迁移,以及本阶段明确不做的能力。 +你会学到:OpenFlare 边缘 `proxy_cache` 如何在「该缓存」与「不该缓存」之间对齐 Cloudflare 默认闭环:请求 eligible(扩展名/策略)× 响应可共享缓存(源站 `Cache-Control` / `Expires` / `Set-Cookie`),以及与过往过严请求旁路的差异。 本设计是 [系统架构](./architecture.md) 中「基础缓存」的产品化专章;访问日志中的缓存结果见 [观测数据模型 §3.5.1](./observability-data-model.md)。 @@ -8,35 +8,61 @@ ## 1. 目标与非目标 -### 1.1 目标(第一期) +### 1.1 目标 -* **开箱接近 CF 默认**:路由开启缓存后,**默认只缓存静态扩展名**,不默认缓存 HTML/无扩展名动态路径。 -* **行为可解释**:与现有安全旁路(非 GET、Authorization、会话 Cookie、请求 `Cache-Control`)叠加,不削弱安全。 +* **开箱接近 CF 默认**:路由开启缓存后,**默认只缓存静态扩展名**,不默认缓存 HTML;**不因请求会话 Cookie / Authorization / 客户端 Cache-Control 一律 BYPASS**。 +* **该缓存的能命中**:带登录 Cookie 的用户访问 `/_app/**/*.js` 等静态资源可出现 `MISS` → `HIT`。 +* **不该缓存的仍挡住**:策略不 eligible(等价 CF `DYNAMIC`);源站 `private` / `no-store`;响应带 **`Set-Cookie` 不入库**(对齐 CF OCC 默认);`all` 为高级选项并文档警示。 +* **无源站 freshness 时有默认 Edge TTL**:对齐 CF 按状态码的默认 TTL(见 §3.5)。 * **可观测一致**:继续依赖 `$upstream_cache_status` → `cache_status` 明细三态。 -* **兼容存量**:旧路由 `cache_policy=url`(近似「过旁路即可缓存」)迁移为显式策略 `all`,行为不变。 +* **兼容存量**:旧路由 `cache_policy=url` 映射为 `all`;策略枚举与迁移规则保持 [§5](#5-兼容与迁移)。 ### 1.2 非目标(后续迭代) * Cache Rules 表达式引擎 -* Edge TTL / `proxy_cache_valid` / 忽略源站 `Cache-Control` -* 可配置 Cookie 旁路列表、Query 忽略列表 +* 忽略源站 `Cache-Control` 的强制 Edge TTL(CF Cache Rules「Ignore cache-control」) * Purge(按 URL/前缀/全站) * 浏览器 TTL 改写、客户端 `CF-Cache-Status` 响应头 +* 完整 RFC 条件:`Authorization` 仅当响应含 `public`/`s-maxage`/`must-revalidate` 才缓存(需 Lua;本期删除请求侧一律旁路,依赖策略 + 源站头) +* HEAD 转 GET 再缓存 * 命中率看板 --- -## 2. 现状摘要 +## 2. Cloudflare 判定闭环(对齐基准) -| 层 | 现状 | +CF 默认是 **两段决策**,**不是**「请求带 Cookie 就不缓存」。 + +### 2.1 阶段 A — 请求时 Eligible + +| 条件 | CF 结果 | | --- | --- | -| 全局 | `proxy_cache_path` / key / lock / stale(Performance 部分字段) | -| 路由 | `cache_enabled` + `cache_policy`:`url` \| `suffix` \| `path_prefix` \| `path_exact` | -| 旁路 | 渲染器硬编码:非 GET、Authorization、会话 Cookie、请求 Cache-Control | -| TTL | **无** `proxy_cache_valid`;存多久主要看源站头 + `inactive` | -| 观测 | 已上报 `cache_status`,UI 三态:命中 / 回源 / 未缓存 | +| 非 GET | 默认不缓存 | +| 扩展名不在默认可缓存表,且无 Rules 强制 Eligible | **`DYNAMIC`**(不查缓存) | +| 扩展名在默认表,或 Rules Eligible | 继续阶段 B | +| **请求 Cookie** | **默认不影响** | +| Cache Rules Bypass | `DYNAMIC` | -问题:默认策略 `url` 对「过旁路的 GET」范围过宽,与 CF「默认主要缓存静态扩展名、默认不缓存 HTML」不一致。 +CF 默认可缓存扩展名按 **扩展名** 而非 MIME;**默认不缓存 HTML / JSON**。 + +### 2.2 阶段 B — 响应是否可入库(OCC on,Free/Pro/Biz 默认) + +| 条件 | 结果 | +| --- | --- | +| `Cache-Control: no-store` / `private` | 不入库 | +| `public` + `max-age>0`,或未来 `Expires` | 可缓存 | +| 无 Cache-Control / Expires | 按状态码 **默认 Edge TTL** 仍可缓存(如 200 → 120m) | +| 响应 **`Set-Cookie`**(默认缓存级别 + OCC) | **不入库**,状态倾向 **BYPASS** | +| 请求 `Authorization` | 仅当响应另有 `public` / `s-maxage` / `must-revalidate` 才可缓存(完整条件本期用 Nginx 简化,见 §3.4) | + +### 2.3 状态语义(对照观测) + +| CF | 含义 | OpenFlare `cache_status` | +| --- | --- | --- | +| HIT / STALE / UPDATING / REVALIDATED | 命中类 | 同名或等价 | +| MISS / EXPIRED | 回源取内容 | 同名 | +| BYPASS | 请求时 eligible,响应不可缓存 | `BYPASS` → UI「未缓存」 | +| DYNAMIC | 请求时不 eligible | 策略 skip 多为 `BYPASS` 或空 → UI「未缓存」 | --- @@ -49,24 +75,24 @@ 两者均开启时才进入缓存逻辑。 -### 3.2 策略枚举(第一期) +### 3.2 策略枚举 | `cache_policy` | 含义 | 新建默认 | 旧值兼容 | | --- | --- | --- | --- | -| **`static`** | 仅 URI 匹配**标准静态扩展名**(内置表)才允许缓存 | **是** | — | -| **`all`** | 过安全旁路后,不限制路径/扩展名(等同今日 `url`) | 否 | 存量 `url` → `all` | +| **`static`** | 仅 URI 匹配**标准静态扩展名**才 eligible | **是** | — | +| **`all`** | 过方法旁路后,不限制路径/扩展名(高级,风险类似 CF Cache Everything) | 否 | 存量 `url` → `all` | | **`suffix`** | 自定义扩展名列表(`cache_rules`) | 否 | 保持 | | **`path_prefix`** | 自定义路径前缀 | 否 | 保持 | | **`path_exact`** | 自定义精确路径 | 否 | 保持 | -> 渲染层:读到历史值 `url` 时按 `all` 处理,避免未迁移数据行为突变;API 校验与 UI 只暴露上表枚举(写入时可将 `url` 规范为 `all`)。 +渲染层:历史值 `url` 按 `all` 处理;API/UI 只暴露上表枚举。 -### 3.3 标准静态扩展名(内置,V1 硬编码) +### 3.3 标准静态扩展名(内置) -对齐 Cloudflare 常见「默认可缓存静态」集合,**默认不包含** `html` / `htm`: +对齐 CF 默认「不缓存 HTML/JSON」;保留现代前端常用增强项: ```text -css js mjs map json +css js mjs map ico cur gif jpg jpeg png webp avif svg svgz ttf otf woff woff2 eot mp3 mp4 webm ogg flac @@ -74,26 +100,68 @@ wasm pdf zip 7z gz tar ``` -* 匹配对象:`$uri` 的扩展名(大小写不敏感),实现上与现有 `suffix` 策略相同: - `if ($uri !~* \.(?:css|js|…)$) { set $openflare_skip_cache 1; }` -* **V1.1(可选)**:全局配置项覆盖该列表;第一期不强制。 +* **不含** `html` / `htm` / **`json`**(对齐 CF 默认不缓存 JSON)。 +* **含** `map` / `mjs` / `wasm`(有意增强,提高 sourcemap / ES module / WASM 命中)。 +* 匹配:`$uri` 扩展名,大小写不敏感: + `if ($uri !~* \.(?:css|js|…)$) { set $openflare_skip_cache 1; }` -### 3.4 安全旁路(保持硬编码) +### 3.4 请求侧旁路(对齐 CF 后) -在策略匹配之前/之外,仍设置 `$openflare_skip_cache=1`: +仅保留: -1. `$request_method != GET`(含 HEAD,与现网一致) -2. `$http_authorization != ""` -3. 会话类 Cookie 正则(现网列表) -4. 请求 `$http_cache_control` 匹配 `no-cache|no-store|private` +1. `$request_method != GET`(含 HEAD,与现网一致;不做 CF 的 HEAD→GET) -`proxy_cache_bypass` / `proxy_no_cache` 均绑定 `$openflare_skip_cache`。 +**删除(过往过严,导致缓存率过低):** -### 3.5 与源站头的关系(本阶段不改) +* 会话类 Cookie 正则 +* `$http_authorization != ""` +* 请求 `$http_cache_control` 匹配 `no-cache|no-store|private` -* 仍不输出 `proxy_cache_valid`。 -* 对象**是否进入缓存流程**由策略 + 旁路决定;**存多久**继续依赖源站 `Cache-Control` / `Expires` 等及全局 `inactive`。 -* Edge TTL / 强制忽略源站头 → 后续专项。 +**安全如何仍成立:** + +| 威胁 | 闸门 | +| --- | --- | +| 误缓存 HTML/API | 默认 `static` 扩展名(不含 html/json) | +| 个性化内容 | 源站 `private` / `no-store`(Nginx 尊重) | +| 响应写会话 | **`Set-Cookie` → 不入库**(§3.6) | +| `all` 过宽 | UI/文档警告:需源站正确 Cache-Control | +| 带 Bearer 的 API | 依赖策略(勿对 API 用 `all`)+ 源站头;完整 Auth 条件缓存为后续 | + +### 3.5 默认 Edge TTL(无源站 freshness 时) + +对齐 CF 无 `Cache-Control`/`Expires` 时的状态码默认 TTL,在启用缓存的 location 输出: + +| 状态码 | TTL | +| --- | --- | +| 200, 206, 301 | 120m | +| 302, 303 | 20m | +| 404, 410 | 3m | + +```nginx +proxy_cache_valid 200 206 301 120m; +proxy_cache_valid 302 303 20m; +proxy_cache_valid 404 410 3m; +``` + +* 源站提供合法 `Cache-Control` / `Expires` 时,仍以源站 freshness 为准(不 `proxy_ignore_headers`)。 +* **不做**强制忽略源站头的 Edge TTL 覆盖。 + +### 3.6 响应侧:Set-Cookie 不入库 + +对齐 CF OCC 默认:eligible 请求若源站返回 **`Set-Cookie`**,**不写入** `proxy_cache`(可读路径仍可能 MISS/BYPASS 语义)。 + +```nginx +proxy_no_cache $openflare_skip_cache $upstream_http_set_cookie; +``` + +(`proxy_no_cache` 多参数:任一非空且非 `"0"` 则不写入。) + +`proxy_cache_bypass` 仍仅绑定 `$openflare_skip_cache`(请求侧 skip);响应侧只影响**写入**,与 CF「eligible 但响应不可缓存」一致。 + +### 3.7 与源站头的关系 + +* **是否 eligible**:策略 + 方法旁路。 +* **是否入库 / 存多久**:源站 `Cache-Control` / `Expires` + 默认 `proxy_cache_valid` + Set-Cookie 闸门 + 全局 `inactive`。 --- @@ -107,11 +175,13 @@ zip 7z gz tar │ no → location 无 proxy_cache ▼ yes set $openflare_skip_cache 0 - → 安全旁路 if → 置 1 + → 非 GET → 置 1 → 策略 if(static/all/suffix/…)→ 可置 1 proxy_cache openflare_cache proxy_cache_methods GET -proxy_cache_bypass / proxy_no_cache $openflare_skip_cache +proxy_cache_bypass $openflare_skip_cache +proxy_no_cache $openflare_skip_cache $upstream_http_set_cookie +proxy_cache_valid … → access.log cache_status=$upstream_cache_status ``` @@ -125,16 +195,16 @@ access.log cache_status=$upstream_cache_status | `suffix` | 不匹配 `cache_rules` 扩展名 → skip | | `path_prefix` / `path_exact` | 同现实现 | -### 4.2 涉及代码面(实现时) +### 4.2 涉及代码面 | 区域 | 路径 | | --- | --- | -| 渲染 | `pkg/render/openresty/render.go`(策略分支 + 内置扩展名常量) | +| 渲染 | `pkg/render/openresty/render.go`(旁路、Set-Cookie、`proxy_cache_valid`、扩展名常量) | | 校验 | `internal/apps/openflare/proxy_route/helpers.go` | | 模型/默认 | 创建路由默认 `cache_policy=static`;读写时 `url`→`all` | -| 快照 | `config_version/snapshot.go` | +| 快照 | `config_version` 快照规范化 | | UI | `proxy-routes/detail/components/cache-section.tsx` | -| 测试 | `pkg/render/openresty/render_test.go`、proxy_route helpers 测试 | +| 测试 | `pkg/render/openresty/render_test.go` 等 | --- @@ -142,49 +212,79 @@ access.log cache_status=$upstream_cache_status | 数据 | 处理 | | --- | --- | -| DB 中 `cache_policy=''` 或 `url`(且已启用缓存) | 读取 / 快照 / 渲染均规范为 **`all`**,保证存量「宽缓存」不变 | -| API 写入时 `enabled` 且 policy 为空 | 规范为 **`all`**(兼容旧客户端);UI 新建开启时**显式提交** `static` | -| 新建路由 | 默认 `cache_enabled=false`;表单开启缓存时默认策略 **`static`** | -| 已开启且 `url` 的站点 | 显示与发布为 `all`,**缓存范围不变** | -| 期望「只缓存静态」的旧站点 | 用户在 UI 改为 `static` 或自定义 `suffix` | +| DB 中 `cache_policy=''` 或 `url`(且已启用缓存) | 读 / 快照 / 渲染 → **`all`** | +| API 写入 enabled 且 policy 为空 | 规范为 **`all`**;UI 新建开启时**显式提交** `static` | +| 新建路由 | 开启缓存时默认 **`static`** | +| 旁路行为变更 | **破坏性相对旧实现**:带 Cookie/Auth 的流量从「未缓存」变为可 HIT;需 **重新发布节点配置** 后生效 | +| 默认扩展名 | 自表中 **移除 `json`**;已依赖缓存 `*.json` 的站点可改 `suffix` 自定义或 `all` | -**发布说明建议:** 说明默认策略变更仅影响**新配置**;存量 `url` 视为 `all`。 +**发布说明:** 说明本次对齐 CF 默认模型;命中率预期上升;`all` 与错误源站头风险需运维自查。 --- ## 6. UI 文案要点(缓存 Tab) -* 开启缓存后默认:**标准静态资源**(列出扩展名摘要,并写明不含 HTML)。 -* 选项:**标准静态资源** / **所有可缓存 GET(高级)** / 自定义后缀 / 路径前缀 / 精确路径。 -* 固定说明:非 GET、带 Authorization、常见登录 Cookie、请求禁止缓存头时跳过缓存。 -* 提示:全局 Performance 中缓存总开关须开启,否则站点开关无效。 +* 开启缓存后默认:**标准静态资源**(摘要扩展名,**不含 HTML/JSON**;含 map/mjs 等)。 +* 选项:标准静态 / 所有可缓存 GET(高级)/ 自定义后缀 / 路径前缀 / 精确路径。 +* 说明对齐 CF: + * 登录 Cookie **不会**单独跳过缓存; + * 源站 `private` / `no-store` / 响应 **`Set-Cookie`** 不会写入边缘缓存; + * 无源站缓存头时使用默认 Edge TTL。 +* **高级 `all`**:警告「类似 Cache Everything,个性化页面必须由源站声明 private/no-store」。 +* 全局 Performance 缓存总开关须开启。 --- ## 7. 验证要点 -* 渲染:`static` 生成扩展名 `if`;`all`/`url` 无路径限制;旁路四条仍在。 -* 单测:内置表含 `css`/`js`/`woff2`,不含 `html`。 -* 手动:开启 `static` 后请求 `/a.css` 可出现 HIT/MISS;`/index.html` 或 `/api` 多为未缓存/BYPASS。 -* 观测:access log `cache_status` 与列表三态一致。 +* 渲染:无 Cookie/Auth/请求 Cache-Control 旁路;含 `proxy_cache_valid` 三行;`proxy_no_cache` 含 `$upstream_http_set_cookie`。 +* 单测:内置表含 `css`/`js`/`map`/`mjs`,**不含** `html`/`json`。 +* 手动: + * 带 session Cookie 请求 `/a.js` → 第二次 `HIT`; + * `/index.html` + `static` → 未缓存; + * 源站对 eligible 路径返回 `Set-Cookie` → 不入库(持续 MISS/不 HIT); + * 源站 `Cache-Control: private` → 不入库。 +* 观测:access log 三态与原始 `cache_status` 一致。 +* 生效:配置版本发布并节点应用后验证。 --- -## 8. 后续路线图(非本设计交付) +## 8. 决策矩阵(防漏判) -1. **Edge TTL / 尊重源站开关**(`proxy_cache_valid`、`proxy_ignore_headers`) -2. **可配置旁路**(Cookie/Query) -3. **Purge API** -4. **Cache Rules**(有序规则 + 动作) -5. **全局默认可缓存扩展名配置** +| 场景 | CF | OpenFlare(本设计) | +| --- | --- | --- | +| GET 静态 + session Cookie + 源站 public max-age | HIT | HIT | +| GET HTML + static 策略 | DYNAMIC | 策略 skip → 未缓存 | +| GET + all + 源站 private | 不入库 | 不入库 | +| GET 静态 + 响应 Set-Cookie | BYPASS(OCC) | 不入库 | +| GET + Authorization + 静态 public | 条件缓存 | 可缓存(简化;依赖源站勿对敏感 API 乱标 public) | +| GET + 无 CC 的 200 静态 | 默认 120m | `proxy_cache_valid` 120m | +| DevTools Disable cache(请求 no-cache) | 边缘默认可仍 HIT | 边缘默认可仍 HIT | +| POST | 不缓存 | 非 GET skip | --- -## 9. 决策记录 +## 9. 后续路线图 + +1. Auth 完整 RFC/CF 条件缓存(Lua) +2. 强制 Edge TTL / `proxy_ignore_headers`(Cache Rules 级) +3. Purge API +4. Cache Rules(有序规则 + 动作) +5. 全局默认可缓存扩展名可配置;可选对齐 CF 更长扩展名表 +6. HEAD→GET + +--- + +## 10. 决策记录 | 决策 | 选择 | 原因 | | --- | --- | --- | -| 默认可缓存范围 | 开启缓存默认 `static` 扩展名表 | 对标 CF 开箱行为,降低 HTML/API 被误缓存 | -| 旧 `url` | 映射为 `all` | 避免存量站点行为变化 | -| HTML | 默认不在白名单 | 对齐 CF 默认不缓存 HTML | -| 第一期不做 Edge TTL/Purge | 明确 Out of Scope | 先收敛「谁可以进缓存」再优化「存多久/怎么清」 | +| 请求 Cookie 旁路 | **删除** | 对齐 CF;恢复登录用户静态命中率 | +| 请求 Authorization / Cache-Control 旁路 | **删除** | 对齐 CF 请求 eligible 模型;响应闸门兜底 | +| Set-Cookie | **proxy_no_cache 绑定** | 对齐 CF OCC「响应 Set-Cookie 不入库」 | +| 默认 Edge TTL | **按状态码 proxy_cache_valid** | 对齐 CF 无头时默认 TTL,避免「永不入库」 | +| 默认表去掉 json | **是** | 对齐 CF 默认不缓存 JSON | +| 保留 map/mjs/wasm | **是** | 现代前端有用命中,有意增强 | +| 默认可缓存范围 | 开启缓存默认 `static` | 对标 CF,降低 HTML/API 误缓存 | +| 旧 `url` | 映射 `all` | 存量行为不收窄 | +| 完整 Auth 条件 / Purge / Rules | 后续 | 先闭合默认闭环再扩展 | diff --git a/docs/design/index.md b/docs/design/index.md index 821a053a..7ad3e985 100644 --- a/docs/design/index.md +++ b/docs/design/index.md @@ -22,7 +22,7 @@ OpenFlare 适合需要统一管理多台 OpenResty 代理节点的团队,具 | 能力 | 说明 | 详细设计/使用指南 | | --- | --- | --- | | **反代配置管理** | 以网站规则(Proxy Route)为聚合边界,支持多域名与多上游负载均衡 | [新建反代配置](../guide/proxy-config.md) | -| **边缘缓存** | 单节点 OpenResty `proxy_cache`;开启后默认仅缓存标准静态扩展名(对标 CF 默认可缓存范围) | [边缘缓存策略设计](./edge-cache-design.md) | +| **边缘缓存** | 单节点 OpenResty `proxy_cache`;默认 static 扩展名 + 源站头/Set-Cookie 闸门 + 默认 Edge TTL(对标 CF 默认模型) | [边缘缓存策略设计](./edge-cache-design.md) | | **Zone 与域名管理** | 以可注册根域为管理入口,聚合明确域名、域名证书与反代路由 | [Zone 与域名资源设计](./zone-design.md) | | **配置版本控制** | 支持全局单一激活版本的预览、发布、不可变快照历史与秒级一键回滚 | [Agent 与发布模型](./agent-design.md) | | **WAF 安全防护** | 支持可视化 DAG 编排规则、手动/自动/订阅型 IP 组、GeoIP 匹配与 PoW CC 防护 | [WAF 设计](./waf-design.md) / [WAF 可编排规则设计](./waf-orchestration-design.md) / [WAF 使用指南](../guide/waf-usage.md) | diff --git a/docs/guide/index.md b/docs/guide/index.md index 580a8f6b..150b13da 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -10,7 +10,7 @@ OpenFlare 是一套自托管的 OpenResty 控制面。它把反向代理网站 1. [快速开始](./quick-start.md):用 Docker Compose 启动 Server,登录管理端,并接入第一个 Agent。 2. [发布第一份配置](./first-site.md):快速新建一条最基础的 HTTP 反代站点规则,并验证节点生效状态。 -3. [新建反代配置](./proxy-config.md):一步一步了解如何从证书导入与申请开始,配置 HTTPS 加密与上游源站管理。 +3. [新建反代配置](./proxy-config.md):一步一步了解如何从证书导入与申请开始,配置 HTTPS 加密、上游源站与边缘缓存。 4. [Zone 域名迁移](./zone-domain-migration.md):从旧托管域名/路由内嵌域名升级到 Zone 模型(goose 自动导入),含备份、验收与回滚说明。 5. [Pages 静态托管使用](./pages-usage.md):了解静态项目 ZIP 上传限制、SPA Fallback、以及内置 API 反向代理配置。 6. [内网穿透与隧道使用](./tunnel-usage.md):部署 Relay 与 Client,实现安全、无公网 IP 反向穿透。 @@ -18,7 +18,7 @@ OpenFlare 是一套自托管的 OpenResty 控制面。它把反向代理网站 8. [WAF 自动 IP 组语法](./waf-ip-group-expr.md):编写自动 IP 组 Expr 规则,了解关键字含义和预设规则。 9. [Uptime Kuma 监控同步](./uptime-kuma.md):配置并使用 Uptime Kuma 自动差分同步和监控范围控制。 10. [SSO 登录配置](./sso.md):配置 GitHub 或 OIDC 实现第三方单点登录 (SSO) 接入。 -11. [故障排查](./troubleshooting.md):按症状排查登录、数据库、节点同步、OpenResty 应用和前端构建问题。 +11. [故障排查](./troubleshooting.md):按症状排查登录、数据库、节点同步、OpenResty、边缘缓存命中与前端构建问题。 12. [引用与致谢](./credits.md):查看系统依赖的优秀开源项目与社区致谢清单。 ## 按角色查找 @@ -27,7 +27,8 @@ OpenFlare 是一套自托管的 OpenResty 控制面。它把反向代理网站 | --- | --- | | 5 分钟内跑起管理端 | [快速开始](./quick-start.md) | | 发布第一条反向代理配置 | [发布第一份配置](./first-site.md) | -| 配置域名证书与高级反代 | [新建反代配置](./proxy-config.md) | +| 配置域名证书、反代与边缘缓存 | [新建反代配置](./proxy-config.md)(含缓存说明) | +| 静态资源不命中缓存 | [故障排查 · 边缘缓存](./troubleshooting.md#边缘缓存命中率异常) | | 托管单页应用或静态网站 | [Pages 静态托管使用](./pages-usage.md) | | 配置内网穿透映射 | [内网穿透与隧道使用](./tunnel-usage.md) | | 配置防 CC 与 IP 组拦截 | [WAF 安全防护使用](./waf-usage.md) | diff --git a/docs/guide/proxy-config.md b/docs/guide/proxy-config.md index db36f7e2..c8a89115 100644 --- a/docs/guide/proxy-config.md +++ b/docs/guide/proxy-config.md @@ -84,3 +84,34 @@ 2. 在历史列表中找到发布前的上一个稳定版本。 3. 点击 **「激活此版本」**。 4. 所有在线 Agent 节点将在秒级自动重载回历史配置,实现秒级避险。 + +--- + +## 边缘缓存(可选) + +站点详情 **「缓存」** 页可开启边缘 `proxy_cache`(须同时开启 **性能设置 → 全局 OpenResty 缓存**)。行为对标 Cloudflare 默认模型,详见 [边缘缓存策略设计](../design/edge-cache-design.md)。 + +### 推荐设置 + +| 项 | 建议 | +| --- | --- | +| 策略 | **标准静态资源**(默认推荐):仅 css/js/map/图片/字体等,**不含 HTML/JSON** | +| 登录 Cookie | **不会**单独跳过缓存;带会话的用户仍可命中静态资源 | +| 源站 | 静态资源建议 `Cache-Control: public, max-age=…`;动态/个性化必须 `private` 或 `no-store` | +| 响应 Set-Cookie | 不会写入边缘缓存 | +| 无源站缓存头 | 按状态码使用默认 Edge TTL(如 200 约 120 分钟) | + +### 高级策略「所有可缓存 GET」 + +类似 Cloudflare Cache Everything:路径不再限制扩展名。若源站对 HTML 未声明 `private`/`no-store`,**可能把个性化页面缓存并串用户**。仅在源站缓存头正确、或内容全局一致时使用。 + +### 生效方式 + +缓存开关与策略写在配置快照中。保存站点后须 **发布并激活配置版本**,Agent 应用后才生效。仅改 UI 不发布则节点仍用旧规则。 + +### 快速自检 + +1. 全局缓存已开,站点缓存已开,策略为「标准静态资源」。 +2. 发布配置并确认节点应用成功。 +3. 带登录 Cookie 连续两次请求同一 `/assets/app.js`(或带 hash 的 immutable 路径),访问日志中 `cache_status` 第二次应为 **HIT**(或 UI「命中」)。 +4. 若仍为「未缓存」:确认策略是否匹配该路径扩展名、是否非 GET、源站是否返回 `Set-Cookie` / `private`,以及节点是否已应用新版本。更多见 [故障排查 · 边缘缓存](./troubleshooting.md#边缘缓存命中率异常)。 diff --git a/docs/guide/troubleshooting.md b/docs/guide/troubleshooting.md index df1735af..4a7300a7 100644 --- a/docs/guide/troubleshooting.md +++ b/docs/guide/troubleshooting.md @@ -15,6 +15,7 @@ | 发布后节点未更新 | 激活版本、节点 heartbeat、应用记录 | | OpenResty 应用失败 | 应用记录、Agent 日志、证书、上游地址、端口占用 | | 访问分析无数据 | OpenResty 容器状态、观测端口、Agent 补报日志 | +| 静态资源总不命中缓存 | 全局/站点缓存开关、策略扩展名、配置是否已发布、访问日志 `cache_status`、源站 Set-Cookie / Cache-Control | ## Server 无法启动 @@ -238,6 +239,36 @@ pnpm build | API 类型不一致 | 检查 `lib/api/` 和 `types/` 中的响应结构 | | E2E 失败 | 确认 Server 和前端开发服务器都已启动 | +## 边缘缓存命中率异常 + +访问日志中缓存三态:**命中**(HIT/STALE/REVALIDATED/UPDATING)、**回源**(MISS/EXPIRED)、**未缓存**(BYPASS 或空,请求时未进入可缓存路径或响应未入库)。设计说明见 [边缘缓存策略设计](../design/edge-cache-design.md)。 + +### 检查清单 + +1. **性能设置** 中全局 OpenResty 缓存已开启。 +2. 站点 **缓存** 已启用,策略与路径匹配(「标准静态资源」只覆盖内置扩展名,**不含** HTML/JSON;`.js.map` 的扩展名是 `map`,在默认表内)。 +3. 已 **发布并激活** 配置版本,对应节点应用记录成功(改缓存规则不发布则节点仍用旧旁路逻辑)。 +4. 请求方法为 **GET**(非 GET 一律不缓存)。 +5. 源站未对目标 URL 返回 **`Set-Cookie`**(有则不会写入边缘)。 +6. 源站未声明 **`Cache-Control: private` / `no-store`**(共享缓存不会存)。 +7. 浏览器 DevTools「禁用缓存」只影响浏览器;边缘是否 HIT 看访问日志 `cache_status`,不要只看 Network 面板。 + +### 常见误解 + +| 现象 | 说明 | +| --- | --- | +| 登录后全是「未缓存」且从未发布新版本 | 旧配置曾因会话 Cookie 旁路;升级后须重新发布节点配置 | +| `static` 下 `/api/foo` 或 `/index.html` 未缓存 | 预期行为(扩展名不在默认可缓存表) | +| 策略为 `all` 后 HTML 被串用户 | 源站未禁止共享缓存;改回 `static` 或给动态响应加 `private`/`no-store` | +| 带 `?v=` 的 URL 命中率低 | 默认缓存键含完整 `$request_uri`,query 不同即不同对象 | +| 第一次 MISS、第二次仍 MISS | 查源站是否每次 `Set-Cookie`、是否 `private`,或节点磁盘/缓存 inactive 过短 | + +### 期望行为(对齐 Cloudflare 默认) + +* 带登录 Cookie 的用户访问 `/_app/**/*.js` 等静态资源:**可以 HIT**。 +* 响应带 `Set-Cookie` 或 `private`:**不入库**。 +* 无源站缓存头的可缓存状态码:使用默认 Edge TTL(如 200 约 120 分钟)。 + ## 文档站构建失败 ```bash diff --git a/docs/plan/20260723-edge-cache-cf-align.md b/docs/plan/20260723-edge-cache-cf-align.md new file mode 100644 index 00000000..bf8e8e23 --- /dev/null +++ b/docs/plan/20260723-edge-cache-cf-align.md @@ -0,0 +1,85 @@ +# 边缘缓存对齐 Cloudflare 默认模型 — 实现计划 + +对应设计:[edge-cache-design.md](../design/edge-cache-design.md) + +## 1. 目标与背景 + +* **需求背景**:现网对会话 Cookie / Authorization / 请求 Cache-Control 一律旁路,登录用户静态资源几乎全是「未缓存」,命中率远低于 Cloudflare 默认。需对齐 CF 两段闭环:请求 eligible × 响应可共享缓存。 +* **Scope(必做)** + 1. 删除请求侧 Cookie、Authorization、请求 Cache-Control 旁路 + 2. 响应侧:`proxy_no_cache` 绑定 `$upstream_http_set_cookie` + 3. 默认 `proxy_cache_valid`(200/206/301→120m,302/303→20m,404/410→3m) + 4. 默认静态扩展名移除 `json`;保留 `map`/`mjs`/`wasm` + 5. 渲染单测 + UI 文案 + 设计/changelog +* **Out of Scope**:Purge、Cache Rules、强制忽略源站 CC、Auth RFC 条件缓存、HEAD→GET + +## 2. 设计决策摘要 + +| 决策 | 选择 | +| --- | --- | +| 请求 Cookie | 不旁路(对齐 CF) | +| Set-Cookie | 不入库 | +| 无源站 CC | 状态码默认 Edge TTL | +| json | 默认表移除 | +| 兼容 | `url`→`all` 不变;行为变更需重新发布配置 | + +## 3. 修改清单 + +### 边缘渲染 + +* #### [MODIFY] `pkg/render/openresty/render.go` + * `renderRouteCacheBlock`:仅保留非 GET 旁路;`proxy_no_cache $openflare_skip_cache $upstream_http_set_cookie`;追加三行 `proxy_cache_valid` +* #### [MODIFY] `pkg/render/openresty/types.go` + * `DefaultStaticCacheExtensions`:去掉 `json` +* #### [MODIFY] `pkg/render/openresty/render_test.go` + * 断言:无 cookie/auth/cache_control 旁路;含 set_cookie 与 proxy_cache_valid;表不含 json、含 map + +### 前端 + +* #### [MODIFY] `frontend/app/(main)/proxy-routes/detail/components/cache-section.tsx` + * 去掉「绕过登录 Cookie / Authorization」类文案 + * 改为 CF 对齐说明:源站 private/no-store、Set-Cookie 不入库、默认静态不含 HTML/JSON + +### 文档 + +* #### [MODIFY] `docs/design/edge-cache-design.md`(已更新) +* #### [MODIFY] `docs/changelog/index.md` `[Unreleased]` +* #### [MODIFY] `docs/plan/index.md` 登记本计划 + +### 不改 + +* 无 DB 迁移 +* 无 API 字段变更(策略枚举不变) + +## 4. 验证计划 + +### 自动化 + +```bash +go test ./pkg/render/openresty/ +make format +make code-check +``` + +### 数据面(配置发布后) + +1. 站点 `cache_enabled` + `static`,全局缓存开 +2. 带 session Cookie:`GET /static/app.js` 第二次应 HIT +3. `GET /index.html` 应为未缓存 +4. 源站返回 `Set-Cookie` 的静态 URL 不应出现稳定 HIT +5. 访问日志 `cache_status` 与三态一致 + +## 5. 发布注意 + +* 节点需 **重新发布/拉取配置版本** 后旁路变更才生效 +* 若站点依赖边缘缓存 `*.json`,改为 `suffix` 含 json 或 `all` + +## 6. 状态 + +- [x] 设计定稿(用户确认:全量对齐 CF) +- [x] 渲染与单测 +- [x] UI 文案 +- [x] changelog / plan index +- [x] `make format` + `make code-check` +- [x] 复查补强:`all` 策略 UI 警告;proxy-config / troubleshooting 运维说明 +- [ ] 用户确认后提交 diff --git a/docs/plan/index.md b/docs/plan/index.md index b95708b1..22e4a1fb 100644 --- a/docs/plan/index.md +++ b/docs/plan/index.md @@ -20,6 +20,7 @@ * [边缘缓存默认 static 策略](./20260718-edge-cache-static-default.md):开启缓存默认仅静态扩展名;存量 url→all。 * [访问日志 IP 明细 Tab](./20260719-access-log-ip-tab.md):第三 Tab 按 IP 聚合列表(时间窗/流量/2xx 比例);IP 情报迁入独立详情;日志详情仅请求字段。 * [边缘限流全局默认](./20260719-http-default-rate-limit.md):全局默认并发/带宽;站点 0 继承、-1 关闭;RenderRouteConfig 合并。 +* [边缘缓存对齐 Cloudflare 默认模型](./20260723-edge-cache-cf-align.md):删除过严请求旁路;Set-Cookie 不入库;默认 Edge TTL;扩展名去 json。 ## 已完成的计划 diff --git a/frontend/app/(main)/proxy-routes/detail/components/cache-section.tsx b/frontend/app/(main)/proxy-routes/detail/components/cache-section.tsx index c1a36904..328c07e6 100644 --- a/frontend/app/(main)/proxy-routes/detail/components/cache-section.tsx +++ b/frontend/app/(main)/proxy-routes/detail/components/cache-section.tsx @@ -2,9 +2,12 @@ import { useEffect } from 'react'; import { zodResolver } from '@hookform/resolvers/zod'; +import { CircleHelp, TriangleAlert } from 'lucide-react'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; +import { Button } from '@/components/ui/button'; import { Form, FormControl, @@ -14,6 +17,11 @@ import { FormLabel, FormMessage, } from '@/components/ui/form'; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@/components/ui/popover'; import { Select, SelectContent, @@ -92,6 +100,55 @@ function needsRulesForPolicy(policy: string) { ); } +function CacheHelpButton() { + return ( + + + + + +
缓存使用说明
+
+

+ 扩展名/策略决定是否可缓存,源站头与Set-Cookie + 决定是否入库。须同时开启性能设置中的全局 OpenResty 缓存。 +

+

+ 推荐策略: + 新建站点建议使用「标准静态资源」(含 + css/js/map/图片/字体/媒体等,不含 HTML/JSON)。 +

+

+ 通用规则:非 + GET 不缓存;登录 Cookie 不会单独跳过缓存;源站 private/no-store + 或响应 Set-Cookie 不会写入边缘。 +

+

+ + 所有可缓存 GET: + + 高级选项,类似 Cache Everything。个性化页面须由源站声明 + private/no-store,否则 HTML 可能被边缘缓存并串给其他用户。 +

+

+ 自定义规则: + 后缀填 jpg/css/js;路径前缀填 /assets;精确路径填 /robots.txt。 + 保存后须重新发布配置版本才会在节点生效。 +

+
+
+
+ ); +} + export function CacheSection({ route, onRouteUpdate, @@ -141,8 +198,8 @@ export function CacheSection({ : watchedPolicy === 'path_exact' ? '每行一个精确路径,例如 /robots.txt。' : watchedPolicy === 'static' - ? '标准静态资源使用内置扩展名列表(不含 HTML),无需填写规则。' - : '所有可缓存 GET 无需额外规则(仍会绕过登录态与 Authorization)。'; + ? '标准静态资源使用内置扩展名列表,无需填写规则。' + : '当前策略无需额外路径规则。'; const rulesPlaceholder = watchedPolicy === 'suffix' @@ -156,7 +213,8 @@ export function CacheSection({ return ( } formId={proxyRouteFormIds.cache} saving={saving} > @@ -187,11 +245,7 @@ export function CacheSection({
启用站点缓存 - - 新建推荐「标准静态资源」(不含 - HTML)。须同时开启性能设置中的全局 OpenResty - 缓存。仍会绕过非 GET、Authorization 与常见登录 Cookie。 - + 新建推荐「标准静态资源」。
精确路径 - - 标准静态资源含 css/js/图片/字体/媒体等,默认不缓存 HTML - 与接口路径。 -
)} /> + {watchedEnabled && watchedPolicy === 'all' ? ( + + + 高级策略风险 + + 个性化 HTML 在源站未声明 private / no-store + 时可能被边缘缓存并串给其他用户。详情见标题旁帮助。 + + + ) : null} +
- {title} +
+ {title} + {titleExtra} +
{description}