【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 MonoVoid 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 毫秒发送一个 FluxString 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 MonoString 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 MonoString callModelProvider(String prompt) { // 模拟异步调用大模型接口 return Mono.just(这是模型生成的回复内容...); } }这段代码涵盖了从入口到清理的全过程。特别是try-finally块里的TenantContext.clear()这是生产环境必须有的动作。七、总结企业级大模型网关核心不是“通”而是“稳”和“安”。多租户隔离是底线限流熔断是保障。别为了追求功能大而全忽略了上下文清理这种细节。技术架构就像盖房子地基打不牢楼盖越高越危险。把每个租户的流量管好你的中台才能真正撑得起业务。