ASP-NET API 并发与速率限制实践

  • 面向 ASP-NET Core 8/9/10(System.Threading.RateLimiting 与内建中间件)
  • 聚焦服务端优雅限流与并发控制:可维护、可观测、可测试、可扩展
  • 兼顾单机与分布式、网关与应用内、策略与参数选择

一、核心概念与适用场景

  • 速率限制(Rate Limiting)
    • 约束单位时间内的请求频次或吞吐(如每秒 N 次、每分钟 M 次)。
    • 目的:抑制恶意/突发流量、保护下游系统、控制成本。
    • 常见策略:固定窗口、滑动窗口、令牌桶。
  • 并发限制(Concurrency Limiting)
    • 约束同时进行中的请求数量(例如最多并发 200 个)。
    • 目的:避免资源争用、线程池/数据库连接耗尽、提升稳定性。
  • 应用层 vs 网关层
    • 网关层(Nginx/Envoy/API Gateway/Cloudflare)适合粗粒度与分布式限流。
    • 应用层(ASP-NET 中间件)适合细粒度、按用户/端点/身份的策略与自定义响应。

选择建议:

  • 外部防护与全局流控:优先网关层;内部细粒度与业务化响应:应用层配合。
  • CPU/内存敏感的重请求:并发限制优先;突发/恶意流量:速率限制优先。
  • 需要突发缓冲但平滑速率:令牌桶;强调时间边界:固定窗口;追求公平与精度:滑动窗口。

二、ASP-NET Core 内建限流中间件概览

ASP-NET Core 7+ 引入 Rate Limiting 中间件,基于 System.Threading.RateLimiting

  • 策略类型
    • FixedWindowLimiterOptions(固定窗口)
    • SlidingWindowLimiterOptions(滑动窗口)
    • TokenBucketRateLimiterOptions(令牌桶)
    • ConcurrencyLimiterOptions(并发限制)
  • 分区(PartitionedRateLimiter)
    • 按用户、IP、租户、端点等维度为不同分区生成独立限流器,互不干扰。
  • 配置入口
    • AddRateLimiter / UseRateLimiter
    • 命名策略(AddPolicy + [EnableRateLimiting]/[DisableRateLimiting]
  • 自定义拒绝响应
    • OnRejected 事件,编写 Retry-After、返回 ProblemDetails 等。

注意:内建中间件是单机进程内限流,分布式需自行实现(如 Redis)或依赖网关。

三、最小可行配置示例(全局速率限制 + 并发限制)

Program.cs(.NET 7/8+):

using System.Threading.RateLimiting;
using Microsoft.AspNetCore.RateLimiting;

var builder = WebApplication.CreateBuilder(args);

// 全局限流:IP 分区的令牌桶 + 并发限制叠加
builder.Services.AddRateLimiter(options =>
{
    options.OnRejected = async (context, token) =>
    {
        // 构造标准化拒绝响应
        context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
        // 可选:告知下次可重试时间
        var retryAfter = TimeSpan.FromSeconds(2);
        context.HttpContext.Response.Headers["Retry-After"] = ((int)retryAfter.TotalSeconds).ToString();
        await context.HttpContext.Response.WriteAsync("Too many requests", token);
    };

    // 全局策略:按 IP 分区的令牌桶
    options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
    {
        var ip = httpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown";
        return RateLimitPartition.GetTokenBucketLimiter(
            partitionKey: ip,
            factory: key => new TokenBucketRateLimiterOptions
            {
                TokenLimit = 100,                // 桶容量(突发)
                TokensPerPeriod = 50,            // 每周期填充数量(持续速率)
                ReplenishmentPeriod = TimeSpan.FromSeconds(1),
                AutoReplenishment = true,
                QueueLimit = 0,                  // 不排队,超限直接拒绝
                QueueProcessingOrder = QueueProcessingOrder.OldestFirst
            });
    });

    // 并发限制(可叠加)
    options.AddPolicy("concurrency", httpContext =>
        RateLimitPartition.GetConcurrencyLimiter(
            partitionKey: "global",
            factory: key => new ConcurrencyLimiterOptions
            {
                PermitLimit = 200,               // 最大并发
                QueueLimit = 100,                // 等待队列长度
                QueueProcessingOrder = QueueProcessingOrder.OldestFirst
            }));
});

