微服务的最终一致性与事件流

微服务是指一个个单个小型业务功能的服务,由于各个微服务开发部署都是独立的,因此微服务天然是分布式的,因此,分布式系统的设计问题如CAP定理同样适合微服务架构,虽然微服务本身是无状态的,但是微服务是需要管理状态的。这些状态是指领域模型的状态或存储在自己的专有数据库中。

虽然我们使用微服务必须面对分布式系统,但是好的一方面是有很多关于如何建立复杂分布式系统的成熟模式和最佳实践。

典型的问题是微服务之间如果需要共享状态怎么办?实际是在分布式节点之间需要共享或复制状态。关于共享状态有几个解决方案:
1.微服务之间通过共享同一个数据库实现状态共享,但是因为微服务是使用自己专用的数据库,因此,数据库共享方案在微服务中是不适用的,违背了微服务架构宗旨。

2.通过调用同一个微服务实现状态共享,比如A服务和B服务需要共享C数据状态,而C数据状态是由C服务管理的,那么,A服务和B服务共同调用C服务不就是获得同一个C状态吗?
但是考虑到分布式系统下,A服务和B服务可能不在同一个节点服务器上,或者不同Docker VM中,那么服务之间调用就需要网络通讯,通常RPC是一种通过网络调用远程服务器上其他服务的同步方式,但是,RPC虽然将网络编程藏起来,其实藏是藏不住,结果造成抽象泄漏了。

"Asynch message-passing makes constraints of network programming firstclass instead of hiding them behind the RPC leaky abstraction"异步消息传递使得网络编程变成第一公民(显式),而不是像RPC隐藏了网络编程却造成抽象泄漏。

在分布式系统中使用异步消息必然会遭遇最终一致性。甚至可以说微服务是使用最终一致性的(microservices use eventual consistency)

最终一致性Eventual Consistency

最终一致性是一种用于描述在分布式系统中数据的操作模型,在分布式系统中状态是被复制然后跨网络多节点保存,其实在关系数据库集群中,最终一致性被用来在集群多个节点之间协调数据复制的写操作,数据库集群中这种写操作挑战是:各个节点接受到的写操作必须严格按照复制的次序进行,这个次序是有时间损耗的,从这个角度看,数据库在集群节点之间的这种状态复制还是可以被认为是一种最终一致性,所有节点状态在未来某个时刻最终汇聚到一个一致性状态,也就是说,最终达成状态一致性。

当构建微服务时,最终一致性是开发者 DBA和架构师频繁打交道的问题,当开始在分布式系统中进行状态处理时,头疼问题更加严重。核心问题是:

如何在保证数据一致性基础上保证高可用性呢?

事务日志

几乎所有数据库都支持高可用性集群,大多数数据库对系统一致性模型提供一个易于理解的方式,保证强一致性模型的安全方式是维持数据库事务操作的有序日志,理论上理由非常简单,一个事务日志是一系列数据更新操作的动作有序记录集合,当其他节点从主节点获得这个事务日志时,能够按照这种有序动作集合重新播放这些操作,从而更新自己所在节点的数据库状态,当这个事务日志完成后,次节点的状态最终会和主节点状态一致,

这种事务日志非常类似于财务中记账模型,或者类似银行储蓄卡打印出来的流水账,哪天存入一笔钞票(更新操作),哪天又提取了一笔钞票(更新操作),最后当前余额是多少(代表数据库当前状态)。
  
Event Sourcing
Event sourcing事件溯源是借鉴数据库事务日志的一种数据持久方式,在ES中,事务单元变得更细粒度,使用一系列有序的事件来代表存储在数据库中的领域模型状态,一旦一个事件被加入事件日志,它就不能被移走或重新排序,事件被认为是不可变的,事件序列只能被追加方式存储。

因为微服务将系统切分成一个个松耦合的小系统,每个系统后面都独占自己的数据库,虽然,微服务是无态的,但是它需要操作自己数据库的状态,如何保证微服务之间操作数据库数据的一致性成了微服务实践中重要问题,使用ES能够帮助我们实现这点。.

