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:
lili 2026-07-01 09:59:31 -07:00
parent 55e833b470
commit 6d6bb52d9d
8 changed files with 179 additions and 2 deletions

View File

@ -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 突发闸拦截切片一 阶段 C2Sentinel FLOW_GRADE_QPS规则热源 Nacos dataId sentinel-gen-flow-rules
* 判入队速率超限 blockHandler 优雅转此业务码经全局 GlobalExceptionHandler CommonResult.errorHTTP 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 */

View File

@ -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>

View File

@ -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 200body.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);
}
}

View File

@ -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/B3AigcGenerateReqVO 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 LLMnew-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())) {

View File

@ -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.errorHTTP 200 body.code 0
assertEquals(AIGC_ADMISSION_RATE_LIMITED.getCode(), ex.getCode(),
"错误码应 = AIGC_ADMISSION_RATE_LIMITED1-101-001-006");
}
}

View File

@ -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;

View File

@ -55,6 +55,9 @@ class AigcTaskServiceImplTest extends BaseMockitoUnitTest {
private ObjectProvider<SafetyCheckClient> safetyCheckClientProvider;
@Mock
private ObjectProvider<ConfigApi> configApiProvider;
// C2 入口 QPS 硬注入 @Component@InjectMocks 注入本 mockvoid admit() 默认 no-op 现行提交/重试基线行为不变
@Mock
private com.wanxiang.huijing.game.module.aigc.admission.GenAdmissionResource genAdmissionResource;
// ============================== submitGenerate ==============================

View File

@ -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 # 流控规则 dataIdJSONresource=aigc:gen:admission
groupId: DEFAULT_GROUP
data-type: json
rule-type: flow # 流控规则FLOW_GRADE_QPS 入口速率闸)
# openfeign:
# lazy-attributes-resolution: true
# client: