1 前言之服务雪崩
在我们实施微服务之后,服务间的调用变得异常频繁,多个服务之前可能存在互相依赖的关系,当某个服务出现故障或者是因为服务间的网络出现故障,导致服务调用的失败,进而影响到某个业务服务处理失败,服务依赖的故障可能导致级联崩溃,如一个微服务不可用拖垮整个系统。【服务雪崩】
服务雪崩通常遵循 "从局部故障到全局崩溃" 的递进路径,可拆解为以下步骤:
- 初始故障
某个基础服务(如数据库、缓存、第三方 API)因过载、网络中断或 bug 出现响应延迟或失败。
例:支付服务因数据库连接池耗尽,处理请求的时间从 100ms 增至 5s。 - 请求堆积
依赖该故障服务的上游服务(如订单服务)因等待响应,线程 / 连接被长时间占用,无法释放资源。
例:订单服务调用支付服务时,每个请求都需等待 5s,而线程池容量有限(如 200 线程),很快所有线程被占满。 - 级联阻塞
上游服务因资源耗尽,无法处理新请求,导致依赖它的更上游服务(如用户服务)也出现资源耗尽。
例:用户服务调用订单服务时,因订单服务无响应,用户服务的线程也被占满,无法处理用户登录请求。 - 全局崩溃
故障像多米诺骨牌一样扩散,最终整个系统的核心功能(如下单、支付、登录)全部失效。
服务雪崩的本质是 "故障的无限制扩散". 就像是蝴蝶效应最后导致美国得克萨斯州的一场龙卷风
2 如何解决服务雪崩
针对雪崩的形成机制,需从 "限制资源消耗""隔离故障""快速失败" 三个维度设计防护策略:
- 熔断机制(Circuit Breaker)
- 原理:当某个服务的失败率超过阈值(如 50% 失败),暂时 "断开" 对它的调用,直接返回降级结果,避免资源浪费。
- 效果:防止故障服务持续消耗上游资源,给故障服务恢复的时间窗口。
- 限流机制(Rate Limiting)
- 原理:限制单位时间内对某个服务的请求量(如每秒 1000 次),防止瞬时流量冲垮服务。
- 场景:针对下游服务的最大承载能力,提前设置流量阈值。
- 隔离机制(Isolation)
- 原理:为不同服务的调用分配独立的资源池(线程池、连接池),避免一个服务的故障耗尽全局资源。
- 例:订单服务调用支付服务时使用单独的线程池(20 线程),调用库存服务时使用另一个线程池(30 线程),即使支付服务故障,也不会影响库存服务的调用。
- 超时控制(Timeout)
- 原理:为服务调用设置明确的超时时间(如 2s),超过时间直接终止请求,避免线程被无限期占用。
- 关键:超时时间需根据下游服务的正常响应时间合理设置(通常略高于 99% 请求的处理时间)。
- 降级策略(Fallback)
- 原理:当服务调用失败时,返回预设的兜底结果(而非直接报错),保证核心流程可用。
- 例:推荐服务故障时,返回默认热门商品列表;支付服务超时后,返回 "支付中,请稍后查询"。
3 什么是Polly
Polly 是 .NET 生态中一款强大的 弹性和瞬态故障处理库,主要用于处理分布式系统中常见的网络故障、超时、资源限流等问题,通过预定义的策略(如重试、熔断、超时等)提高应用程序的稳定性和容错能力。从而增强服务的可用性
3.1 超时策略
超时策略可防止因长时间阻塞导致的资源耗尽或级联故障.
在 Polly 中,TimeoutStrategy 是控制超时行为的核心枚举,
定义了两种不同的超时处理机制:乐观超时(Optimistic)和悲观超时(Pessimistic)
Optimistic使用CancellationToken取消服务,默认是使用乐观超时的处理机制
Pessimistic 不需要使用CancellationToken,强制终端业务逻辑,这种情形可能会导致相关联的资源没有关闭。需要对这部分逻辑做处理
策略类型 触发条件 依赖操作协作 抛出的异常类型 乐观超时 超时 + 操作响应取消 是 OperationCanceledException
悲观超时 超时(强制触发) 否 TimeoutRejectedException
csharp
/// <summary>
/// Polly的超时策略【使用乐观处理机制】
/// </summary>
/// <param name="url"></param>
/// <returns></returns>
[HttpGet]
public async Task<IActionResult> TimeOutOptimisticPolicy(string url) {
// 示例:设置 5 秒悲观超时 超时后将执行回调函数
// 超时策略可防止因长时间阻塞导致的资源耗尽或级联故障
// 在 Polly 中,TimeoutStrategy 是控制超时行为的核心枚举,
// 定义了两种不同的超时处理机制:乐观超时(Optimistic)和悲观超时(Pessimistic)
// Optimistic使用CancellationToken取消服务,默认是使用乐观超时的处理机制
// Pessimistic不需要使用CancellationToken取消服务
// onTimeoutAsync回调函数是在超时发生时执行的附加操作,但它不会改变 ExecuteAsync 的返回值。
// 这个回调主要用于日志记录、监控或其他副作用操作,而不是直接返回 HTTP 响应。
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3));
var token = cts.Token;
var timeOutPolicy = Policy.TimeoutAsync<IActionResult>(
timeout: TimeSpan.FromSeconds(3),
timeoutStrategy: TimeoutStrategy.Optimistic,
onTimeoutAsync: (context, timespan, task) =>
{
Console.WriteLine($"操作超时:{timespan.TotalSeconds}秒");
return Task.CompletedTask;
}
);
return await timeOutPolicy. ExecuteAsync(async token =>
{
try
{
using (HttpClient client = new HttpClient())
{
HttpResponseMessage message = await client.GetAsync(url, token);
//序列化响应结果
string resContent = await message.Content.ReadAsStringAsync();
return new OkObjectResult(resContent);
}
}
//任务由于超时异常被取消 ,在任务内部抛出 TaskCanceledException 异常
catch (TaskCanceledException e) {
return new ObjectResult(new ReturnMessageModel<string>
{
Code = "408",
Message = "操作超时",
})
{ StatusCode = 408 };
}
catch (Exception e)
{
return new ObjectResult(new ReturnMessageModel<string>
{
Code = "500",
Message = $"服务错误:{e.Message}" + e.GetType().Name,
Status = (int)HttpStatusCode.InternalServerError
})
{ StatusCode = 500 };
}
}, token);
}
/// <summary>
/// Polly的超时策略【使用悲观处理机制】
/// </summary>
/// <param name="url"></param>
/// <returns></returns>
[HttpGet]
public async Task<IActionResult> TimeOutPessimisticPolicy(string url)
{
var timeOutPolicy = Policy.TimeoutAsync<IActionResult>(
timeout: TimeSpan.FromSeconds(3),
timeoutStrategy: TimeoutStrategy.Pessimistic,
onTimeoutAsync: (context, timespan, task) =>
{
Console.WriteLine($"操作超时:{timespan.TotalSeconds}秒");
return Task.CompletedTask;
}
);
try
{
return await timeOutPolicy.ExecuteAsync(async () =>
{
try
{
using (HttpClient client = new HttpClient())
{
HttpResponseMessage message = await client.GetAsync(url);
//序列化响应结果
string resContent = await message.Content.ReadAsStringAsync();
return new OkObjectResult(resContent);
}
}
catch (Exception e)
{
return new ObjectResult(new ReturnMessageModel<string>
{
Code = "500",
Message = $"服务错误:{e.Message}" + e.GetType().Name,
Status = (int)HttpStatusCode.InternalServerError
})
{ StatusCode = 500 };
}
});
}
catch (TimeoutRejectedException e) {
// 服务超时。
return new ObjectResult(new ReturnMessageModel<string>
{
Code = "408",
Message = "操作超时",
})
{ StatusCode = 408 };
}
}
3.2 重试策略
在分布式系统中,网络请求可能会因为临时故障 (如网络抖动、服务暂时不可用)而失败。Polly 的重试策略(Retry Policy)是处理这类问题的有效工具,它可以在请求失败时自动重试,提高系统的稳定性和容错能力。
固定次数重试策略
csharp
/// <summary>
/// 固定次数重试策略
/// </summary>
/// <param name="url"></param>
/// <returns></returns>
[HttpGet]
public async Task<IActionResult> RetryPolicy(string url) {
// 重试策略,对所有捕获的异常进行重试次数策略,当重试次数<= 设定值时,均会进入回调函数逻辑
var policy = Policy.Handle<Exception>().RetryAsync(3, (exception, retryCount) => {
Console.WriteLine($"重试第 {retryCount} 次: {exception.Message}");
});
try
{
return await policy.ExecuteAsync(async () =>
{
using (HttpClient client = new HttpClient())
{
HttpResponseMessage message = await client.GetAsync(url);
//序列化响应结果
string resContent = await message.Content.ReadAsStringAsync();
return new OkObjectResult(resContent);
}
});
}
catch (Exception e) {
return new ObjectResult(new ReturnMessageModel<string>
{
Code = "500",
Message = $"服务错误:{e.Message}" + e.GetType().Name,
Status = (int)HttpStatusCode.InternalServerError
})
{ StatusCode = 500 };
}
}
3.3 降级策略
服务降级(Service Degradation 是一种应对系统过载或依赖服务故障的弹性策略,通过暂时牺牲部分非核心功能或服务质量,确保系统核心功能的可用性和稳定性.一般是碰到异常时会给一个默认回调的形式去代替原有的服务
csharp
/// <summary>
/// 模拟从缓存中获得暂时的响应数据
/// </summary>
/// <returns></returns>
private async Task<HttpResponseMessage> GetDataFromRedis() {
HttpResponseMessage httpResponse = new HttpResponseMessage();
httpResponse.StatusCode = HttpStatusCode.OK;
var resdata=new ReturnMessageModel<bool>("0",0,"从缓存中获得数据",true);
string json = JsonConvert.SerializeObject(resdata);
httpResponse.Content = new StringContent(json, Encoding.UTF8, "application/json");
return httpResponse;
}
/// <summary>
/// 服务降级策略【降级策略常常搭配其他策略一起使用】
/// </summary>
/// <param name="url"></param>
/// <returns></returns>
[HttpGet]
public async Task<ReturnMessageModel<string>> DegradationPolicy(string url) {
// 定义重试策略
var retryPolicy = Policy<HttpResponseMessage>.Handle<Exception>().RetryAsync(3,
onRetryAsync: (ex, times) => {
Debug.WriteLine($"当前重试次数为 {times}");
return Task.CompletedTask;
});
// 定义降级策略
var fallbackPolicy = Policy<HttpResponseMessage>.Handle<Exception>()
.FallbackAsync(async (cancellaction) =>
{
// 该回调函数中执行降级的逻辑,比如说从缓存中获取逻辑
Debug.WriteLine("进行了服务降级策略");
return await GetDataFromRedis();
});
// Polly 处理策略,先重试,后降级
// Policy.WrapAsync 包含多种执行策略
// 在Polly中,策略的包裹顺序非常重要,因为策略的执行顺序是从最外层策略开始,逐层向内执行。
// 当我们使用PolicyWrap时,通过Policy.WrapAsync方法组合策略,会形成一个策略管道,请求首先进入最先包裹的策略(最外层),然后依次向内传递,直到执行用户提供的原始委托。
var policy = Policy.WrapAsync(fallbackPolicy,retryPolicy);
// 使用降级策略去处理任务
HttpResponseMessage res= await policy.ExecuteAsync(async() =>
{
using (HttpClient client = new HttpClient())
{
HttpResponseMessage message = await client.GetAsync(url);
return message;
}
});
string resContent = await res.Content.ReadAsStringAsync();
return new ReturnMessageModel<string>(resContent);
}
3.4 熔断策略
Polly 的熔断策略(Circuit Breaker)用于在系统检测到持续故障时,自动断开(熔断)对特定服务的请求,防止系统资源被耗尽并快速失败。当熔断器处于开启状态时,所有请求会立即被拒绝(抛出
BrokenCircuitException
),直到经过设定的恢复期后,熔断器会进入半开状态,允许部分请求尝试恢复服务。如果这些请求成功,则关闭熔断器;否则,继续保持开启。熔断器包含三个状态
Closed(闭合):正常状态,操作允许执行。
Open(断开):熔断状态,所有操作被快速失败,不再尝试执行。
Half-Open(半开):熔断器尝试恢复,允许少量操作执行以测试依赖服务是否恢复。
Polly提供两种熔断器策略:
基于连续失败次数的熔断器(Basic Circuit Breaker)
基于高级失败率(滑动窗口)的熔断器(Advanced Circuit Breaker)
熔断器状态需要跨多个调用共享,因此通常将熔断器策略实例声明为静态或通过依赖注入容器注入(单例生命周期)。
BasicCircuitBreaker代码演示
csharp
// 静态熔断器实例(状态持久化)
private static readonly AsyncCircuitBreakerPolicy _circuitBreaker = Policy
.Handle<Exception>()
.CircuitBreakerAsync(
exceptionsAllowedBeforeBreaking: 4,
durationOfBreak: TimeSpan.FromSeconds(30),
onBreak: (ex, breakDelay) =>
{
Debug.WriteLine($"熔断开启!持续 {breakDelay.TotalSeconds} 秒。");
},
onReset: () =>
{
Debug.WriteLine("熔断关闭,恢复正常请求。");
},
onHalfOpen: () =>
{
Debug.WriteLine("熔断器进入半开状态,尝试恢复。");
}
);
csharp
/// <summary>
/// 熔断策略[基于连续失败次数的熔断器]
/// </summary>
/// <param name="url">请求API</param>
/// <returns></returns>
[HttpGet]
public async Task<ReturnMessageModel<string>> BasicCircuitBreakPolicy(string url) {
// 重试策略
var retryPolicy = Policy.Handle<Exception>().RetryAsync(5, (ex, qty) => {
Thread.Sleep(100);
Debug.WriteLine($"当前重试次数为 {qty} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}");
return Task.CompletedTask;
});
// 先重试 再熔断 最后降级服务
var fallbackPolicy = Policy<string>.Handle<Exception>().FallbackAsync("先熔断降级策略结果",
onFallbackAsync: _ => {
Debug.WriteLine($"降级策略将要执行 {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}");
return Task.CompletedTask;
}).WrapAsync(_circuitBreaker.WrapAsync(retryPolicy));
string res = await fallbackPolicy.ExecuteAsync(async() =>
{
using (HttpClient client = new HttpClient())
{
HttpResponseMessage message = await client.GetAsync(url);
string messageStr=await message.Content.ReadAsStringAsync();
return messageStr;
}
});
return new ReturnMessageModel<string>(res);
}

Advanced Circuit Breaker代码演示,将上述的基础异常次数_circuitBreaker熔断器换成_advancedCircuitBreaker
csharp
private static readonly AsyncCircuitBreakerPolicy _advancedCircuitBreaker = Policy
.Handle<Exception>()
.AdvancedCircuitBreakerAsync(failureThreshold: 0.5, // 失败率阈值(50%)
samplingDuration: TimeSpan.FromSeconds(10), // 采样时间窗口(10秒)
minimumThroughput: 5, // 最小吞吐量(10秒内至少8个请求)
durationOfBreak: TimeSpan.FromSeconds(30), // 熔断持续时间
onBreak: (ex, breakDelay) =>
{
Debug.WriteLine($"熔断开启!持续 {breakDelay.TotalSeconds} 秒。");
},
onReset: () =>
{
Debug.WriteLine("熔断关闭,恢复正常请求。");
},
onHalfOpen: () =>
{
Debug.WriteLine("熔断器进入半开状态,尝试恢复。");
});