聚合可以被认为是产生任何对象的一致性状态,它提供校订方法用来进行重播产生对象中状态变化的历史。它能使用事件流提供分析数据许多必要输入,能够采取补偿方式对不一致应用状态实现事件回滚。


事件流共享
我们在微服务之间相互调用中通过引入异步机制,如果不同微服务之间存在共享的状态,或者说需要访问其他微服务的专用数据库,那么我们无需将本来专有的数据库共享出来,也无需在服务层使用2PC+RPC进行性能很慢的跨机同步调用,而是将改变这些共享状态的事件保存并共享,将领域事件以事务日志的方式记录下来,保存在一个统一的存储库,现在EventSourcing标准的存储库是 Apache Kafka。

也就是说,微服务之间共享的不是传统数据库,而是Apache Kafka,通过读取ES的事务日志和重新播放,我们可以得到任何时间内的状态,从而实现状态的时间旅行。

时间旅行概念非常类似前端的Redux模式,Redux是Facebook的Flux模式的改进,将可变状态和函数不变性进行分离,状态值一旦创建就无法被可变的,如果需要改变状态值,只能通过重新创建新的状态值实现,将旧的状态和新的状态通过树形结构连接起来,因此遍历树形结构就能回到历史上任何状态,从而实现了时间旅行,

我们将微服务之间共享状态通过共享事件流实现,这点符合函数式编程的声明风格,在微服务中,我们不是需要状态时就发出命令从数据库中查询获得,这样,可变的状态会遍布微服务代码中,带来很多副作用,而我们将这些状态操作统一为事件流声明订阅,订阅了某个事件流,通过重播事件流中各个事件一直到最新最后的事件,也就获得了最终的状态。函数式编程Stream风格为这种播放提供了方便。

这种实现其实已经在Reactive前端中有着同样实现思路,见:为什么要使用GraphQL和Falcor?,应用程序(微服务)将可变的状态被限定在一个单个的序列化对象中,从而整个应用就变成了无态,可变状态不会扩散到整个应用代码的各个本地变量中。

在后端Reactive中,我们可以通过一些Reactive框架来实现事件Stream,比如RxJava 或Spring的Reactor,比如我们为了获得一个购物车的当前状态,通过使用Spring Reactor如下代码实现ES重播:


// 从kafka获得某个用户的购物车操作事件流,Flux是Reactor的库包,cartEventRepository是kafka仓储

Flux<CartEvent> cartEvents =

Flux.fromStream(cartEventRepository.getCartEventStreamByUser(user.getId()));



//执行事件流的事件直至最后一个事件发生的最终状态。也就是购物车的最终状态

ShoppingCart shoppingCart = cartEvents

.takeWhile(cartEvent -> !ShoppingCart.isTerminal(cartEvent.getCartEventType()))

.reduceWith(() -> new ShoppingCart(catalog), ShoppingCart::incorporate)

.get();

以上代码详细见这里
该案例源码来自于基于Spring Cloud+Event Sourcing实现案例:https://github.com/kbastani/spring-cloud-event-sourcing-example

上面代码中ShoppingCart是包含用户当前放入购物车的商品种类和数量以及总价,这些都是购物车的状态,如果不使用ES事件流,我们通常是使用一个数据表来保存购物车这些数据状态,或者放在Redis等内存缓存中,但是在微服务架构中,共享的数据表或内存系统会成为分布式的瓶颈,不符合真正横向扩展分布的理论定义,当然实际中这么做是比比皆是。而在这里我们使用了事件流的事件播放来获得最终购物车的状态,共享的只是消息系统kafka,那么如果为了完成真正没有共享的分布式,将共享的消息系统Kafka去除了怎么办?


事件流复制
上面我们展示了通过微服务之间共享消息系统Kafka来实现事件事务日志的共享,除了共享方案以外,还有复制方案,也就是将需要共享的事务日志在多个微服务节点之间复制,每个节点上都有一份共享事务日志的拷贝,比特币的区块链技术正是基于此方案:比特币区块链是一种分 布 式的事件流日志


[该贴被banq于2016-04-21 18:19修改过]