现在,我们不断地赞美云原生cloud native架构(容器化和微服务),然而现实是大多数公司仍然运行单体系统。为什么?这不是因为我们非常不时尚,而是因为分布式是非常困难的。尽管如此,它仍然是创建超大规模的、真正弹性的和快速响应的系统的唯一途径,因此我们必须围绕它进行整合。
在这篇文章中,我们将介绍分布式系统中一些障碍以及人们应对方法。忘记康威定律(Conway’s Law),分布式系统遵循的是墨菲定律:“任何可能出错的地方都会出错。
在分布式系统的大规模上看,统计不是你的朋友(事后诸葛亮)。你所拥有的任何服务器实例越多,其中一个或多个当机的可能性就越高。而且极可能在同一时间。
在你收到警报邮件之前,服务器已经当机,网络将会丢失数据包,磁盘将失败,虚拟机将意外终止。
有一些在单体架构中的保证在分布式系统中就不再会得到保障。组件(现在的服务)不再以可预测的顺序启动和停止。服务可能意外重新启动,更改其数据库状态或版本。结果是,没有服务可以对另一个服务进行假设 - 系统不依赖于1对1的通信。
许多从故障中恢复的传统机制可能会使分布式环境恶化。强力重试可能会使您的网络被洪水般数据包淹没,备份恢复也并不简单。过去解决所有这些问题的设计模式,都需要重新思考和测试。
如果没有错误,分布式系统会很容易。乐观主义会造成对安全的错觉。分布式系统的设计必须具有弹性,能够容纳接受所有可能的发生错误,而不影响日常业务。
这里通讯会失败
在不可靠(即分布式)系统中,传统应用程序消息传递有两种高级方法:
- 可靠但缓慢:保存每条消息复制副本,直到您确认职责链中的下一个进程已经为此承担全部责任。
- 不可靠但快速:将多个复制副本发送给潜在的多个接受人,并允许消息丢失和重复。
我们在这里讨论的可靠和不可靠的应用级通信与网络可靠性(例如TCP与UDP)是不同。想象一下,两个通过TCP直接发送消息(比如RPC通讯)的无状态服务。即使TCP是可靠的网络协议,这也不是可靠的应用级通信。任何服务都可能会丢失并丢失正在处理的消息,因为无状态服务不能安全地保存正在处理的数据。(banq注:这是针对同步的RPC框架,比如国内的Dubbo或谷歌的gRPC)
我们可以通过在每个服务之间放置有状态的队列来使此设置应用程序级别可靠,以保存每个消息,直至其完全处理(banq注:引入消息队列)。这样做的不足之处在于它会慢一点,但是我们可能很乐意与之相处,因为如果它使生活更简单,特别是如果我们使用可管理的有状态的队列服务时,那么我们就不必担心规模和弹性问题。
可靠的方法是可预测的,但会涉及到延迟(延迟)和复杂性:大量确认消息和弹性保存数据(状态),直到您已经从职责链中的下一个服务确认完成了他们已经承担责任。
一个可靠的方法却不能保证快速的传递,但它确保所有的消息将最终至少一次传递。在每个消息至关重要且不能容忍丢失(例如信用卡交易)的环境中,这是一个很好的方法。AWS简单队列服务(Amazon的托管队列服务)是以可靠方式使用状态服务的一个例子。(banq注: Apache kafka提供类似正好一次的有效一次传递也是适用类似信用卡之类的交易)
第二种情况是,使用不可靠的方法可以实现端对端通讯得更快(比如RPC同步方式),但这意味着服务通常不得不期待重复和无序消息,并且一些消息将丢失。当消息是时间敏感的(即,如果他们不迅速采取行动,就不值得采取行动)或稍后的数据只是覆盖早期的数据,这种情况下可能会使用不可靠的通信。对于非常大规模的分布式系统,可以使用不可靠的消息传递,因为它的开销小且要快得多。然而,微服务设计却需要处理应对消息的丢失和重复。
在上述每种情况方法中,存在许多变量(例如,有保证和不保证的顺序性),所有这些变量需要在速度、复杂性和故障率方面进行不同的权衡。
一些系统可以同时使用上述多种方法,这取决于正在发送的消息的类型甚至系统上的当前负载。如果您有很多行为不同的服务,就很难正确恰当使用这些方法。需要在其API中明确定义服务的行为。为您的系统中的服务进行约束或推荐的通信行为的定义通常是有意义的,以获得一定程度的一致性。
现在时间是几点?
在分布式系统中没有这样的常见的所谓全球时钟。例如,在团体聊天中,我的评论和我的朋友在澳大利亚、哥伦比亚和日本发表的评论的出现将不会遵循严格顺序先后出现。没有任何保证机制保证我们看到的都是相同的时间表 - 虽然总有一个顺序,但是前提是我们有段时间先不说话。
基本上,在分布式系统中,每台机器都有自己的时钟,整个系统没有一个正确的时间。机器时钟可能会进行同步,但是即使在同步时传输时间也会不同,物理时钟也会以不同的速率运行,所以一切都会立即失去同步。
在单个机器上,一个时钟可以为所有线程和进程提供通用的时间。在分布式系统中,这在物理上都不可行。
在我们的新世界中,时钟时代不再提供无可置疑的顺序定义。在微服务世界中并不存在“什么时候”的单一概念,所以,我们的设计不应该依赖于服务间消息。
真相就在那里?
在分布式系统中,没有全局共享内存,因此没有单一版本的真相。数据将分散在不同物理机器上。此外,任何指定的数据在机器之间更可能处于相对较慢和无法访问的传输中,而不像在单体架构下的情况。因此,真正运行情况需要基于当前的当地的信息。
这意味着系统的不同部分的运行情况并不总是一致的。在理论上,它们最终应该在整个系统中传播消息时变得一致,但是如果数据不断变化,我们可能永远不会达到完全一致的状态,除非关闭所有新的输入和等待。因此,服务必须处理这样一个事实,即他们相互调用时可能会因为自己的问题而获得“旧”的或者不一致的信息。
说话快点!
在一个单体的应用程序中,大多数重要的通信发生在一个组件和另一个组件之间的单个进程中。流程内部的通信非常快,所以很多内部消息的传递不是问题。但是,一旦将单体组件拆分成单独的服务,通常会在不同的机器上运行,那么事情变得越来越复杂。
假设你知道如下背景知识:
- 在最好的情况下,将消息从一台机器发送到另一台机器比将内部从一个组件传递到另一个组件时要花费大约100倍的时间。
- 许多服务使用基于文本的RESTful消息进行通信。RESTful消息是跨平台的,易于使用,读取和调试,但传输速度慢。相比之下,与二进制消息协议配对的远程过程调用(RPC)消息不是人类可读的,因此更难调试和使用,但传输和接收速度要快得多。通过RPC方式发送消息的速度快20倍,比如gRPC相对RESTful而言。
在分布式环境中的结果却是:
- 你应该发送更少的消息。您可以选择在分布式微服务器之间发送的消息数量少于在单件中的组件之间发送的消息量,因为每个消息都会引入延迟(即延迟)。
- 考虑更有效地发送消息。对于您发送的内容,您可以通过使用RPC而不是REST来传输消息来帮助您的系统运行得更快。或者甚至就使用UDP并手工自己处理不可靠性。(banq注:RPCC通讯属于通讯快但不可靠类型)
状况报告?
如果您的系统可以次秒级(时间上短于1秒)速度更改,这是动态管理的分布式架构的目标,那么您需要以这种速度了解问题。许多传统的日志工具并不是为了跟踪这种情况而设计的。你需要确保能使用它们。
测试破坏
了解您的分布式系统是否正常工作并从不可预测的错误中恢复的唯一方法是:持续攻克这些错误并持续修复系统。Netflix使用Chaos Monkey(混沌猴)随机造成故意的崩溃测试。您的系统的弹性和完整性是需要测试的,同样重要的是,测试您的日志记录,以确保如果发生错误,您可以追溯地诊断和修复它 - 即使您的系统已经恢复在线运行。
这听起来很困难 我一定需要吗?
创建分布式、可扩展的、有弹性的系统是非常困难的,特别是对于有状态的服务(服务需要写数据库保存变动的数据)。现在是决定是否需要它的时候了。你的客户需求是否可以容忍慢一点响应还是小型规模系统?这样,您可以先设计一个更小、更慢、更简单的系统,并在构建专业知识同时逐步增加更多的复杂性。
像AWS,Google和Azure这样的云计算提供商也正在开发和推出这些大部分功能,特别是弹性状态(托管的消息队列和数据库)。这些服务似乎是昂贵的,但构建和维护复杂自己的分布式服务也是昂贵的。
任何虽然可能限制您但是会处理这些复杂性的框架(如Linkerd或Istio或Azure的服务架构)是非常值得考虑的。
关键的挑战是不要低估建立正确的弹性和高度可扩展的服务的难度。如果决定你真的需要它,那么全面教育大家,引入有用的约束,逐渐做好一切,并期待挫折和成功。