.NET 10 弹性服务构建:9 大容错模式实现系统自愈与高可用

作者:微信公众号:【架构师老卢】
7-31 8:34
17

在现代分布式系统中,故障虽不常发生,但一旦出现,可能导致整个系统崩溃。网络会抖动,数据库会停滞,外部 API 会限流。您的服务需要的不仅仅是错误处理,更需要弹性、自愈能力,以及对故障原因的清晰洞察。

.NET 10 和 C# 12 为您提供了一流的工具——用于重试和熔断的 Polly、用于内存队列的 Channel,以及人工智能驱动的警报——来构建保持在线并能优雅恢复的服务。在本文中,我们将探索九种基本的容错模式。对于每种模式,您将获得:

  • 对其解决问题的清晰描述
  • 在实际项目中何时以及为何使用它
  • 完整的、可直接复制粘贴的代码示例

读完本文后,您将拥有一套模式工具箱,可让任何 .NET 服务抵御生产环境中的混乱。

1. 重试 + 超时

问题所在

短暂的网络故障或过载的端点等瞬时错误可能导致单个请求失败,尽管稍后重试可能会成功。

重要性

如果没有重试机制,用户会看到本不应出现的错误。如果没有超时设置,您的调用可能会挂起,占用线程并耗尽应用资源。

工作原理

结合使用 Polly 的重试和超时策略。首先,您使用指数退避算法多次尝试操作。如果操作仍然超过您设置的阈值时间,则中止。

代码示例

// 1) 定义重试策略:3 次尝试,指数退避
var retryPolicy = Policy
    .Handle<HttpRequestException>()
    .WaitAndRetryAsync(3, att => TimeSpan.FromSeconds(Math.Pow(2, att)),
        (ex, delay, count, ctx) =>
            logger.LogWarning("Retry {Count} after {Delay}: {Message}", count, delay, ex.Message));

// 2) 定义超时策略:5 秒后中止
var timeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(5);
// 3) 组合策略:在超时范围内重试
var policyWrap = Policy.WrapAsync(retryPolicy, timeoutPolicy);
// 4) 执行请求
HttpResponseMessage response = await policyWrap.ExecuteAsync(() =>
    httpClient.GetAsync("https://api.example.com/data"));

使用场景

始终对不稳定的第三方服务或缓慢的数据存储的调用进行包装。

2. 断路器

问题所在

对已崩溃服务的重复请求只会使情况变得更糟——每次调用都会增加已经不堪重负的端点的负载。

重要性

断路器在达到失败阈值后会“打开”,在冷却期内短路后续调用。这可以防止级联故障,并给下游服务恢复的时间。

工作原理

使用 Polly 的断路器,您可以声明连续多少次失败会触发熔断,以及保持打开状态的时间。

代码示例

var circuitBreaker = Policy
    .Handle<HttpRequestException>()
    .CircuitBreakerAsync(
        handledEventsAllowedBeforeBreaking: 5,         // 5 次失败后熔断
        durationOfBreak: TimeSpan.FromSeconds(30),     // 保持打开 30 秒
        onBreak: (ex, ts) => 
            logger.LogWarning("Circuit opened for {Duration} due to {Error}", ts, ex.Message),
        onReset: () =>
            logger.LogInformation("Circuit closed; requests will go through again"),
        onHalfOpen: () =>
            logger.LogInformation("Circuit half-open; trial calls allowed"));

await circuitBreaker.ExecuteAsync(() =>
    httpClient.GetAsync("https://api.example.com/data"));

使用场景

保护关键的下游依赖,这些依赖在不健康时需要喘息空间。

3. 回退策略

问题所在

有时重试和断路器仍然会失败。如果没有回退机制,您的 API 会向用户返回错误或完全崩溃。

重要性

提供合理的默认值(如缓存数据或空列表)可以改善用户体验,并使您的系统在降级模式下继续运行。

工作原理

Polly 的回退策略允许您在主操作失败时提供替代结果。

代码示例

var fallbackPolicy = Policy<HttpResponseMessage>
    .Handle<HttpRequestException>()
    .FallbackAsync(
        fallbackValue: new HttpResponseMessage(HttpStatusCode.OK)
        {
            Content = new StringContent("[]")
        },
        onFallbackAsync: (ex, ctx) =>
        {
            logger.LogError("Primary request failed, using fallback payload");
            return Task.CompletedTask;
        });

HttpResponseMessage result = await fallbackPolicy.ExecuteAsync(() =>
    httpClient.GetAsync("https://unstable-api.com/items"));

使用场景

优先使用陈旧或默认数据而非错误的 API 端点。

4. 隔板隔离

问题所在

工作负载激增(如流量高峰)可能会耗尽服务某一部分的资源,导致其他部分也随之崩溃。

重要性

隔板隔离对资源(如线程或并发调用)进行分区,使一个部分的故障或高峰不会拖垮整个系统。

工作原理

Polly 的隔板策略限制并行执行数量,并对多余的调用进行排队,拒绝或延迟它们。

代码示例

var bulkheadPolicy = Policy.BulkheadAsync(
    maxParallelization: 10,        // 允许 10 个并发执行
    maxQueuingActions: 20,         // 最多排队 20 个
    onBulkheadRejectedAsync: ctx => 
    {
        logger.LogWarning("Bulkhead queue is full; rejecting request");
        return Task.CompletedTask;
    });

await bulkheadPolicy.ExecuteAsync(() =>
    ProcessOrderAsync(order));

使用场景

高流量端点或 CPU 密集型操作,需要防范过载的情况。