var app = builder.Build();

app.UseRateLimiter();

// 针对特定端点启用并发策略
app.MapGet("/heavy", async () =>
{
    await Task.Delay(500); // 模拟重处理
    return Results.Ok("heavy done");
}).RequireRateLimiting("concurrency");

// 测试端点(受全局令牌桶约束)
app.MapGet("/ping", () => "pong");

app.Run();

要点:

  • 全局令牌桶防止突发请求冲击。
  • 对重型端点叠加并发限制(即使速率不高也可避免同时过多占用资源)。
  • OnRejected 保证统一可诊断的 429/503 响应。

四、命名策略与端点级应用

命名策略便于针对不同路由、方法、租户应用不同限流参数。

builder.Services.AddRateLimiter(options =>
{
    options.AddPolicy("read-sliding", httpContext =>
        RateLimitPartition.GetSlidingWindowLimiter(
            partitionKey: GetUserOrIp(httpContext),
            factory: key => new SlidingWindowLimiterOptions
            {
                PermitLimit = 300,                       // 窗口总许可
                Window = TimeSpan.FromMinutes(1),
                SegmentsPerWindow = 6,                   // 10s 段
                QueueLimit = 50,
                QueueProcessingOrder = QueueProcessingOrder.OldestFirst
            }));

    options.AddPolicy("write-fixed", httpContext =>
        RateLimitPartition.GetFixedWindowLimiter(
            partitionKey: GetUserOrIp(httpContext),
            factory: key => new FixedWindowLimiterOptions
            {
                PermitLimit = 60,                        // 每分钟最多 60 次写
                Window = TimeSpan.FromMinutes(1),
                QueueLimit = 20,
                QueueProcessingOrder = QueueProcessingOrder.OldestFirst
            }));
});

app.MapGroup("/api")
   .MapGet("/items", GetItems)
   .RequireRateLimiting("read-sliding");

app.MapGroup("/api")
   .MapPost("/items", CreateItem)
   .RequireRateLimiting("write-fixed");

也可使用特性:

using Microsoft.AspNetCore.RateLimiting;

[EnableRateLimiting("write-fixed")]
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase { /* ... */ }

五、分区(Partitioning)设计

分区键建议稳定且可识别,典型维度:

  • 用户标识(UserId/Subject)
  • API Key / ClientId
  • 租户(TenantId)
  • 客户端 IP(注意代理场景 X-Forwarded-For)
  • 端点维度(RouteId + Method)

示例:

static string GetUserOrIp(HttpContext ctx)
{
    var userId = ctx.User?.FindFirst("sub")?.Value;
    if (!string.IsNullOrEmpty(userId)) return $"user:{userId}";

    var apiKey = ctx.Request.Headers["X-API-Key"].FirstOrDefault();
    if (!string.IsNullOrEmpty(apiKey)) return $"key:{apiKey}";

    var ip = ctx.Request.Headers["X-Forwarded-For"].FirstOrDefault()
             ?? ctx.Connection.RemoteIpAddress?.ToString()
             ?? "unknown";
    return $"ip:{ip}";
}

实践要点:

  • 统一源识别:在反向代理后要正确处理 X-Forwarded-For。
  • 防御伪造:API Key/授权体系的分区优于仅用 IP。
  • 分区数量管理:分区过多可能增大内存占用,需配合缓存过期策略(见后述)。

六、策略类型与参数选择

  • FixedWindow(固定窗口)
    • 优点:实现简单、常用于“每分钟最多 N 次”。
    • 缺点:窗口边界附近不公平(窗口重置时可能突发)。
    • 参数:PermitLimit、Window、QueueLimit、QueueProcessingOrder。
  • SlidingWindow(滑动窗口)
    • 优点:更平滑、公平;突发更可控。
    • 参数:SegmentsPerWindow(越多越平滑但开销略增)。
  • TokenBucket(令牌桶)
    • 优点:支持突发(桶容量)、可配置补充速率(每周期填充)。
    • 适用于需短时突发但长期速率受控的场景。
    • 参数:TokenLimit、TokensPerPeriod、ReplenishmentPeriod、AutoReplenishment、QueueLimit。
  • ConcurrencyLimiter(并发限制)
    • 限制同时处理的请求数;用于保护 CPU、数据库连接池、线程池。
    • 参数:PermitLimit、QueueLimit、QueueProcessingOrder。

