目录
- 快速失败
- 编程世界里的超时/取消
快速失败
抛开各种细枝末节的原因,外部组件/依赖不可用的情况时常发生。站点应用有容错能力,能够从常规情况下的失败中恢复过来继续处理后续请求,但部分条件下,失败会从源头逐步蔓延直至拖垮所有应用,称之为雪崩效应。
为了避免此类问题发生,我们需要使用一些手段规避,比较典型的像熔断器比如 Netflix/Hystrix,使用窗口机制,主动移除或者恢复失败的组件/依赖,文档很多且不是本 issue 重点,请自行查阅。
另外的有效手段是添加超时机制,即外部依赖如果明显失效则应快速失败,而不是无限制地等待。举例来说,Redis 超过1秒都没能返回响应,要么是数据量过大(即设计或实现不合理),要么是已经失活了;SSO 调用时间可能长一点,但根据以往经验总是能得到常规值,而不是傻傻地等下去。
编程世界里的超时/取消
高级语言形如 .net 提供了非常多超时的 API,像 Connection.Timeout,Request.Timeout 或者 FileStream.ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) 等等,但麻烦的是 .net 的任务均为基于 CancellationToken 的协作式取消(有关资料仍请自行查阅),这意味着
- 超时/取消 API 是随着版本更迭逐步添加的,于是覆盖率有限
- 在业务级别设置 CancellationToken 并不容易,并暴露了低级别对象,对封装性存在挑战
好在 aspnet core 里异步无处不在,我们可以很安全地使用 task 以很小代价完成超时设置。
原始的超时实现往往使用 System.Threading.Timer 实现
- 将原本无法使用超时/取消的 API,使用异步任务启动;
- 初始化 Timer 并使用超时时间作为触发时间;
- 在 Timer 的回调逻辑里检查第1的任务的完成情况
可能未等到 Timer 回调触发任务已完成,但在回调逻辑里检查任务未完成时,就可以主动告诉外部调用方时间已到,从而达到超时与取消目的。
好在 CancellationTokenSource 封装了类似逻辑,合适使用可以达到相同目的。一个供参考的实现如下:
public static async Task SetTimeout(this Task task, TimeSpan timeout) {
using (var cts = new CancellationTokenSource(timeout)) {
var tsc = new TaskCompletionSource();
using (cts.Token.Register(state => tsc.SetCanceled(), tsc)) {
if (task != await Task.WhenAny(task, tsc.Task)) {
throw new OperationCanceledException(cts.Token);
}
}
return await task;
}
}
代码使用 Task.WhenAny 返回一个等待特定时间完成的简单任务,并与目标任务竞争;当返回结果和入参不同时表示超时,单元测试如下:
[Fact]
public async Task canel_timeout_void_task_should_throw_exception()
{
var task = WorkHardly(1000);
await Assert.ThrowsAsync(async () => await task.SetTimeout(TimeSpan.FromMilliseconds(500)));
}
[Fact]
public async Task canel_timely_void_task_should_pass()
{
var task = WorkHardly(500);
await task.SetTimeout(TimeSpan.FromMilliseconds(1000));
}
async Task WorkHardly(Int32 ms)
{
await Task.Delay(ms);
return Guid.NewGuid();
}
至此超时/取消的目标实现,但其中有若干微秒的地方:
- 线程调度、用户/内核模式转换,使得一个什么也不做的任务完成也要 50 Milliseconds 左右,只能模糊计时;
- 极少数情况下超时(即调用方 catch 到 OperationCanceledException 异常)并不代表任务没有完成;
leoninew 原创,转载请保留出处 www.cnblogs.com/leoninew