
FastGPT 限流模块拆分设计场景化 Rate Limit 接口的分层架构、Key 规范与故障策略【免费下载链接】FastGPTFastGPT is a knowledge-based platform built on the LLMs, offers a comprehensive suite of out-of-the-box capabilities such as data processing, RAG retrieval, and visual AI workflow orchestration, letting you easily develop and deploy complex question-answering systems without the need for extensive setup or configuration.项目地址: https://gitcode.com/GitHub_Trending/fa/FastGPT本文基于 FastGPT 仓库的设计文档 rate-limit.md 讲解限流能力从通用 group id 拼装向场景化语义接口重构的完整设计DAL 层的 Redis 固定窗口 Cache、Service 层的统一 key 命名空间与故障策略收口、以及按业务场景划分的接口层。读完后你可以理解 FastGPT 如何在不改变 Redis key 结构的前提下把限流的主体口径、动作隔离和故障语义从 API 层彻底剥离并能参考该分层方式设计自己项目的限流基础设施。背景旧方案的三个泄漏问题重构前FastGPT 的限流由packages/service/common/system/frequencyLimit/redisFixedWindow.ts暴露一个基于group id的通用接口。虽然公共函数内部补充了统一前缀但业务调用方仍然要自己拼装id这导致三个本应内聚在基础设施层的问题泄漏到了 API 层Redis key 结构泄漏调用方需要知道 key 长什么样、如何拼接动作隔离泄漏同一主体下不同动作如生成验证码与消费验证码是否独立计数全靠调用方自觉主体口径与故障策略泄漏按账号、团队、成员还是 IP 限流Redis 故障时放行还是拒绝都由业务代码各自决定。本次改造的目标是将限流基础能力收口到packages/service/common/rateLimit通过场景接口向业务层暴露语义函数。业务层只传账号、团队、成员或 IP 等业务标识完全不维护 key。该模块在仓库中的实际结构如下packages/service/common/rateLimit/ ├── core.ts # defineRateLimitInterface 工厂 ├── type.ts # 场景、故障模式、接口定义与执行接口类型 ├── index.ts └── interface/ # 场景接口 ├── ip.ts ├── accountVerification.ts ├── enterpriseAuth.ts ├── outLink.ts ├── upload.ts ├── member.ts └── team.ts模块边界DAL、Service Core 与场景接口三层整个模块严格划分为三层每层的职责边界在设计文档中写得很明确且与源码实现一致。DAL 层RateLimitCache只做计数不做业务判断DAL 层由 rateLimitCache 提供职责限定为四件事校验额度、窗口和增量limit与increment必须是正的安全整数否则抛出RedisInvalidArgumentError原子递增计数并设置固定窗口 TTL委托给 adapter 的consumeFixedWindow完成原子性由 adapter 保证返回当前计数、剩余额度和窗口重置时间不识别业务场景不处理业务错误。其消费入口的核心逻辑见 RateLimitCache.consumeasync consume({ key, limit, windowSeconds 60, increment 1 }): PromiseRateLimitResult { const parsedLimit PositiveSafeIntegerSchema.safeParse(limit); if (!parsedLimit.success) { throw new RedisInvalidArgumentError({ operation: rateLimit.consume, message: limit must be a positive safe integer }); } // ... const { currentCount, ttlSeconds } await this.redis.consumeFixedWindow({ key: asRedisLogicalKey(key), windowSeconds, increment: parsedIncrement.data }); return { allowed: currentCount parsedLimit.data, currentCount, remaining: Math.max(0, parsedLimit.data - currentCount), ttlSeconds, resetAt: this.now() ttlSeconds * 1000 }; }值得注意的是两点设计取舍固定窗口算法是 adapter 的内部实现细节。因此 DAL 侧方法名保留consumeFixedWindow而面向上层的 Cache 接口只用中性的consume命名——将来如果算法换成滑动窗口上层无感知Redis 执行错误向上抛出而不是在 DAL 层吞掉。故障时放行还是拒绝是业务语义由 Service 层按场景映射。Service Core统一命名空间 集中 key 生成core.ts 是整个模块的核心它用一个工厂函数defineRateLimitInterface收口所有 key 的生成与执行。其约束可以逐条对照源码验证使用rate-limit作为统一逻辑 key namespace源码中即export const RATE_LIMIT_KEY_PREFIX rate-limit见 core.ts#L7使用createRedisLogicalKey编码所有 key segment工厂内部的getKey把scene、policy和场景提供的 segments 一起交给 DAL 的 keyspace 工具做 RFC3986 编码业务标识中的:、glob 特殊字符等不可能泄漏进物理 key实现见 createRedisLogicalKey其中每个 segment 都会经过encodeURIComponent处理不向业务层暴露任意字符串 keydefinition持有 key、额度和故障策略调用方只能提交业务输入不依赖 HTTP request/response、NextAPI 或中间件启停配置rateLimit 模块是纯 Service 层能力与 Web 框架解耦。工厂返回的三种执行方式定义在 type.ts/** 所有场景接口统一提供原始消费、布尔判断和业务断言三种调用方式。 */ export type RateLimitInterfaceTInput { consume: (input: TInput) PromiseRateLimitResult; check: (input: TInput) Promiseboolean; assert: (input: TInput) Promisevoid; };三者语义层层递进consume返回完整的计数结果含allowed、currentCount、remaining、resetAtcheck原子消费一次并返回是否允许assert基于同一次消费结果判断超限则抛出definition.createError(input)定义的错误。这里有一个关键的并发正确性约束在 core.ts 的 check 实现 中体现得非常清楚const check: RateLimitInterfaceTInput[check] async (input) { try { return (await consume(input)).allowed; } catch (error) { if (error instanceof RedisInvalidArgumentError) throw error; logger.error(Rate limit execution failed, { key: getKey(input), failureMode: definition.failureMode, error }); return definition.failureMode open; } };设计文档明确禁止把增加统计和读取校验拆成两次 Redis 操作——那样会在并发请求间产生竞态先读后写都可能读到旧值导致实际消费超过限额。源码中check/assert全部复用同一次原子consume的结果从机制上杜绝了拆分校验的诱惑。场景接口每个场景一个文件强类型输入interface/目录下按场景维护 key 和限流策略每个文件都使用type.ts中的统一定义创建接口保证场景、动作、主体和执行结果结构一致。API 和业务 Service 只能调用这些语义接口不能绕过接口直接操作 DAL。以 member.ts 为例展示了额度集中在接口层维护的完整形态const memberRateLimitConfig { // 查看单条 LLM 请求记录避免高频查询明细。 [MemberRateLimitPolicy.GetLlmRequestRecord]: { limit: 1, seconds: 1 }, // Chat Agent 辅助生成限制同一成员触发补全的频率。 [MemberRateLimitPolicy.ChatAgentHelperCompletions]: { limit: 10, seconds: 60 }, // 轮询支付结果允许前端在一分钟内持续查询。 [MemberRateLimitPolicy.CheckPayResult]: { limit: 60, seconds: 60 }, // ...其余 policy } satisfies RecordMemberRateLimitPolicy, { limit: number; seconds: number }; const memberRateLimit defineRateLimitInterfaceMemberRateLimitParams({ scene: RateLimitSceneEnum.Member, policy: ({ policy }) policy, failureMode: open, getKeySegments: ({ memberId }) [member, memberId], getLimit: ({ policy }) memberRateLimitConfig[policy].limit, getWindowSeconds: ({ policy }) memberRateLimitConfig[policy].seconds, createError: () ERROR_ENUM.tooManyRequest }); export const assertMemberRateLimit memberRateLimit.assert;policy是强类型联合由MemberRateLimitPolicy常量对象推导业务层传入错误的 policy 名称会直接编译报错satisfies约束则保证每个 policy 都有且仅有一份额度配置。一个容易被忽视的场景是 ip.ts它只导出通用 IP 限流接口接收接口标识、已解析 IP、额度和窗口。真实 IP 解析、环境开关、强制启用和 HTTP 429 响应继续由中间件 reqFrequencyLimit.ts 封装不能下沉到 rateLimit 模块。从源码看该中间件的行为是useIPFrequencyLimitexport function useIPFrequencyLimit({ id, seconds, limit, force false }) { return async (req, res) { if (!serviceEnv.USE_IP_LIMIT !force) return; // 环境开关force 可绕过 const ip getClientIpFromRequest(req) ?? unknown; const allowed await checkIPRateLimit({ id, ip, limit, seconds }); if (!allowed) { return jsonRes(res, { code: 429, error: Too many request, request ${limit} times every ${seconds} seconds }); } }; }这个分层划法的依据是IP 解析依赖 HTTP 请求对象、USE_IP_LIMIT是部署环境开关、429 响应是 Web 协议细节——三者都属于 Web 层关切放进 rateLimit 模块会破坏不依赖 HTTP request/response的边界约束。中间件对外签名useIPFrequencyLimit({ id })保持不变只是内部改调通用 IP rateLimit 接口。Key 规则逻辑 key 与物理 key限流的 key 命名是整个设计中最重要的规范因为它决定了 Redis 中限流数据的可观测性和隔离粒度。逻辑 keyService 层持有rate-limit:scene:policy/action:subject-type:subject-id...DAL 写入 Redis 后的物理 key自动加上 FASTGPT_REDIS_PREFIX 前缀fastgpt:fastgpt:rate-limit:scene:policy/action:subject-type:subject-id...设计文档给出的真实示例rate-limit:ip:wechat-login-qrcode:ip:192.0.2.1 rate-limit:account-verification:captcha-create:register:account:userexample.com rate-limit:enterprise-auth:start:team:team-id rate-limit:out-link:request:out-link:out-link-id:uid:visitor-id rate-limit:upload:presign:identity:member-id rate-limit:member:export-dataset:member:member-id从示例可以读出两条设计意图主体类型显式出现在 key 中ip:、account:、team:、out-link:、identity:、member:即使 id 恰好碰撞不同主体的计数也天然隔离动作层policy/action不能省略。同一账号验证动作链中的生成验证码与消费验证码必须使用独立计数窗口否则消费动作会把生成额度耗尽。以account-verification场景为例captcha-create这样的动作段直接参与 key 拼接getKey中segments: [definition.scene, policy, ...definition.getKeySegments(input)]见 core.ts#L19-L27保证了一动作一窗口。另外createRedisLogicalKey会校验 namespace 字符^[A-Za-z0-9_-](:[A-Za-z0-9_-])*$、禁止逻辑 key 携带物理前缀fastgpt:、并对每个 segment 做 RFC3986 编码避免业务 id 中的:或 glob 字符污染 key 结构。接口约束与成员限流策略表type.ts 定义了五个核心类型构成整个模块的类型契约类型职责RateLimitScene允许的一级场景ip/account-verification/enterprise-auth/out-link/upload/member/team由RateLimitSceneEnum常量推导见 type.ts#L3-L13RateLimitFailureModeopen \| closedRedis 故障时放行或拒绝RateLimitInterfaceDefinitionTInput场景接口定义scene、policy、failureMode、key segments、额度、窗口、增量、错误工厂RateLimitInterfaceTInput统一的consume、check和assert执行接口defineRateLimitInterface创建强类型场景接口集中生成 key成员限流的 policy、额度和窗口由 member.ts 集中维护业务层只传policy和memberId不能在路由中重复声明额度。完整的策略表如下Member policy额度窗口get-llm-request-record11 秒chat-agent-helper-completions1060 秒transcriptions11 秒redeem-coupon11 秒refund-bill11 秒create-bill11 秒export-members160 秒check-pay-result6060 秒export-usage160 秒export-dataset160 秒export-chat-logs160 秒从策略取值可以推断出每类动作的限流意图支付、退款、兑换优惠券等资金类操作采用1 次/秒的短窗口目的是挡住并发重复提交而非限制使用频率check-pay-result放宽到 60 次/分钟是专门为前端轮询支付结果设计的各类导出操作统一 1 次/分钟保护后端计算与存储资源。另有一个显式的复用决策search-test知识库测试检索不创建独立 policy而是复用团队的chat-qpm与聊天请求共同消费团队套餐 QPM。这意味着测试检索会计入团队套餐的每分钟请求配额而不是无限制免费使用——这对商业化套餐的公平性很重要。故障策略定义时固定调用方不可选择故障策略是场景接口定义的一部分写死在failureMode字段里调用方不能临时选择。源码中check的 catch 分支完整实现了这条规则core.ts#L37-L50Redis 执行失败时记录日志然后按failureMode返回放行或拒绝但RedisInvalidArgumentError非法限额、窗口或增量是配置错误而非基础设施故障会被重新抛出任何场景下都不会被故障策略掩盖。设计文档给出的策略选择原则迁移自历史 Mongo 限流的普通业务保持 fail-openRedis 短暂不可用时宁可放过也不阻断业务维持迁移前的用户体验明确要求保护认证或成本资源的场景可以定义为 fail-closed例如验证码、企业认证这类场景Redis 故障时拒绝比放过的代价更低本次迁移先保持各调用方现有行为不借重构改变产品策略重构只改变限流的组织方式不改变哪些请求会被限流这一产品语义。迁移映射从旧调用到新接口旧调用到新接口的完整映射表如下这也是本次重构覆盖范围的全集当前调用新接口useIPFrequencyLimit({ id })中间件保持不变内部改用通用 IP rateLimit 接口账号验证group action scene account账号验证语义接口accountVerification.ts企业认证手写 group/id企业认证 start/verifyAmount 接口enterpriseAuth.ts上传手写 member id按身份限制每分钟签发上传 URL 次数upload.ts外链_id ip按outLinkId outLinkUid限制外链 QPMoutLink.ts成员接口把 action 写入 id强类型 member policy 接口member.ts几个值得注意的映射细节IP 限流的对外 API 零变更useIPFrequencyLimit的函数签名和 429 响应格式不变存量路由不需要任何改动只有中间件内部实现切换到了checkIPRateLimit外链限流主体口径发生变化旧方案按_id ip访客 IP限流新方案按outLinkId outLinkUid计 QPM。后者以访客身份标识替代网络出口 IP对 NAT 后的大量共享出口 IP 场景更公平——同一公网 IP 下的不同访客不再互相挤占额度。key 示例rate-limit:out-link:request:out-link:out-link-id:uid:visitor-id正体现了out-link和uid两个主体段共存的结构上传限流的动作是签发上传 URL限制的是presign动作rate-limit:upload:presign:identity:member-id而不是文件内容传输本身这与对象存储预签名 URL 的工作流匹配——滥用的成本大头在于反复换取签名 URL。验证方式与测试覆盖设计文档的 TODO 清单已全部完成DAL Cache 文件重命名、Service rateLimit core/type/interface 目录新建、主仓库和 Pro 调用方迁移、旧frequencyLimit通用实现删除、Redis mock 与单测更新、DAL/Service/App/Pro 相关测试运行。从仓库结构看测试与源码目录一一对应DAL 层 key 编码与 keyspace 行为packages/dal/test/redis/runtime/keyspace.test.ts场景接口行为与 key 断言packages/service/test/common/rateLimit/interface/ 下的 member.test.ts、outLink.test.ts、accountVerification.test.tsIP 中间件含USE_IP_LIMIT开关与 429 响应packages/service/test/common/middle/reqFrequencyLimit.test.ts。设计启示这套分层模式可以复制到哪些系统FastGPT 这次限流重构沉淀出的模式对任何需要多主体IP / 账号 / 团队 / 成员限流的系统都有参考价值基础设施层只做无状态算法固定窗口计数故障向上抛不猜业务语义key 的生成权收口到唯一工厂业务输入通过强类型TInput传入key segment 编码由 DAL 统一负责从源头消灭各路由拼 key的口径漂移额度、窗口、故障策略三元组写死在场景定义里业务代码想改额度必须先改接口层并经过评审而不是在某个路由里偷偷加一行配置动作层不可省略同一主体的不同动作独立计窗是验证码、签名签发这类生成-消费配对场景的正确性基础并发正确性靠原子消费保证统计与校验共用一次原子操作的结果禁止先读后写两次 Redis 往返。结合 keyspace 工具 中的逻辑 key / 物理 key 分离createRedisLogicalKey构造、toPhysicalRedisKey显式加前缀、toLogicalRedisKey校验前缀归属可以看出FastGPT 的 Redis keyspace 管理是这套限流设计的地基限流只是 keyspace 规范的一个消费方fastgpt:物理前缀让运维在SCAN时也能一眼区分归属。【免费下载链接】FastGPTFastGPT is a knowledge-based platform built on the LLMs, offers a comprehensive suite of out-of-the-box capabilities such as data processing, RAG retrieval, and visual AI workflow orchestration, letting you easily develop and deploy complex question-answering systems without the need for extensive setup or configuration.项目地址: https://gitcode.com/GitHub_Trending/fa/FastGPT创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考