第 3 章 让系统稳定运行

第 3 章 让系统稳定运行

新上市的软件如同新毕业的大学生:以往在校园里所表现出的乐观和活力,突然被严酷的现实世界泼上一头冷水。对软件来说,在实验室里不会发生而在现实世界中发生的事情,通常不是好事情。在实验室里,所有的测试都经过精心设计,安排测试的人预先知道他们想获得的答案。然而,要应对现实世界里的挑战,并没有那么简单。

企业软件的开发团队必须保持杞人忧天的心态,即预计坏事情肯定会发生。这样的软件甚至都不信任自己,所以会在内部竖起屏障,防止自己遭遇系统失效。因为担心受到伤害,所以它会拒绝与其他系统保持过于密切的关系。

第2章讨论的CF系统项目,就没有保持杞人忧天的心态。和其他许多团队一样,这个团队也陷入了对新兴技术和先进架构的兴奋之中而不能自拔。当然,这两者的结合很诱人,团队显然大意了,他们被潜在收益冲昏了头,结果一下子就出了一起重大事故。

糟糕的稳定性会带来巨大的损失,最明显的就是收入的损失。第1章提到系统停机5小时会给用户造成100万美元的损失,而且这还只是淡季期间。对交易系统来说,单笔交易中断就可能造成高达100万美元的损失!

行业研究表明,在线零售商获得每位顾客的成本高达150美元。如果每小时有5000位独立访客,假设其中10%最终选择别家,就会浪费7.5万美元的营销资金。

与财务损失相比,声誉损失虽然不那么触手可及,但企业在这方面所承受的痛苦是一样的。与失去顾客相比,对品牌的玷污可能不太明显,但航空公司可以试一试让《彭博商业周刊》在假期出行高峰期间,报道一下其出现的运营问题。用来宣传在线乘客登机服务的企业形象广告所花费的数百万美元,会因为几个坏掉的硬盘而在几小时之内打了水漂。

要使系统获得良好的稳定性,不一定非得支付巨额的费用。当构建系统的架构、设计甚至底层实现时,许多决策点对系统的最终稳定性具有很大的影响力。面对这些决策点,有两条路能满足功能性需求(通过QA测试):一条路会导致每年停机几小时,而另一条路则不会。令人惊叹的是,在实现过程中,高度稳定的设计与不稳定的设计投入成本通常是相同的。

3.1 定义稳定性

为了讨论稳定性,首先需要定义一些术语。事务是系统处理的抽象工作单元,这与数据库事务不同。单个工作单元可能包含许多数据库事务。例如,在电子商务网站中,一种常见的事务是“客户创建订单”。该事务涉及多个页面,通常包括与外部系统的集成,例如信用卡验证。事务是系统存在的原因。如果一个系统只能处理一种事务,那么它就是专用系统。混合工作负载是系统能处理的不同事务类型的组合。

系统是指用户处理事务所需的一套完备且相互依赖的硬件、应用程序和服务。小到单个应用程序,大到一个庞大的多层应用程序和服务器网络,都可以算作系统。

即使在瞬时冲击、持续压力或正常处理工作被失效的组件破坏的情况下,稳健的系统也能够持续处理事务。这就是大多数人所说的“稳定性”。这不仅仅是指服务器或应用程序仍能保持运行,更多地是指用户仍然可以完成工作。

冲击压力这两个术语来自机械工程。冲击是指对系统快速施加大量的访问流量,就好像“用锤子猛击”系统。相反,对系统施加压力是指长时间持续地对系统施加访问流量。

由于提前发布PlayStation 6产品的消息,一大群快闪族般的疯狂玩家同时涌向该产品的详情页,这就引发了一次冲击。一万个新会话在一分钟内全都挤过来,任何服务实例都难以招架。一位名人在推文里提到你的网站,就会对网站造成一次冲击。在11月21日1凌晨,将1200万条消息丢入一个队列,就是一种冲击。这些事情可能眨眼间就会让系统崩溃。

111月21日为世界问候日。——译者注

