Fail-fast | 一种可靠的软件设计策略

2023-03-18 14:54:18 浏览数 (2)

1. 背景

系统不应该失败,应用程序不应该崩溃,网络连接不应该超时....这就是我们都想要的。但是,有时它会失败并崩溃,我们都在努力防止这种情况发生。有很多方法可以防止软件失败:

  • 尝试恢复,当你的系统处于失败状态时,总是能够尽最大可能恢复,比如 K8S 的调度器,总是不断的把服务的当前状态调谐到期望状态,一次不行,那么它就以指数级递增的方式进行恢复。
  • 延迟失败,增大超时时间,包括连接超时、读超时、写超时,甚至出现部分网络错误之后,多次发起重连,尽最大可能保证成功。

然而,软件开发中有一条完全相反的原则:Fail-fast - 快速失败。

2. 什么是快速失败?

软件一定会出现错误。唯一没有错误的代码是从未编写过的代码。

在将要失败的情况下阻止某事失败并不能解决任何问题。它没有解决问题,它只是隐藏了问题。而且问题出现在表面上的时间越长,解决起来就越困难,成本也越高。

而在现实中,系统故障和软件崩溃并不是最糟糕的,有时它们根本不是一件坏事。还有更糟糕的事情:死锁、在原始错误之后很久就崩溃、数据丢失和损坏以及数据不一致,甚至影响整个集群。

这就是快速失败的原则:如果发生错误,立即和可见地失败。如果出现异常或意外情况,让软件立即失败,而不是推迟失败或者尝试低效解决失败。

3. 为什么要快速失败?

Fail-fast 使错误和故障出现得更快,如此错误更早被检测到,更容易重现和更快地修复。 更少的错误和缺陷将投入生产,从而产生更高质量和更多可用于生产的软件。

通过可见的快速失败,减少看不到的失败,更快地解决导致失败的根因。

这方面的研究很少,我想不出一种方法可以客观地应用科学方法来衡量这种快速失败的方法论。它既有技术方面,也有核心业务方面。可以肯定地说,这不仅仅是某个人决定的。这是一种业务与软件工程相结合以降低整体风险的策略。

所以只要的能够降低整体风险的快速失败都是正确的决策。

现代的故障安全方法试图通过使用阈值来限制故障来避免该方法的一些缺陷。

这方面的例子很多,比如 ms 级别的超时、熔断器、限流等,这些功能使我们的系统不会产生级联故障,降低失败给系统带来的打击。

4. 什么类型的系统需要快速失败

个人喜欢快速失败,它能够更快速暴露问题所在,从而使系统更加稳定。但这是轶事,我不知道该如何证明快速失败一定是对的。

从经验上来说具有以下特点的系统是需要快速失败的

高并发

如下一个简单的一个 CS 架构,客户端连接服务端,服务端进行必要的业务逻辑,最后跟数据库进行交互。

如果 server 和 db 之间没有快速失败机制,当它们之间出现了某个网络问题,连接数暴涨,服务内部的大量线程处于 iowait 状态,导致系统线程资源耗尽,无法响应客户端请求,最终服务被 Kill。另外如果 client 和 server 之间没有添加限流或者超时时间也会产生类似问题

这种问题一般通过控制连接池大小、线程池抛弃策略、网络连接超时时间从一定程度上解决,如果说网络确实存在问题,通过这种方式可以保证服务不被 Kill。另外 client 和 server 添加限流或者超时机制能够保证部分 client 不受影响。

可能这里会有一点疑问,网络连接或者服务处理慢跟服务被 Kill 有什么关系,很简单,逻辑线程处理慢了,占用资源就会增高,触发到资源边界限制点就会发意想不到的崩溃。再说了,云时代的服务基本都有外部探针,当你逻辑处理慢了,无法响应外部请求,很自然都被 kill 掉了。

低延迟

对于延迟敏感的服务,抛开网络影响,从第一个服务开始就要添加一个超时 deadline,其它服务每次减去自己消耗时间,当超过超时时间就没必要向下传递。client 不会等待请求的返回,这会造成不必要的连锁资源浪费。当然部分无需 client 等待的事务型请求,另当别论。

5. 总结

不仅仅是软件系统,快速失败(试错)是许多高效工作必须遵循的原则,比如持续集成:软件开发中的一种敏捷实践,开发人员需要每天多次将他们当前的工作集成到共享存储库/分支中。每个集成都通过自动构建进行验证,从而帮助团队及早发现和修复问题。

0 人点赞