微服务架构 - 数据完整性
前言
在之前的文章中我们介绍了服务的拆分和通讯,每一个服务都是一个可独立部署并且数据自治的进程,系统通过服务间的网络通讯完成一个个业务步骤,最终完成一个个完整的工作流。这里的每一个服务都可以部署在相同或者不同的物理空间,数据亦是如此。所以对于微服务或者说原生云架构(Cloud Native)来说分布式是其与生俱来的特性,而就分布式来说数据的一致性是项目开发中必须面临的问题。今天我们就来谈谈微服务如何保证数据的一致性。
1.事务的一致性
数据一致性必然伴随着事务和状态这两个概念。事务由一系列的操作组成,事务的完成意味着执行事务的对象从初始状态转变到另一个状态,而且其所有操作必须没有失败。任何操作的失败,都将导致整个事务失败,任何因为中间操作导致事务对象产生的中间状态必须恢复其初始状态。这里最为人所知的就是数据库的事务处理,以下是具体的例子
...//这里省去一万行
var dbTransaction = dbConnection.BeginTransaction();
try{
dbConnection.Command(SQL);
....//同上
dbTransaction.Commit();
}
catch(Exception ex){
dbTransaction.Rollback();
}
finally{
dbConnection.Close();
}
数据库的事务完整性必须满足ACID(酸)原则。
- 原子性(Atomicity):事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行。
- 一致性(Consistency):事务应确保数据库的状态从一个一致状态转变为另一个一致状态,并且满足完整性约束。
- 隔离性(Isolation):多个事务并发执行时,一个事务的执行不应影响其他事务的执行。
- 持久性(Durability):已被提交的事务对数据库的修改应该永久保存在数据库中。
2.分布式的事务处理
在ACID的保证下,数据库进程内的数据完整性可以得到很好的满足。但是不同服务间的事务完整性怎么保证呢?分布式事务所包含的操作可能由不同物理位置的服务组成,任何一个服务的失败都应该回滚整个分布式事务。传统的分布式事务处理通过引入事务协调者(DTC),利用两阶段提交算法完成分布式事务的提交与回滚,正如我在浅谈区块链提及的,两阶段提交法不仅影响系统的伸缩性而且可用性非常糟糕,它依赖于服务间的网络稳定性。如果网络不好,事务成功的可能性就非常低,这对于使用者来说是很难接受的。为什么两阶段提交法有这样的局限性?这就要回到分布式自身带来的问题。
1.鱼与熊掌不可兼得
分布式系统因为网络本身的属性,它很难保证事务实时一致性(这里的实时一致性包括两点:一个是数据的一致性,另一个是事务的可用性)。这就是CAP原则:
- 一致性(Consistency):在分布式系统中的所有数据备份,在同一时刻是否同样的值。(等同于所有节点访问同一份最新的数据副本)
- 可用性(Availability):在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。(对数据更新具备高可用性)
- 分区容忍性(Partition tolerance):以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择。
CAP原则指出以上三个要素最多只能同时实现两点,不可能三者兼顾。所以在分布式(P)情况下我们必须在C和A之间做出选择,而之前提到的数据库事务一致性选择的就是C和P。因为两阶段提交法更倾向于数据的一致性(任一服务失败,整个事务就放弃),所以可想而知它的可用性是非常低的(网络不稳定的情况下一个复杂事务几乎不可能完成提交)。在互联网时代,随着大规模的分布式计算的兴起,用户体验也是产品成功至关重要的因素。所以在AP重要的时代,我们是否应该放弃一致性?答案显而易见,不能!因为一致性在很多情况下代表着正确性。一个基本正确都不能保证的系统,有何谈可用性和分区容忍。
2.延迟满足-最终一致性
数据的完整性和一致性是系统整体正确性的保证,但很多时候我们是无法保证一个事务实时的完整性,尤其是在多个服务协作完成。比如网络购买火车票,我们可以将下订单(抢票)和支付看作一个完整的事务,但是支付很多时候是第三方服务,我们并不能很好的掌控。如果抢票成功,但是因为第三发服务出了问题或者客户支付账户余额不足,系统回滚了整个事务,那么乘客一定会很抓狂(尤其在春运时节)。相对折衷的方法就是订单系统和支付系统各自保证自身领域内的数据完整性,一旦支付失败,订单依然存在,用户可以选择其他的支付手段或者支付账户,如果在指定的时间内未完成支付,那么系统将会回滚整个事务。系统的数据完整性并不是实时一致的,但是最终的结果一定是一致的。当然这种的用户体验不一定是完美的,但是他是折衷后最适合的。而且保证最终一致性,也给系统的灵活扩展带来很大的好处,比如春运期间,我们可以单独的扩容订单系统的服务器数量来保证吞吐量,对接支付服务的系统依然可以保持原有的处理能力。这就是另一个非常有名的原则BASE原则:
- 基本可用(Basically Available):分布式系统在出现不可预知的故障的时候,允许损失部分可用性。
- 软状态(Soft state):允许系统在不同的节点之间的数据副本进行数据的同步过程存在延迟。
- 最终一致性(Eventually consistent):系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态,而不需要实时保证系统数据的强一致性。
这里的基本可用就是订单系统下订单成功,但是支付系统可以暂时失败,而软状态就是一个中间状态(订单成功,但是支付暂时失败),一旦事务完成(失败或成功)这个软状态就不应该存在。系统最终的结果要么成功,要么失败,这就是最终一致性。
3. 酸(ACID)碱(BASE)调和-Saga模式
微服务因为自身的特性要保证事务完整性是不小的挑战。因为每个服务都是独立的个体,它们拥有各自的数据存储,如果一个事务是由多个服务协作完成,并且每个服务所在的物理空间又不相同,那么这个协调工作将会非常的复杂,网络的稳定性,服务的稳定性,存储的稳定性以及保证稳定性的前提下系统的可伸缩性等等,这一系列的问题相比单体软件来说又是另一个数量级。
Saga是一种解决分布式事务完整性的折衷方法。在单个服务内我们通过ACID保证本地事务在单个服务内的完整性,然后通过BASE来保证跨多个服务下的整个事务最终状态的完整性。说起来很简单,但是里面的实现细节依然非常复杂。我们需要手动实现事务失败后的回滚(这里我们可以通过补偿事务来回滚失败之前提交的所有本地事务),需要考虑并发事务的隔离性(因为服务间的操作是相互独立的,在整个事务完成前,他的中间状态可能被其他Saga操作访问或修改,如何处理这些中间状态,是需要结合业务场景进行具体分析)等等。
分布式事务处理我们引入了DTC(事务协调器)来负责事务的协调和回滚,同样我们也可以实现一个Saga聚合器来协调事务的参与者,只要其中一个事务参与者出现错误,协调者可以发起回滚或者重试机制,来保证事务的完整性。当然所有的这些都是通过消息驱动完成的异步操作。这里我不想深入讲解Saga模式,希望以后可以有专门的一个篇文章来介绍这个模式。
3.Event Sourcing
随着云计算以及云存储基础架构的发展,人们对大数据以及AI处理变得水到渠成,人们对数据完整性要求进一步提升。我们不再仅仅满足于保存当前数据状态,我们希望我们可以保存过去所有的数据状态,那样我们我们就可以拥有复现过去历史的能力。也许你会质疑这种方式是否会浪费存储空间,增加没有必要的数据冗余。但是设想如果我们拥有了这种能力,它给我们现有工作方式带来的改变以及巨大创新可能性相比,存储的成本是微不足道的(更何况是否增加存储,这还和具体的使用场景相关),想想1T的存储空间在互联网初期是多么的奢侈,而现在呢?
Event Sourcing是一种新的存储理念,它不再保存数据的当前状态,而是保留了一系列数据变更事件(更新,添加,删除等)以及变更条件,参数等。设想一下如果如果你在银行存了100元,然后通过支付宝你充值了50元的话费,通过微信给好友转账了30的午餐费。但是一个月后你通过银行卡消费时发现里面的余额为0了,你咨询银行,银行拿不出你所有消费的记录,只是告诉我只能查到你当前的余额,我相信你一定不会接受这样的解释。Event Sourcing可以很好的解决上面的问题,系统会保存所有收支的事件,通过重新触发这些事件还原出所有的消费和支出记录。虽然它和审计日志(Audit Logging)在呈现方式上没有太大的区别,但是技术实现细节上却带来很大的好处。
- 技术领域关注点分离:与日志系统相比,Event Souring本身就与领域事件(Domain Event)紧密相关,它是领域知识的一部分,我们对领域事件的存储一定是随着领域模型一起演变的。但是日志系统往往被我们认为是独立于领域的基础架构,但我们必须将审计日志系统的逻辑与领域逻辑耦合在一起。这在一定程度上是对领域模型的污染。另外日志系统拥有自己的数据存储,一方面它有部分信息和领域模型信息存在冗余,另一方面因为和领域操作有密切关系,所以在任何一方进行数据迁移时必须考虑另一方以保证一致性。而Event Sourcing则不同,它只有一份数据存储,而且数据都是面相领域的。
- CQRS数据的读写分离:因为Event Sourcing是一份完整的数据,我们可以根据自己的需要组织不同的呈现方式,我们可以提取所有事件作为审计日志,我们也可以累计所有事件的作用结果得到账户的余额,我们还可以提取一段时间内的事件来呈现这段时间的流水等等。只要数据足够完整,我们就可以开发出不同目的的读服务。这对系统的伸缩带来极大的好处,因为不同目的的操作频度是不一样的,我们可以将不同目的的操作实现成不同的服务,这样我们就可以根据需要,在需要频繁写操作的系统多部署写服务,而在需要频繁读操作的系统多部署读服务。
- 与生俱来的分布式存储:Event Sourcing并不需要一个中心化的物理存储,你可以将数据保存在不同的物理地址,这正是微服务希望的存储方式-每个服务拥有自己的数据存储。我们可以通过上面提到的saga模式来保证系统数据的完整性。
Event Sourcing其实并不是什么新鲜事务,除了上面提到的银行交易系统,我们工程上使用的版本管理工具git也是一样的思想,还有最近比较火的区块链等等。但是Event Sourcing也有它的问题,因为它和传统的存储思路有很大的区别,所以对于很多没有经验的开发来说有一定的难度,而且对于一些简单的系统如果使用Event Sourcing会增加系统的复杂性。还有如果需要查询最后一次的数据状态我们需要对原始数据应用所有操作事件得到最终状态,这大大影响查询的性能,当然我们可以通过定时的创建数据快照(Snapshot)来避免每次都从起始数据开始,从而提高性能(空间换时间)。
后记
数据的完整性是分布式系统(也是微服务)中一个非常重要的挑战,我们需要从传统的非分布式思维走出来去面对它,很多时候不是像上面提及的一些理论或者方法一样简单直接,我们需要具体的问题具体分析,但是我相信很多问题都可以从上面的思路举一反三。如果你能拥抱这些挑战,微服务会想你呈现更大的魅力。