关键要点
- 当今的分布式系统环境必须采用弹性软件设计。
- 关键挑战不在于编码,而在于“外围”。
- 做好分布式其实很难,大多数人都严重低估了它的难度。
- 恰当的功能设计是构建健壮的分布式系统的关键,但人们却知之甚少。
- 在公司中引入弹性软件设计的主要挑战是如何建立意识和可持续性。
写这篇文章的起因是我在2018年GOTO柏林大会上所做的演讲,我在演讲中分享了在进行弹性软件设计时会面临哪些挑战。我将简要地介绍弹性软件设计的“why”和“what”,中间部分是我近年来经常遇到的挑战,最后,我添加了一些关于如何在组织中实现弹性软件设计的最佳实践。阅读完本文之后,希望你能够更好地了解弹性软件设计所面临的挑战,并知道如何解决这些问题。
什么是弹性软件设计,为什么它很重要?
弹性软件设计(Resilient Software Design,简称RSD)是一个无法用一两句话解释清楚的概念——有时候很难向习惯于“电梯游说”(在很短时间内说清楚一个问题)的人解释这个概念。尽管如此,我仍然试着尽可能简短地解释我所知道的RSD,一个是“why”,一个是“what”。
让我先从“why”开始。通常,我们试图通过系统来实现某种商业价值:赚钱或者让客户满意。显然,只有当系统在生产环境中看用时,我们才有可能实现这一价值。这一直以来都是这样的。
但是,现在有所不同的是,几乎每个系统都是分布式的。系统之间相互通信,并且系统本身也可能被分成几个部分,而这些部分进一步被拆分,并以此类推。微服务、移动计算和物联网的发展使得协作系统之间的连接变得更加复杂,将这些系统的开发提升到新的水平。
系统及其部件之间相互交互所需的远程通信会引入一些故障模式,这些故障模式只存在于跨进程边界的情况,不会在进程内部出现。如果我们忽略它们的存在,这些故障模式,例如非响应性、等待时间、不完整或无序消息将在应用程序级别上引起所有类型的非预期故障。换句话说,如果你需要一个健壮高可用的系统,就不应该忽略分布式所带来的影响。
这让我想到了RSD的“what”:我倾向于将弹性软件设计定义为“在理想情况下,如果发生意外故障(由分布式的非确定性行为造成的),用户根本不会注意到,或者用户至少可以继续使用应用程序已定义范围的部分功能(即服务的优雅降级)”。
请注意定义中的“已定义”一词。这意味着,计划在发生意外情况时该做些什么,这个与你通常在不采用弹性软件设计时遇到的意外系统行为有很大不同。
弹性软件设计的任务
我的旅程通常从在代码级别设计和实现弹性开始。但随着时间的推移,我意识到,在代码层面实现弹性是最容易的部分,实际的挑战其实是在其他方面。
虽然在事后看来,这并不足为奇(大型项目的问题从来都不是在编码层面),但我不得不承认,我刚开始还是感到有些奇怪。你只有长期接触这个问题,才有可能清楚地了解这些挑战。因此,我在“弹性软件设计的7个任务”演讲中分享了我的一些经验,其中每个“任务”代表了我遇到过的一个挑战。以下是从我的演讲中挑选出来的挑战。
- 了解RSD的“业务案例”。作为一名软件工程师,如果你尝试开发新的东西,通常会被问及相关的业务案例是什么。虽然这是一个无可厚非的问题,但如果被应用在RSD上,就会导致方向跑偏,因为RSD不是为了赚钱,而是为了不损失钱。尽管如此,定义弹性预算仍然是有必要的,你可以看一下如果系统变得不可用会有哪些即时的损失,以及由于累积效应(例如感到厌烦的客户会增加你的流失率)导致的更频繁的不可用性造成的长期损失。通过这种方式,我们可以将RSD嵌入到一个理智的经济框架中。
- 了解分布式系统的非确定性行为及其后果。问题是分布式系统不仅难以驾驭,而且难以理解。远程通信为系统行为添加了概率性因素。不幸的是,我们的大脑无法轻易地处理概率性行为。此外,几乎所有的IT教育或培训都是以进程内交互为前提,在这些环境中,我们面临的是确定性的行为,导致大多数人对分布式不甚了解。我也不知道有哪些简单的分布式解决方案(我其实认为不存在这样的解决方案)。也许在我们的IT教育中添加更多与分布式系统相关的教材可能会有所帮助。数以百计的计算机科学论文已经清楚地告诉我们,进程内的东西一旦变成分布式的,即使再简单也会变得很难,甚至是不可能的。但并不是所有人会去阅读这些资料。然后有些人说,我们应该将远程通信问题留给基础设施去解决,让应用工程师不受干扰。过去的很多分布式框架都遵循了这一理念——事实证明,这只适用于非常小的系统,总体来说并不是可行的解决方案……
- 避免“100%可用”的陷阱。这与上一个挑战有关,但影响范围要小得多。由于典型的确定性思维,人们(无论是IT人员还是非IT人员)都倾向于假设他们连接的所有系统的可用性都可以达到100%。你可以在需求、设计和代码方面看到这种隐含的假设。并不是人们有不良意图,也不是他们粗心大意,只是他们忘记了一些东西。在分布式系统中,问题不在于系统是否会发生故障,而是会在什么时候发生故障。可用性始终会小于1(或者说小于100%)。因此,我们需要小心100%可用性的陷阱,并检查我们的要求、设计和代码,避免掉入这个陷阱。
- 建立OpsDev反馈闭环。在很多公司,我们仍然看到开发和运维之间存在巨大的屏障,甚至已经触及C级人员。造成这种局面有一定的原因,同时也给RSD带来了一个大问题。弹性需要在应用程序层面实现,也即在开发中。但是,你只能通过运维来衡量实际的弹性效果。此外,在运维中,你会检测到需要由开发来处理的应用程序缺陷。但是如果你在Dev和Ops之间有一堵大墙,就会破坏这个重要的反馈闭环。开发人员盲目地实施他们的弹性举措,而运维发现的问题却得不到修复。因此,无论你是使用DevOps或SRE等既定方法,还是使用自定义的方法,都需要建立反馈闭环。
- 正确的功能设计。如果分布式功能在设计方式上出了问题,那么即使再好的弹性措施也无法帮你构建出健壮的系统。问题出在这里:假设你在服务之间创建了紧密的功能依赖(“强耦合”)。如果被依赖的服务变得不可用,那么所有依赖这个服务的服务也将变得不可用,因为这些服务需要借助被依赖服务的业务逻辑来完成自己的任务。遗憾的是,我们学到的几乎所有与设计系统相关的技术,即如何划分功能,都会导致强耦合,因为它们侧重于进程内设计,而进程内设计的可用性不受这种耦合性的影响。因此,我们需要重新学习分布式系统的功能设计,重点是降低功能耦合——这与进程内的低耦合不同。我将在下一节深入探讨这个主题。
- 了解应该使用哪些模式以及如何合理地组合它们。在你学习完新模式后,总想着去用它们。问题是,使用弹性模式是要付出代价的。它们通常会增加实现和运维成本。更重要的是,它们增加了解决方案的复杂性,而复杂性是健壮性的敌人。解决方案越复杂,就越难以理解,意外故障给健壮性带来不利影响的可能性也就越大。因此,关键在于不要使用太多的模式,而是要在弹性和复杂性之间找到一个平衡点。
- 不要因为技术的更新换代每几年就淘汰积累起来的社区知识。这不是RSD特有的,它适用于整个IT行业。作为一个社区,我们倾向于每隔几年就将集体智慧淘汰,然后从头开始。我们倾向于忽视我们已经知道的东西,然后寻找下一个被炒作起来的银弹,以此来解决我们的问题,而不是像其他工程学科一样建立和维护经过验证的知识体系。同样,这不仅限于RSD,但我们现在可以观察它们,因为大多数RSD概念不是新东西,其中一些已经存在了几十年了。说实话,我也不知道该如何解决这个问题。因此,我能够想到的最好的事情是提醒人们要注意这个问题,并希望如果有足够多的人意识到这一点,最终将成长为一个真正的工程学科。
当然,还有其他更多的挑战,但从我的角度来看,这些是最应该引起我们注意的。
基于弹性软件设计创建更健壮的应用程序
根据我的经验,理解分布式系统以及如何进行好的功能设计是创建健壮应用程序的最大障碍。因此,让我们更深入地探讨这两个主题。
理解分布式故障模式的含义是非常困难的。单进程系统中一些很简单的事情到了分布式系统中有会变得非常困难,甚至是不可能的,而且基础设施无法为应用程序隐藏掉所有这些影响。另一方面,大多数(如果不是全部)大学和大学后的IT教育都是基于本地计算。此外,试图掌握分布的非确定性影响对我们的大脑来说并不是一件容易的事。
根据我的经验,IT领域之外的大多数人几乎不可能真正理解分布式,因为他们把计算机理解为“可以完成人类布置的任务的机器”,教会他们有关分布式计算的概念和所面临的挑战需要很长时间——我们通常没有那么多时间。
但其实对大多数开发人员来说也是非常困难的。开发人员在面对不可用的分布式系统时,他们通常也会不知所措。由于他们的IT教育完全忽略了分布式系统,他们甚至会避免处理与分布式有关的问题。这导致在设计和实现系统时忽略了分布式的影响,而这反过来让系统变得既脆弱又慢。
我之前说过,我不知道这个问题有什么简单的解决方案,我其实认为并不存在所谓的简单的解决方案。我能做的就是建议在我们的IT教育(大学期间和大学之后)中增加更多有关分布式系统设计的课程,因为我们的系统环境变得越来越分散,我们需要更好地了解设计和编码的实际效果。
另一个巨大的阻碍是功能设计。如果功能传播的方式是错误的,最终只会得到一个脆弱的系统。举个简单的例子:服务A接收外部请求,为响应该请求,它需要来自服务B的一些信息,这也就是所谓的在服务之间传播功能。如果服务B被关闭,服务A就无法响应外部请求。
这就是所谓的级联故障。弹性软件设计的主要任务之一是避免级联故障。通常,你可以使用简单的超时检测机制或断路器来检测服务B是否已关闭,然后回退使用服务A的备份计划。但由于功能在服务之间传播,不可能有备份计划,即断路器只会让级联故障可见,不会提供任何绕过它的方法。
这只是其中的一个例子,类似的情况还有很多。如果你将通常的“设计最佳实践”应用于分布式系统,通常会出现这类问题。在给定的示例中,服务B是“可重用服务”。可重用性是单进程的理想属性,但它也会带来非常强的耦合性,在跨进程的环境中表现出非预期的特性。
在过去,如果我们的功能设计出现错误,到最后系统会变得难以维护——这已经够糟糕的了。但是,在分布式系统中,糟糕的功能设计在运行时就会体现出脆弱性、不可靠和性能问题,这个更糟糕。问题是大多数有关如何做出“正确设计”的建议只适用于进程内设计。如果将这些建议应用在分布式系统上,大多数都无法正常工作。
我从过去的经历中学到的是,我们需要重新学习如何设计系统,即如何在分布式环境中传播功能。
然后,大多数人会提到领域驱动设计(或简称“DDD”),但根据我的经验,这也不是灵丹妙药。不要误会我的意思,实际上,DDD为更好的设计提供了很多非常好的建议。但是当谈到分布式系统的设计时,单靠DDD是不够的,它还缺少了一些额外的建议。好的方面是:据我所知,人们正在尝试扩展DDD的原始思想,加入分布式系统因素。因此,我对这一领域的未来发展非常期待。
发展构建弹性应用程序所需的技能
如果你想将RSD引入到你自己的公司,你可能会要求制定一个可以达到最佳效果的计划。根据我的经验,并不存在完美的计划。我的建议是实现通用的“意识—能力—可持续性”模式。
首先,了解为什么需要RSD以及如何将其传达给不参与软件开发的人员。这涉及理解和接受分布式系统的不可用性(包括避免“100%可用性”陷阱)和弹性软件设计的业务案例。此外,你还需要学习如何在不使用深奥的IT知识的情况下将其传达给人们。即使你知道需要通过RSD来构建健壮的系统,但却不能与你的经理或你的企业主讨论它,并帮助他们更好地理解这个主题,然后做出正确的决策,那么这一切将无济于事。
获得知识可能是最容易的部分。同时,还可以找到一些有关这个主题的资源和培训——只需要注意与分布式系统或微服务相关的会议的研讨会部分,或者从阅读文末参考部分提供的两本书开始。当然,你需要在工作中应用它们。再强调一下,做出正确的功能设计是一项艰巨的任务,但弹性模式本身相对容易学习和应用。
要建立可持续性,首先需要一个有效的OpsDev反馈循环。如果没有这种循环,任何弹性倡议都将注定失败,因为你的弹性度量在实践中缺少了反馈。
此外,你可能希望建立混沌工程计划。混沌工程不仅有助于揭示系统缺陷,它还通过持续、可控的学习(了解系统的健壮性并进一步提高系统健壮性)帮你实现可持续的弹性。
“混沌工程”这个词有点容易被误解,它不是为了制造混乱,而是为了避免混乱。在混沌工程中,你可以设计受控的实验,以便更好地了解系统的实际健壮性以及需要做出哪些额外的弹性措施。混沌工程师总是小心翼翼地控制实验潜在的影响范围,并在执行实验之前与所有受影响的人进行沟通。
它从一个假设开始,例如“如果我们切断与此服务器的连接,将发生自动故障转移,最终用户不会察觉到任何差异”。然后,与受影响的开发人员和运营人员讨论该假设。假设是有效的吗?我们怎么测试它呢?我们如何衡量正确性?如果我们错了,怎样才能以安全的方式停止实验?
在讨论和定义好所有内容之后,就可以进行实验。根据试验结果,可能需要定义(RSD)度量。除了帮你找出应用程序中之前未被发现的问题之外,混沌工程也会显著提高你对系统的信心——这是一种很好的感觉。
总结
总的来说,在今天的分布式系统环境中,RSD是一个必选项。虽然学习如何设计和实现弹性模式相对容易,但RSD所面临的实际挑战通常不在于编码方面。
分布式系统本身的复杂性和分布式系统的功能设计让实现可持续的弹性变得更加困难。同时,Ops和Dev之间缺少反馈循环、过于复杂的弹性设计,或者缺乏对RSD业务案例的理解,等等,通常都会带来阻碍。不过,了解挑战是成功掌握它们的第一步……
参考
- “Release It!”第2版,作者Michael T. Nygard,Pragmatic Bookshelf于2018年出版
- “Patterns for Fault Tolerant Software”,作者Robert S. Hanmer,Wiley于2007年出版
关于作者
Uwe Friedrichsen在IT领域有多年经验。作为codecentric(https://codecentric.rs/)的CTO和合伙人,他总是在寻找创新的想法和概念。他目前关注的领域是(分布式)系统设计、深度学习和未来的IT。通常,你可以在一些分享大会上看到他的身影。他也喜欢写文章、发推文,等等。
查看英文原文:https://www.infoq.com/articles/towards-resilient-software-design