阅读 294

从0到1搞懂分布式架构:Uber大型支付系统构建经验总结


作者|Gergely Orosz
编译 & 编辑|Debra
AI 前线导读:本文介绍了 Uber 支付系统重建过程中,有关分布式系统 SLA、一致性、数据持久性、消息持续性、幂等性等方面的考量和注意事项。

更多干货内容请关注微信公众号“AI 前线”,(ID:ai-front)

两年前,我以一名略懂后端的移动软件工程师身份加入了 Uber,负责开发该应用的支付功能,并最终重写了整个应用

https://eng.uber.com/new-rider-app/。 随后我转向工程管理 http://blog.pragmaticengineer.com/things-ive-learned-transitioning-from-engineer-to-engineering-manager/, 负责团队本身的管理工作。这意味着需要更多地接触后端,因为支付环节涉及到的很多后端系统都是我的团队负责的。

入职 Uber 之前,我对分布式系统几乎全无任何经验。作为传统计算机科学毕业生,十多年来我一直在从事全栈软件开发,然而虽然很擅长画架构图并讨论各种权衡,但我对诸如一致性、可用性或幂等性等分布式概念并没有太多了解。

本文我将总结自己在构建大规模高可用分布式系统(Uber 所用的支付系统)过程中学习和应用的一些心得体会。这个系统需要处理每秒高达数千次的请求,同时就算系统的一些组件故障,也要保证某些关键支付功能依然正常运转。我要说的内容足够全面吗?未必!但至少这些内容让我的工作变得前所未有得简单。接下来,就一起看看这些工作中不可避免会遇到的 SLA、一致性、数据持久性、消息持续性、幂等性之类的概念吧。

SLA

对于每天需要处理数百万事件的大型系统,几乎不可避免会遇到问题。在正式开始规划整个系统前,我发现更重要的是确定怎样的系统才算是“健康”的。“健康”应该是一种真正可以衡量的指标。衡量“健康”与否的一种常见做法是使用 SLA:服务级别协议。而我用过的一些最常用的 SLA 包括:

可用性:服务处于正常运转状态的时间所占比率。虽然每个人都想拥有一个具备 100% 可用性的系统,但这一点往往很难实现,同时也极为昂贵。就算 VISA 卡网络、Gmail 及互联网服务提供商这样的大型关键系统也不可能在长达一年的时间里维持 100% 可用性,系统可能会停机数秒、数分钟或数小时。对很多系统来说,四个九的可用性(99.99%,即每年约停机 50 分钟 https://uptime.is/)

已经足够高了,而通常这样的可用性也需要在背后付出大量工作。准确性:系统中部分数据不准确或丢失,这种情况可以接受吗?如果可以,那么可接受的最大比率是多少?我所从事的支付系统必须确保 100% 准确,意味着数据决不能丢失。容量:系统预计要为多大规模的负载提供支持?这通常是用每秒请求数衡量的。

延迟:系统要在多长时间内做出响应?95% 的请求及 99% 的请求会在多长时间内获得响应?系统通常会收到很多无意义的请求,因此 p95 和 p99 延迟 https://www.quora.com/What-is-p99-latency 更能代表实际情况。为何说 SLA 对大型支付系统至关重要?我们发布一个新系统,要取代一个老的系统。为了确保工作有价值,新的系统必须比上一代“更出色”,而我们要使用 SLA 来定义自己的各种预期。可用性是最重要的要求之一,一旦确定了目标,就需要考虑架构中的各项权衡,以此来满足自己的目标。

水平和垂直缩放

假设使用新系统的业务数量开始增长,那么负载将只增不减。在某一刻,现有配置可能将无法支撑更多负载,需要扩容。垂直缩放和水平缩放是目前最常用的两种缩放方式。

水平缩放旨在给系统中增加更多计算机(节点),借此获得更多容量。水平缩放是分布式系统最常用的缩放方式,尤其是为集群增加(虚拟)计算机通常只需要点击按钮即可完成。

垂直缩放可以理解为“买一台更大 / 更强的计算机”,或者换用内核更多、处理能力更强、内存更大的(虚拟)计算机。对于分布式系统,通常不会选择垂直缩放,因为相比水平缩放这种做法更贵。然而一些大型网站,例如 Stack Overflow 就曾成功地进行了垂直缩放并完满达成目标

(https://www.slideshare.net/InfoQ/scaling-stack-overflow-keeping-it-vertical-by-obsessing-over-performance)。

为何说缩放策略对大型支付系统很重要?尽早决定,就可以着手构建能够水平缩放的系统。虽然一些情况下也可以进行垂直缩放,但我们的支付系统已经在运行生产负载了,而最初我们就很悲观地认为,哪怕一台极为昂贵的大型机也无法应对当前的需求,更不用提未来的需求了。我们团队还有工程师曾在大型支付服务公司任职,他们当时曾试图用能买到的最高容量的计算机进行垂直缩放,只可惜最终还是失败了。

一致性

对任何系统来说,可用性都很重要。分布式系统通常会使用可用性不那么高的多台计算机构建。假设我们的目标是构建具备 99.999% 可用性(每年停机约 5 分钟)的系统,但我们所使用的计算机 / 节点,可用性平均仅为 99.9%(每年停机约 8 小时)。为了获得所需可用性,最简单的办法是向集群中添加大量此类计算机 / 节点。就算某些节点停机了,其他节点依然可以正常运行,确保系统的整体可用性足够高,甚至远高于每一个组件的可用性。

一致性对高可用系统很重要。如果所有节点可以同时看到并返回相同的数据,那么就认为这个系统具备一致性。上文曾经说过,为了实现足够高的可用性,我们添加了大量节点,那么不可避免也要考虑到系统的一致性问题。为了确保每个节点具备相同信息,它们需要相互发送消息,确保所有节点保持同步。然而相互之间发送的消息有可能没能成功送达,可能会丢失,一些节点可能不可用。

我大部分时间都用来理解并实现一致性。目前有多种一致性模型(https://en.wikipedia.org/wiki/Consistency_model),分布式系统最常用的包括强一致性(Strong Consistency https://www.cl.cam.ac.uk/teaching/0910/ConcDistS/11a-cons-tx.pdf)、 弱一致性(Weak Consistency https://www.cl.cam.ac.uk/teaching/0910/ConcDistS/11a-cons-tx.pdf) 和最终一致性(Eventual Consistency http://sergeiturukin.com/2017/06/29/eventual-consistency.html)。 Hackernoon 有关最终一致性和强一致性 (https://hackernoon.com/eventual-vs-strong-consistency-in-distributed-databases-282fdad37cf7) 对比的文章非常清晰实用地介绍了需要在这些模型之间进行的权衡。一般来说,一致性要求越低,系统速度就越快,但也越有可能返回并非最新状态的数据。

为何一致性对大型支付系统很重要?系统中的数据必须保持一致。但要如何实现一致?对于系统的某些部件,只能使用强一致的数据,例如为了知道某个支付操作是否已经成功发起,这种信息就必须以强一致的方式存储。但对于其他部件,尤其是非关键业务部件,最终一致通常是一种更合理的做法。例如在显示历史行程时,使用最终一致的方式实现就足够了(也就是说,最新一次行程在短时间内可能只会出现在系统的某些组件中,这样,相关操作就可以用更低延迟或更小资源占用的方式返回结果)。

数据持久性

持久性(https://en.wikipedia.org/wiki/Durability_%28database_systems%29) 意味着一旦数据成功放入存储,那么以后将一直可用,就算系统中的节点下线、崩溃或数据出错,已存储的数据依然不应受到影响。

不同的分布式系统可以实现不同程度的持久性。一些系统会在计算机 / 节点层面实现持久性,一些则会在集群层面实现,而也有一些系统本身并不提供这样的能力。为了提高持久性,通常会使用某种形式的复制操作:如果数据存储在多个节点中而一个或多个节点故障了,数据依然可以保证可用。这里有一篇很棒的文章(https://drivescale.com/2017/03/whatever-happened-durability/) 介绍了为何分布式系统中的持久性那么难实现。

为何说数据持久性对支付系统很重要?对于诸如支付等系统中的很多组件来说,任何数据都不能丢失,任何数据都是至关重要的。为了实现集群层面的数据持久性,需要使用分布式数据存储,这样就算有实例崩溃,依然可以持久保存完整的事务。目前,大部分分布式数据存储服务,例如 Cassandra、MongoDB、HDFS 或 Dynamodb 均支持多种层面的持久性,并且都可以通过配置实现集群层面的持久性。

消息持续性和持久性

分布式系统中的节点需要执行计算操作,存储数据,并在节点之间发送消息。对于所发送的消息,一个重要特征在于这些消息的传输可靠度如何。对于关键业务系统,通常需要保证绝对不会有任何一条消息丢失。

对于分布式系统来说,通常会使用某种分布式消息服务来发送消息,例如可能会使用 RabbitMQ、Kafka 等。这些消息服务可以支持(或通过配置可支持)不同层面的消息传输可靠性。

消息持续性意味着如果处理消息的某个节点出现故障,那么在故障解决完毕后,依然可以继续处理之前未完成的消息。消息持久性通常则主要用于消息队列层面(https://en.wikipedia.org/wiki/Message_queue) ,在具备持久的消息队列情况下,如果发送消息的过程中队列(或节点)脱机,那么可以在重新上线后继续发送这些消息。关于该话题建议阅读这篇文章(https://developers.redhat.com/blog/2016/08/10/persistence-vs-durability-in-messaging/) 。

为何说消息持续性和持久性对大型支付系统很重要?因为一些消息丢失的后果是没有人能承担的,例如乘客针对行程发起支付所产生的消息。这意味着我们所使用的消息系统必须是无损的:每条消息都需要发送一次。然而构建一种能够将每条消息严格发送一次的系统,以及构建一种将每条消息至少发送一次的系统,这两种系统在复杂度上有着天壤之别。我们决定实现一种可以确保至少发送一次的持久消息系统,并选择一种消息总线,以此为基础开发我们的支付系统(最终我们选择了 Kafka,并针对该系统设置了一个无损集群)。

幂等性

分布式系统不可避免会出错,例如连接可能中途断开,或者请求可能超时。客户端通常会重试这些请求。幂等的系统确保了无论遇到任何情况,无论某个具体的请求被执行了多少遍,最终针对该请求的执行只进行一次。付款过程就是一个很好的例子。如果客户端发起付款请求,请求已经执行成功了,但客户端超时,客户端可能会重试同一个请求。对于幂等的系统,用户并不会付费两次;但如果是不幂等的系统,很可能就会了。

设计一个幂等的分布式系统需要运用一些分布式锁定策略,而早期的一些分布式系统概念也正是源自于此。假设要通过乐观锁定(Optimistic Locking)实现一个幂等的系统,以避免产生并发更新。为了实现乐观锁定,系统需要实现强一致,这样在执行操作时我们就可以使用某种类型的版本控制机制查看是否已经发起了另一个操作。

取决于系统本身的约束以及操作类型,幂等的实现方式有很多。幂等方法的设计过程充满了挑战,Ben Nadel 曾撰文(https://www.bennadel.com/blog/3390-considering-strategies-for-idempotency-without-distributed-locking-with-ben-darfler.htm) 介绍过他用过的不同策略,这些策略都用到了分布式锁或数据库约束。在设计分布式系统时,幂等也许是最容易被忽略的问题之一。我曾遇到过很多情况因为没能给某些关键操作实现正确的幂等,而导致整个团队焦头烂额。

为何说幂等性对大型支付系统很重要?最重要的一点在于:避免重复收费或重复退费。考虑到我们的消息系统选择了至少一次的无损传递,我们需要确保哪怕所有消息都被传递多次,但最终结果必须保证幂等。我们最终决定通过版本控制和乐观锁定,并为系统使用强一致的数据源,借此为系统实现所需的幂等行为。

分片和仲裁

分布式系统通常需要存储大量数据,数据量远超单一节点的容量。那么如何用特定数量的多台计算机存储一大批数据?此时最常见的做法是分片(Shardinghttps://en.wikipedia.org/wiki/Shard_%28database_architecture%29) 。

数据将使用某种类型的哈希进行水平分割并分配到不同的分区。虽然很多分布式数据库自带数据分片功能,但数据分片依然是个很有趣,值得深入学习的话题,尤其是有关重分片(https://medium.com/@jeeyoungk/how-sharding-works-b4dec46b3f6) 的技术。Foursquare 在 2010 年曾因遭遇分片上限遇到长达 17 小时的停机,针对此次事件的根源,有一篇很不错的事后分析文章(http://highscalability.com/blog/2010/10/15/troubles-with-sharding-what-can-we-learn-from-the-foursquare.html) 告诉了我们来龙去脉。

很多分布式系统的数据或计算工作需要在多个节点上复制,为确保所有操作均能以一致的方式完成,还需要定义一种基于投票的方法,在这种方法中,只有超过某一数量的节点获得相同结果后,才认定操作已经成功完成。这个过程叫做仲裁。

为何说仲裁和分片对 Uber 的支付系统很重要?分片和仲裁,这些都是很常用的基本概念。我本人是在研究如何配置 Cassandra 的复制时遇到这些概念的。Cassandra(以及其他分布式系统)会使用仲裁(https://docs.datastax.com/en/archived/cassandra/3.x/cassandra/dml/dmlConfigConsistency.html#dmlConfigConsistency__about-the-quorum-level) 以及本地仲裁来确保整个集群的一致性。但这也导致了一个有趣的副作用,在我们的几次会议中,当已经有足够多的人抵达会议室后,就会有人问:“可以开始了吗?仲裁结果如何?”

参与者模式

用于描述编程实践的常用词汇,例如变量、接口、调用方法等,全部都基于只有一台计算机的假设。但对于分布式系统,我们需要使用一种不同的方法。在描述此类系统时,一种最常见的做法是使用参与者模式(Actor Model https://en.wikipedia.org/wiki/Actor_model ),用通信的思路来理解代码。这种模式很流行,并且也很贴合我们思考时的心智模型。例如在描述组织中的人们相互通信的具体方法时。此外还有一种流行的分布式系统描述方法:CSP - 交谈循序程序(https://en.wikipedia.org/wiki/Communicating_sequential_processes) 。

参与者模式中,多名参与者相互发送消息并对收到的消息做出响应。每个参与者只能执行有限的操作,例如创建其他参与者,向其他参与者发送消息,决定针对下一条消息要采取的操作。借此通过一些简单的规则,就可以很好地描述复杂的分布式系统,并能在一个参与者崩溃后实现自愈。如果想进一步了解这个话题,建议阅读 Brian Storti(https://twitter.com/brianstorti) 撰写的 10 分钟了解参与者模式一文(https://www.brianstorti.com/the-actor-model/) 。

目前很多语言都已实现了参与者库或框架(https://en.wikipedia.org/wiki/Actor_model#Actor_libraries_and_frameworks, 例如 Uber 就在某些系统中使用了Akka toolkit(https://doc.akka.io/docs/akka/2.4/intro/what-is-akka.html)。

为何说参与者模式对大型支付系统很重要?我们有很多工程师联手打造这个系统,很多人在分布式计算方面有丰富的经验。因此我们决定在工作中遵照某种标准化的分布式模型以及相应的分布式概念,以便尽可能利用现成的车轮。

响应式架构

在构建大型分布式系统时,目标通常在于使其更具适应性、弹性以及缩放性。无论支付系统或其他高负载系统,模式都是类似的。很多业内人士已经发现并分享了各种情况下的最佳实践,响应式(Reactive)架构则是这一领域最流行,应用最广泛的。

如果要了解响应式架构,建议阅读响应式宣言一文(https://www.reactivemanifesto.org/) 并观看这段 12 分钟的视频(https://www.lightbend.com/blog/understand-reactive-architecture-design-and-programming-in-less-than-12-minutes)。

为何说响应式架构对大型支付系统很重要?我们在构建新支付系统时使用的 Akka 工具包就受到了响应式架构的巨大影响。我们的很多工程师也很熟悉响应式方面的最佳实践。遵循响应式原则,构建具备适应性和弹性,由消息驱动的响应式系统,这也成了一种自然而然的做法。这样一种可以回退并检查进度的模型,在我看来很实用,以后开发其他系统时我也会使用这样的模型。

总结

Uber 的支付系统,能够参与到这样一个大规模、分布式、关键业务系统的重建工作,我觉得自己很幸运。在这样的工作环境中,我掌握了大量以往根本不了解的分布式概念。通过本文的分享,希望能为他人提供一些帮助,帮助大家更好地从事或继续学习分布式系统知识。

本文主要专注于这类系统的规划与架构。在构建、部署,以及高负载系统间的迁移和可靠的运维等方面,还有很多重要工作。有机会再另行撰文介绍吧。


更多干货内容请关注微信公众号“AI 前线”,(ID:ai-front)


关注下面的标签,发现更多相似文章
评论
说说你的看法