队列与拒绝:

  • QueueLimit=0:超限直接拒绝(更可预测,避免排队导致尾部延迟)。
  • QueueLimit>0:可在短时拥塞时缓冲,但需权衡延迟与公平。
  • 建议重型端点并发限制 QueueLimit 适度(如 50-100),轻型端点令牌桶 QueueLimit=0

七、拒绝响应设计与客户端提示

统一拒绝语义:

  • 429 Too Many Requests(速率超限)
  • 503 Service Unavailable(并发/资源不足)

HTTP 头:

  • Retry-After(秒或日期):告知客户端重试时间。
  • RateLimit-Limit / RateLimit-Remaining / RateLimit-Reset(IETF 草案):便于客户端自适应。
  • 自定义 X-RateLimit-*(兼容历史客户端)。

示例(OnRejected):

options.OnRejected = async (context, token) =>
{
    var response = context.HttpContext.Response;
    response.StatusCode = StatusCodes.Status429TooManyRequests;
    response.Headers["Retry-After"] = "2"; // 简单示意

    var problem = new
    {
        type = "https://httpstatuses.com/429",
        title = "Rate limit exceeded",
        status = 429,
        detail = "Please retry later",
        traceId = context.HttpContext.TraceIdentifier
    };
    response.ContentType = "application/json";
    await response.WriteAsJsonAsync(problem, cancellationToken: token);
};

八、观测性与指标埋点

建议以 OpenTelemetry/Meter 记录核心指标:

  • 允许数(acquire_success)、拒绝数(acquire_rejected)
  • 当前并发(concurrency_in_use)
  • 排队长度(queue_length)
  • 每分区统计(标签:policy、partitionKey)

示例(简单 Meter):

using System.Diagnostics.Metrics;

var meter = new Meter("MyApp.RateLimiting", "1.0");
var rejectedCounter = meter.CreateCounter<long>("ratelimit_rejected");
var permitCounter   = meter.CreateCounter<long>("ratelimit_permits");

builder.Services.AddRateLimiter(options =>
{
    options.OnRejected = async (context, token) =>
    {
        var policy = context.Lease?.ToString() ?? "unknown";
        rejectedCounter.Add(1, KeyValuePair.Create<string, object?>("policy", policy));
        context.HttpContext.Response.StatusCode = 429;
        await context.HttpContext.Response.WriteAsync("Too many requests", token);
    };

    // 注意:成功获取许可的事件需在业务层记录,因为中间件内部不直接暴露
});

同时启用 ASP-NET Core 日志:

  • 分类:Microsoft.AspNetCore.RateLimiting.RateLimitingMiddleware
  • 提取 traceId 与策略名,便于关联分析。

九、内存与分区缓存管理

PartitionedRateLimiter 会为不同分区创建限流器。分区数很大时,需控制生命周期。

策略:

  • 使用 MemoryCache 为每个分区创建一次限流器,设置滑动过期(无请求一段时间后回收)。
  • 将 PartitionedRateLimiter.Create 的 factory 包裹在缓存逻辑中。

示例:

using Microsoft.Extensions.Caching.Memory;

var cache = new MemoryCache(new MemoryCacheOptions
{
    SizeLimit = 100_000 // 视规模而定
});

builder.Services.AddRateLimiter(options =>
{
    options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(ctx =>
    {
        var key = GetUserOrIp(ctx);

        var limiter = cache.GetOrCreate(key, entry =>
        {
            entry.SlidingExpiration = TimeSpan.FromMinutes(10);
            entry.Size = 1;

            return new TokenBucketRateLimiter(
                new TokenBucketRateLimiterOptions
                {
                    TokenLimit = 200,
                    TokensPerPeriod = 100,
                    ReplenishmentPeriod = TimeSpan.FromSeconds(1),
                    AutoReplenishment = true,
                    QueueLimit = 0,
                    QueueProcessingOrder = QueueProcessingOrder.OldestFirst
                });
        });

        return RateLimitPartition.Create(key, _ => limiter);
    });
});

