到目前为止,我使用 Polly 的标准 CircuitBreaker 策略,如下所示:
int retryCount = 2;
PolicyBuilder policyBuilder = Policy
.HandleInner<WebException>()
.Or<WebException>()
.Or<TimeoutException>()
.Or<TimeoutRejectedException>();
AsyncCircuitBreakerPolicy asyncCircuitBreakerPolicy = policyBuilder
.CircuitBreakerAsync(1, TimeSpan.FromSeconds(1), OnBreak, OnReset);
AsyncRetryPolicy asyncWaitAndRetry = policyBuilder
.WaitAndRetryAsync(retryCount, SleepDurationProvider, OnRetry);
AsyncPolicyWrap defaultPolicy = Policy.WrapAsync(
asyncCircuitBreakerPolicy,
asyncWaitAndRetry);
即抛出异常时重试 2 次,总共失败 3 次后,异常冒泡到立即触发的 CircuitBreaker。
Polly V8 不再有这种“标准”断路器,而是类似于旧的“高级”断路器。它希望最小吞吐量至少为 2,并且需要失败率而不是固定计数。此外,它会重新抛出所有异常。
现在我想知道如何迁移到V8。我正在考虑翻转重试和断路器的顺序,设置
MinimumThroughput = retryCount + 1
和FailureRatio = 1
。但还有 SamplingDuration,所以我需要让它以某种方式取决于预期的超时,加上重试之间的等待时间等。
还有其他方法可以做到这一点吗?我应该编写自己的 ResilienceStrategy 吗?
在撰写本文时,V8 和 V7 之间有一个 互操作层(包装器)。不幸的是,它只能在一个方向上通过
.AsAsyncPolicy()
和 .AsSyncPolicy()
从 V8 到 V7。换句话说,目前没有内置支持将现有的 V7 策略包装到 V8 策略中。
那么,我们在这里能做什么?
根据您的描述,使用 V8 的断路器有两个阻碍:
MinimumThroughput
的最小值SamplingDuration
很高兴每当您将断路器切换到手动控制时,这些都不重要:
FailureRatio
、MinimumThroughput
、SamplingDuration
和BreakDuration
。
换句话说,无论它们的值如何,您都可以要求断路器转换到
Isolated
状态或返回到 Closed
。
这里是如何做到这一点的示例代码
static void Report(string message) => Console.WriteLine($"{DateTime.UtcNow.TimeOfDay}:{message}");
const int MaxRetries = 2;
var manualControl = new CircuitBreakerManualControl();
var timer = new System.Timers.Timer(1500);
timer.Elapsed += async (s, e) => await manualControl.CloseAsync();
var pipeline = new ResiliencePipelineBuilder()
.AddCircuitBreaker(new CircuitBreakerStrategyOptions
{
ManualControl = manualControl,
OnOpened = static args =>
{
Report("Open");
return default;
},
OnClosed = static args =>
{
Report("Close");
return default;
}
})
.AddRetry(new RetryStrategyOptions
{
ShouldHandle = new PredicateBuilder().Handle<SomeExceptionType>(),
MaxRetryAttempts = MaxRetries,
Delay = TimeSpan.Zero,
OnRetry = async args =>
{
if(args.AttemptNumber == MaxRetries -1)
{
await manualControl.IsolateAsync();
timer.Start();
}
}
})
.Build();
for (int i = 0; i < 10; i++)
{
try
{
await pipeline.ExecuteAsync((ct) => { Report("Called"); throw new SomeExceptionType(); }, CancellationToken.None);
}
catch(Exception ex)
{
Report(ex.GetType().Name);
await Task.Delay(450);
}
}
manualControl
添加了对其状态转换的手动控制。SomeExceptionType
并最多重试 MaxRetries
次。
OnRetry
Closed
这里有几个问题:
首先,我们在最后一次尝试之前而不是之后将断路器移至
13:04:55.2422830:Called
13:04:55.2486120:Called
13:04:55.2505380:Open
13:04:55.2523110:Called
13:04:55.2536750:SomeExceptionType
13:04:55.7084040:IsolatedCircuitException
13:04:56.1594240:IsolatedCircuitException
13:04:56.6119060:IsolatedCircuitException
13:04:56.7744830:Close
13:04:57.0636680:Called
13:04:57.0637870:Called
13:04:57.0638610:Open
13:04:57.0640420:Called
13:04:57.0654120:SomeExceptionType
13:04:57.5166330:IsolatedCircuitException
13:04:57.9679080:IsolatedCircuitException
13:04:58.2533750:Close
13:04:58.4195490:Called
13:04:58.4199850:Called
13:04:58.4218660:Open
13:04:58.4220250:Called
13:04:58.4244720:SomeExceptionType
13:04:58.8763750:IsolatedCircuitException
13:04:59.3277210:IsolatedCircuitException
13:04:59.7532630:Close
状态。这是因为
Isolated
在下一次重试尝试之前运行。解决这个问题的一种方法是将 OnRetry
的逻辑移到 catch 块中OnRetry
现在示例输出如下所示:
...
var pipeline = new ResiliencePipelineBuilder()
.AddCircuitBreaker(new CircuitBreakerStrategyOptions
{
ManualControl = manualControl,
OnOpened = static args =>
{
Report("Open");
return default;
},
OnClosed = args =>
{
Report("Close");
return default;
}
})
.AddRetry(new RetryStrategyOptions
{
ShouldHandle = new PredicateBuilder().Handle<SomeExceptionType>(),
MaxRetryAttempts = MaxRetries,
Delay = TimeSpan.Zero,
})
.Build();
for (int i = 0; i < 10; i++)
{
try
{
await pipeline.ExecuteAsync(_ => { Report("Called"); throw new SomeExceptionType(); }, CancellationToken.None);
}
catch(SomeExceptionType ex)
{
await manualControl.IsolateAsync();
timer.Start();
}
catch (Exception ex)
{
Report(ex.GetType().Name);
}
await Task.Delay(450);
}
第二个问题,也是更大的问题:断路器的控制代码分散在各处。很难将当前的解决方案封装成可重用的格式。
选项#2
13:09:21.2632760:Called
13:09:21.2694840:Called
13:09:21.2695320:Called
13:09:21.2724070:Open
13:09:21.7255970:IsolatedCircuitException
13:09:22.1786920:IsolatedCircuitException
13:09:22.6303060:IsolatedCircuitException
13:09:22.8046920:Close
13:09:23.0817970:Called
13:09:23.0821610:Called
13:09:23.0822510:Called
13:09:23.0826860:Open
13:09:23.5354960:IsolatedCircuitException
13:09:23.9883700:IsolatedCircuitException
13:09:24.2743980:Close
13:09:24.4395530:Called
13:09:24.4398200:Called
13:09:24.4398690:Called
13:09:24.4401100:Open
13:09:24.8934820:IsolatedCircuitException
13:09:25.3463760:IsolatedCircuitException
13:09:25.7748690:Close
这里我们基本上模仿一个原子布尔标志来捕获断路器是否处于闭合或打开状态。
如果您运行这样的测试
public class Foo
{
static readonly ResiliencePipeline pipeline = new ResiliencePipelineBuilder()
.AddRetry(new RetryStrategyOptions
{
ShouldHandle = new PredicateBuilder().Handle<SomeExceptionType>(),
MaxRetryAttempts = 2,
Delay = TimeSpan.Zero,
})
.Build();
const int NotAllowed = 0;
const int Allowed = 1;
private static int isAllowed = Allowed;
public async ValueTask Bar(Func<ValueTask> callback)
{
if(isAllowed == NotAllowed)
throw new BrokenCircuitException();
try
{
await pipeline.ExecuteAsync(_ => callback(), CancellationToken.None);
}
catch(SomeExceptionType)
{
Report("Open");
Interlocked.Exchange(ref isAllowed, NotAllowed);
System.Timers.Timer timer = new (1500);
timer.Elapsed += (s, e) => { Report("Close"); Interlocked.Exchange(ref isAllowed, Allowed); };
timer.Start();
}
}
}
那么输出将是这样的:
var foo = new Foo();
for (int i = 0; i < 10; i++)
{
try
{
await foo.Bar(() => { Report("Called"); throw new SomeExceptionType();});
}
catch(BrokenCircuitException)
{
Report("Broken");
await Task.Delay(450);
}
}
这里核心逻辑被封装并且可以复用。但它不能与其他弹性策略组合。
因此,如果您想实现这种特定的弹性逻辑,那么有很多选择。如果您需要进行撰写,那么您必须实现自定义
13:50:50.2469980:Called
13:50:50.2539870:Called
13:50:50.2540370:Called
13:50:50.2546650:Open
13:50:50.2552080:Broken
13:50:50.7074720:Broken
13:50:51.1621140:Broken
13:50:51.6145440:Broken
13:50:51.7849980:Close
13:50:52.0653560:Called
13:50:52.0657160:Called
13:50:52.0658150:Called
13:50:52.0660950:Open
13:50:52.0663610:Broken
13:50:52.5180710:Broken
13:50:52.9704980:Broken
13:50:53.2559450:Close
13:50:53.4215020:Called
13:50:53.4221810:Called
13:50:53.4222580:Called
13:50:53.4225390:Open
。