超越分布式事务
该文是Salesforce的软件架构师Pat Helland于2016年12月发表的针对其在2007年CIDR(创新数据库研究会议)上首次发表的同名文章的更新和缩写版本。他曾经发表“不变性改变一切”。
业界谈到分布式事务通常指两段提交2PC事务(Spring/JEE中JTA等)或者Paxos与Raft,这些事务都有其明显缺点和局限性,Pat Helland在本文讨论的是另外一种基于本地事务情况下的事务机制,它是基于实体和活动(Activity)的概念,其实类似DDD聚合根和领域事件的概念,这种工作流类型事务虽然需要程序员介入,依靠消息系统实现,但可以实现接近无限扩展的大型系统。Pat文中提出了重要的观点:“如果你不能使用分布式事务,那么你就只能使用工作流。”
以下是Pat Helland的Life Beyond Distributed Transactions这篇著名文章的翻译:
事务是非常强大的机制,我在它上面花费了我40年职业生涯中的大部分时间。1982年,我首先在Tandem NonStop系统上实现事务机制。这个系统有一个每年平均故障时间,包括跨地理位置的分布式的两阶段提交,为强一致性事务提供了极好的可用性。
新的创新,其中包括谷歌的Spanner,提供强一致性环境,在非常大的规模基础上提供了卓越的可用性。构建一个以支持高度可用的分布式事务应用程序是卓越的创新和重大的挑战。不幸的是,这个产品不能被广泛使用。
在大多数分布式事务系统中,单个节点的失败会导致事务提交失败。这反过来导致应用程序被卡住。在这样的系统中,系统越大,系统越可能停机。当一个飞机需要所有发动机都必须工作时才能飞行,增加发动机只会降低飞机的可用性。如果没有特殊的机制来容忍中断,那么在数千个节点上运行分布式事务系统是不切实际的。当应用程序开发人员使用非高度可用的分布式事务构建系统时,解决方案很脆弱,必须丢弃。自然选择就是放弃它(放弃两段提交事务)...
相反,应用程序在没有提供事务性保证前提下仍能满足其业务需求。
本文探讨并在拒绝传统分布式事务的世界中实施大规模任务关键型应用程序时使用的一些实用方法。包括管理细粒度的应用程序数据,随着应用程序的增长,这些应用程序数据可能会随着时间的推移重新分区。包括一些设计模式用来支持在这些可重新分区的数据之间发送消息。
这里的目标是减少人们手工制作大型可扩展应用程序所面临的挑战。另外,通过观察这些设计模式,业界可以通过创建平台来更轻松地开发可扩展的应用程序。最后,尽管本文的目标是同类性质可扩展的应用程序,但这些技术对于支持可伸缩的异构应用程序(如支持移动设备)也非常有用。
目标
本文重点介绍如何在只有本地数据库或本地事务系统时构建成功的可伸缩企业应用程序。可用性不是重点,主要瞄准规模和正确性。
1.可扩展应用程序的讨论
大多数可扩展应用程序的设计者都了解业务需求。问题在于事务和可扩展系统交互的问题,只有概念和抽象却没有名称,也没有清晰的理解。本文的一个目标是启动一个讨论,以提高对这些概念的认识,为可扩展的程序带来一套通用的术语和一致的方法。
2.考虑应用程序的几乎无限缩放
文章提出了一个关于几乎无限缩放的影响的非正式的思考实验。也就是说可以允许客户,可购买商品实体,订单,发货,生病患者,纳税人,银行账户以及所有其他业务概念的数量随着时间不断显着增长,这就是无限扩展的定义。通常情况下,每件事情都不会太大; 只是数量越来越多。如果CPU,DRAM,存储或其他资源首先饱和,那并不重要。在某种程度上,需求的增加会导致在一台机器上运行程序需要在大量的机器上运行。这个思想实验使我们考虑数十或数十万台机器。
几乎无限的缩放是一种松散的,不精确的,刻意的无定形的方式,它激发人们需要非常清楚何时何地可以知道某个机器上装有什么东西,以及如果无法确保它适合一台机器,该怎么办。此外,您想要与数据和计算负载几乎线性地缩放。当然,用一个大的日志以N-log-N的速度进行缩放将会很好。
3.描述可扩展应用程序的一些常见模式
几乎无限的扩展对业务逻辑有什么影响?我断言缩放意味着在编写程序时使用称为实体的新抽象。一个实体一次只能在一台机器上运行,而应用程序一次只能操纵一个实体。几乎无限缩放的结果是,这种程序化的抽象必须暴露给业务逻辑的开发者。
通过命名和讨论这个尚未命名的概念,我们也许可以商定一致的方案方法,并对构建可扩展系统所涉及的问题有一个一致的理解。
此外,实体的使用对用于连接它们的消息传递模式有影响。这导致了状态机的创建,这些状态机不一致性包括无辜的应用程序开发人员试图为业务问题构建可伸缩解决方案时所产生的消息传递不一致。
4.一些假设
考虑以下三个假设,假设这些基于经验是真实的,这些假设是没有道理的。。
(1)应用层面和规模不可知论
我们首先假设每个可伸缩应用程序至少有两层:
代码高层 ---> 感知规模的API ----> 代码下层
这些层在缩放的感知上有所不同。他们可能有其他的区别,但这些与这个讨论无关。
该应用程序的下层理解是:更多的计算机被添加扩大系统规模。除了其他工作之外,它还管理上层代码到物理机器及其位置的映射。下层是可以识别的,因为它理解这个映射。我认为下层为上层提供了一个与尺度无关的编程抽象。有很多与规模无关的编程抽象的例子,包括MapReduce。
使用这种与规模无关的编程抽象,编写应用程序代码的上层而不用担心缩放问题。通过坚持与规模无关的编程抽象,您可以编写应用程序代码,而不必担心在部署日益增加的负载时发生的变化。
随着时间的推移,这些应用程序的底层可能会演变成一个新的平台或中间件,从而简化了与规模无关的API的创建。
(2)事务范围
在分布式系统上提供高度一致的事务的概念方面已经做了许多学术工作。这包括2PC(两阶段提交),1个 Paxos和最近的Raft。 经典2PC在一台机器发生故障时会阻塞,除非事务协调员和参与者本身是容错的,比如Tandem NonStop系统。Paxos和Raft不会因为节点故障而阻塞,但会像Tandem的系统一样进行额外的协调工作。
这些算法可以被描述为在分布式系统上提供强一致的事务。他们的目标是允许任意原子更新传播到许多机器上的数据。更新存在于跨越多台机器的单个事务范围中。
不幸的是,在许多情况下,这不是应用程序开发人员的选择。应用程序可能需要跨越信任边界,不同平台以及不同的运营和部署区域。当你对分布式事务“说不”时会发生什么?
即使在今天,在这篇论文写了10年之后,真正的系统开发人员也很少尝试在超过几台计算机上实现强大的一致性事务。相反,他们承担多个独立的事务范围。每台电脑都是一个独立的范围,内部有本地事务。
(3)大多数应用程序使用至少一次消息
如果你是一个短暂的Unix风格的进程,TCP / IP是非常棒的,但考虑一个应用程序开发人员面临的困境,他的工作是处理消息并修改数据库中相应的一些持久数据。该消息已被消费但尚未确认。只有数据库被更新后消息才被确认。如果发生故障,则重新启动并重新处理消息。
这个困境来源于这样的事实:除了通过应用动作之外,消息传递不是直接耦合到持久数据的更新。虽然可以将消息的消费与持久数据的更新结合起来,但这并不常见。两者紧密耦合的丧失会导致失败窗口的消息被传递多次。为了防止丢消息,消息传递至少一次(banq注:Apache kafka在1.0之后提供了Stream的紧耦合更新,不同数据源需要不同Stream插件)。
这种行为的后果是应用程序必须容忍消息重试和无序传递。
一些意见分析
撰写评论文章的好处是可以表达狂野的意见。这里有一些这样的文章。
(1)可扩展的应用程序使用唯一标识的实体
该意见认为,每个应用程序的上层代码必须处理一个称为实体的数据集合。单个实体的大小没有限制,除了必须在单个事务范围内(即一台机器)生存。
每个实体都有一个唯一的标识符或键,实体键可以是任何形式,它必须唯一标识一个实体及其包含的数据。
对实体的陈述没有任何限制。它可能被表示为SQL记录,XML,JSON,文件或其他任何东西。一种可能的表示形式是SQL记录的集合,可能跨越许多表,其主键的实体键作为其前缀。
实体表示不相交的数据集。每个数据只在一个实体中。
一个应用程序由许多实体组成。例如,订单处理应用程序封装了许多订单,每个订单都由一个唯一的订单ID标识。要成为可扩展的订单处理应用程序,来自一个订单的数据必须与其他订单的数据不相交(banq注:主键ID通过全局分布式主键生成器唯一分配)。
(2)原子事务不能跨越实体
每台计算机被假定为一个单独的事务范围。本文稍后介绍原子事务不能跨实体的论点。程序员必须始终确保每个事务中数据包含在的单个实体中(banq注:有些类似DDD的聚合根实体概念)。
从程序员的角度来看,唯一标识的实体就是事务范围。这个概念对用于设计可缩放的应用程序具有强大的影响。需要探讨的一个含义是,在设计几乎无限的缩放比例时,可能不能保持事务一致。
(3)消息被分配给实体
大多数消息传递系统不考虑数据的分区,而是针对无状态进程使用的队列。标准做法是在消息中包含一些数据,通知无状态应用程序代码在哪里获取所需数据。这是实体的关键。实体的数据由应用程序从某个数据库或其他持久存储中获取。
一些有趣的趋势正在发生。首先,这组实体的大小正在变得比适合一台计算机的大。每个单独的实体通常适合一台计算机,但是它们的集合不一样。越来越多的无状态应用程序正在路由以基于某种分区方案来获取实体。
其次,抓取和分区方案正被分成应用程序的下层。这是故意与负责业务逻辑的上层隔离的。
该模式通过使用实体键进行路由来有效地定位实体。无状态的Unix风格的进程和应用程序的低层都只是为业务逻辑提供的与规模无关的API的实现的一部分。上层规模不可知的业务逻辑简单地通过唯一标示将消息定位到某个实体。
(4)实体管理每个合作伙伴状态(活动)
与规模无关的消息能够在实体之间有效的传递。发送实体以其持久状态显示,并由其实体键标识。它发送一个消息给另一个实体,并通过它的实体唯一键来识别它。实体接收者由与规模无关的上层业务逻辑和表示其状态的持久数据组成。这是通过它的实体键来标识的。
回想一下这样的假设:消息至少被传递一次。接受者实体可能有些困扰,因为必须忽略的冗余重复消息。在实践中,消息分为两类:影响实体状态的消息和不影响状态的消息。不影响实体状态的消息很容易 - 它们是幂等的。改变状态的消息需要更多的关注(banq注:改变状态的消息一般称为事件)。
为了确保幂等性(即保证重试消息的处理是无害的),接收者实体通常被设计为记住消息已经被处理。成功处理后,重复的消息通常会生成新的另一个响应以表示该消息重复。
接收到的消息的实体创建了基于伙伴为基础的状态。每个状态只能存在每个实体中,
术语“活动activity ”这个词语适用于管理双方关系,会对关系中每个伙伴状态有影响的消息。每个活动都是精确地生活在一个实体中,一个实体有一个为其伙伴实体准备的活动。(banq注:这个活动是一种业务活动,业务工作流)
除了管理消息传递方式之外,还可以使用活动来管理松散耦合的协议。在一个原子事务不可能的世界里,尝试性的操作被用来进行谈判以获得共同的结果。这些都是在实体之间执行的,由活动管理。
建立工作流程以达成一致是充满挑战的,这些挑战在其他地方都有详细记录。本文并没有断言活动解决了这些挑战,而是为针对解决这些挑战而存储的状态奠定了基础。
几乎无限的缩放规模要求竟然导致了出人意料的细粒度的工作流风格的解决方案。
参与者都是实体,每个实体使用有关其他实体的特定知识来管理其工作流程。在实体内维护的双方知识被称为活动。
活动的例子有时是微妙的。订单应用程序将消息发送到发货应用程序。它包括货运ID和发送订单ID。消息类型可用于激发运送应用程序中的状态更改,以记录指定的订单已准备好出货。通常情况下,实施者不会设计重试,直到出现错误。应用程序设计师偶尔也会考虑活动的计划。
实体
本节更深入地研究实体的性质。
1.不相交的事务范围
每个实体都被定义为一个拥有一个唯一标识的数据集合,这个标识只存在于一个事务范围内。原子交易可能总是在一个实体内完成。(banq注:类似DDD中的聚合根实体)
2.唯一键的实体
应用程序上层的代码自然是围绕具有唯一键的数据集合来设计的。客户ID,社会安全号码,产品SKU和其他唯一标识符可以在应用程序中看到。它们被用作查找应用程序数据的键。交易原子性的保证只能在由唯一标识的实体中。
3.重新分配和实体
之前提到的假设之一是新兴的上层是规模不可知的,下层决定了随着规模的变化,部署如何演变。随着部署的演变,特定实体的位置可能会发生变化。应用程序的上层不能对实体的位置做出假设,因为这不会成比例。
实体可使用Hash散列或基于键范围分配策略进行跨区分配。
比如key是ABC ABZ DEF FAW在A区服务器 FXQ GHI JKL KZU LMN在B区服务器,PAZ PJH TVA UTV ZZZ在C区服务器。
4.原子事务和实体
在可扩展的系统中,您不能假设在这些不同实体之间实现更新修改的事务。每个实体都有一个唯一的标识,每个实体很容易放入一个事务范围。回想一下这样的前提:几乎无限的缩放会导致实体的数量不可避免地增加,但是个体的大小仍然足够小以适应事务范围(即一台计算机)。
你怎么知道两个独立的实体保证在同一个事务范围内,因而可以自行在自己事务中原子更新?只有当全局只有一个唯一的key主键时,你能知道它真的是只有一个实体!
如果Hash散列用于实体键的分区,则不知道具有不同键的两个实体何时落在同一个服务器内。如果键范围分区用于实体键,大部分时间相邻键值会驻留在同一台机器上。偶尔会遇到不幸,而你的邻居会在另一台机器上。
一个简单的测试案例,通过键范围分区中的邻居之间的计算原子性通常会成功。后来,当重新部署将实体移动到机器上时,潜在的错误就出现了。更新不再是原子的。你永远不能指望在同一个地方居住的不同的实体键值(不要期望实体固定在哪个服务器内被创建)。
简而言之,应用程序的底层将确保每个实体键(及其实体)驻留在一台机器上。不同的实体可能在任何地方。(banq注:DDD中仓储工厂模式就是负责一个个聚合根实体的创建)
规模不可知的编程抽象必须具有将实体作为原子性边界的概念。理解实体,使用实体关键字以及明确承诺实体之间缺乏原子性是规模无关的编程必不可少的知识。
大规模的应用程序悄悄地在今天的行业中已经做到这一点; 只是没有一个实体的概念的名称。从上层应用程序的角度来看,它必须假定实体是代表一个事务的范围。假设部署变化时会产生更多的中断。
5.考虑候选索引
我们习惯于使用多个键或索引来处理数据。例如,有时一个客户被社会安全号码(有时是信用卡号码,有时是街道地址)引用。假设进行大量的缩放,这些索引不能驻留在同一台机器上,也不能驻留在单个大型集群中。无法知道关于单个客户的所有数据都驻留在单个事务范围内。虽然实体本身驻留在单个事务范围内。所面临的挑战是,用于创建这些候选索引的信息副本必须被假定为与实体自身信息驻留在不同的事务范围中。
首先考虑确保候选索引驻留在相同的事务范围内。当几乎无限的缩放开始时,实体的集合被涂抹在巨大数量的机器之中。主索引和候选索引信息必须位于相同的事务范围内。确保这一点的唯一方法是使用主索引找到候选索引。这将把你带到相同的事务范围。如果您在没有主索引的情况下启动并且必须搜索所有事务范围,则每个候选索引查找都必须在几乎无限数量的范围内检查,因为它会使用候选键进行匹配查找。这将最终成为站不住脚的。
唯一合乎逻辑的选择是做两步查找。首先,查找候选键。其次,使用实体键访问实体。这与关系数据库非常相似,因为它就是使用两个步骤通过候选键访问记录的。但是几乎无限缩放的前提意味着两个指标(主要和候选)不能被认为是在相同的事务范围内。
过去自动管理的候选索引现在必须由应用程序手动管理。通过异步消息传递的工作流风格更新是剩下的唯一选择。当您从候选索引读取数据时,您必须了解它可能与实体本身不同步。候选索引现在更难。这是一个巨大系统的残酷世界中的生活事实。(banq注:索引是为查询服务,可使用读写分离)
6. 在实体之间传递的消息
本节考虑使用消息来连接独立的实体。它检查命名,事务和消息,消息传递语义以及重新分区实体的影响。
(1)消息在实体之间进行通信
如果您无法在同一事务中的两个实体之间更新数据,则需要一种机制来更新不同事务中的数据。实体之间的连接是通过消息。
(2)异步发送事务
由于消息在实体之间传递,与发送消息的决定相关的数据在一个实体中,并且消息的目的地在另一个实体中。根据一个实体的定义,这些实体不能被原子更新。消息不能通过这些不同的实体自动发送和接收。
如果应用程序开发人员在处理事务时发送消息,发送消息,然后事务中止,那将会非常复杂。这意味着你没有任何措施记住发生的事情,但这些事情确实发生了。出于这个原因,消息的事务排队是必要的。
如果消息在发送事务提交之后才能在目的地被看到,则消息对于发送事务是异步的。每个实体在一个事务中会进入一个新状态。消息是来自一次事务的刺激,并到达另外一个实体引起新的事务。
(3)命名消息的目的地
考虑应用程序的规模无关部分的编程,因为一个实体想要发送消息给另一个实体。规模不可知的代码不知道目标实体的位置。实体主键就是关键。
实体键能够实现应用程序的规模感知,将实体主键与实体的具体位置相关联。
(4)重新分区和消息传递
当应用程序与规模无关的部分发送消息时,较低级别的扩展感知部分搜索目标并且传递消息至少一次。
随着系统规模的扩大,实体在移动。这通常被称为重新分区。实体的位置以及因此消息的目的地可能在不断变化。有时消息会追溯到旧的位置,只是为了找出讨厌的实体已经发送到别处什么地方聊。现在,消息将不得不这么做。
随着实体的移动,发送者和目的地之间的先进先出队列的清晰度偶尔中断。消息被重复。稍后的消息在较早的消息之前到达 生活变得更加混乱。
由于这些原因,与规模无关的应用程序正在不断演进到以支持所有应用程序对可见消息的幂等处理。这也意味着在消息传递中可进行重新排序。