另外,由于信用卡处理系统的容量不足以满足所有顾客的需求,因此其响应速度变得十分缓慢,这给系统带来了压力。在机械系统中,当施加压力时,材料会发生形变。这种形状的变化称为劳损。压力产生劳损。计算机系统也会发生同样的情况。来自信用卡处理系统的压力,会导致劳损波及系统的其他部分,进而导致系统运行异常。这可能表现为Web服务器的内存使用率升高,数据库服务器的I/O占用率超出正常范围,或者系统的其他部分发生异常。

寿命长的系统会长时间处理事务。“长时间”是多久?这需要看情况。一个有用且较具信服力的定义指出,“长时间”是指两次代码部署的间隔时间。如果每星期都有新的代码部署到生产环境中,那么系统能否在不重启的情况下运行两年就不那么重要了。此外,以美国蒙大拿州西部的数据收集系统为例,这种系统实际上不需要每星期手动重启一次,你要是想住在那里,那就尽管重启吧。

3.2 延长系统寿命

威胁系统寿命的主要敌人是内存泄漏和数据增长。这两种长期存在的问题2会在生产环境中摧毁系统,却很少能在测试中被发现。

2请参阅5.4节,了解更多关于系统中长期存在的问题。——译者注

测试使问题浮出水面,从而使人们可以修复系统。根据墨菲定律,凡是没有被测试出的问题,将来都会发作。因此,如果没能利用测试避免系统在午夜之后立即崩溃,或应用程序在正常运行49小时后发生内存不足的差错,那么这些问题都会一个个跳出来。只有连续运行7天之后,系统才会显露内存泄漏问题,而如果没有提前测试,那么系统在上线7天后就会遭遇内存泄漏。

问题在于,应用程序在开发环境中运行的时长永远不足以暴露关乎系统寿命的缺陷。在开发环境中,一台应用程序服务器通常会运行多久?可以打赌,其平均运行时长会少于Netflix公司在线视频网站上一集情景喜剧的长度。系统在QA环境中运行的时间可能会长一点,但通常也不会超过一天。即便在开发环境中启动并运行系统,也不会有持续的用户访问负载。这些环境不利于长时间运行测试,例如在每日流量压力下让服务器持续运行一个月。

这些缺陷通常也不能被负载测试发现。负载测试会运行指定的一段时长,然后退出。负载测试服务供应商每小时收取一大笔金额,所以没有人会请他们一次将负载测试连续运行一周。另外,开发团队可能会通过公司网络运行负载测试,所以团队也不能为了测试而使发送电子邮件和浏览网页等活动中断数日。

究竟如何才能发现这些关乎系统寿命的缺陷呢?在这些缺陷给你造成损失之前,发现它们的唯一方法就是运行自己编写的寿命测试。如果可以,找一台开发工程师用的计算机,在它上面运行JMeter、Marathon或其他负载测试工具。不要给系统太大负荷,只要能始终保持向系统发送请求就可以。(另外,一定要让脚本每天有几个小时不怎么向系统发送请求,来模拟半夜的低峰时段。这样做能暴露连接池和防火墙的超时问题。)

出于经济上的考虑,搭建完整的测试环境有时并不可行。此时,至少要测试那些重要的组件,用测试替身替代其余组件。即便是这样的测试,也好过不做测试。

如果上述一切都不可行,那么生产环境便自动成为寿命测试环境。那里肯定存在软件缺陷,这会给快乐生活埋下隐患。

3.3 系统失效方式

突发的冲击和过度的压力都会引发灾难性系统失效。在这两种情况下,系统的某些组件会先于其他组件失效。在Inviting Disaster一书中,James R. Chiles将这些组件称为“系统中的裂纹”。他将处于失效边缘的复杂系统与存在细微裂纹的钢板进行类比。在施加压力之后,裂纹开始越来越快地蔓延。最终,裂纹蔓延的速度会超过声速,钢板会炸裂。导致裂纹产生的导火索、裂纹向系统其余部分蔓延的方式以及损坏的结果,统称为系统失效方式

