XiHan Framework 处于预览阶段

查看获取方式
跳转到内容

弹性与限流 ​

本章说明 Resilience 阶段的截止时间、熔断、固定窗口限流与并发上限,以及如何按操作配置它们。适用于需要保护下游、限制昂贵操作或控制超时的开发者。

框架不提供网关与流量治理模块:按操作 ID 的限制由内核完成,反向代理与 API 网关交给 YARP、Nginx 等专门的组件。

完整 API 见 Kernel 包。

截止时间 ​

调用上下文带一个绝对截止时间 InvocationContext.Deadline(UTC,基于 TimeProvider)。内核的 DeadlineMiddleware 位于 Resilience 阶段开头,到点即取消下游:

结果错误码HTTP
到达截止时间KERNEL_TIMEOUT_001504
截止时间到达之前调用方取消(连接断开等)KERNEL_CANCELLED_001408

截止时间随调用传播:HTTP 用请求头 xihan-deadline(RFC 3339,UTC),gRPC 用 grpc-timeout,命令行用 --timeout;下游调用的截止时间不晚于上游。操作与仓储只需要把 context.CancellationToken 传下去,不必自己计时。context.RemainingTime(now) 与 context.IsExpired(now) 可读剩余时间。

截止时间到了一律报超时,调用方同时取消也一样:DeadlineMiddleware 只看令牌归因,截止令牌已触发即 KERNEL_TIMEOUT_001,截止令牌没有触发、调用方已取消才是 KERNEL_CANCELLED_001。下游抛出的取消,与下游接住取消、自己写下 KERNEL_CANCELLED_001 结果正常返回(不抛异常)的,按同一规则归因。不要在下游用 context.IsExpired(now) 区分超时与取消——定时器按系统节拍触发,可能早于时钟读数到达截止时间,几毫秒的差就会把超时判成取消,晚于时又会在调用方取消时判成超时;写下取消结果,交给 DeadlineMiddleware 归因。框架自己的插件运行时也这样做:进程外插件运行时把 gRPC 的 Cancelled 原样写成取消,Wasm 运行时原样返回插件写下的取消。

DeadlineMiddleware 自己也不看时钟(已定,ADR-0095):截止令牌还没触发时,即使时钟读数已过截止时间,调用方的取消仍是 KERNEL_CANCELLED_001。定时器晚于时钟读数到达(线程池繁忙、定时器合并)而调用方恰在这段通常不超过一个系统节拍的间隙里取消,这类调用报取消;换来的是归因只有令牌一个来源,在注入的 TimeProvider 下可以确定地复现,抛出与写成结果两条路径也不会分成两种回答。

超时的归因与对外结果(ADR-0085、ADR-0088、ADR-0092)

进程外插件运行时(接住 gRPC 的 Cancelled)、Wasm 宿主与运行时、自己接住取消的操作或 Provider,在截止时间到了的调用上一律是 KERNEL_TIMEOUT_001:

看哪里截止时间到了调用方在截止时间到达之前取消
错误码 / Problem Details typeKERNEL_TIMEOUT_001KERNEL_CANCELLED_001
HTTP 状态504408
gRPC 状态DeadlineExceededCancelled
命令行退出码7(超时)8(取消)

截止时间与调用方取消同时触发(如客户端在截止时间到点前后断开)时,下游抛出的取消与写成结果的取消都报超时(ADR-0092)。

按状态码或错误码决定重试的调用方要按超时处理:截止时间已经用尽,被调方可能已经完成了副作用,非幂等操作重试前带上幂等键(Baggage xihan.idempotency-key,见幂等键)。调用方在截止时间到达之前自己取消(连接断开、取消令牌)是 KERNEL_CANCELLED_001 / 408。熔断按原结果计数:ResilienceMiddleware 在 DeadlineMiddleware 之内,看到的是下游写下的原结果。审计、追踪与访问日志看到的是改写后的超时。

按操作的限制 ​

内核的 ResilienceMiddleware 紧随 DeadlineMiddleware,只作用于带操作描述的调用;宿主自定义端点、消息派发与作业派发不受它约束。每个操作 ID 各持一份限制实例,判定顺序固定为熔断 → 限流 → 并发:

