feat(game-cloud): Sentinel 入口 QPS 突发保护(FLOW_GRADE_QPS·规则热源 Nacos)(切片一 阶段〇 C2)
入队入口套 @SentinelResource(aigc:gen:admission)做入口速率闸,与 DB 三门(per-creator 配额)分层; 越限 blockHandler 优雅拒非 500。并发≤15 权威=C3 consumeThreadMax(非本 Task)。规则改 Nacos dataId 热生效。 单测锁降级语义;真限流集成验证随后端窗口。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
55e833b470
commit
6d6bb52d9d
@ -7,7 +7,7 @@ import com.wanxiang.huijing.framework.common.exception.ErrorCode;
|
||||
*
|
||||
* aigc 模块,独占 1-101-***-*** 段(见 .agents/rules/engineering-conventions.md §1.3)。
|
||||
* 约定:禁止与其它模块错误码段重叠;新增错误码在此登记。
|
||||
* 段内细分:000 任务基础校验 / 001 生成控制平面(W-G1 组A · D12+GP9)/ 002 状态机非法流转(取值对齐契约 aigc.yaml)/ 003 Dify 回调(HJ-AGENT-LOOP-EXEC-001 §8)。
|
||||
* 段内细分:000 任务基础校验 / 001 生成控制平面(W-G1 组A · D12+GP9 + C2 Sentinel 入口速率闸)/ 002 状态机非法流转(取值对齐契约 aigc.yaml)/ 003 Dify 回调(HJ-AGENT-LOOP-EXEC-001 §8)。
|
||||
*
|
||||
* @author 绘境AI
|
||||
*/
|
||||
@ -44,6 +44,13 @@ public interface ErrorCodeConstants {
|
||||
* 避免 new-api 抖动时正常用户被误标"内容不安全/系统异常"而流失;故消息独立为中性提示,归因 tag 仍记 llm_error(见日志)。
|
||||
*/
|
||||
ErrorCode AIGC_LLM_SAFETY_ERROR = new ErrorCode(1_101_001_005, "安全检查暂不可用,请稍后重试");
|
||||
/**
|
||||
* 入口 QPS 突发闸拦截(切片一 阶段〇 C2):Sentinel FLOW_GRADE_QPS(规则热源 Nacos dataId sentinel-gen-flow-rules)
|
||||
* 判入队速率超限 → blockHandler 优雅转此业务码(经全局 GlobalExceptionHandler 转 CommonResult.error,HTTP 仍 200、
|
||||
* body.code 非 0),绝不 500、不静默丢。与门①②③(per-creator 业务配额)分层:本码 = 入口速率闸削洪峰;
|
||||
* 「在跑生成并发≤15」由 C3 RocketMQ consumeThreadMax 保证、不由本门保证(三层正交)。
|
||||
*/
|
||||
ErrorCode AIGC_ADMISSION_RATE_LIMITED = new ErrorCode(1_101_001_006, "生成服务繁忙(入队速率超限),请稍后重试");
|
||||
|
||||
// ========== 生成任务状态机 1-101-002-*** ==========
|
||||
/** 取消非法:仅非终态(queued/running)可取消(契约 aigc.yaml cancel:错误码 1-101-002-001) */
|
||||
|
||||
@ -54,6 +54,21 @@
|
||||
lili-mac spike 1.1.2 实测纠偏:**绝不加 /anthropic 前缀**,否则命中 SPA catch-all 返 HTML 假挂起)驱动
|
||||
MiniMax-M3 走 Anthropic Messages 协议(thinking 原生分离),与 OpenAI-兼容路 flag 旁挂。 -->
|
||||
<dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-anthropic</artifactId></dependency>
|
||||
|
||||
<!-- ===== Sentinel 入口 QPS 突发保护(切片一 阶段〇 C2)=====
|
||||
· spring-cloud-starter-alibaba-sentinel:@SentinelResource 切面 + 流控运行时(SentinelResourceAspect 自动装配);
|
||||
· sentinel-datasource-nacos:流控规则从 Nacos dataId sentinel-gen-flow-rules 热加载(改即生效、不重启)。
|
||||
版本由 huijing-dependencies 的 spring-cloud-alibaba-dependencies:2025.0.0.0 BOM 托管(实际 Sentinel 1.8.9),不写死。
|
||||
职责 = 生成入队入口速率闸(FLOW_GRADE_QPS 削洪峰),与 DB 三门(业务配额)/ C3 consumeThreadMax(在跑并发≤15)三层正交。 -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.csp</groupId>
|
||||
<artifactId>sentinel-datasource-nacos</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Cloud 基础环境 -->
|
||||
<dependency>
|
||||
<groupId>com.wanxiang</groupId>
|
||||
|
||||
@ -0,0 +1,71 @@
|
||||
package com.wanxiang.huijing.game.module.aigc.admission;
|
||||
|
||||
import com.alibaba.csp.sentinel.annotation.SentinelResource;
|
||||
import com.alibaba.csp.sentinel.slots.block.BlockException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import static com.wanxiang.huijing.game.module.aigc.enums.ErrorCodeConstants.AIGC_ADMISSION_RATE_LIMITED;
|
||||
import static com.wanxiang.huijing.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
|
||||
/**
|
||||
* 生成入队的 Sentinel 资源门 —— 入口 QPS 突发保护(FLOW_GRADE_QPS,防洪峰)。切片一 阶段〇 C2。
|
||||
*
|
||||
* <p><b>职责边界(经 Opus 评审纠正,别把空体 admit() 当缺陷)</b>:本门统计「每秒入队请求数」,不是「在跑生成并发」。
|
||||
* 空体入口的 entry/exit 是纳秒级,thread-grade 统计不到真正在跑的生成——那发生在 admit() 返回之后的 RocketMQ
|
||||
* 消费线程里。故「在跑生成并发≤15」由 C3 消费者 consumeThreadMax=15 保证,不由本门保证。
|
||||
*
|
||||
* <p><b>三层正交</b>:
|
||||
* <ul>
|
||||
* <li>入口速率(本门 · Sentinel FLOW_GRADE_QPS):每秒入队请求数上限,削洪峰;</li>
|
||||
* <li>在跑并发(C3 · RocketMQ consumeThreadMax=15):同时在跑的生成数结构性上限;</li>
|
||||
* <li>业务配额(既有 DB 三门):per-creator×level 日额度 + per-creator 并发 + 全局背压,在 admit() 之后执行。</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p><b>越限行为</b>:Sentinel 抛 {@link BlockException} → {@link #onAdmissionBlocked} 优雅转成业务异常
|
||||
* {@code AIGC_ADMISSION_RATE_LIMITED}「生成服务繁忙…请稍后重试」(经全局 GlobalExceptionHandler 转 CommonResult.error,
|
||||
* HTTP 仍 200、body.code 非 0),<b>绝不 500、不静默丢</b>。
|
||||
*
|
||||
* <p><b>规则源</b>:Nacos dataId {@code sentinel-gen-flow-rules}(JSON),改规则即热生效、不重启;
|
||||
* 其回滚杠杆(调大 count / 删 dataId)独立于控制平面 {@code aigc.control-plane.enabled} 开关。
|
||||
*
|
||||
* @author 绘境AI
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class GenAdmissionResource {
|
||||
|
||||
/** 生成准入资源名(须与 Nacos dataId sentinel-gen-flow-rules 里的 resource 字段字面对齐)。 */
|
||||
public static final String RESOURCE = "aigc:gen:admission";
|
||||
|
||||
/**
|
||||
* 入口速率闸:受 Sentinel {@code FLOW_GRADE_QPS} 规则约束(入队 QPS 超限即 block)。
|
||||
* 通过则直接返回(调用方继续走 DB 三门 + 入队);被 block 则由 Sentinel 切面转调 {@link #onAdmissionBlocked}。
|
||||
*
|
||||
* <p><b>空体是设计,不是缺陷</b>:{@code FLOW_GRADE_QPS} 只统计「每秒通过本资源的次数」(与方法耗时无关),
|
||||
* 空体正合适——让 Sentinel 计入队 QPS 削洪峰。业务准入(配额)在调用方 DB 三门;在跑并发≤15 在 C3 consumeThreadMax,均不在此。
|
||||
*/
|
||||
@SentinelResource(value = RESOURCE, blockHandler = "onAdmissionBlocked")
|
||||
public void admit() {
|
||||
// 有意留空:见方法注释——本资源只为 Sentinel 统计入队 QPS 服务,不承载任何业务逻辑(业务准入在调用方 DB 三门)。
|
||||
}
|
||||
|
||||
/**
|
||||
* 越入口 QPS 闸的优雅降级(Sentinel blockHandler)。
|
||||
*
|
||||
* <p>Sentinel 约定:blockHandler 签名 = 原方法参数列表 + 末尾追加一个 {@link BlockException},返回类型与原方法一致(void),
|
||||
* 且可见性 public、与原方法同类——否则切面找不到 blockHandler、退化为直接向上抛 BlockException(会被当 500)。
|
||||
* admit() 无参,故此处仅一个 BlockException 参。
|
||||
*
|
||||
* @param ex Sentinel 抛出的限流/降级异常(如 FlowException 等 BlockException 子类)
|
||||
*/
|
||||
public void onAdmissionBlocked(BlockException ex) {
|
||||
// 可追溯日志(创始人铁律:错误路径必须留痕)——记资源名 + block 类型(哪种 Sentinel 规则触发,如 FlowException=流控)。
|
||||
// 刻意不调 ex.getRule():Sentinel 1.8.9 FlowException.getRule() 在 rule 为空时会 rule.as(..) 抛 NPE,
|
||||
// 若该 NPE 冒泡会把「优雅限流」渲染成 500——与本门「绝不 500、不静默丢」相悖;exception 类名恒可取、已足够定位规则类型。
|
||||
log.warn("[aigc-admission] Sentinel 入口 QPS 闸拦截:入队速率超限,拒绝新入队请求 resource={} blockType={}",
|
||||
RESOURCE, ex.getClass().getSimpleName());
|
||||
// 优雅转业务异常(错误码段 aigc 控制平面 1-101-001-006),前端得「繁忙,请稍后重试」提示;绝不 500、不静默丢。
|
||||
throw exception(AIGC_ADMISSION_RATE_LIMITED);
|
||||
}
|
||||
}
|
||||
@ -77,6 +77,15 @@ public class AigcTaskServiceImpl implements AigcTaskService {
|
||||
@Resource
|
||||
private ObjectProvider<ConfigApi> configApiProvider;
|
||||
|
||||
/**
|
||||
* 入口 QPS 突发闸(Sentinel FLOW_GRADE_QPS,规则热源 Nacos;切片一 阶段〇 C2)。
|
||||
* 与上方控制平面软注入件不同:GenAdmissionResource 是无条件 @Component(恒在席),故直接 @Resource 硬注入
|
||||
* (同 aigcTaskMapper/playerApi),不走 ObjectProvider。职责 = 入队入口速率闸削洪峰,与 DB 三门(业务配额)
|
||||
* / C3 consumeThreadMax(在跑并发≤15)三层正交,详见 {@link GenAdmissionResource} 类注释。
|
||||
*/
|
||||
@Resource
|
||||
private com.wanxiang.huijing.game.module.aigc.admission.GenAdmissionResource genAdmissionResource;
|
||||
|
||||
/**
|
||||
* v0 会员档位占位值 = L1(执行版 §5.1/B3):AigcGenerateReqVO 无 level 字段、会员档数据模型未建,
|
||||
* 提交侧统一落 L1,避免为未建会员体系造孤儿字段。分档逻辑 v0 即可用(§8 手塞 level=2 读 L2 档单测覆盖)。
|
||||
@ -289,7 +298,10 @@ public class AigcTaskServiceImpl implements AigcTaskService {
|
||||
// ============================== D12 控制平面 + GP9 共享入队(W-G1 组A·执行版 §6.1)==============================
|
||||
|
||||
/**
|
||||
* 共享入队门:submitGenerate 与 retryTask 同走(决策D)。门序固定(最便宜→最贵):
|
||||
* 共享入队门:submitGenerate 与 retryTask 同走(决策D)。
|
||||
* 【C2 入口速率闸 · 先于下方全部 DB 门】方法体最前调 genAdmissionResource.admit():Sentinel FLOW_GRADE_QPS
|
||||
* 统计每秒入队请求数削洪峰(规则热源 Nacos),越限由 blockHandler 优雅拒(非 500);独立于控制平面 enabled 开关。
|
||||
* 门序固定(最便宜→最贵,均在 admit() 之后):
|
||||
* ① 降级门(infra ConfigApi 读 aigc.generate.paused,无 LLM,new-api 挂了直接挡)
|
||||
* → ② 配额+并发门(per-creator×level 当日非取消计数 + 在飞并发计数,纯计数)
|
||||
* → ③ 背压门(全局在飞计数,纯计数)
|
||||
@ -308,6 +320,13 @@ public class AigcTaskServiceImpl implements AigcTaskService {
|
||||
* @param task 已组装好(含 creatorUserId/level/prompt/status=queued/traceId)但尚未落库的任务
|
||||
*/
|
||||
private void enqueueWithControlPlane(AigcTaskDO task) {
|
||||
// 入口 QPS 突发闸(Sentinel FLOW_GRADE_QPS,规则热源 Nacos dataId sentinel-gen-flow-rules)——先于下方全部 DB 门:
|
||||
// 统计「每秒入队请求数」削洪峰,越限由 blockHandler 优雅转 AIGC_ADMISSION_RATE_LIMITED(非 500、不静默丢)。
|
||||
// 与下方 DB 三门(per-creator 业务配额)分层正交;「在跑生成并发≤15」由 C3 RocketMQ consumeThreadMax 保证、不在此。
|
||||
// 置于 props 判空之前 = 独立于控制平面 enabled 开关(基础设施级速率保护恒生效);无 Nacos 规则时为纳秒级 no-op,
|
||||
// 其回滚杠杆是 Nacos 流控规则本身(调大 count / 删 dataId),与控制平面 enabled=false 各自独立。
|
||||
genAdmissionResource.admit();
|
||||
|
||||
AigcControlPlaneProperties props = controlPlanePropertiesProvider.getIfAvailable();
|
||||
// 控制平面关闭/未装配:全门 + GP9 旁路,直接落库(现行行为逐字不变,§10 回滚首选层)
|
||||
if (props == null || !Boolean.TRUE.equals(props.getEnabled())) {
|
||||
|
||||
@ -0,0 +1,43 @@
|
||||
package com.wanxiang.huijing.game.module.aigc.admission;
|
||||
|
||||
import com.alibaba.csp.sentinel.slots.block.BlockException;
|
||||
import com.alibaba.csp.sentinel.slots.block.flow.FlowException;
|
||||
import com.wanxiang.huijing.framework.common.exception.ServiceException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static com.wanxiang.huijing.game.module.aigc.enums.ErrorCodeConstants.AIGC_ADMISSION_RATE_LIMITED;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* {@link GenAdmissionResource} 单元测试(切片一 阶段〇 C2)。
|
||||
*
|
||||
* <p>只锁 blockHandler 的<b>优雅降级语义</b>:越 QPS 闸 → 转可读业务异常(含「繁忙」、带业务错误码),绝不 500、不静默丢。
|
||||
* admit() 空体的真 QPS 限流依赖 Sentinel 运行时统计 + Nacos 规则(集成态),不在单测范围——由后端窗口的集成验证覆盖。
|
||||
*
|
||||
* @author 绘境AI
|
||||
*/
|
||||
class GenAdmissionResourceTest {
|
||||
|
||||
/**
|
||||
* 越入口 QPS 闸:blockHandler 必须抛业务 {@link ServiceException}(消息含「繁忙」、错误码 = aigc 控制平面段 1-101-001-006),
|
||||
* 而非裸 500 服务器错误——锁死「优雅拒、不静默丢」这条降级契约不回退。
|
||||
*/
|
||||
@Test
|
||||
void onAdmissionBlocked_throwsBusinessServiceException_notServerError() {
|
||||
GenAdmissionResource resource = new GenAdmissionResource();
|
||||
// 模拟 Sentinel 流控拦截:FlowException 是 BlockException 子类,资源名对齐 RESOURCE
|
||||
BlockException blockEx = new FlowException(GenAdmissionResource.RESOURCE);
|
||||
|
||||
ServiceException ex = assertThrows(ServiceException.class,
|
||||
() -> resource.onAdmissionBlocked(blockEx));
|
||||
|
||||
// ① 优雅业务提示(含「繁忙」)——反 500、反静默丢
|
||||
assertTrue(ex.getMessage().contains("繁忙"),
|
||||
"blockHandler 应转成含「繁忙」的可读业务提示,实际=" + ex.getMessage());
|
||||
// ② 携带 aigc 控制平面段业务码(经全局 handler 转 CommonResult.error、HTTP 200 body.code 非 0)
|
||||
assertEquals(AIGC_ADMISSION_RATE_LIMITED.getCode(), ex.getCode(),
|
||||
"错误码应 = AIGC_ADMISSION_RATE_LIMITED(1-101-001-006)");
|
||||
}
|
||||
}
|
||||
@ -54,6 +54,10 @@ class AigcControlPlaneTest extends BaseMockitoUnitTest {
|
||||
private ObjectProvider<SafetyCheckClient> safetyCheckClientProvider;
|
||||
@Mock
|
||||
private ObjectProvider<ConfigApi> configApiProvider;
|
||||
// C2 入口 QPS 闸(硬注入 @Component):@InjectMocks 会注入本 mock,其 void admit() 默认 no-op(不 block、不 NPE),
|
||||
// 故控制平面各门用例行为不变;真限流降级语义由 GenAdmissionResourceTest 单独锁。
|
||||
@Mock
|
||||
private com.wanxiang.huijing.game.module.aigc.admission.GenAdmissionResource genAdmissionResource;
|
||||
|
||||
@Mock
|
||||
private SafetyCheckClient safetyCheckClient;
|
||||
|
||||
@ -55,6 +55,9 @@ class AigcTaskServiceImplTest extends BaseMockitoUnitTest {
|
||||
private ObjectProvider<SafetyCheckClient> safetyCheckClientProvider;
|
||||
@Mock
|
||||
private ObjectProvider<ConfigApi> configApiProvider;
|
||||
// C2 入口 QPS 闸(硬注入 @Component):@InjectMocks 注入本 mock,void admit() 默认 no-op → 现行提交/重试基线行为不变。
|
||||
@Mock
|
||||
private com.wanxiang.huijing.game.module.aigc.admission.GenAdmissionResource genAdmissionResource;
|
||||
|
||||
// ============================== submitGenerate ==============================
|
||||
|
||||
|
||||
@ -69,6 +69,21 @@ spring:
|
||||
enabled: true # 重启用 Nacos 配置中心(was false):从 Nacos 读外部化配置
|
||||
namespace: public
|
||||
file-extension: yaml # 配置文件格式
|
||||
# Sentinel 入口 QPS 突发保护(切片一 阶段〇 C2):流控规则从 Nacos dataId sentinel-gen-flow-rules 热加载(改即生效、不重启)。
|
||||
# 职责 = 生成入队入口速率闸(FLOW_GRADE_QPS 削洪峰),与 DB 三门(业务配额)/ C3 consumeThreadMax(在跑并发≤15)三层正交。
|
||||
sentinel:
|
||||
transport:
|
||||
dashboard: ${SENTINEL_DASHBOARD:} # 留空 = 不接 Sentinel 控制台(mini-infra RAM 紧、不部面板);规则直存 Nacos
|
||||
datasource:
|
||||
gen-flow: # 生成入队流控规则数据源(名字任意,仅作 datasource 标识)
|
||||
nacos:
|
||||
server-addr: 100.64.0.8:8848 # 复用 C1 同一 mini-infra Nacos
|
||||
username: nacos # 鉴权同 C1 nacos 段(见 docs/内网凭据与端点.md)
|
||||
password: ${NACOS_PASSWORD} # 口令走 env 注入,严禁明文入仓
|
||||
dataId: sentinel-gen-flow-rules # 流控规则 dataId(JSON;resource=aigc:gen:admission)
|
||||
groupId: DEFAULT_GROUP
|
||||
data-type: json
|
||||
rule-type: flow # 流控规则(FLOW_GRADE_QPS 入口速率闸)
|
||||
# openfeign:
|
||||
# lazy-attributes-resolution: true
|
||||
# client:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user