只崩溃软件

作者

乔治-坎迪亚、阿曼多-福克斯

原文

Crash-Only Software

1. 摘要

只崩溃程序可以安全崩溃迅速恢复。只有一种方法可以停止这类软件:让它崩溃。同样也只有一种方法可以启动软件:执行恢复。只崩溃系统使用只崩溃组件构建,使用组件级自动重试,避免内部组件崩溃影响终端用户。在本文中我们建议对互联网系统采用只崩溃设计,证明了只崩溃设计能够产生可靠的代码、简单的故障预防和快速高效的故障恢复。我们介绍了关于建立只崩溃互联网服务的思路,将成功技术发挥到极致。

2. 奥卡姆剃刀与重启杂烩

重启程序的理由和方法有很多。研究表明大型软件意外中断服务的主要原因是间断或偶发故障[14,22,21,1]。大多数非嵌入式系统都支持多种停机方式,例如操作系统可以正常关机、异常退出、挂起、崩溃、掉电等。

正常关闭程序时,服务中断时间包括关闭时间和重启时间。而从崩溃恢复时,中断时间只包括恢复时间。看起来很讽刺,关闭和重启有时比崩溃恢复更漫长。表1展示了崩溃时间的简单比较,在实验中没有丢失任何重要数据。

表1  正常重启和崩溃重启时间对比
系统正常重启崩溃重启
RedHat 8(使用ext3fs)104 秒75 秒
JBoss 3.0 应用服务器47 秒39 秒
Windows XP61 秒48 秒

建立一套永远不会崩溃的系统是不可能的,即使对于电信级电话交换机或高端大型主机。既然无法避免,软件必须支持崩溃,就像支持正常关闭一样。遵从奥卡姆剃刀精神,如果软件可以安全的崩溃,为何还要支持多余的非崩溃式关闭功能呢?常见的理由是为了提高性能。

例如为了避免缓慢的同步磁盘写操作,很多UNIX文件系统在内存中缓存元数据。导致UNIX服务器崩溃时,文件系统进入不完整状态,需要长时间运行fsck命令进行修复。修复操作原本可以通过正常关机来避免。这体现出一种设计决策:提高稳定状态下系统的性能,代价是停机和恢复性能。由于崩溃无法避免,这样的文件系统是脆弱的:崩溃会损失数据,有时崩溃后状态的完整性都无法修复。这个决策不仅影响稳定性,也因为提供了多种维护状态的方法、更多的代码和更多的编程接口,让系统更加复杂。代码难以维护,增加了产生缺陷的隐患——对于高性能系统,是个不错的选择;但对于高可用系统,是个糟糕的想法。如果性能的代价是可靠性,也许应该重新评估设计策略。

以往我们曾使用递归微重启来提高一个崩溃安全的柔性状态系统的可用性。在此我们建议互联网系统采用只崩溃设计(安全崩溃且快速恢复)。只崩溃系统具有以下显著特点:规模大、严格的高可用要求、使用大量异构组件构建、通过标准的请求-应答协议(如HTTP)访问、工作负载包括大量的更新系统状态的短暂任务,以及需要快速持续演进。再次我们只关心在单个数据中心内的,不会跨管理域部署的系统。

在高阶术语中,只崩溃系统由等式“停止=崩溃”和“启动=恢复”定义。在本文中,我们通过与物理进行类比的方式介绍只崩溃设计的优点,描述了只崩溃系统中组件的内部属性和控制组件交互的整体结构属性,以及使用只崩溃设计的重启/重试架构。同时也介绍了我们在一个J2EE原型上的工作。

3. 为何采用只崩溃设计?

成熟的工程准则使用宏观物理定律建立和理解物理系统的行为。这些定律,比如牛顿力学,以简单的形式识别出人们观察到物理不变性。然而软件是没有物理具象的抽象对象,不服从物理定律。计算机科学家尝试使用规范规则,如形式模型和不变性证明,来理解软件的行为。这些规则往往是基于一个无法完全描述实际系统(包括硬件、操作系统、运行时等)行为的抽象模型设计的。因此规范模型不能提供软件实际行为的完整描述,真实系统许多在物理上可能出现状态无法在抽象模型中得到反映。

