分布式系统的复杂性是工程师和开发人员面临的一项重要挑战。随着系统的发展,复杂性往往会增加,因此积极主动很重要。让我们谈谈您可能会遇到哪些类型的复杂性,以及在工作中处理它的有效策略。
分布式系统和复杂性
在开发中,分布式系统是相互连接并处理单个任务的计算机网络。每个计算机或节点都有自己的本地内存和处理器,并运行自己的进程。但是,他们使用通用网络进行协调和集中化。分布式系统非常可靠;一个组件的故障不会中断整个网络。
在集中式计算系统中,一台具有一个处理器和一个内存的计算机用于解决问题。在中心化系统中,有节点,但它们访问中心节点,这可能会导致网络拥塞和速度缓慢。集中式系统具有单点故障 - 这是它的一个重要缺点。
复杂性
复杂性可以从不同的角度和方面来定义。有两个主要定义很重要。
在系统理论中,复杂性描述了系统的不同独立部分如何相互作用和相互通信:它们如何定义彼此之间的相互作用,它们如何相互依赖,它们有多少依赖关系,以及它们如何在整体中相互作用。
从软件和技术的角度来看,复杂性是指软件架构的细节,例如组件的数量。
整体架构
单体架构是集中式系统的一个很好的例子。它表示为单个可部署组件和单个可执行组件。例如,此类组件可能包含用户界面和位于一个地方的不同模块。
尽管这种架构是用于构建软件的传统架构,但它有几个重要的缺点:
无法独立扩展模块
更难控制日益增长的复杂性
缺乏模块独立部署
维护庞大的代码数据库具有挑战性
技术和供应商耦合
微服务架构
微服务架构是一种架构风格,也是面向服务的架构的变体,它将系统构建为松散耦合服务的集合。例如,公司、客户、客户和 UI 表示为部署在多个节点上的单独流程。
所有这些服务都有自己的不时共享数据库,但这可能是一种不好的做法或反模式。
这种架构有一些优点。
水平可扩展性是游戏规则的改变者!可以水平扩展数据库,也可以水平扩展服务。从技术上讲,任何基础设施组件都可以通过克隆进行水平扩展,但必须解决许多挑战。
高可用性和容错性:每当您有多个克隆时,您可以组织一些技术,以帮助您避免在崩溃、内存泄漏或断电时出现任何停机时间。
地理分布:如果我们在美国、欧洲或亚洲都有客户,并且我们也想为客户带来最佳体验,我们需要将这些服务分布到世界各地,并组织更复杂的数据复制技术。
技术选择:您可以自由选择解决方案。
质量属性
任何系统在某种程度上都具有三个主要的质量属性:
可靠性:尽管面临挑战,仍能继续正常运行,这意味着具有容错能力或弹性;即使系统现在可靠运行,也不能保证未来的可靠性。性能下降的常见原因是负载增加:例如,系统可能已从 10,000 个并发用户扩展到 100,000 个,或者从 100 万个扩展到 1000 万个。
可伸缩性是我们用来描述系统处理增加负载的能力的术语。需要注意的是,整个系统的可伸缩性弱点是由其最弱的组件决定的。
可维护性是为了让需要使用系统的工程和运营团队的生活更美好。良好且稳定的抽象有助于降低复杂性,并使系统更容易修改和适应新的使用功能。
主要问题是什么?
“任何可能出错的事情都会出错,而且是在最糟糕的时候。”
— 墨菲定律
不可靠的网络
网络不可靠的原因有很多,例如:
您的请求可能已丢失。
您的请求可能在队列中等待,稍后将送达。
远程节点可能已发生故障(可能已崩溃或已关闭电源)。
远程节点可能已暂时停止响应。
远程节点可能已处理您的请求,但响应已在网络上丢失。
远程节点可能已处理您的请求,但响应已延迟,稍后将交付。
策略:超时
最简单的解决方案是在调用方端应用超时逻辑。例如,如果调用方在超时后未收到响应,则只会引发错误并向用户显示错误。
策略:重试
在大规模上,我们不能只是为每个网络问题抛出异常,让用户感到不安或延迟系统执行。因此,如果响应指示出现问题,请重试。但是,如果请求由服务器处理,并且只有响应丢失,该怎么办?在这种情况下,重试可能会导致严重的后果,例如多个订单、付款、交易等。
策略:幂等
为了避免这种情况,我们可以利用一种名为幂等的技术。
幂等性的概念与多次执行同一动作与仅执行一次具有相同的效果的概念有关。为了实现 exactly-once 语义的属性,可以采用一种解决方案,将幂等键附加到请求。在重试具有相同幂等密钥的同一请求时,服务器将验证是否已处理具有此类密钥的请求,并仅返回先前的响应。因此,使用相同密钥进行任意次数的重试都不会对系统的行为产生有害影响。
策略:熔断机制
另一种可能有助于防止过载并在发生故障时完全压垮服务器的模式是断路器。
断路器充当代理,以防止正在维护的呼叫系统可能出现故障,或者现在严重故障。它可能出错的原因有很多:内存泄漏、代码中的错误或外部依赖项出错。在这种情况下,最好快速失败,而不是冒着级联故障的风险。
并发和丢失写入
并发性是分布式系统中最复杂的挑战之一。并发意味着同时发生多个计算。
因此,当尝试从不同的操作中同时更新帐户余额时会发生什么?在缺乏防御机制的情况下,极有可能出现竞争条件,这将不可避免地导致写入丢失和数据不一致。在此示例中,有两个操作尝试同时更新帐户余额。由于它们是并行运行的,因此最后一个完成的人获胜,从而导致一个重大问题。为了规避这个问题,可以采用各种技术。
策略:快照隔离
ACID 首字母缩略词代表原子性、一致性、隔离性和耐久性。所有常用的 SQL 数据库都实现这些属性。
Atomicity 指定操作将完全执行或失败,无论它发生在哪个阶段。它允许我们确保另一个线程看不到操作的半成品结果。
一致性意味着在成功提交事务和更改状态之前,所有不变量都已定义并得到满足。
ACID 意义上的隔离意味着并发执行的事务彼此隔离。有一个可序列化的隔离级别,它是按顺序处理所有事务的最严格的级别,但主要使用流行数据库中的另一个称为快照隔离的级别。
持久性承诺,一旦交易提交,所有数据都会被安全存储。
此级别的关键思想是,数据库跟踪记录的版本,并且无法提交已在当前事务之外修改的事务。
策略:比较和设置
大多数 NoSQL 数据库在选择支持 BASE 时不提供 ACID 属性,其中此类数据库进行比较并使用集合。此操作的目的是通过仅当值自上次读取以来未更改时才允许更新发生来避免丢失更新。如果当前值与之前读取的值不匹配,则更新不起作用,必须重试读取-修改-写入循环。
例如,Cassandra 提供了轻量级事务,允许您利用各种 、 和条件来防止并发问题。IFIF NOT EXISTSIF EXISTS
策略:租赁
另一个潜在的解决方案是租赁模式。为了说明这一点,请考虑必须以独占方式更新资源的方案。租约模式需要首先获取具有资源到期期限的租约,然后更新它,最后返回租约。
如果发生故障,租约将自动过期,允许另一个线程访问资源。尽管此技术非常有益,但存在进程暂停和时钟不同步的风险,这可能会导致并行资源访问问题。
双重写入问题
双重写入问题是分布式系统中出现的一个挑战,特别是当多个数据源或数据库必须保持同步时。为了说明这一点,请考虑一个场景,其中必须将新数据存储在数据库中,并将消息发送到 Kafka。由于这两个操作不是原子操作,因此在发布新消息时可能会失败。
如果在发送消息时尝试事务,则结果是更成问题的情况。如果事务提交失败,外部系统可能已经收到实际上并未发生的更改的通知。
策略:事务发件箱
一个潜在的解决方案是实现 事务发件箱。这涉及将事件存储在与操作本身相同的事务中的“OutboxEvents”表中。由于进程的原子性,在事务失败时不会存储任何数据。
另一个必要的组件是 Relay,它定期轮询 OutboxEvents 表并将消息发送到目标。这种方法允许实现至少一个交付保证。然而,这不是一个问题,因为由于网络的不可靠性,所有消费者都必须是幂等的。
策略:对数尾矿
构造自定义事务发件箱的另一种解决方案是利用数据库事务日志和自定义连接器直接从此日志读取并将更改发送到目标。
这种方法有其自身的优点和缺点。例如,它需要与数据库解决方案耦合,但允许在应用程序中编写更少的代码。
不可靠的时钟
时间跟踪是任何软件或基础设施的一个基本方面,因为它可以强制执行超时、到期和收集指标。然而,时钟的可靠性在分布式系统中是一个重大挑战,因为时间的准确性取决于单个计算机的性能,这些计算机的时钟可能比其他计算机更快或更慢。
计算机使用的时钟主要有两种类型:时间时钟和单调时钟。时间时钟根据特定日历返回日期和时间,并且通常与网络时间协议 (NTP) 同步。但是,延迟和网络问题可能会影响同步过程,从而导致时钟不同步。单调时钟不断前进,使其适合测量持续时间。
但是,单调增加的值对于每台计算机是唯一的,这限制了它们在多服务器日期和时间比较中的使用。实现高精度时钟同步是一项具有挑战性的任务。在大多数情况下,这种解决办法的必要性并不明显。但是,在遵守法规需要使用的情况下,可以使用精确时间协议,尽管这将需要大量投资。
可用性和一致性
CAP 定理假设任何分布式数据存储只能满足三个保证中的两个。但是,由于网络不可靠性不是可以显着影响的因素,因此在网络分区的情况下,唯一可行的选择是在可用性或一致性之间进行选择。
考虑两个客户端从不同节点读取数据的场景:一个从主节点读取,另一个从从节点读取。复制配置为在更改领导者后更新追随者。但是,如果由于某种原因,领导者停止响应会发生什么?
这可能是崩溃、网络分区或其他问题。在高可用性系统中,必须分配一个新的领导者,但是我们如何在现有的追随者之间进行选择呢?为了解决这个问题,必须采用 分布式共识算法。但是,在深入研究该算法的细节之前,必须全面了解各种类型的一致性。
一致性类型
有两类主要的一致性用于描述保证。
弱一致性,或最终为弱一致性,意味着如果您停止对领导者进行更改,则数据将在一段时间后同步到所有关注者上。
强一致性是一种属性,可确保系统中的所有节点同时看到相同的数据,而不管它们正在访问哪个节点。
策略:分布式共识算法(例如,Raft)
回到领导者崩溃时的问题,需要选举新的领导者。乍一看,这个问题看起来很容易,但实际上,在选择适当的方法时,必须考虑许多条件和权衡。
根据 Raft 协议,如果追随者在指定的时间段内没有收到来自领导者的数据或心跳,则开始新的领导者选举过程。每个复制单元(单体写入节点或多个分片)都与一组 Raft 日志和 OS 进程相关联,这些日志和 OS 进程维护日志并将更改从主节点复制到从节点到后续节点。
Raft 协议保证追随者以领导者生成日志记录的相同顺序接收日志记录。只要有一半的追随者确认收到提交记录并将其写入 Raft 日志,就会在领导者上提交用户事务。
策略:从领导者那里阅读
一种可能有效且简单的策略是,由刚刚保存新数据的用户从关注者那里读取,以避免复制滞后。
从单体架构到微服务,每种方法都有其自身的优势和挑战。虽然单体架构提供了简单性,但它们经常在可扩展性和可维护性方面遇到困难,这促使开发人员转向更加模块化和可扩展的微服务架构。
讨论的核心是复杂性的管理,它以各种形式表现出来,从网络不可靠性到并发问题和双重写入问题。超时、重试、幂等和断路器等策略为降低与不可靠网络相关的风险提供了有效的工具,而快照隔离、比较和设置以及租约等技术则解决了并发和丢失写入的挑战。
此外,时钟不可靠这一关键问题凸显了分布式系统中精确时间同步的重要性,解决方案范围从NTP同步到精确时间协议。此外,CAP 定理提醒我们可用性和一致性之间的固有权衡,因此需要对 Raft 等分布式共识算法有透彻的了解。
总之,掌握分布式系统中的复杂性需要多方面的方法,将理论知识与实践策略相结合。通过采用这些策略并不断适应 不断变化的分布式计算环境,工程师和开发人员可以自信地驾驭复杂性,确保其系统的可靠性、可扩展性和可维护性,以应对不断变化的挑战。