5. 使用 Channel 的异步消息队列

问题所在

当您的服务接收工作的速度超过其处理能力时,可能会丢失工作或耗尽内存。

重要性

Channel 提供了一个线程安全的内存队列,可选择设置边界。生产者写入,消费者读取,通道会施加背压。

工作原理

创建有界通道,让生产者向其中写入,运行一个后台消费者逐个或并行处理项目。

代码示例

// 1) 创建容量为 100 的有界通道
var orderChannel = Channel.CreateBounded<Order>(new BoundedChannelOptions(100)
{
    FullMode = BoundedChannelFullMode.Wait
});

// 2) 生产者:将传入订单入队
app.MapPost("/orders", async (Order order) =>
{
    await orderChannel.Writer.WriteAsync(order);
    return Results.Accepted();
});
// 3) 消费者:后台任务
_ = Task.Run(async () =>
{
    await foreach (var ord in orderChannel.Reader.ReadAllAsync())
        await HandleOrderAsync(ord);
});

使用场景

缓冲峰值,如 Webhook、高容量事件摄取或上传的文件。

6. 可重试的后台工作者

问题所在

后台作业可能会间歇性失败——如果没有重试机制,您会默默地丢失工作。

重要性

将 Channel 与 Polly 结合使用,您可以构建一个后台队列,对每个项目重试几次后再放弃或将其移至死信队列。

工作原理

在循环中消费通道,将处理过程包装在重试策略中,并明确处理失败情况。

代码示例

var retryPolicy = Policy.Handle<Exception>()
    .WaitAndRetryAsync(3, att => TimeSpan.FromSeconds(att),
        (ex, delay, count, _) =>
            logger.LogWarning("Job retry {Count} after {Delay}: {Msg}", count, delay, ex.Message));

_ = Task.Run(async () =>
{
    await foreach (var job in orderChannel.Reader.ReadAllAsync())
    {
        try
        {
            await retryPolicy.ExecuteAsync(() => ProcessJobAsync(job));
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Job failed after retries, sending to DLQ");
            await deadLetterChannel.Writer.WriteAsync(job);
        }
    }
});

使用场景

任何可靠性至关重要的后台处理,如电子邮件发送、计费、数据同步等。

7. 死信队列 (DLQ)

问题所在

即使有重试机制,有些消息总是会失败。如果您默默地丢弃它们,就会丢失重要工作,并且系统中会出现盲点。

重要性

死信队列保存失败的项目,以便手动检查、重新处理或发出警报。它确保没有数据在没有通知的情况下消失。

工作原理

为死信创建一个单独的通道或持久存储(数据库/表、Blob 存储)。当所有重试都失败时,将项目入队到那里。

代码示例

// 假设 deadLetterChannel 的创建方式与 orderChannel 类似
// 在可重试工作者的 catch 块中:
await deadLetterChannel.Writer.WriteAsync(failedJob);

使用场景

关键系统,其中每个消息都必须被记录,例如金融交易。

8. 人工智能驱动的警报

问题所在

日志和指标如潮水般涌来。您的值班团队不可能实际扫描数千行日志来发现关键问题。

重要性

人工智能可以发现异常、分组相关警报,并将原始日志转换为通俗易懂的摘要——这样人类就可以根据洞察而非噪音采取行动。

工作原理

定期提取最近的日志或指标,将它们提供给大语言模型(如 GPT),并将汇总的警报发送到电子邮件、Slack 或票务系统。

代码示例

var recentLogs = await logService.GetLogsAsync(TimeSpan.FromMinutes(10));
string prompt = $"Detect anomalies:\n{recentLogs}";
string aiSummary = await aiClient.SummarizeAsync(prompt);

await notificationService.SendAsync(
    subject: "Production Alert",
    message: aiSummary);

使用场景

高容量系统,其中模式检测超出了手动监控的能力。

9. 使用结构化日志和 OpenTelemetry 的可观测性

问题所在

非结构化的文本日志和缺失的跟踪信息使根本原因分析变得缓慢而令人沮丧。

重要性

结构化日志(带命名字段)和分布式跟踪使您能够关联跨服务的事件、测量延迟并精确定位故障。

工作原理

使用 [LoggerMessage] 进行零分配结构化日志记录,并配置 OpenTelemetry 自动检测您的 Web 和 HTTP 客户端。

代码示例

[LoggerMessage(1001, LogLevel.Error,
    "Order {OrderId} failed for user {UserId}")]
public static partial void LogOrderError(
    ILogger logger, Guid orderId, string userId);

// 在 Program.cs 中
builder.Services.AddOpenTelemetryTracing(tr => tr
    .AddAspNetCoreInstrumentation()
    .AddHttpClientInstrumentation()
    .SetResourceBuilder(
        ResourceBuilder.CreateDefault()
            .AddService("OrderService")));

使用场景

始终使用——可观测性是可靠操作的基石。

最终思考

构建弹性服务不是事后诸葛亮——而是基础。.NET 10 将 Polly、Channel、结构化日志甚至人工智能结合在一起,使您能够设计出预期会出现故障并能优雅应对的系统。

从小处着手:用重试策略包装单个 HTTP 调用。在关键端点周围添加断路器。启动一个基于通道的队列来吸收峰值。随着进展逐步引入人工智能警报和结构化跟踪。

您采用的每种模式都会提高服务的正常运行时间、可调试性和团队信心。拥抱这些工具,您将从“希望它能工作”转变为“它能经受任何考验”。

相关留言评论
昵称:
邮箱:
阅读排行