提高应用程序可用性的5个要点
构建一个高可用、可伸缩的应用程序不是一件容易的事,也不会有天上掉下来的馅饼。问题总会以你从未预期的方式出现,让你精心设计的功能对所有用户都停止工作。
这些可用性问题通常会在你想不到的地方出现,甚至一些最严重的问题会来自最不可能出现的地方。
一次简单的图标故障
这是我亲身经历的一个因为忽视依赖故障的典型案例。我们的一个应用程序向用户提供了一个服务,为每个页面顶部提供一个自定义的图标,来表示当前登录的用户。这个图标由一个第三方系统负责生成。
有一天,这个生成图标的第三方系统发生了故障。我们的应用程序以为该系统总是会正常运行,因此并不知道如何处理这种情况。结果,我们的应用程序也跟着发生故障。仅仅因为生成图标这样一个非常小的“功能”出现故障,整个系统无法提供任何服务。
如何才能避免这样的问题呢?如果我们能够预料到第三方系统可能发生故障,就可以在设计过程中考虑到这个故障发生的场景,从而发现我们的应用程序也会随之发生故障。这样,我们就能添加一些逻辑来检查第三方服务,在问题发生时删除图标,或者在问题发生时捕获错误,避免它传递下去并影响页面的其他部分。
一次小小的检查和一些错误恢复机制,就可以帮助应用程序保持正常运行。否则,我们的应用程序就会经历严重的服务中断。
所有这一切都是因为缺少了一个图标。
没人能够预料到问题会在何处发生,也不可能依靠测试来发现所有这些问题。许多问题都是系统性的问题,而不仅仅是代码的问题。
为了发现这些可用性的问题,我们需要后退一步,系统地去了解应用程序的运行机制。以下是5个你可以关注并且应当关注的要点,它们能够帮助你的系统在规模增长的同时保证高可用性:
时刻考虑应对故障
时刻考虑如何伸缩
缓和风险
监控可用性
以可预期及明确的方式来处理可用性问题
让我们来详细讲解其中的每一个要点。
要点1:时刻考虑应对故障
正如Amazon的CTO Werner Vogels所说,“所有事情每时每刻都会失败”。你应当提前为应用程序和服务发生故障而做出计划。问题迟早会产生。不过现在,我们要讲的是如何解决它。
假设你的应用程序发生了故障,那么它是如何发生的?当你构建系统的时候,应当在设计和实现的方方面面都考虑可用性。
设计
你考虑过任何设计模式吗?你使用它们来帮助你提升软件的可用性了吗?
通过使用一些设计模式,例如捕获底层异常、重试逻辑和断路器,可以帮助你捕获错误并尽可能避免影响其他功能。这样,你就能够限制问题的影响范围,即使应用程序的某些部分出现问题,依然能够提供其他一些有用的功能。
依赖
如果你依赖的组件出现了故障,你会怎样做?如何进行重试?如果问题是一个无法恢复的(硬件)故障,你会怎样做?如果是一个可恢复的(软件)故障呢?
断路器模式在处理依赖故障时非常有用,因为它可以降低依赖故障对你的系统的影响。如果没有断路器,可能会因为依赖故障而降低系统的性能(例如,需要一个很长的超时机制来检测故障)。而使用了断路器,你可以“放弃”并停止使用某个依赖,直到你确认它已经恢复了正常工作。
用户
如果出现问题的原因是系统的某个用户,你会怎样做?你能够处理海量的请求吗?你能够限制海量的流量吗?你能够处理传入的垃圾数据吗?如果数据量非常大,你会怎样做?
有些时候,拒绝式服务可能来自“友方”。例如,可能会因为应用程序的用户看到一个临时活动,而导致增加大量请求。或者,用户程序中的一个bug,可能导致他们向你的应用程序拼命地发送请求。如果这样的事情发生了,你会怎样做?流量突增会让你的应用程序宕机吗?你能否检测出这种问题,并通过限制请求的速度来降低或者消除其影响吗?
要点2:时刻考虑如何伸缩
你的系统现在运行正常,并不意味着它明天还能够继续运行正常。大多数Web应用程序的流量都是在不断增加的。一个今天产生一定流量的网站,明天可能会产生远比你想象大得多的流量。当你构建系统时,不要只考虑当前的流量,还要考虑未来的流量。
具体一点,这可能意味着:
需要设计出能够增加数据库数量和容量的架构。
考虑限制数据伸缩的原因。当数据库达到容量极限的时候会发生什么?你需要确认这些限制因素并在到达极限之前解决它们。
应当能够很容易地添加额外的应用程序服务器。这通常需要仔细考虑在何处和如何来维护状态,以及流量是如何路由的。
将静态流量导向离线提供方。这样系统只需要处理必要的动态流量。使用外部的内容分发网络(CDN)不仅可以降低网络需要处理的流量,还能够利用CDN的伸缩效率将静态内容更快地分发给用户。
考虑是否可以静态生成一些动态资源。通常来说,看上去动态显示的内容实际上大多数是静态的,并且生成静态内容可以提高应用程序的可伸缩性。这种“应该静态的动态资源”有些时候隐藏在你想象不到的地方。
内容究竟应该是静态的还是动态的
通常,看上去是动态的内容实际上大多数是静态的。设想网站上常见的顶部导航栏,绝大多数时候,其中的内容都是静态的,但是偶尔也会出现一些动态的内容。例如,如果你没有登录,页面的顶部可能会显示“请登录”,如果你已经登录了则显示“你好,Lee”(当然前提是你的名称是Lee)。
这是否意味着整个页面都必须动态生成呢?显然不是。除了页面的登录/问候部分,其他部分都是静态的,通过CDN可以轻松地进行分发并节省你的计算资源。
当导航栏中大多数内容都是静态内容时,你可以在用户的浏览器中动态地将变更内容添加到页面上(例如,根据具体情况添加“请登录”或者“你好,Lee”的内容)。通过将这些动态数据进行分组,并与静态内容加以区分,可以提高Web页面的性能,降低应用程序需要处理的动态数据量。这样可以提高可伸缩性,并最终提高可用性。
要点3:缓和风险
保持系统高可用需要消除系统中的风险。当系统发生故障时,通常我们已经在这之前将故障原因确定为了风险。因此,确定风险是提高可用性的一个重要方法。所有的系统中都存在以下这些风险:
系统崩溃的风险
数据库崩溃的风险
返回结果不正确的风险
网络连接失败的风险
新部署的软件功能出现故障的风险
保持系统高可用需要消除风险。但是当系统变得越来越复杂时,消除所有风险也变得越来越不现实。要保持一个大型系统的高可用,更多的是管理系统的风险,知道这些风险是什么,哪些风险是可接受的,以及你能够做什么来缓和风险。
我们将之称为风险管理。我们会在本书的第9章着重讨论风险管理,它是构建高可用系统的核心内容。
风险管理中的一个部分是风险缓和。风险缓和指的是当问题发生时,我们知道如何去尽可能降低问题所带来的影响。缓和意味着即使当服务和资源不可用时,依然尽可能确保你的系统以最好的、最完整的状态工作。风险缓和需要考虑哪些事情可能会出错,并且立即制订相应的计划,以便当问题发生时能够提供相应的解决方案。
风险缓和—一个没有搜索功能的网上商店
假设有一个售卖T恤的网上商店。它是一个很常见的在线商店,你可以在它的首页上浏览T恤,跳转到其他页面查看不同的T恤分类,并且可以搜索指定风格和类型的T恤。
为了实现搜索的功能,这类网上商店通常需要调用一个独立的搜索引擎,这可能是一个单独的服务,或者由第三方的搜索服务提供。
但是,因为搜索功能是一个独立的功能,所以你的系统就存在搜索服务不可用的风险。你的风险管理计划需要确定出该问题,并且将“搜索引擎失败”列为风险之一。
如果没有风险缓和计划,当搜索服务失败时,可能会产生一个错误页面,或者返回不正确或无效的结果—不管怎样,它都会带来很差的用户体验。
这个示例中的风险缓和计划可能是这样的:
我们知道最受欢迎的T恤是红色条纹T恤,60%访问网站的用户最终都停留在(并很可能最后会购买)这个商品上。因此,如果搜索服务停止了,我们可以显示一个“很抱歉”的页面,下方显示最受欢迎的T恤列表,其中就包括红色条纹T恤。这会鼓励遇到这个错误页面的用户,继续浏览别人曾经感兴趣的T恤。
此外,我们还可以显示一个“下一次购买享受10%折扣”的优惠券,这样就可以鼓励之前没有进行购买的用户,在搜索服务恢复正常后,继续回到我们的网站上进行购买。
上面的示例演示了什么是风险缓和,而确认风险、确定该如何处理风险,以及如何实现这些缓和措施的过程则被称为风险管理。
风险管理经常会暴露应用程序中未知的、需要立即修复的问题,还可以用来处理已知的故障问题,减少故障恢复时间或者降低严重性。
可用性和风险管理息息相关。构建一个高可用的系统,主要就是要考虑如何管理风险。
要点4:监控可用性
除非你看到问题发生,否则你不会知道应用程序中存在着问题。你应当确保对应用程序进行了适当的监控,以便可以从外部和内部两个视角来观察应用程序的运行状况。
监控的程度取决于应用程序的特点和要求,但是通常必须具备以下这些监控。
服务器监控
监控服务器的健康状况,并且确保它们始终在有效运行。
配置变化监控
监控系统配置的变化,以便确定它们对应用程序的影响。
应用程序性能监控
深入了解你的应用程序和服务,确保它们按照预期运行。
人为测试
从用户的角度来实时检测应用程序的运行情况,以便在用户真正发现问题之前发现它们。
报警
当问题发生时通知相关人员,以便使问题可以得到快速有效的解决,将对用户的影响降到最低。
如今市面上有许多非常优秀的监控系统,包括免费的和付费的服务。我个人推荐New Relic,它提供了之前提到的所有监控和报警能力。作为一款软件即服务(SaaS)的软件,它能够支持任何规模的应用系统的监控需求。
当你对应用程序和服务进行监控之后,就可以开始寻找它们的运行趋势。当你明确了一定的趋势之后,可以开始寻找一些异常值,将它们作为可能存在的可用性问题。你可以利用这些异常值,在系统发生故障之前通过监控工具来发送警报。除此之外,还可以在系统增长过程中时刻进行跟踪,确保可伸缩性计划的实施。
你应当为服务间通信建立内部的、私有的运行目标,并持续对它们进行监控。通过这种方式,当出现任何与性能或者可用性相关的问题时,你都可以快速诊断出哪个服务或者系统出现了问题,并定位问题的原因。此外,你可以发现一些“热点”—性能超出预期的地方,以及针对这些问题制订相应的开发计划。
要点5:以可预期及明确的方式来处理可用性问题
如果你对监控中所发生的问题置之不理,那么监控系统就毫无用处。这意味着当问题发生时必须发出报警,这样你才能有所行动。除此之外,你应当建立整个团队都遵循的流程,帮助诊断问题,并轻松修复常见的故障。
例如,如果某个系统无法响应,你可能会有一系列措施来解决。这其中可能包括运行一个测试来诊断问题原因,重启一个已知会导致系统无法响应的守护进程,或者当其他手段都失败时重启整个服务器。为常见的故障问题提供标准化流程可以减少系统不可用的时间。此外,它们还可以提供更多有用的诊断信息,帮助工程师团队找到常见问题的根本原因。
当触发某个服务的报警时,该服务的负责人必须是第一个被通知到的。毕竟,他们负责修复自己服务中的所有故障。但是,其他与之紧密相关或依赖的团队也应当收到报警信息。例如,如果某个团队使用了一个特殊服务,他们希望知道该服务什么时候出现故障,从而在问题发生时能够更加主动地保证自己的系统不受影响。
这些标准的流程和办法应当被写进支持手册中,团队中每个负责人人手一份。这本支持手册还应该包含相关系统和服务的联系人列表,以及当无法用简单手段解决问题时,需要上报问题的联系人列表。现在有一些SaaS应用程序,可以自动化支持文档的管理和版本控制的工作,使得我们在事件进行中也可以使用它们。
所有这些流程、办法以及支持手册都应该提前准备好,以便当服务出现问题时,值班人员能准确知道如何在不同的情况下进行操作,以便快速恢复服务。这些流程和办法之所以非常有效,是因为故障通常都发生在一些不太恰当的时间点,例如午夜或者周末这些效率比较低的时间。这些建议可以帮助你的团队更聪明、更安全地将系统恢复到可运行状态。