Files
OpenFlare/docs/superpowers/specs/2026-07-20-site-limit-req-design.md
T
ryan 80c47f6ff3 feat(rate-limit): 站点级请求频率限制支持继承与自定义
在站点详情流量限制中配置 limit_req_per_ip;渲染按 effective rate 生成多 limit_req_zone,并以站点+IP 隔离计数。
2026-07-20 15:02:38 +08:00

160 lines
6.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 站点级访问频率限制设计
日期:2026-07-20
状态:已评审待实现
方案:站点详情 Limits 暴露 `limit_req_per_ip`;渲染时按 effective rate 生成多 `limit_req_zone`,并用站点键隔离 IP 计数
## 背景
全局默认已有:
- `openresty_default_limit_conn_per_server`
- `openresty_default_limit_conn_per_ip`
- `openresty_default_limit_rate`
- `openresty_default_limit_req_per_ip`
站点级并发/带宽已在「反代站点详情 → 流量限制」中配置,语义为:空/`0` 继承、`-1` 关闭、自定义覆盖。
请求频率(`limit_req`)后端字段与 merge 已存在,但:
1. 前端站点详情未暴露 `limit_req_per_ip`
2. 渲染侧仅在全局默认非空时输出**单一** `limit_req_zone ... rate=全局值`,站点自定义 rate 无法真正独立生效(nginx 的 rate 写在 zone 上,不能仅靠 location 覆盖)
## 目标
1. 在**仅站点详情「流量限制」区块**配置单 IP 请求频率。
2. 语义与现有三项一致:空/`0` 继承全局;`-1` 关闭;合法 `Nr/s` / `Nr/m` 为站点自定义。
3. 站点自定义 rate **真正按该 rate 生效**(A 站 5r/s、B 站 10r/s 互不影响)。
4. 同 IP 在不同站点的频率配额**按站点隔离**。
5. 修改后仍需发布配置版本;Agent 使用与 Server 同源的 render 路径。
## 非目标
- 在「安全性 → 限流」页增加按站点列表编辑
- 新建站点表单中的频率字段
- 按路径 / URI 差异化频率限制
- 改变 `limit_conn_*` / `limit_rate` 的现有 zone 与合并模型
- 业务 Zone(顶级域 + 二级域名资源)模型变更
## 语义
### 站点字段 `limit_req_per_ip`(字符串)
| 值 | 含义 |
|----|------|
| 空 / `"0"` | 继承全局 `openresty_default_limit_req_per_ip` |
| `"-1"` | 本站显式关闭频率限制 |
| `^\d+r/[sm]$`(大小写不敏感,存小写) | 本站自定义 rate |
### 全局默认
| 值 | 含义 |
|----|------|
| 空 / `"0"` | 默认关闭;继承方亦不输出 `limit_req` |
| 合法 rate | 未配置站点的 effective rate |
### 合并(与现有 `mergeLimitRate` 一致)
```
if route == -1: effective = off
else if route is set: effective = route // 合法 rate
else: effective = global // route 空/0
// global 空/0 → off
```
## 渲染
### 问题
nginx `limit_req_zone` 的 `rate=` 在 zone 声明时固定;多个站点若 effective rate 不同,必须使用不同 zone。
### 步骤
1. 在 `RenderRouteConfig` / main 配置生成前,对全部 route 计算 effective `LimitReqPerIP`。
2. 收集非空 effective rate 的**去重集合**,在 `http {}`(`renderOpenRestyLimitZoneBlock` 扩展,需能访问 routes 或 precomputed rates)输出:
```nginx
# 变量键:站点名 + IP,保证跨站点计数隔离
# 实现可用 map 或在 server 内 set 后引用;zone key 采用组合键
limit_req_zone $openflare_req_key zone=openflare_req_<rate_token>:10m rate=<rate>;
```
`rate_token` 由 rate 规范化生成(如 `10r/s` → `10rs`,`100r/m` → `100rm`),仅作 zone 名片段,合法 nginx zone 名。
3. 每个业务 server 在 access 相关位置之前设置:
```nginx
set $openflare_req_key "$openflare_waf_site$binary_remote_addr";
```
(与现有 `set $openflare_waf_site "..."` 同源 site_name;若某 server 无 waf site 变量则用同一 displayName/site_name。)
4. `renderRouteLimitBlock` 在 effective rate 非空时输出:
```nginx
limit_req zone=openflare_req_<rate_token> burst=<calculateBurst> nodelay;
limit_req_status 429;
```
5. **无任何** effective rate 时:不输出任何 `limit_req_zone` / `limit_req`(避免引用不存在的 zone)。
6. 应用范围与现有 limit 块一致:HTTP/HTTPS 反代 `location /`、Pages 相关 location;不含 HTTP→HTTPS 重定向-only server。
### 与旧行为差异
| 项 | 旧 | 新 |
|----|----|----|
| zone 数量 | 全局最多 1 个 | 按不同 effective rate 多个 |
| zone key | `$binary_remote_addr` | `$openflare_req_key`(站点+IP) |
| 站点自定义 rate | 无法真正独立 | 引用对应 rate 的 zone |
快照 JSON **仍保留站点原始值**(含空/`-1`),不把 merge 结果写回 route。
## 数据与 API
- 列 `of_proxy_routes.limit_req_per_ip` 已存在;无新迁移(若环境已跑过既有迁移)。
- API `Input` / `View` 已有字段;normalize / 校验已存在。
- 前端类型与详情表单补齐即可。
## 前端
仅改站点详情 `limits-section.tsx`:
- 增加「单 IP 请求频率」输入
- 校验:空、`0`、`-1`、或 `^\d+r/[sm]$i`
- 规范化:trim + lower;`0` → `""`
- `ProxyRouteItem` / `ProxyRouteMutationPayload` 增加 `limit_req_per_ip`
- `buildPayloadFromRoute` 带上该字段,避免其它区块保存时丢失
文案:与并发/带宽一致(空或 0 继承;-1 关闭;例如 10r/s、100r/m 自定义)。
## Agent / 发布
- 配置保存后须**发布配置版本**
- Agent **本地** `RenderJSON`;必须部署含本设计 render 的 Agent,否则 source 有字段但 conf 无指令
- 若 Agent 已记录同 version/checksum,升级二进制后需触发重新 apply(重启或强制重同步)
## 测试
- `mergeRouteLimitConfig`:继承 / 覆盖 / `-1`(已有则补 rate 断言)
- 多站点不同 effective rate:main conf 含多个 `limit_req_zone`,各 location 引用正确 zone 名
- 全关闭:无 `limit_req` 相关指令
- 仅全局有值:一个 zone + 未自定义站点引用该 zone
- 前端类型与表单校验(手工或既有模式)
## 验收
1. 全局 `10r/s`,站点空 → 该站 location 有 limit_req,zone rate=10r/s
2. 站点改 `5r/s` 并发布 → 该站引用 5r/s zone
3. 站点 `-1` → 该站无 limit_req
4. 两站不同 rate,同 IP 压测互不抢同一配额
## 实现边界
| 层 | 工作量 |
|----|--------|
| 渲染 `pkg/render/openresty` | 多 zone + 站点键 + location 引用 |
| 前端详情 Limits + types + payload | 补字段 |
| 后端 API/DB | 已具备,仅回归 |
| 文档/changelog | 用户可见变更记中文 changelog |