在现代软件系统中,应用程序越来越依赖外部资源HTTP API、微服务、数据库、消息队列、第三方服务。然而,分布式系统中失败是不可避免的。比如:网络短暂不可用、服务响应变慢、请求量突然增加、第三方API限流。如果应用程序没有任何保护机制,一个简单的异常都可能导致整个系统不可用。因此,在高可靠系统设计中,需要引入弹性(Resilience)设计模式。.NET生态中,最流行的弹性处理库就是——Polly。
什么是Polly?
Polly是一个用于.NET的弹性与瞬态故障处理库。它帮助开发者为应用程序添加各种可靠性策略:
- Retry(重试)
- Circuit Breaker(熔断)
- Hedging(请求对冲)
- Timeout(超时)
- Rate Limiter(限流)
- Fallback(降级)
简单理解:Polly就是在业务代码和外部资源之间增加一层可靠性保护。
结构如下:
图1 整体结构
使用以下命令添加Polly包:
1
2
3
|
dotnet add package Polly
dotnet add package Polly.RateLimiting
dotnet add package System.Threading.RateLimiting
|
Retry 重试
Retry的核心思想是当一次操作失败时,不立即认为系统不可用,而是尝试重新执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
using Polly;
class Program
{
static async Task Main(string[] args)
{
int count = 0;
var retryPipeline = new ResiliencePipelineBuilder().AddRetry(new()
{
MaxRetryAttempts = 3,
Delay = TimeSpan.FromSeconds(1),
OnRetry = retry =>
{
Console.WriteLine($"第{retry.AttemptNumber + 1}次失败,准备重试");
return ValueTask.CompletedTask;
}
}).Build();
await retryPipeline.ExecuteAsync(async token =>
{
count++;
Console.WriteLine($"执行第{count}次请求");
if (count < 3)
{
throw new Exception("服务器异常");
}
Console.WriteLine("请求成功");
});
}
}
|
Circuit Breaker 熔断
Circuit Breaker类似电路保险丝。当发现服务持续失败,暂停访问,让系统恢复。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
|
using Polly;
class Program
{
static async Task Main(string[] args)
{
int requestCount = 0;
var pipeline = new ResiliencePipelineBuilder().AddCircuitBreaker(new()
{
FailureRatio = 1.0, // 失败比例
MinimumThroughput = 3, // 最少请求数量
SamplingDuration = TimeSpan.FromSeconds(10), // 统计窗口
BreakDuration = TimeSpan.FromSeconds(5), // 熔断持续时间
OnOpened = openedArgs =>
{
Console.WriteLine(">>> Circuit Open熔断开始");
return ValueTask.CompletedTask;
},
OnClosed = closedArgs =>
{
Console.WriteLine(">>> Circuit Closed恢复正常");
return ValueTask.CompletedTask;
},
OnHalfOpened = halfOpenedArgs =>
{
Console.WriteLine(">>> Circuit Half Open尝试恢复");
return ValueTask.CompletedTask;
}
}).Build();
for (int i = 0; i < 10; i++)
{
try
{
await pipeline.ExecuteAsync(token =>
{
requestCount++;
Console.WriteLine($"请求 {requestCount}");
if (requestCount <= 3)
{
throw new Exception("服务异常");
}
Console.WriteLine("服务返回成功");
return ValueTask.CompletedTask;
});
}
catch (Exception ex)
{
Console.WriteLine($"异常: {ex.Message}");
}
await Task.Delay(1000);
}
}
}
|
图2 Circuit Breaker执行结果
| 参数 |
决定什么 |
| FailureRatio |
失败多少比例触发熔断 |
| MinimumThroughput |
至少多少请求才开始判断 |
| SamplingDuration |
统计失败率的时间窗口 |
| BreakDuration |
熔断后多久尝试恢复 |
四个参数共同决定了Circuit Breaker的灵敏度。
Hedging 请求对冲
Hedging同一个请求发送多个副本,使用最快返回的结果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
|
using System.Threading.RateLimiting;
using Polly;
using Polly.Hedging;
using Polly.RateLimiting;
class Program
{
static async Task Main(string[] args)
{
var serverIndex = 0;
var pipeline = new ResiliencePipelineBuilder<string>().AddHedging(new HedgingStrategyOptions<string>()
{
MaxHedgedAttempts = 1,
Delay = TimeSpan.FromMilliseconds(500),
OnHedging = hegingArgs =>
{
Console.WriteLine($"发起备用请求");
return default;
}
}).Build();
var result = await pipeline.ExecuteAsync<string>(async token =>
{
serverIndex++;
if (serverIndex == 1)
{
return await ServerA(token);
}
else
{
return await ServerB(token);
}
});
Console.WriteLine($"最终结果: {result}");
}
static async Task<string> ServerA(CancellationToken token)
{
Console.WriteLine("Server A开始");
await Task.Delay(3000, token);
Console.WriteLine("Server A完成");
return "Server A";
}
static async Task<string> ServerB(CancellationToken token)
{
Console.WriteLine("Server B开始");
await Task.Delay(500, token);
Console.WriteLine("Server B完成");
return "Server B";
}
}
|
图3 Hedging执行结果
Timeout 超时
Timeout限制一个操作最多执行多久。例如,HTTP请求最多等待3秒,超时则取消。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
using Polly;
class Program
{
static async Task Main(string[] args)
{
var timeout = new ResiliencePipelineBuilder().AddTimeout(TimeSpan.FromSeconds(2)).Build();
try
{
await timeout.ExecuteAsync(async token =>
{
Console.WriteLine("开始请求");
await Task.Delay(5000, token);
Console.WriteLine("请求完成");
});
} catch(Exception ex)
{
Console.WriteLine($"发生异常: {ex.Message}");
}
}
}
|
Rate Limiter 限流
Rate Limiter控制单位时间内允许执行的请求数量。例如,每秒最多100个请求,超过则拒绝请求。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
using System.Threading.RateLimiting;
using Polly;
using Polly.RateLimiting;
namespace PollyDemo2;
class Program
{
static async Task Main(string[] args)
{
var pipeline = new ResiliencePipelineBuilder().AddRateLimiter(new RateLimiterStrategyOptions()
{
DefaultRateLimiterOptions = new ConcurrencyLimiterOptions()
{
PermitLimit = 1
},
OnRejected = rejectedArgs =>
{
Console.WriteLine("限流中");
return default;
}
}).Build();
var tasks = Enumerable.Range(0, 10)
.Select(i =>
pipeline.ExecuteAsync(async token =>
{
Console.WriteLine($"请求{i}开始");
await Task.Delay(1000);
Console.WriteLine($"请求{i}完成");
}).AsTask())
.ToArray();
await Task.WhenAll(tasks);
}
}
|
Fallback 降级
Fallback主方案失败后,使用备用方案。例如,调用天气API,返回天气。如果天气API异常,返回默认天气。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
using Polly;
using Polly.Fallback;
class Program
{
static async Task Main(string[] args)
{
var pipeline = new ResiliencePipelineBuilder<string>().AddFallback(new FallbackStrategyOptions<string>()
{
ShouldHandle = new PredicateBuilder<string>().Handle<Exception>(),
FallbackAction = fallbackArgs =>
{
Console.WriteLine("服务失败,执行降级");
return Outcome.FromResultAsValueTask("默认天气:晴天");
}
}).Build();
var result = await pipeline.ExecuteAsync<string>(token =>
{
Console.WriteLine("调用天气API");
throw new Exception("天气服务异常");
});
Console.WriteLine(result);
}
}
|
图4 Fallback执行结果
六大策略对比
| 策略 |
作用 |
解决问题 |
| Retry |
失败重新尝试 |
瞬态故障 |
| Circuit Breaker |
暂停访问异常服务 |
防止雪崩 |
| Timeout |
限制执行时间 |
防止无限等待 |
| Rate Limiter |
限制请求数量 |
防止过载 |
| Fallback |
提供备用方案 |
保证功能可用 |
| Hedging |
多个请求竞争 |
降低延迟 |
总结
可靠的软件不是避免失败,而是在失败发生时优雅地恢复。