十、分布式限流(Redis)示例

内建中间件不跨进程。分布式场景(多实例/容器化)可使用 Redis 实现 Lua 原子脚本。

令牌桶(Redis)示例思路:

  • Key:rl:{partition}
  • 字段:
    • tokens(当前令牌)
    • timestamp(上次补充时间)
  • 每次请求:
    • 依据当前时间补充令牌:tokens = min(capacity, tokens + elapsed * rate)
    • tokens >= cost(通常为 1),扣减并允许;否则拒绝。

Lua 伪代码:

local key = KEYS[1]
local capacity = tonumber(ARGV[1])       -- 桶容量
local rate = tonumber(ARGV[2])           -- 每秒令牌
local now = tonumber(ARGV[3])            -- 当前时间戳秒
local cost = tonumber(ARGV[4])           -- 请求消耗令牌数(默认 1)

local data = redis.call("HMGET", key, "tokens", "ts")
local tokens = tonumber(data[1]) or capacity
local ts = tonumber(data[2]) or now

local elapsed = math.max(0, now - ts)
tokens = math.min(capacity, tokens + elapsed * rate)

if tokens >= cost then
  tokens = tokens - cost
  redis.call("HMSET", key, "tokens", tokens, "ts", now)
  redis.call("EXPIRE", key, 3600)
  return {1, tokens} -- 允许
else
  redis.call("HMSET", key, "tokens", tokens, "ts", now)
  redis.call("EXPIRE", key, 3600)
  return {0, tokens} -- 拒绝
end

ASP-NET 中间件接入(自定义 Middleware):

app.Use(async (ctx, next) =>
{
    var key = $"rl:{GetUserOrIp(ctx)}";
    var allowed = await RedisAllowAsync(key, capacity:200, ratePerSec:100, cost:1);
    if (!allowed)
    {
        ctx.Response.StatusCode = 429;
        await ctx.Response.WriteAsync("Too many requests");
        return;
    }
    await next();
});

并发限制(分布式 Semaphore):

  • 采用 Redis 有界信号量(SetNX + TTL 或基于 Redlock 的资源锁集合)。
  • 进入时申请一个令牌 key,如 sem:{resource}:{id},数量不超过 PermitLimit。
  • 退出时删除令牌。崩溃由 TTL 自动回收。

伪实现要点:

  • 进入:INCR 计数,如果 > PermitLimit 则 DECR 并拒绝;成功则设置 TTL。
  • 退出:DECR;确保幂等(使用请求/连接 id 作为成员,集合去重)。

十一、成本加权限流(Cost-based)

某些请求“机重”不同(轻查询 vs 重写),希望动态消耗不同令牌。内建 RateLimitingMiddleware Acquire 的默认行为是每请求消耗 1 单位。要实现成本加权需自行中间件或使用 Redis 令牌桶的 cost 参数。

示例(自定义中间件 + TokenBucketRateLimiter):

var limiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions
{
    TokenLimit = 1000,
    TokensPerPeriod = 500,
    ReplenishmentPeriod = TimeSpan.FromSeconds(1),
    AutoReplenishment = true,
    QueueLimit = 0
});

app.Use(async (ctx, next) =>
{
    var cost = EstimateCost(ctx); // 根据路由/方法/查询参数估算代价
    using var lease = await limiter.AcquireAsync(cost, ctx.RequestAborted);
    if (!lease.IsAcquired)
    {
        ctx.Response.StatusCode = 429;
        await ctx.Response.WriteAsync("Rate limit exceeded");
        return;
    }
    await next();
});

注意:ASP-NET Core 内建 RateLimitingMiddleware 的命名策略接口不会传入动态成本,需要自建中间件。

十二、与 Polly、YARP、网关的协同

  • Polly v8 提供 RateLimit 策略,可在 HttpClient 或委托管道层对下游调用施加限流,与应用入口限流互补。
  • YARP(反向代理)可在集群入口实现路由级限流,结合 ASP-NET 应用内策略实现双层防护。
  • Nginx/Envoy/API Gateway/Cloudflare:
    • 统一接入层限流(基于 IP/路径),抵御恶意流量。
    • 应用层细粒度(基于用户/租户)保证业务公平与透明响应。

