每一个人对于什么东西是可靠的或者不可靠的都有一个直观的概念。对于软件来讲,典型的期望包括:
• 应用程序提供用户期望的功能;
• 它可以容忍用户犯错或者以意想不到的方式使用软件;
• 在预期的负载和数据量下,其性能足以满足所需的用例;
• 系统防止任何未经授权的访问和滥用。
如果所有的这些东西表示“正确的工作”,那么我们就可以理解可靠性的含义,大致来讲就是,“即使出了差错,也能够正确的工作”。
那些可能出错的东西被称为错误,而预测错误并能应付的系统被称为容错。前一种说法有点误导人:它表明我们可以使一个系统容忍任何可能的错误,事实上这是不可能的。如果整个地球(及其所有的服务器)都被黑洞吞噬,那么对该故障的容忍将需要在太空上进行主机托管。所以说只容忍某些类型的错误才是有意义的。
同时请注意错误与失败是不一样的。错误通常被定义为系统的一个组件偏离其规范,而失败则是当系统作为一个整体停止向用户提供所需的服务时。把错误的概率降低到零是不可能的;因此,通常最好设计一种容错机制,以防止故障。在这本书中,我们将介绍几种构建可靠系统的技术。
在这样的容错系统中,通过不事先警告地随机杀死单个进程,从而增加错误的几率是有意义的。许多关键的错误实际上是由于比较弱的错误处理;通过故意地诱发错误,你可以确保容错机制不断地被执行和测试,这可以增加你对错误在自然发生时正确处理的信心。Netflix的Chaos Monkey就是这种方法的一个例子。
尽管我们通常更喜欢容忍错误而不是预防错误,但有些情况下,预防胜于治疗(例如,因为没有治愈方法)。例如,安全问题就是这样的:如果攻击者破坏了系统并获得了对敏感数据的访问权,那么这个事件就不能被撤销。但是,在本书中,我们主要讨论的是可以被治愈的错误,如下几节所述。
硬件故障
当我们想到导致系统故障的原因时,我们很快就会想到硬件故障。硬盘崩溃,RAM出现故障,电网停电,有人错误的拔掉了网络电缆等。任何做过大型数据中心的人都可以告诉你,当你拥有大量的机器时,这些事情就会发生。
据报道,硬盘有一个平均失效时间(MTTF),大约为10到50年。因此,在一个有10000个磁盘的存储集群上,理论上平均每天都会有一个磁盘挂掉。
我们的第一反应通常是为单个硬件组件增加冗余,以降低系统的故障率。磁盘可以在RAID配置中设置,服务器可能有双电源和可热插拔的cpu,而数据中心可能有电池和柴油发电机作为备用电源。当一个组件挂掉时,冗余组件可以替换上去。这种方法不能完全防止硬件问题导致的故障,但是它很好理解,并且常常可以使一台机器连续运行数年。
直到现在,对于大多数应用程序来说,硬件组件的冗余已经足够了,因为它使得单个机器的整体故障已经相当低了。只要您能够相当快地将备份恢复到新机器上,在大多数应用程序中出现故障的停机时间并不是灾难性的。因此,只有少数对高可用性有绝对需要的应用程序需要多机器冗余。
然而,随着数据量和应用程序的计算需求增加,越来越多的应用程序开始使用更大数量的机器,这在一定程度上增加了硬件故障的发生率。此外,在一些云平台上,如Amazon Web Services(AWS),也存在在没有警告的情况下,虚拟机实例变得不可用,因为这些平台的设计目的是优先考虑灵活性和弹性,而不是单机可靠性。
因此,通过使用软件容错技术或硬件冗余来实现对整个机器的故障转移。这种系统也有操作优势:如果您需要重启机器时,单服务器系统需要按计划停机(例如,应用操作系统安全补丁),而一个能够容忍机器失败的系统可以修补一个节点而保证整个系统不停机(滚动升级,详见第4章)。
软件错误
我们通常认为硬件故障是随机而独立的:一台机器的磁盘故障并不意味着另一台机器的磁盘将会挂掉。虽然可能存在弱相关性(例如,由于一个常见原因,比如服务器机架中的温度),但是不太可能出现大量硬件组件同时出现故障的情况。
而另一类错误是系统中的系统错误。这样的错误是难以预料的,而且由于它们是跨节点的,它们往往比硬件故障更容易导致系统故障。举几个例子:
• 一种软件错误,它会导致应用程序服务器的每个实例在特定的输入错误的情况下崩溃。例如,由于linux内核中的一个BUG--2012年6月30日的闰秒,导致许多应用程序同时挂掉。
• 一个失控的进程,占用了一些共享资源——CPU、内存、磁盘空间或网络带宽。
• 系统依赖的服务慢下来,变得无响应或者返回崩溃的响应。
• 级联故障,其中一个组件的小故障触发另一个组件的错误,从而触发进一步的故障。
导致这类软件故障的BUG通常会休眠很长一段时间,直到它们被一组不同寻常的环境所触发。在这种情况下,软件会对其环境做出某种假设,而这种假设通常是正确的,但它最终会因为某些原因而停止。
软件中系统故障的问题可能没有一个快速的解决方案。但很多小事情都可以帮助我们:仔细思考系统中的假设和交互;全面测试;进程隔离;允许进程崩溃和重新启动;监控和分析生产中的系统行为。如果系统被期望提供一些保证(例如,在消息队列中,传入消息的数量等于传出消息的数量),它可以在运行时不断地检查自己,如果发现有差异,则会发出警告。
人为错误
人们设计和构建软件系统,而保持系统运行的操作人员也是人。即使他们的目的非常好,人们也知道人是不可靠的。例如,一项大型互联网服务的研究发现,运营商的配置错误是导致服务中断的主要原因,而硬件故障(服务器或网络)导致的宕机比率只有10-25%。
那我们如何使我们的系统可靠呢?好的系统通常结合了以下几种方法:
• 设计系统的时候来不断的缩小犯错误的机会。例如,设计良好的抽象、api和管理界面使得做“正确的事情”变得容易,并且阻止“错误的事情”。但是,如果接口设计的过于严格,人们也许就会尽量避免使用它或者绕过它,这就降低了它们的用处,所以说,这是一种难以把握的平衡。
• 解耦因人为错误可能导致失败的地方。非常特殊的,可以提供一个非生产沙盒环境,让人们可以在不影响真实用户的情况下,使用真实的数据进行安全的探索和试验。
• 全面的测试,从单元测试到全系统集成测试和手工测试。自动化测试被广泛使用,这很好理解,特别是对于在正常操作中很少出现的个别案例来说是非常有价值的。
• 允许从人为错误中快速而方便地恢复,从而最小化故障的影响。例如,快速回滚配置更改,逐步部署新代码(这样,任何意外的bug只会影响一小部分用户),并提供重新计算数据的工具(以防原来的计算不正确)。
• 建立详细和清晰的监控,例如性能指标和错误率。在其他工程学科中,这被称为遥测技术。(一旦火箭离开地面,遥测技术对于跟踪正在发生的事情,以及对故障的理解是必不可少的。)监控可以向我们展示早期预警信号,并允许我们检查是否有任何假设或约束被违反。当出现问题时,metrics在诊断问题时是非常宝贵的。
• 执行良好的管理实践和培训。
可靠性有多重要?
可靠性不仅适用于核电站和空中交通控制软件——其实更普通的应用也期望可靠地运行。商业应用程序中的bug会导致生产力损失(如果数据被错误地报告,则会带来法律风险),而电子商务网站的宕机也会造成巨大的损失,收入和声誉都会受到损害。
即使在“非关键”的应用程序中,我们也要对用户负责。假设有一位家长在你的照片应用程序中存储了他们孩子的所有照片和视频。如果数据库突然被破坏了,他们会怎么想?他们知道如何从备份中恢复吗?
有些情况下,我们可能会选择牺牲可靠性以减少开发成本(例如,在开发一个未经市场证实的原型产品)或运营成本(例如,非常狭窄的利润率)---但是我们也应该意识到我们正在偷工减料。