错误模型(二)
Bug是不可恢复的错误
我们早期所做的一个重要区别是可恢复错误和错误之间的区别:
- 可恢复的错误通常是编程数据验证的结果。一些法典审查了世界状况,认为这种情况不可接受,无法取得进展。可能是一些正在解析的标记文本、来自网站的用户输入,或者是暂时的网络连接失败。在这些情况下,程序有望恢复。编写这段代码的开发人员必须考虑在失败时应该做什么,因为无论您做什么,它都会发生在构造良好的程序中。响应可能是将情况告知最终用户、重试或完全放弃操作,但是这是一种可预测的、经常是计划好的情况,尽管被称为“错误”
- Bug是一种程序员没想到的错误。输入没有正确验证,逻辑写错了,或者出现了许多问题。这样的问题通常都不会被及时发现;需要一段时间才能间接观察到“二次效应”,这时可能会对程序的状态造成重大损害。因为开发商没想到会发生这种事,所以所有的赌注都没有了。此代码可以访问的所有数据结构现在都是可疑的。因为这些问题不一定能及时发现,事实上,更多的问题值得怀疑。依赖于语言的隔离保证,也许整个过程都被污染了。
这种区别是最重要的。令人惊讶的是,大多数系统并不能产生一个,至少在原则上不是这样的!如上所述,Java、C#和动态语言只对所有内容使用异常;C和Go使用返回代码。C++使用的是一个取决于观众的混合,但通常的故事是一个项目选择一个单独的并且到处使用它。但是,您通常听不到语言建议使用两种不同的错误处理技术。
考虑到bug本质上是不可恢复的,我们没有尝试。在运行时检测到的所有错误都会导致所谓的终止,也就是所谓的“快速失败”。
上述每个系统都提供了类似于放弃的机制。C#有环境.FailFast,C++有std::terminate;等等。每一个都突然而迅速地撕开周围的背景。这个上下文的范围取决于系统——例如,C和C++终止进程。
尽管我们确实以一种比普通人更规范、更普遍的方式使用放弃,但我们肯定不是第一个认识到这种模式的人。哈斯克尔的这篇文章很好地阐明了这一区别:
我参与了一个用C++编写的图书馆的开发。一位开发人员告诉我,开发人员分为喜欢异常的和喜欢返回代码的两类。在我看来,返回码的朋友赢了。然而,我得到的印象是,他们争论了错误的一点:异常和返回代码同样具有表现力,但是它们不应该用来描述错误。实际上,返回代码包含数组索引超出范围等定义。但是我想知道:当我的函数从一个子例程获取这个返回代码时,它应该如何反应?它要给它的程序员发一封信吗?它可以依次将此代码返回给调用方,但也不知道如何处理它。更糟糕的是,由于我无法对函数的实现进行假设,我不得不期望每个子例程都有一个超出范围的数组索引。我的结论是数组索引超出范围是一个(编程)错误。它不能在运行时处理或修复,只能由开发人员修复。因此不应该有相应的返回代码,而是应该有断言。
放弃细粒度的可变共享内存作用域是可疑的,比如Goroutines或threads之类的,除非您的系统以某种方式保证所造成的潜在损害的作用域。不过,这些机制是伟大的,我们有使用!这意味着在这些语言中使用废弃规则确实是可能的。
然而,这种方法要在规模上取得成功,有一些架构元素是必要的。我敢肯定你在想“如果我每次在我的C#程序中出现空引用时都抛出整个过程,我会有一些非常恼火的客户”;同样地,“那根本就不可靠!”!“事实证明,可靠性可能不是你想的那样。
可靠性、容错性和隔离性
在我们进一步讨论之前,我们需要陈述一个中心信念:史无前例。
建立一个可靠的系统
普遍的看法是,你通过系统地保证失败永远不会发生来建立一个可靠的系统。直觉上,这很有道理。有一个问题:在极限,这是不可能的。如果你能像许多任务关键型实时系统一样,仅在这处房产上就花费数百万美元,那么你就可以取得重大进展。或许可以使用SPARK这样的语言(Ada的一组基于契约的扩展)来正式证明所写每一行的正确性。然而,经验表明,即使是这种方法也不是万无一失的。
我们没有反抗生活的现实,而是拥抱它。显然,你会尽可能地消除失败。错误模型必须使它们透明且易于处理。但更重要的是,你设计你的系统,这样即使个别部分出现故障,整个系统仍能正常工作,然后教你的系统优雅地恢复那些故障部分。这在分布式系统中是众所周知的。那为什么是小说呢?
最重要的是,操作系统只是一个由协作进程组成的分布式网络,就像一个由微服务或互联网本身组成的分布式集群。主要的区别包括延迟、可以建立的信任级别和容易程度,以及关于位置、标识等的各种假设,但是在高度异步、分布式和I/O密集型系统中,失败是必然发生的。我的印象是,在很大程度上,由于单片内核的持续成功,整个世界还没有实现“操作系统作为分布式系统”的飞跃。然而,一旦你这样做了,很多设计原则就会变得显而易见。
与大多数分布式系统一样,我们的体系结构假定过程失败是不可避免的。我们花了大量的时间来防止层叠故障,定期记录日志,并实现程序和服务的可重启性。
当你假设这一点的时候,你会以不同的方式构建事物。
特别是,隔离是至关重要的。系统的流程模型鼓励轻量级细粒度隔离。因此,程序和现代操作系统中通常的“线程”是独立的独立实体。防止一个这样的连接失败比在地址空间中共享可变状态要容易得多。
- 上一篇: 仅通过转储来排除内存泄漏
- 下一篇: 使用Java中的InputStream读取文件数据