【Token限流计费系列】第3讲:大模型成“裸奔”的裸机!企业级多租户网关架构实战
前言
大模型平台如果只做简单 API 转发,就很容易出现租户身份不清、上下文串用、配额失控和资源争抢。尤其在私有化部署或多业务线共用模型池时,缺少企业级网关会让平台暴露在性能与数据安全双重风险下。
本文围绕企业级多租户网关实践,说明如何通过身份认证、配额控制、上下文隔离和动态路由构建可控的大模型访问入口。
一、底层原理
1.1 核心机制
做企业级网关,核心就三件事:认身份、管配额、保隔离。
认身份靠的是 API Key 和 JWT 令牌。
管配额得用令牌桶算法,防止单个租户把资源吃干抹净。
保隔离则是大模型场景的特殊要求。
每个租户的对话上下文(Context)必须严格分开。
A 公司的老板问“今年财报咋样”,不能把 B 公司的数据吐出来。
我们设计了这样一套流量处理链路:
sequenceDiagram participant Client as 租户客户端 participant Gateway as 网关接入层 participant Auth as 鉴权与限流中心 participant Router as 动态路由引擎 participant LLM as 大模型集群 Client->>Gateway: 发起请求 (携带 Tenant-ID) Gateway->>Auth: 校验身份与配额 Auth-->>Gateway: 放行或拒绝 Gateway->>Router: 根据租户策略路由 Router->>LLM: 转发请求 (隔离上下文) LLM-->>Router: 返回流式响应 Router-->>Gateway: 聚合数据 Gateway-->>Client: 返回最终结果这套架构的优势在于“无感隔离”。
业务代码不需要知道租户是谁,网关在入口处就把标签打好了。
通过 ThreadLocal 将租户信息透传到整个调用链。
这样 downstream 的服务只管处理逻辑,不用操心安全问题。
1.2 与同类方案的对比
市面上有不少现成的网关,但直接拿来用往往水土不服。
我们对比了三种主流方案,看看差异在哪。
| 方案 | 隔离能力 | 扩展性 | 适用场景 |
|---|---|---|---|
| Spring Cloud Gateway | 弱 (需二次开发) | 高 (Java 生态) | 内部微服务治理 |
| APISIX | 中 (插件化) | 中 (Lua 脚本) | 公网 API 管理 |
| 自研大模型网关 | 强 (深度定制) | 高 (贴合业务) | 企业级 AI 中台 |
自研网关虽然初期投入大,但在“上下文隔离”和"Token 计费”上更精准。
毕竟大模型的 Token 就是真金白银,不能靠通用网关粗略估算。
二、快速上手
咱们先用 3 分钟写个最小可运行的“租户识别器”。
这个过滤器负责从 Header 里提取租户信息,并放入上下文。
package com.douli.gateway.filter; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.core.Ordered; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; /** * 租户识别过滤器 * 作用:在请求入口处提取租户标识,为后续隔离做准备 */ @Component public class TenantIdentificationFilter implements GlobalFilter, Ordered { // 定义 Header 中租户标识的键名 private static final String TENANT_ID_HEADER = "X-Tenant-Id"; @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { // 1. 获取原始请求对象 ServerHttpRequest request = exchange.getRequest(); // 2. 从请求头中读取租户 ID // 注意:生产环境这里必须做非空校验,防止恶意请求 String tenantId = request.getHeaders().getFirst(TENANT_ID_HEADER); if (tenantId == null || tenantId.isEmpty()) { // 3. 如果没有租户 ID,直接拒绝,返回 401 // 这里可以封装统一的错误响应体 return exchange.getResponse().setComplete(); } // 4. 将租户信息“贴”到请求对象上,传递到下游 // 使用 mutate 构建新的请求,避免修改原始对象 ServerHttpRequest modifiedRequest = request.mutate() .header("X-Internal-Tenant", tenantId) .build(); // 5. 继续执行过滤器链 // 此时 downstream 服务可以通过获取 Header 拿到租户信息 return chain.filter(exchange.mutate().request(modifiedRequest).build()); } @Override public int getOrder() { // 设置优先级,确保在鉴权过滤器之后执行 return Ordered.HIGHEST_PRECEDENCE + 10; } }这段代码看着简单,却是整个隔离体系的基石。
它保证了后续所有服务看到的请求,都带着“身份证”。
三、核心 API / 深水区
3.1 核心方法速查
在网关层,我们封装了几个核心工具类,方便业务方调用。
| 方法名 | 功能描述 | 适用场景 |
|---|---|---|
TenantContext.set() | 设置当前线程的租户上下文 | 过滤器入口 |
TenantContext.get() | 获取当前租户 ID | 业务逻辑中 |
RateLimiter.check() | 检查租户剩余配额 | 限流判断 |
ContextManager.clear() | 清理线程上下文 | 防止内存泄漏 |
3.2 生产级配置
光有代码不行,配置得跟上。
特别是限流策略,不能一刀切。
我们要支持按租户配置不同的 QPS 阈值。
比如 VIP 客户给 1000 QPS,普通客户只给 100 QPS。
这需要对接 Redis 实现分布式计数。
# application.yml 配置示例 tenant-limit: enabled: true redis-host: 192.168.1.100 default-qps: 50 # 默认阈值 vip-qps: 500 # VIP 阈值 burst-size: 20 # 突发流量缓冲异常处理也很关键。
大模型接口通常响应时间长,容易超时。
网关层必须设置独立的超时控制。
比如上游服务超时 30 秒,网关不能傻等,得主动断开。
3.3 高级定制
有些场景需要“动态路由”。
比如租户 A 用的是 GPT-4,租户 B 用的是开源的 Llama3。
网关得根据租户等级,把流量转发到不同的模型集群。
我们在路由配置里加了“模型标签”。
// 伪代码:根据租户等级选择模型集群 String modelCluster = tenantService.getModelCluster(tenantId); URI targetUri = UriComponentsBuilder.fromUriString(modelCluster).build().toUri();这样就能实现“千人千面”的模型调度。
四、实战演练
光说不练假把式。
咱们模拟一个真实场景:两个租户同时发起高并发请求。
租户 A 是 VIP,租户 B 是普通用户。
我们编写一个测试类,模拟并发压测。
package com.douli.gateway.test; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import reactor.core.publisher.Flux; import reactor.test.StepVerifier; import java.time.Duration; /** * 多租户限流实战测试 * 模拟 10 个并发请求,观察限流效果 */ @SpringBootTest public class TenantRateLimitTest { @Autowired private TenantRateLimitService rateLimitService; @Test public void testConcurrentAccess() { // 1. 定义租户 A (VIP) 和 租户 B (普通) String tenantA = "vip_client_001"; String tenantB = "normal_client_002"; // 2. 模拟并发请求流 // 使用 Flux 创建 10 个任务,每隔 100 毫秒发送一个 Flux<String> requestStream = Flux.interval(Duration.ofMillis(100)) .take(10) .map(i -> i % 2 == 0 ? tenantA : tenantB); // 3. 验证限流结果 // 预期:租户 A 全部通过,租户 B 部分被限流 StepVerifier.create( requestStream.flatMap(tenantId -> rateLimitService.tryAcquire(tenantId) ) ) .expectSubscription() .expectComplete() .verify(); System.out.println("测试结束,请查看控制台日志中的限流统计"); } }运行结果会显示,租户 B 的请求有部分返回了429 Too Many Requests。
而租户 A 的请求则畅通无阻。
这就验证了隔离和限流策略生效了。
五、避坑指南与最佳实践
这几年踩过的坑,希望能帮你省点头发。
💡技巧:ThreadLocal 的清理
我们在TenantContext里用了ThreadLocal存储租户信息。
一定要在过滤器链的最后,或者finally块里调用remove()。
否则线程池复用线程时,会把上一个请求的租户信息带过来。
这就成了“数据串味”的元凶。
⚠️警告:大模型响应慢
大模型生成文本需要时间,连接占用久。
网关的连接池不能设太小,否则容易耗尽。
建议配置独立的连接池,专门用于大模型路由。
同时开启“熔断”,当模型集群响应超过阈值,直接快速失败。
✅推荐:全链路追踪
多租户环境下,排查问题很难。
必须接入 SkyWalking 或 Zipkin。
把Tenant-ID作为 Trace 标签传下去。
这样查日志时,直接搜租户 ID,就能看到该租户的所有调用链。
六、综合实战演示
最后,咱们把前面提到的逻辑串起来。
写一个完整的TenantGatewayService,包含鉴权、限流、路由。
package com.douli.gateway.service; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import reactor.core.publisher.Mono; /** * 企业级大模型网关核心服务 * 整合了鉴权、限流、上下文管理 */ @Service public class TenantGatewayService { private static final Logger log = LoggerFactory.getLogger(TenantGatewayService.class); // 模拟限流器 private final RateLimiter rateLimiter; // 模拟租户信息数据库 private final TenantRepository tenantRepo; public TenantGatewayService(RateLimiter rateLimiter, TenantRepository tenantRepo) { this.rateLimiter = rateLimiter; this.tenantRepo = tenantRepo; } /** * 处理大模型请求的核心入口 * @param request 封装了租户信息的请求对象 * @return 模型响应结果 */ public Mono<String> handleLLMRequest(LLMRequest request) { // 1. 提取租户 ID String tenantId = request.getTenantId(); // 2. 设置线程上下文,确保后续逻辑能获取到租户信息 // 注意:这里使用了 try-finally 保证清理 TenantContext.set(tenantId); try { // 3. 校验租户是否存在且可用 if (!tenantRepo.exists(tenantId)) { log.warn("非法租户尝试访问:{}", tenantId); return Mono.error(new SecurityException("租户不存在")); } // 4. 执行限流检查 // 如果超过配额,直接抛出异常,不再转发给大模型 if (!rateLimiter.tryAcquire(tenantId)) { log.warn("租户 {} 请求超过配额,已限流", tenantId); return Mono.error(new RateLimitException("请求过于频繁,请稍后重试")); } // 5. 执行真正的模型调用 // 这里模拟调用下游模型服务 return callModelProvider(request.getPrompt()) .doOnSuccess(response -> log.info("租户 {} 调用成功", tenantId)) .doOnError(e -> log.error("租户 {} 调用失败", tenantId, e)); } finally { // 6. 关键步骤:清理上下文,防止内存泄漏和数据污染 TenantContext.clear(); } } private Mono<String> callModelProvider(String prompt) { // 模拟异步调用大模型接口 return Mono.just("这是模型生成的回复内容..."); } }这段代码涵盖了从入口到清理的全过程。
特别是try-finally块里的TenantContext.clear(),这是生产环境必须有的动作。
七、总结
企业级大模型网关,核心不是“通”,而是“稳”和“安”。
多租户隔离是底线,限流熔断是保障。
别为了追求功能大而全,忽略了上下文清理这种细节。
技术架构就像盖房子,地基打不牢,楼盖越高越危险。
把每个租户的流量管好,你的中台才能真正撑得起业务。