限流与上限
各端点的 limit、候选扫描上限、速率限制、客户端兼容矩阵与延迟拆解。
各端点的上限一览
| 端点 | limit 默认 | limit 上限 | 后端候选扫描上限 | 速率限制(默认) |
|---|---|---|---|---|
/rss/anime.xml | 100 | 200 | Typesense filter | 120 次 / 60 秒 / IP |
/rss/group/<slug>.xml | 100 | 200 | — | 120 次 / 60 秒 / IP |
/rss/magnets.xml | 100 | 200 | — | 120 次 / 60 秒 / IP |
/api/bgm/search | 5 | 25 | Typesense search | 60 次 / 60 秒 / IP |
/api/animes/search | 24 | 50 | Typesense search | 120 次 / 60 秒 / IP |
/api/animes/torrent-match | 10 | 50 | Typesense search | 180 次 / 60 秒 / IP |
/api/anime/lookup | — | — | Convex lookup | 60 次 / 60 秒 / IP |
/api/mikan/group-lookup | — | — | Mikan upstream + Redis | 60 次 / 60 秒 / IP |
/api/subtitle-groups | — | 全量返回 | — | 60 次 / 60 秒 / IP |
/api/seasons/anime | — | — | — | 30 次 / 60 秒 / IP |
/api/anime/groups | — | — | — | 30 次 / 60 秒 / IP |
limit 越界处理:超过上限会被自动截断到上限,不会报错。例如 ?limit=5000 给 /rss/anime.xml 实际返回 200 条。
公开搜索翻页深度
公开搜索和磁力列表使用 Typesense 页码分页,服务端最多读取 100 页:
/magnets?page=<n>会把页码规范化到1–100;例如?page=10000会重定向到?page=100。/api/animes/search的cursor只接受ts_page:<页码>。非法前缀会返回400 VALIDATION_ERROR;超大页码会被规范化到第 100 页并返回空页或最后一页结果。limit与页码上限是两个独立保护:limit控制单页大小,cursor/page控制最多翻到第几页。
过滤模型(/rss/anime.xml)
/rss/anime.xml 由主站 Web runtime 查询 Typesense release index。bgmId、groupSlug、resolution、language、subtitle、format 等过滤会尽量下推到 Typesense;episode / episodeKey 这类展示字段可能在 Web runtime 做小窗口后过滤。不会回源 Convex 扫库。
极冷门组合如果叠加过多过滤,可能返回少于 limit 条;这是索引窗口和性能保护的结果。需要更多历史数据时,优先放宽过滤或按字幕组短链分页式轮询。
提高上限
- 拉满
limit:把 URL 的?limit=改成端点上限(200)。一次请求拿更多,等于把 N 次轮询合成 1 次。 - 用 304 替代轮询:每次请求带
If-None-Match。服务端确认数据没变就返回 304 空 body,不计入很多客户端的"流量统计",但仍会消耗 1 次速率配额。 - 拉长间隔:默认建议 ≥ 5 分钟。番剧发布频率本身按"集数"计算,每分钟扫一次没有意义。
- 合并并发:不要对同一 URL 并发发多个请求,缓存层会让后续请求拿到一样的结果,但白白消耗速率额度。
通过 Web runtime / 部署平台的环境变量调整(重启后生效):
# 把每 IP 60 秒 120 次提高到 60 秒 600 次
RSS_ANIME_RATE_LIMIT_MAX=600
RSS_GROUP_RATE_LIMIT_MAX=600
RSS_MAGNETS_RATE_LIMIT_MAX=600
# 或者改窗口为 10 秒(变成 120 次 / 10 秒)
RSS_RATE_LIMIT_WINDOW_MS=10000
# 关闭 CDN 边缘缓存(不推荐,仅测试用)
RSS_CACHE_TTL_SECONDS=0/rss/anime.xml 的 limit 上限是 Web 路由源码常量(src/routes/rss/anime[.]xml.ts),不通过环境变量调整。如果私有部署需要更高,改源码后重新构建并部署 Web runtime。
提高之后需要观察:
- 单次响应 XML 大小:200 条通常已经足够 RSS reader 使用,继续提高要注意带宽。
- Typesense 查询 P95 与 Web SSR cache hit/miss:公开 RSS 不应出现 Convex RSS 查询或扫库 fallback。
- 客户端解析时长:很多 RSS reader 默认只保留最近 N 条,请同步调整。
- 响应包含
Retry-After: <seconds>,按这个退避,不要立即重试。 - 短期内被打 429 通常是配置错(轮询太快 / 多个 reader 用同 URL)。把间隔放大到 5 分钟 + 启用 304,几乎不会再触发。
1000 用户场景下的真实负载
核心结论:origin 真实承受的回源率与并发用户数几乎无关,只与
s-maxage和查询参数多样性有关。
请求流转:
用户 ──► 浏览器 / RSS 客户端缓存 (max-age)
──► Cloudflare CDN (s-maxage + SWR)
──► origin (TanStack Start Web + Redis) ──► Typesense- CDN 内未过期 → 0 origin 请求
- CDN 过期但仍在 SWR 窗口 → 用户拿旧版本(0 延迟),origin 收到 1 个异步刷新请求
- 完全过期 → origin 收到 1 个同步请求(~50–200ms 回源)
- 客户端带
If-None-Match→ ETag 命中时 origin 仅返回 304(无 body,几乎零开销)
各端点在 1000 用户、客户端默认 15 分钟轮询 假设下的预估负载:
| 端点 | 客户端日请求量 | origin 真实回源(次/小时) | 备注 |
|---|---|---|---|
/rss/anime.xml(全站) | ~96k | ~12 | 5min CDN 窗口,全部用户共享同一缓存 |
/rss/group/<slug>.xml | 取决于 slug 数 | ~12 × slug 数 | 50 个 slug ≈ 600 次/小时 |
/rss/magnets.xml(简单) | — | ~30 | 全部用户用相同筛选时 |
/rss/magnets.xml(复杂、按 query 分缓存) | — | 接近 1× 唯一查询数 / 60s | 每人查询条件不同时 CDN 命中率 ≈ 0,SWR 缓解延迟 |
/api/subtitle-groups | ~96k | ~12 | 数据极少变化,几乎全 304 |
/api/seasons/anime | ~48k | ~6 | 季度数据周更,长缓存 |
/api/anime/groups(按 bgmId) | — | ~12 × 热门番剧数 | 冷门番剧首次回源较多 |
综合:1000 用户全套订阅 + 前端使用,origin 一小时实际承受 < 1000 次请求,其中 90%+ 是 304。SWR 让"用户感知延迟"和"origin 回源压力"完全解耦——回源慢不会拖慢用户。
给客户端的轮询建议
属性
类型
绝对不要做的事:
- 多线程并发拉同一 URL(白白消耗速率配额,CDN 会让结果完全相同)
- 关闭 ETag /
If-None-Match(每次都拿全量响应,浪费带宽) - 间隔 < 1 分钟(必触发 429,且对你没任何收益)
主流 RSS 客户端的实际行为
不同客户端默认的轮询频率、ETag/304 支持差异很大。好消息:本站对所有客户端都返回正确的 ETag + Last-Modified + Cache-Control,符合 HTTP 标准的客户端会自动走 304,你不用做任何配置。
| 客户端 | 默认轮询 | If-None-Match | 尊重 Cache-Control: max-age | 备注 |
|---|---|---|---|---|
| NetNewsWire(macOS/iOS) | 1 小时 | ✅ | ✅ | 表现最好,几乎全 304 |
| Inoreader(云端) | ~30 分钟 | ✅ | ✅ | 自带服务端聚合,1000 用户共享同一上游请求 |
| Feedly(云端) | ~1 小时 | ✅ | ✅ | 同上,对 origin 极友好 |
| FreshRSS(自建) | 用户配置(默认 1h) | ✅ | ✅ | 推荐配置 ≥ 15min |
| Tiny Tiny RSS(自建) | 用户配置 | ✅ | ⚠ 部分版本无视 | 建议手动设 ≥ 15min |
| Reeder(macOS/iOS) | 跟随 Inoreader/Feedly | — | — | 透传第三方聚合服务 |
| qBittorrent RSS 自动下载 | 默认 30 分钟 | ❌(每次拉全量) | ❌ | 对 origin 最不友好,但配合 CDN 仍 OK |
| Transmission + Flexget | 用户配置 | ✅(Flexget 支持) | ✅ | 推荐 |
自建脚本(Python requests) | 你写多少就多少 | 取决于实现 | 取决于实现 | 见 Python 客户端最小实现 |
针对场景的预期
- 个人订阅 1–2 个番剧 RSS(NetNewsWire / Inoreader):每小时 1 个请求,全部 304,origin 几乎感知不到。
- qBittorrent 用 RSS 规则自动下载:每 30 分钟拉一次全量 XML(约 50–200 KB),但因为 CDN 命中,实际打到 origin 也只是 5 分钟一次。
- 公开聚合站(Inoreader / Feedly):1000 个订阅者后端共享同一份订阅,对 origin 等同 1 个用户。
调优建议
- 用 Inoreader / Feedly / NetNewsWire iCloud 这类支持云端聚合的客户端 → 你和其他用户共享缓存
- qBittorrent / Transmission 直接拉的话,把规则间隔调到 ≥ 15 分钟(默认 30 分钟也很合理)
- 自建脚本必须实现
If-None-Match复用,否则等于浪费带宽
后端发布 → 用户看到的完整延迟拆解
以"字幕组发布一个新种子"到"订阅者的 RSS 客户端弹出通知"为例,逐段算一遍最坏情况:
T=0 字幕组提交 release 到 AniBT Web API
(Web 解析请求体 / 种子,Convex mutation 写入 releases / files 表)
T=0 数据库立即可读(Convex 强一致)
Web 写侧事件会触发外部 Worker 清 Redis RSS cache + purge CDN
T≤120s 事件 purge 缺失时的兜底 CDN / Web Redis 短缓存窗口内:
客户端请求被 CDN 拦截,仍拿旧版本
→ 这一段是用户感知延迟的主要来源
T=120s CDN 缓存进入 SWR 期:
下一个用户请求触发 CDN 异步回源
原 origin 拿到新数据 → CDN 缓存更新
T=120~300s 客户端的下次轮询周期(取决于客户端):
- 若客户端轮询间隔 = 5min → 最快下一次拿到新版本
- 若间隔 = 15min → 平均再等 7.5 min
- 若间隔 = 1h → 平均再等 30 min典型场景的端到端延迟(中位数)
| 客户端配置 | 后端发布 → 用户看到 |
|---|---|
| 自建脚本 5 min 轮询 + ETag | 3–5 分钟 |
| NetNewsWire 默认 1h 轮询 | 30–35 分钟(瓶颈在客户端) |
| Inoreader 云端聚合 | 15–30 分钟(瓶颈在 Inoreader 自己的轮询频率) |
| qBittorrent 默认 30min | 15–30 分钟 |
优化哪一段最有效
- 缩短客户端轮询间隔(最有效):从 1h 改成 5min,立刻把延迟从 30min 砍到 5min。
- 缩短
s-maxage(次要):从 300s 改成 60s,origin 压力涨 5 倍但用户延迟只少 4 分钟,性价比低。 - 强制 origin 立即推送(不可行):HTTP RSS 是 pull 模型,没办法。
如果你需要实时(< 1 分钟)通知,RSS 不是合适方案,应该用 webhook / Telegram bot / 站内推送。RSS 的设计目标就是"最终一致 + 极低成本",5–30 分钟的延迟是它的功能不是 bug。