雨中
来 Uber 工作之前,我几乎没有分布式系统的工作经验。我的背景是一个传统的计算机科学学位和十年的全栈软件开发。然而,虽然我能够画架构图并讨论折中方案,但我对分布式的相关概念(一致性,可用性或者幂等性)了解的并不多。
在本文中,我总结了一些我认为在构建大规模,高可用分布式系统(为 Uber 提供底层支持的支付系统)时必须学习和应用的概念。这是一个每秒负载高达数千个请求的系统,其中关键的支付功能必须能够正确的工作,即使整个系统的某些部分出现故障。本文会是一个完整的清单吗?应该不会。但如果我早点知道这些概念的话,我的工作和生活会轻松很多。因此,就让我们开始来了解诸如 SLA,一致性,数据持久化,消息持久化,幂等性以及其他一些我在工作中需要学习的东西吧。
SLA
对大型系统来说,每天需要处理百万级别的事件,因此必可避免的会出现问题。在深入设计一个系统前,我发现最重要的事情是确定什么是一个健康的系统。系统的健康度应该是可衡量的,常用的方法是 SLA:服务等级协议(Service Level Agreements)。我见过的一些最常见的 SLA 有:
- 可用性:服务正常运行时间的百分比。虽然拥有一个 100% 可用的系统的想法很诱人,但实现这个目标是非常困难的,而且费用高昂。即使像 VISA 信用卡网络,Gmail 或者互联网提供商这样的大型和关键系统也达不到 100% 的可用性,多年来,它们也会停机几秒钟,几分钟或者几小时。对于许多系统来说,四个九的可用性(99.99%,即大约每年有 )就被认为是高可用的,通常为了达到这个水平就要花费不少的工作。
- 准确性:表示在系统中是否允许某些数据不准确或者丢失?如果是,可接受的百分比是多少?对于我从事的支付系统,准确性要求是 100%,这意味着不允许丢失任何数据。
- 负载能力:系统预期能够支持多少负载?这通常以每秒请求数来表示。
- 延迟率:系统应该在多长时间内做出响应?95% 的请求和 99% 的请求的响应时间是多少?系统通常有大量的噪声请求,因此,对现实系统而言更加实用。
构建大型支付系统时 SLA 为什么很重要呢?我们建立一个新系统,并用来取代现有的系统。为了确保我们构建了正确的系统,需要保证新系统比旧系统更好。这时我们就可以使用 SLA 来定义期望值。可用性是最高要求之一。一旦确定了可用性目标,我们就需要在设计架构时为了满足这一目标作出折中的选择。
水平扩展和垂直扩展
假设使用新系统的业务不断增长,负载会随着不断增加。在某个时间点,现有的配置将无法支持更多的负载,需要增加更多的系统容量。这时有两种最常用的扩展策略:水平扩展和垂直扩展。
水平扩展指的是向系统中增加更多的机器/节点,以增加系统总体容量。水平扩展是最流行的分布式系统扩容方法,尤其是向集群中添加(虚拟)机器通常简单到只需要在网页上点击一下按钮。
为什么构建大规模的支付系统时,系统扩展策略至关重要呢?我们很早就决定建立一个可水平扩展的系统。虽然在某些情况下垂直扩展是可能的,但由于我们的支付系统已经处于预估的负载,我们对单台昂贵的大型机在今天这种情况下能否支撑它持悲观态度,更不用说将来了。我们团队中也有工程师曾经在大型支付供应商工作过,他们曾试图在当时能够买到的大型机上进行系统的垂直扩展,但以失败告终。
一致性
任何系统的可用性都是很重要的。分布式系统通常建立在具有较低可用性的机器上。假设我们的目标是建立一个有 99.999% 可用性的系统(大约每年 5 分钟时间不可用)。我们使用的机器/节点有平均 99.9% 的可用性(大约每年 8 小时时间不可用)。一个简单的达到我们目标可用性的方法是把一批机器/节点添加到一个集群中。即使集群中一些节点出现故障,也有其他的节点可用,系统总体的可用性将比单个节点的可用性更高。
一致性在高可用系统中是一个关键问题。如果集群中所有节点同时看到并返回相同的数据,则系统是一致的。回到之前的模型,我们通过添加一组节点来获得更高的可用性,这时确保系统保持一致性并不是一件微不足道的事情。为了确保每个节点具有相同的数据,它们需要互相发送消息,以保持之间的数据同步。但是发送到对方的消息可能无法到达,它们可能丢失,或者有些节点可能不可用。
为什么构建一个大型支付系统时一致性很重要呢?系统中的数据需要保持一致,但到底多一致呢?对于系统中某些部分,只有强一致性的数据才行。例如知道用户付款操作是否已经开始是需要以强一致性的方式存储下来的。对于其他不是关键业务的部分来说,最终一致性被认为是合理的权衡。一个好的例子是列出最近交易这个功能,这种可以以最终一致性方式实现(也就是说,最近一次交易可能只会在一段时间后才在集群中某些节点中显示出来,作为回报,查询操作将以较低的延迟或者耗费较少资源的方式返回)。
数据持久化
image为什么构建大型支付系统时数据持久化很重要呢?对于系统的大部分功能来说,是不允许数据丢失的,因为数据是非常关键的,例如支付功能。我们构建的分布式数据存储需要支持集群级别的数据持久化:这样即使集群中有实例崩溃,已完成的交易依然会被持久化。目前大多数分布式数据存储服务,如 Cassandra,MongoDB,HDFS 或 Dynamodb 都支持不同级别的数据持久化,并且都可以通过配置提供集群级别的持久化。
消息持久化
分布式系统中的节点负责执行计算,存储数据和相互间发送消息。消息发送的一个关键特性是消息的可靠性。对于业务关键性系统,通常要求消息零丢失。
对于分布式系统,消息传递通常由某些分布式消息服务完成,例如 RabbitMQ,Kafka 等。这些消息服务可以支持(或者通过配置支持)不同级别的消息传递可靠性。
image为什么构建大型支付系统时消息持久化至关重要呢?因为我们系统存在不能丢失的消息,例如消费者为他们的乘车付款的消息。这意味着我们使用的消息系统必须是无损的:每条消息都必须传递一次。但是构建一个每条消息只传递一次的系统,和构建一个每条消息至少传递一次的系统,这两者复杂度是不同的。我们决定实现一个消息至少传递一次的持久化消息系统,并选择一个消息总线,并将在此基础上构建它(我们最终选择了 Kafka,为此案例配置了消息无损的集群)。
幂等性
分布式系统往往存在出错的可能性,例如连接中断或请求超时等。客户端通常会重试这些请求。幂等系统能够确保无论特定请求执行多少次,该请求的实际执行只发生一次。一个很好的例子就是付款,如果客户端发出付款的请求,请求成功但客户端超时了,客户端可能会重试相同的请求。对于幂等系统,付费的人不会被两次扣款,对于非幂等系统,则会发生两次扣款操作。
设计幂等的分布式系统需要某种分布式锁定策略。这是一些早期分布式系统概念发挥作用的地方。假设我们打算通过乐观锁来实现幂等性,以避免并发更新。为了获得乐观锁,系统必须是强一致性的,这样在操作时,我们可以使用某种版本控制来检查是否已经有另外一个操作正在进行。
为什么构建大型支付系统时幂等性很重要呢? 最重要的是:避免双重收费或双重退款。 鉴于我们的消息系统至少有一次无损传递,我们需要假设所有消息可能多次传递,但系统需要确保幂等性。 我们选择通过版本控制和乐观锁来处理这个问题,让实现幂等行为的系统使用强一致性存储作为其数据源。
分片和 Quorom
许多分布式系统具有跨多个节点复制的数据或者计算。为了确保以一致的方式执行这些操作,定义了基于投票的方法,其中一定数量的节点需要获得相同的结果,以使操作成功,这称为 Quorum。
Actor 模型
为什么构建大型分布式系统时 actor 模型很重要呢?我们有很多工程师在一起开发系统,其中很多人有分布式的经验。我们决定遵循一个标准的分布式模型,而不是我们自己提出一个分布式模型概念,从而可能导致重新发明轮子。
响应式架构
在构建大型分布式系统时,目标通常是弹性可扩展。可能这是一个支付系统,或者是另外一个高负载系统,但这样做的模式可能是类似的。业内人士一直在发现和分享这些情况下能够良好运行的最佳实践,而其中响应式架构在这个领域是一种流行且广泛应用的模式。
为什么构建大型支付系统时,响应式架构很重要呢? Akka,我们用于构建大部分新支付系统的工具包,就深受响应式架构的影响。我们在开发这个系统的很多工程师也熟悉响应式的最佳实践。遵循响应式原则:建立一个响应的,弹性的且基于消息驱动的系统,因此这对我们来说非常自然的。我发现它的好处在于拥有一个可信赖的模型,并检查进度是否处于正确的轨道上,我将继续使用这个模型来构建以后的系统。
总结
我很幸运的参与了对 Uber 的支付系统这样一个高可扩展,分布式且关键的系统的重建。通过在这种环境中工作,我学到了很多以前没有使用过的分布式概念。通过本文的总结,希望能够有助于其他人开始或者继续对分布式系统的学习。
本文重点关注这些系统的设计和架构,关于在高负载系统之间构建,部署和迁移以及可靠的操作它们,还有很多东西要说。但所有这些都是另一篇文章的主题了。