不管怎样,系统总是会有各种各样的失效方式。否认系统失效的必然性,会夺走人们控制和限制它的能力。一旦接受“系统必然会失效”这一事实,就有能力使系统对特定的失效做出相应的反应。正如汽车工程师创造出的碰撞缓冲区——为保护乘客而首先被撞毁的区域——你可以为系统创建一种安全失效模式,这种模式包含被损坏区域,并且为系统其他部分提供保护。这种自我保护决定了整个系统的韧性。

Chiles将这些保护措施称为“裂纹阻断器”。就像通过构建碰撞缓冲区来吸收撞击力并保护乘客安全,可以先确定系统的哪些特性是必不可少的,然后内建系统失效方式,防止重要特性出现裂纹。如果不设计系统失效方式,那么系统就会出现各种不可预测的问题,一旦出现,这些问题通常都是危险的。

3.4 阻止裂纹蔓延

现在来看看如何在第2章描述的航空公司的例子中应用系统失效方式。该航空公司的CF系统项目并未规划系统失效方式。裂纹始于对SQLException异常的处理失误,但在其他许多环节上能够阻止其蔓延。下面来看从底层细节到高层架构的一些例子。

在没有资源可用时,如果资源池阻塞请求资源的线程,那么最终所有请求资源的线程都会被阻塞(每台应用程序服务器实例都会遇到此事)。为了避免这个问题,可以将资源池配置为在可用资源被耗尽时能创建更多的连接资源;也可以将其配置为当所有连接资源被占用时,短暂地阻塞资源请求者。这两种方法都能阻止裂纹蔓延。

从调用链往上游再看一个级别,CF系统中的一个方法调用出现了问题,导致运行在其他主机上的应用程序在调用该方法时失效。由于CF系统将其服务公开为EJB,因此它使用RMI。默认情况下,RMI调用永远不会超时。换句话说,被阻塞的那些调用方会一直等待从CF系统的EJB中读取它们期待的响应。每个应用程序实例的前20个调用方都会收到异常。(准确地说,是SQLException异常,它先封装在InvocationTargetException异常中,继而封装在RemoteException异常中。)之后,这些请求就被阻塞了。

客户端本来可以在RMI的套接字上设置超时时间。例如,可以引入一个套接字工厂,在它创建的所有新套接字上调用Socket.setSoTimeout()设置超时时间。在某个时间点,CF系统也可以构建一个基于HTTP的Web服务来取代EJB。然后,客户端可以在其HTTP请求上设置超时时间,也可以通过编写调用代码解决问题,即允许抛弃被阻塞的线程,而不是让它们继续调用外部系统。因为上述措施都没有做到位,所以裂纹从CF系统蔓延到了所有使用它的系统。

在系统规模更大的情况下,CF系统服务器本身可以分隔出多个服务组。这样一来,其中一个服务组出现的问题就不会拖垮CF系统的所有用户。(在这个案例中,所有的服务组都会以同样的方式出现裂纹,但实际情况并非总是如此。)这是另一种阻止裂纹在企业系统中蔓延的方式。

从更大的软件架构视角审视,CF系统本可以通过请求-回复这样的消息队列方式来构建。在这种情况下,调用方知道可能永远不会收到回复,因此它必须将其作为处理协议本身的一部分来处理。更为根本的是,调用方可以在元组空间3里搜索符合相关标准的航班,CF系统则必须把航班记录信息存储在元组空间里。系统架构的耦合度越高,编程差错蔓延的机会就越大。相反,低耦合的架构可以起到减震器的作用,这能减少(而不是放大)编程差错的影响。

3“元组空间”即分布式共享内存的一种实现形式。——译者注

上述方法中的任何一种都可以阻止SQLException异常问题蔓延到航空公司的其他部门。可悲的是,设计师在创建共享服务时,并没有考虑到裂纹产生的可能性。

3.5 系统失效链

在每次系统事故的背后,都有一条由一个个事件构成的失效链。一个小问题会导致另一个问题,后者再导致下一个问题。若事后查看整个系统失效链,会发现系统失效似乎不可避免。如果试图估算失效链上所有事件都会发生的概率,会发现概率极低,但这仅限于将每个事件都视作独立事件。硬币没有记忆力,所以每次投掷它时,出现正反两面的概率都相同,并与以前的投掷无关。然而,导致失效的事件并不是相互独立的。一个点或一个层次的系统失效,实际上增加了其他点或其他层次发生系统失效的概率。如果数据库响应变慢,应用程序服务器更有可能耗尽内存。因为这些层次是耦合在一起的,所以这些事件并非彼此独立。