实践:

  • 外层粗粒度 + 内层细粒度,避免双重排队(队列长度总体可控)。
  • 共享限流上下文信息(X-RateLimit-* 头、TraceId)便于端到端调试。

十三、测试与基准

  • 压测工具:k6、wrk、bombardier
    • 验证并发限制效果:同时起多个连接,观察 503/排队。
    • 验证速率限制:持续高压,统计 429 比例与延迟分布。
  • 集成测试(TestServer)
    • 通过 Task.Delay 模拟重型端点,断言拒绝比例。
    • 时序行为:控制请求节律,测试滑动窗口精度。
  • 观测验证:
    • 指标:拒绝数、排队长度、平均延迟、P95/P99。
    • 日志:策略名、分区键、traceId。

十四、常见陷阱与规避

  • 仅用 IP 作为分区在代理场景不可靠:需处理 X-Forwarded-For,或优先用户/API Key。
  • 全局固定窗口在边界突发不公平:对精度敏感用滑动窗口或令牌桶。
  • 过度排队导致长尾延迟:QueueLimit 保守;重型端点优先拒绝而非排队。
  • 并发限制过小引发吞吐崩溃;过大引发资源耗尽:通过基准测算 PermitLimit。
  • 测试环境时钟与补充周期不稳定:令牌桶 AutoReplenishment 依赖定时器,需留出缓冲。
  • 大量分区内存膨胀:采用缓存滑动过期;对匿名流量合并分区或启用网关限流。
  • 429 与重试风暴:客户端需指数退避;服务端提供清晰 Retry-After。
  • 流式/长连接端点(SSE、gRPC streaming):
    • 令牌桶只在连接建立时限流,连接存活应采用并发限制或连接数限制。
  • 多层限流叠加导致难以调参:统一文档化策略、参数、优先级与覆盖范围。

十五、性能与架构考量

  • 中间件开销低,限流器操作为 O(1);但分区查找与缓存需注意热路径优化。
  • 在 ThreadPool 压力下,并发限制有助于避免上下文切换与过度排队。
  • 与数据库/下游调用结合:并发限制的上限不应超过连接池/资源最大可承载值。
  • 配置与代码分离:策略参数放入配置文件/FeatureFlag,支持动态调整(重启或热更新)。

十六、按端点成本模型与权重路由

在复杂系统中,不同端点的单位成本差异较大:

  • 建议建立端点权重表(成本估算),结合成本加权令牌桶。
  • 重写/导出类端点设置更严格的并发限制与队列上限。
  • 对只读高频查询采用滑动窗口或令牌桶,允许短时突发。

示例(端点权重表):

static int EstimateCost(HttpContext ctx)
{
    var route = ctx.GetRouteData()?.Values["action"]?.ToString() ?? "default";
    return route switch
    {
        "Export" => 10,
        "Report" => 5,
        _ => 1
    };
}

十七、配额(Quota)与限流结合

限流是瞬时控制,配额是周期总量控制(每日/每月总调用数)。两者需结合:

  • 配额检查:在授权后读取 Redis/数据库计数,超过配额直接拒绝。
  • 限流:在配额内仍需平滑频率。

简单配额计数:

// 伪代码:每日配额
var key = $"quota:{DateTime.UtcNow:yyyyMMdd}:{GetUserOrIp(ctx)}";
var count = await redis.StringIncrementAsync(key);
await redis.KeyExpireAsync(key, TimeSpan.FromDays(1));
if (count > dailyLimit)
{
    ctx.Response.StatusCode = 429;
    await ctx.Response.WriteAsync("Daily quota exceeded");
    return;
}

十八、代码组织与可维护性

  • 中间件层:公共限流与并发控制、统一拒绝响应。
  • 端点层:命名策略绑定、特殊端点的额外约束。
  • 分区服务:统一获取分区键,内置防御与缓存。
  • 配置中心:策略参数与分区规则可配置。
  • 观测层:统一指标、日志上下文、TraceId 贯通。
  • 测试层:压测脚本与集成测试用例与阈值基线。

十九、参考实现:完整 Program.cs(复合策略)

using System.Threading.RateLimiting;
using Microsoft.AspNetCore.RateLimiting;

var builder = WebApplication.CreateBuilder(args);