使用只崩溃性质,我们可以从软件系统外部,从宏观行为上把系统变成一个简单的、容易预测的,拥有更少状态和简单不变性的整体。每个只崩溃组件都有幂等的“开启开关”和“关闭开关”。大型系统的开关由子系统开关连接组成。具体方法在第3节介绍。组件的关闭开关完全在组件之外实现,不会调用组件的任何代码,也不要求组件内部行为是正确的。这类开关包括向UNIX进程发送kill -9信号,或者关闭运行软件的物理机或虚拟机。

在组件之外实现关闭开关可以建立一种可靠的崩溃机制。系统的每个组件必须做好被突然关闭的准备。关闭和开启开关提供了可靠的简单行为,获得一个很小的状态空间。当然虚拟机关机或kill -9的产生的状态空间比物理机关机更大,但仍比虚拟机中托管程序的状态空间小,并且不会因为托管程序的不同的而改变。事实上与托管程序相比,虚拟机的状态空间更小更简单。这一事实已被广泛用作使用虚拟机隔离应用的依据[28]。

3.1. 只崩溃性质和事务

通过使用事务,数据存储和检索领域已经获得了只崩溃设计的诸多优势。我们的方法旨在为互联网系统带来相似的影响。只崩溃设计不像事务模型那么具体,而是有着广泛的通用性。

互联网应用并非必须使用事务才能实现只崩溃机制。实际上ACID语义有时被滥用了。例如会话数据将一系列用户请求产生的服务器端数据积累起来,在用户后续的操作中使用。它主要是单读/单写的,不需要对操作进行排序和并发控制。会话状态持续时间往往不超过几分钟,没有必要使用像SQL这样功能强大的查询语言。SSM[20](译注:一个会话状态管理器)利用这些观察结果建立了一个哈希表式的只崩溃会话状态存储。

3.2. 只崩溃和状态模型执行

只崩溃系统可以将检测到的故障转换成组件崩溃,产生了一个简单的故障模型,组件只需要知道如何从单一类型的故障中恢复。故障模型使用这种方法将未知故障转换为崩溃,有效的将实际故障转变为一个充分理解的简单故障模型。通过使用这个故障模型进行恢复,[23]成功的提高了集群系统的可用性。很多文章假定一个不真实的故障模型(例如故障独立的、按照容易处理的方式发生)来分析系统行为。故障模型执行可以增强现有文献提出的方法。

类似的,如果构建系统的组件可以支持低成本崩溃,系统能够使用软件重生技术[18]预防故障。软件重生可以根据失败-停顿行为[3]、负载过低或软件老化数学模型触发。

3.3. 只崩溃和面向恢复设计

恢复代码处理异常场景,必须完美执行。不幸的是异常情况很难处理,极少发生,在开发过程中难以模拟。这些因素往往让恢复代码不可靠。只崩溃系统每次启动都会执行恢复代码,大幅提高了恢复代码的可靠性。由于我们降低代码缺陷率的速度落后于系统中代码行数的增加速度,结果就是系统中的缺陷数量会随着时间不断增加[9]。更多缺陷意味更多故障,频繁故障的系统也需要频繁恢复。

4. 只崩溃软件的属性

本节我们描述了一组属性,足以让系统支持只崩溃机制。有些系统并不需要其中的某些属性。在第5节我们通过定义多种只崩溃级别来说明这个事实。

为了让组件支持只崩溃性质,我们要求全部持久状态保存在专用状态存储中,状态存储为应用提供适当的抽象模型,同时也是只崩溃的。让一个由组件相互连接构成的系统支持只崩溃性质,组件设计需要容忍其他组件崩溃或短暂失效。因此我们需要一种强壮的模块化设计,拥有相对不可跨越的组件边界、基于超时的通信和基于租约的资源分配,以及携带生存时间和幂等标志的自描述请求。许多互联网系统已经具备这些属性中的一部分,但我们不知道有哪个系统将所有属性组合成真正的只崩溃系统。

在第4节中我们将展示如何将只崩溃组件组装成一个基于重启/重试架构的健壮的互联网系统。本节我们将详细描述只崩溃系统的7个属性。前3个属性和组件内状态管理有关,后4个则是关于组件间交互的。我们知道一些设计牺牲了性能,但我们相信系统健壮性恢复其基础地位的时机已经到来。