下面是可以用来精确描述系统失效链的一些常用术语。

失误 软件出现内部错误。出现失误的原因既可能是潜在的软件缺陷,也可能是在边界或外部接口处发生的不受控制的状况。

错误 明显的错误行为。当交易系统突然购买100亿美元的期货时,就明显发生了错误。

失效 系统不再响应。不同的人对系统失效有不同的定义……计算机已经通电,但不响应任何请求,这也是系统失效。

失误一旦被触发,就会产生裂纹。失误会变成错误,错误会引发失效。这就是裂纹的蔓延方式。

在系统失效链中的每一个环节,由失误导致的裂纹蔓延,可能会加速、减缓或停止。多度耦合且高度复杂的系统,会为裂纹提供更多的蔓延途径。在这样的系统中,错误更容易发生。

紧耦合会加速裂纹的蔓延。例如,EJB调用的紧耦合会让CF系统出现资源耗尽问题,并令其调用方出现更大的问题。将处理请求的线程与这些系统中的外部集成调用耦合,会造成远程系统出现的问题逐渐转变为系统停机。

若要准备好应对每一种可能出现的系统失效,一种方法是查看每个外部调用、每个I/O操作、每次对资源的使用和每个预期结果,并询问“这里都有可能出什么错”,同时思考可能出现的各种冲击和压力。

  • 如果不能进行初始连接怎么办?
  • 如果连接需要花10分钟怎么办?
  • 如果连接建立后又断开怎么办?
  • 如果连接建立后无法从另一端得到响应怎么办?
  • 如果响应用户查询需要花两分钟怎么办?
  • 如果需要同时处理一万个请求怎么办?
  • 当网络陷入蠕虫病毒攻击,应用程序尝试记录SQLException错误信息的日志时,如果磁盘已满怎么办?

以上假设只是冰山一角。除了关乎生死的关键系统和火星探测器,对其他任何系统来说,蛮力法显然都是不切实际的。但如果真的需要在未来10年内交付稳定的系统,那该怎么办?

技术社区在如何处理失误方面存在分歧。一个阵营表示要构建具有容错功能的系统。应该捕捉异常、检查错误代码,并且通常要防止失误演变为错误。另一个阵营则表示,以容错为目标是徒劳的。这就像试图制造具有防误操作的设备一样白费功夫,因为总会出现更傻的傻瓜。无论试图发现和消除什么失误,都会发生意想不到的事情。所以,应该任其崩溃并替换,这样就可以从已知的良好状态重新开始。

然而,两个阵营对以下两件事的看法是一致的。第一,失误总会发生,且永远无法杜绝,必须防止失误转变为错误。第二,即使在尽力防止系统出现失效和错误时,也必须决定承担失效或错误的风险是否利大于弊。后面章节会讨论一些通过创建“减震器”减轻这些压力的模式。

3.6 小结

生产环境中出现的每次系统失效都是独一无二的。没有两起事故会完全沿着同一条系统失效链发展:由相同的因素触发,具有相同的损坏情况,以相同的方式蔓延。然而,随着时间的推移,确实能发现一些系统失效模式。例如,某一方面存在脆弱性,这个问题会以那个方式放大。这些是稳定性的反模式,第4章将详细讨论。

如果系统失效存在模式,那么应该会有一些对症下药的常见解决方案。这些方案的确存在。第5章会讨论一些设计和架构模式,用以应对反模式。然而,这些模式都不能防止系统出现裂纹,其实任何办法都做不到。一些条件总是能引起裂纹。但是,这些模式能阻止裂纹蔓延,它们有助于遏制损害并保留部分可用功能,而不是让系统完全失效。

在到达阳光明媚的高地之前,必须先穿过阴暗的峡谷。让我们先看看会搞垮系统的那些反模式吧。

目录