限制语义拒绝码
熔断连续 FailureThreshold 次失败即打开 BreakSeconds 秒;到期半开,只放一次试探,成功关闭、失败重新打开KERNEL_UNAVAILABLE_004
限流固定窗口:每 WindowSeconds 秒最多 PermitLimit 次,超出的最多 QueueLimit 个排队到下一窗口,其余拒绝KERNEL_UNAVAILABLE_002
并发同时执行最多 ConcurrencyLimit 次,超出的最多 ConcurrencyQueueLimit 个排队,其余拒绝KERNEL_UNAVAILABLE_003

只有 KIND 为 UNAVAILABLE、TIMEOUT、UNEXPECTED 的结果与未处理异常算熔断意义上的失败;校验不通过、找不到、无权限这类调用方错误不算。熔断打开期间仍在执行的调用结束时不改变熔断器状态。

拒绝都以 UNAVAILABLE 种类结束调用(HTTP 503),不进入后续阶段,details 带 id(操作 ID);限流与熔断另带 retryAfter(秒,向上取整,至少 1)。排队等待受调用的取消令牌约束,截止时间到了按 KERNEL_TIMEOUT_001 结束,不会无限等待。限流与并发使用 BCL 的 System.Threading.RateLimiting,熔断是内核自己的三态状态机。

配置 ​

配置节 XiHan:Kernel:Resilience:节本身的键是所有操作的默认策略,Operations:<操作 ID> 下的同名键覆盖该操作,缺省的键沿用默认策略。

键类型缺省说明
PermitLimit正整数不限流每个窗口允许的调用数
WindowSeconds正数1限流窗口
QueueLimit非负整数0窗口用尽时排队等待的调用数
ConcurrencyLimit正整数不限并发同时执行的调用数上限
ConcurrencyQueueLimit非负整数0并发用尽时排队等待的调用数
FailureThreshold正整数不熔断连续失败多少次后打开熔断器
BreakSeconds正数30熔断打开的时长
json
{
  "XiHan": {
    "Kernel": {
      "Resilience": {
        "PermitLimit": 1000,
        "WindowSeconds": 1,
        "ConcurrencyLimit": 64,
        "FailureThreshold": 5,
        "BreakSeconds": 30,
        "Operations": {
          "reports.export": { "ConcurrencyLimit": 2, "PermitLimit": 10, "WindowSeconds": 60 }
        }
      }
    }
  }
}

一个限制都没配置的操作直接透传。非法值(非正数、Operations 下不合文法的操作 ID)在构建应用时抛出 ArgumentException。XiHan:Kernel:UseBuiltInMiddleware=false 时两个内置中间件都不登记,配置节不再读取。

内核范围之外 ​

按调用方(主体、租户、IP)分桶的限流与自适应并发不在内核范围。需要时在 Resilience 阶段末尾登记应用自己的中间件,用 System.Threading.RateLimiting 的 PartitionedRateLimiter 按 context.GetSecurity() 或调用来源分区:

csharp
using System.Threading.RateLimiting;
using XiHan.Framework.Kernel.Pipeline;
using XiHan.Framework.Kernel.Security;
using XiHan.Framework.Primitives;

namespace Shop.Throttling;

public sealed class TenantRateLimitMiddleware : IPipelineMiddleware
{
    private readonly PartitionedRateLimiter<string> _limiter = PartitionedRateLimiter.Create<string, string>(
        tenant => RateLimitPartition.GetFixedWindowLimiter(tenant, static _ => new FixedWindowRateLimiterOptions
        {
            PermitLimit = 100,
            Window = TimeSpan.FromSeconds(1),
        }));

    public async ValueTask InvokeAsync(InvocationContext context, PipelineStep nextStep)
    {
        // 租户在 Tenant 阶段之后才解析,这里按调用方自报的 Baggage 分区,只用于限流、不用于授权
        var tenant = context.TryGetBaggage(BaggageKeys.Tenant, out var id) ? id : "anonymous";
        using var lease = await _limiter.AcquireAsync(tenant, 1, context.CancellationToken);
        if (!lease.IsAcquired)
        {
            context.SetOutcome(Error.Unavailable("SHOP_UNAVAILABLE_002", "shop.throttling.tenant_limited"));
            return;
        }

        await nextStep(context);
    }
}

HTTP 层面的连接数、请求体大小与反向代理限流仍用 ASP.NET Core 与网关自身的设置。

相关 ​

Released under The MIT License