4.1. 组件内属性

今天的互联网应用保持着少数几种状态:事务性持久化状态、单读/单写持久化状态(例如几乎没有并发访问的用户配置文件)、消耗性持久化状态(为了正确性或性能可以丢弃的服务器端信息,例如点击流数据和访问日志)、会话状态(例如购物车内容)、柔性状态(可以根据其他数据源随时重建的状态)和易失性状态。虽然主要通过生命周期进行区分,不同状态类别的需求差异导致了不同质量的实现方式。

非易失状态由专用的状态存储管理 ,应用只需要处理程序逻辑。与受过少量系统编程训练的开发者所编写的代码相比,专用状态存储(如关系数据库和面向对象数据库、文件系统、分布式数据结构[16]、非事务性散列表[17]、会话状态存储[20]等)更适合管理状态。应用成为状态存储的无状态客户端,拥有简单快速的恢复过程。三层互联网架构就是状态与逻辑分离的常见案例,中间层几乎是无状态的,依靠后端数据库保存数据。

状态存储是只崩溃的 ,否则只是把问题丢个下一层。目前很多商用状态存储都是崩溃安全的(崩溃时不用担心丢失数据),比如数据库和各种通过网络挂载的存储设备。但它们大多不是只崩溃的,恢复速度缓慢。不过许多产品提供了优调方法,允许管理员牺牲性能换取恢复时间,例如在Oracle DBMS中频繁生成检查点[19]。一个完全只崩溃状态存储的例子是Postgres数据库[27],它舍弃了“先写日志”机制,将全部数据保存在只追加日志中。恢复实际上即时完成的,只需要将崩溃时未提的交事务标记为废弃。

状态存储提供的抽象模型和约定必须满足应用的需求 。因此状态存储提供的抽象模型不能太弱(例如用文件接口来存取客户记录),也不能太强(例如用带有ACID语义的SQL接口存取键值对)。首先这个属性允许应用在“自然”的语义级别上运行。其次,在保证了需求和实现的精确匹配之后,我们可以利用应用语义构建简单、快速、可靠的状态存储。

例如Berkeley DB[24]是一个支持B+树、散列表、队列和记录等抽象模型的存储系统,可以通过四种不同类型的访问接口,从没有并发控制、事务或故障恢复的,到多用户、带有日志、细粒度锁和数据复制的事务性编程接口。应用可以选择适合其目标的抽象模型,底层状态存储负责优化具体操作以满足应用需求。状态存储可以根据工作负载特征进行优化。例如对于以读为主的负载,状态存储可以使用直写缓存机制,显著改善恢复时间和性能。

我们认为将来互联网系统会把少数几种状态存储类型制订为标准:ACID存储(例如保存客户和事务数据的数据库)、非事务性持久存储(例如DeStor[17],一种专门处理非事务性持久数据,如用户配置文件的只崩溃系统)、会话状态存储(例如保存购物车信息的SSM)、简单只读存储(例如保存静态HTML页面和图像的文件系统)和柔性态存储(例如Web缓存)。如果仔细设计每个应用组件所需要的状态抽象模型,并选择合适的状态存储,我们可以构建出只崩溃组件。

4.2. 组件间属性

子系统崩溃后无法处理请求。要让只崩溃系统完善处理这种情况,组件和组件间关联必须遵守一定的规则。

组件具有强制的外部边界 ,提供强大的故障控制。所需的隔离可以通过VMware之类的虚拟机、隔离内核[28]、基于任务的JVM内部隔离[26,10]、操作系统进程等实现。Denali隔离内核设计就支持这种轻量级封装。Web托管服务提供商通常在一台物理机上使用多个虚拟机,为客户提供独立管理互不影响的Web服务器。组件边界规定了请求处理过程中唯一的独立恢复阶段。

交互具有时限 。使用类似RPC的显式通信:如果在分配的时间片内没有收到响应,调用方假定被调用方故障,向恢复代理[6]报告,恢复代理适时崩溃并重启被调用方。崩溃重启保证了被调用组件处于已知状态。组件是崩溃安全的,崩溃重启是幂等操作,因此这个操作是安全的。时限提供了一种正交机制,将组件或网络引起的非拜占庭故障转换为故障停止事件(即故障实体要么返回结果,要么停止),即使组件不支持故障停止。这样的故障行为容易处理,对故障控制是一种改进。