// 观测与配置(省略),假设从配置读取下列值
var globalBurst = 200;
var globalRate = 100;
var heavyConcurrency = 100;
var heavyQueue = 50;

builder.Services.AddRateLimiter(options =>
{
    options.OnRejected = async (context, token) =>
    {
        var res = context.HttpContext.Response;
        res.StatusCode = StatusCodes.Status429TooManyRequests;
        res.Headers["Retry-After"] = "1";
        await res.WriteAsync("Rate limit exceeded", token);
    };

    // 全局令牌桶(按用户/API-Key/IP)
    options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(ctx =>
    {
        var key = GetUserOrIp(ctx);
        return RateLimitPartition.GetTokenBucketLimiter(key, _ => new TokenBucketRateLimiterOptions
        {
            TokenLimit = globalBurst,
            TokensPerPeriod = globalRate,
            ReplenishmentPeriod = TimeSpan.FromSeconds(1),
            AutoReplenishment = true,
            QueueLimit = 0
        });
    });

    // 读多写少:读滑动窗口,写固定窗口
    options.AddPolicy("read", ctx =>
        RateLimitPartition.GetSlidingWindowLimiter(GetUserOrIp(ctx), _ => new SlidingWindowLimiterOptions
        {
            PermitLimit = 300,
            Window = TimeSpan.FromMinutes(1),
            SegmentsPerWindow = 6,
            QueueLimit = 20
        }));

    options.AddPolicy("write", ctx =>
        RateLimitPartition.GetFixedWindowLimiter(GetUserOrIp(ctx), _ => new FixedWindowLimiterOptions
        {
            PermitLimit = 60,
            Window = TimeSpan.FromMinutes(1),
            QueueLimit = 10
        }));

    // 重型端点并发限制
    options.AddPolicy("heavy", ctx =>
        RateLimitPartition.GetConcurrencyLimiter("heavy", _ => new ConcurrencyLimiterOptions
        {
            PermitLimit = heavyConcurrency,
            QueueLimit = heavyQueue,
            QueueProcessingOrder = QueueProcessingOrder.OldestFirst
        }));
});

var app = builder.Build();
app.UseRateLimiter();

app.MapGroup("/api")
   .MapGet("/items", GetItems)
   .RequireRateLimiting("read");

app.MapGroup("/api")
   .MapPost("/items", CreateItem)
   .RequireRateLimiting("write");

app.MapGet("/export", async () =>
{
    await Task.Delay(1000); // 模拟重型导出
    return Results.Ok();
}).RequireRateLimiting("heavy");

app.Run();

static string GetUserOrIp(HttpContext ctx)
{
    var userId = ctx.User?.FindFirst("sub")?.Value;
    if (!string.IsNullOrEmpty(userId)) return $"user:{userId}";

    var apiKey = ctx.Request.Headers["X-API-Key"].FirstOrDefault();
    if (!string.IsNullOrEmpty(apiKey)) return $"key:{apiKey}";

    var ip = ctx.Request.Headers["X-Forwarded-For"].FirstOrDefault()
             ?? ctx.Connection.RemoteIpAddress?.ToString()
             ?? "unknown";
    return $"ip:{ip}";
}

static IResult GetItems() => Results.Ok(new[] { "a", "b" });
static IResult CreateItem() => Results.Ok();

二十、生产实践建议清单

  • 网关层启用基础限流与 DDoS 防护;应用层实施细粒度策略。
  • 明确分区规则与优先顺序(用户/Key > 租户 > IP)。
  • 对重型端点设置并发限制与较小队列;轻型端点令牌桶 QueueLimit=0。
  • 启用统一拒绝响应与 Retry-After;客户端遵循退避策略。
  • 指标与日志必须可视化:拒绝率、排队时长、当前并发、分区分布。
  • 限流参数基于压测校准;高峰期支持动态调整(FeatureFlag/配置刷新)。
  • 分布式场景采用 Redis/Lua 或网关方案;避免在应用层做复杂分布式锁,保持简单可控。
  • 对流式/长连接单独评估并发与连接数限制。
  • 文档化策略与端点映射,纳入变更流程与回归测试。
  • 针对突发与资源紧张场景进行混沌演练(注入高并发/高流量),验证拒绝与恢复行为。