全部资源都是租用的 ,不会永久分配。包括多种类型的持久状态,例如免费电子邮件供应商提供的帐户配置:每次用户登录自动续订6个月的租约,租约期满后,系统中删除相关数据。CPU资源也一样:超过时间限制的计算将被终止(例如PHP。PHP是一种编写动态网页的服务器端脚本语言,它提供了函数控制脚本的最大执行时间,超时的脚本将被终止并报告错误)。租约机制[13]让我们能够判断租约期满后系统的状态:特定容量的资源被释放,同时组件也处于一种已知状态。不能将时限或租期设置为无上限,最大时限和租期应当作为应用的全局策略。这样系统就不容易挂起或阻塞。

请求是自描述信息流(continuation) ,带有幂等标志和生存时间。信息流(continuation)[11]明确记录了处理请求所需的状态和上下文,简化了状态管理。幂等和生存时间信息可以Web前端进行设置:时限与负载或服务级别协议有关,幂等标志则由应用特定信息(例如可以从URL子字符串判断请求类型)决定。互联网服务的很多重要操作是幂等的,或者可以通过跟踪序列号或将请求封装在事务中实现幂等性。一些大型互联网服务已经证实这种做法的可行性[25]。请求在生命周期中被分拆成多个子操作,子操作可以重新连接,如同嵌套事务一样。从失败的幂等子操作中恢复,只需要简单地重新执行。对于非幂等操作,系统可以回滚操作、执行补偿操作,或者忽略一致性隐患直接重试。这种针对请求流的透明恢复机制,可以对终端用户屏蔽系统内组件故障。

5. 重启/重试架构

组件可以根据调用异常或超时判断出其他组件发生了故障。收到故障报告后,恢复代理可以崩溃重启故障组件。崩溃关闭是幂等的,是在执行恢复之前确认组件已经关闭的好方法。组件重启时,等待它应答的其他组件收到“n毫秒稍后重试”异常,表明请求可以在n毫秒(预计恢复时间)后重新提交。这是一个优化措施,因为调用方组件可以使用超时作为备选方案。如果请求是幂等的,并且生存时间允许,调用方组件会重新提交请求。否则故障异常将沿着请求链向上传播,直到某个组件决定重试,或者通知客户系统发生故障。Web前端向客户端发出HTTP/1.1 Retry-After指令和预计恢复时间。支持重试功能的客户端可以重新提交HTTP请求。

图1  简单的重启/重试架构

图1展示了一个简单的重启/重试例子,查看购物车的请求在应用服务器内部分拆成一个连接会话状态存储的EJB实体的子请求,和一个访问无状态会话EJB的子请求。如果状态存储不可用,应用服务器收到稍后重试异常或超时,此时应用可以决定是否向会话存储重新提交请求。在图1的每个子系统中,每个请求可能被进一步分拆成更小的请求,提交给各相关子系统组件。我们已经实现了支持崩溃重启(我们叫做微重启)的EJB对象。重启时间不超过一秒钟。

基于时限的故障检测机制可以增加传统的心跳机制和进度计数器。计数器——处理进度的简化表示——通常保存在状态存储器和通信设施中,将状态访问操作和通信活动映射成组件的工作进度。很多性能监视器加上请求原始信息就可以变成进度监视器。组件本身也可以实现计数器,更精确的表达应用语义。但这类计数器不太可信,如果组件发生故障。

松散耦合系统的行为有时让人惊讶。例如向恢复中的组件重新提交请求,可能让组件过载并再次失效。因此稍后重试异常需要提供预计恢复时间。通过向不同调用方提供差异化预计恢复时间,可以分散重试请求的集中度。最大重试次数和租期、通信时限一样,是应用的全局策略。具体数值可以根据恢复管理器收集的历史信息动态估算 [6],也可以直接保存在组件的静态配置中,类似EJB部署描述符。如果无法估算恢复时间,调用方可以使用简单的负载均衡算法或指数后退方法。

6. 只崩溃级别

第3节描述的属性可以让系统支持只崩溃性质,这些属性并非在所有情况下都是必需的。只崩溃不是一个绝对性质,而是存在多种不同程度的“只崩溃性”。如同RAID[8],对只崩溃级别和实现细节进行定义,在系统分类和系统构建方面很有用。

表2  只崩溃级别
只崩溃属性
 专用的状态存储  是  是  是 
 只崩溃的状态存储  是 
 适配的状态抽象模型  是  是 
 强隔离  是  是 
 基于时限的交互  是 
 租赁资源  是 
 自描述请求  是 

表2显示了只崩溃级别分类。例如使用cgi-bin脚本实现的Web电子邮箱应用可能属于低等只崩溃级别:使用文件服务器作为专用状态存储,但不符合其他要求。要达到中等只崩溃级别,我们可以在J2EE平台上实现相同的邮箱应用,使用面向对象数据库(增加了适配的状态抽象模型)和EJB组件模型(在应用内增加了强隔离性)。 最后,为了实现高等只崩溃级别,应用必须基于理想的、支持所有只崩溃机制的应用服务器实现。

7. 讨论

构建只崩溃系统不容易(例如为了让数据库支持只崩溃性质,花费了大量细致的工程工作)。广泛应用只崩溃机制的关键在于选择正确的架构和工具。随着最近组件式架构(如J2EE和.NET)获得成功,出现了将应用服务器作为互联网应用基础实施的现象。可以通过平台自身提供只崩溃性质,允许运行在平台上的所有应用利用平台特性成为只崩溃的。

我们将这些原则应用到一个开源J2EE应用服务器上,将各个J2EE服务(命名、查找目录、传递消息等)分拆成充分隔离的组件,将请求实现为自描述信息流,修改RMI层支持限时操作,修改EJB容器实现资源租赁,并集成非事务性状态存储(如DStore和会话状态管理器)。参考文献[6]介绍了初步的改造方法。

我们最初关注的是以更新系统状态的短暂任务为工作负载的应用。互联网服务几乎都是这样。部分原因在于,建立互联网系统所使用的工具的发展趋势,迫使设计者使用“三层架构”模式。随着企业服务和应用(如工作流、客户管理)开始支持Web,系统采用类似架构。我们认为在互联网领域之外,许多应用(如交互式桌面应用)无法轻易采用这种设计思路。对这类应用使用只崩溃设计不切实际,也不可行。我们同时将互联网系统的范围限定在使用HTTP交互的那些,尽管互联网服务也可以使用其他协议。

重启/重试架构拥有“执行至少一次”语义。为了保证高可用和正确,系统处理的大部分请求必须是幂等的。这个要求也许不适用于某些应用。我们的方案没有处理拜占庭故障或数据错误,这些行为可以通过已知的正交机制转换成故障停止行为,例如三模冗余[15]或智能状态复制[7]。

对于今天的互联网系统,快速恢复通过超额部署以及由快速故障检测触发的故障转移来实现。故障转移有时可以屏蔽数小时的恢复时间。只崩溃软件为这种方法提供了补充,有助于减轻对高度冗余硬件的复杂昂贵的管理需求。因为软件恢复得越快,需要的冗余硬件越少。此外,只崩溃系统可以快速集成已恢复组件,更好的支持组件的移除、添加或升级[2]。

我们估计只崩溃系统的吞吐量会受到影响,与获得的高可用性和健壮性相比,我们认为这种担心是次要的。虽然软件行业主流仍在关注极致的释放性能,但在过去的几年中,可靠性开始在开发议程中占据一席之地。

8. 结论

通过使用只崩溃方法,我们预计互联网系统获得更好的稳定性和更高的可用性。通过外在强制的“停止=崩溃”、“启动=恢复”方法,应用可以简化所需要的故障模型,使用简单的恢复过程,提高系统的准确性。编写只崩溃组件是困难的,但是简单的故障行为让组件更容易集成到大型系统中。

如果给只崩溃系统提供适当的恢复基础设施,我们可以获得一个递归的可重启系统。基于组件微重启的自动恢复机制让重启/重试架构能够对用户屏蔽系统内错误,提升用户所感知的服务稳定性。让我们感到鼓舞的是,最初的原型在故障条件下能够多处理78%的客户请求,相比没有采用微重启恢复的非只崩溃版本。

9. 参考文献

日期: 2024年08月02日

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值