微服务的数据一致性分发问题解决方案.docx
《微服务的数据一致性分发问题解决方案.docx》由会员分享,可在线阅读,更多相关《微服务的数据一致性分发问题解决方案.docx(13页珍藏版)》请在冰豆网上搜索。
微服务的数据一致性分发问题解决方案
微服务的数据一致性分发问题解决方案
介绍
系统架构微服务化以后,根据微服务独立数据源的思想,每个微服务一般具有各自独立的数据源,但是不同微服务之间难免需要通过数据分发来共享一些数据,这个就是微服务的数据分发问题。
Netflix/Airbnb等一线互联网公司的实践[参考附录1/2/3]表明,数据一致性分发能力,是构建松散耦合、可扩展和高性能的微服务架构的基础。
本文解释分布式微服务中的数据一致性分发问题,应用场景,并给出常见的解决方法。
本文主要面向互联网分布式系统架构师和研发经理。
为啥要分发数据?
场景?
我们还是要从具体业务场景出发,为啥要分发数据?
有哪些场景?
在实际企业中,数据分发的场景其实是非常多的。
假设某电商企业有这样一个订单服务OrderService,它有一个独立的数据库。
同时,周边还有不少系统需要订单的数据,上图给出了一些例子:
1.一个是缓存系统,为了提升订单数据的访问性能,我们可以把频繁访问的订单数据,通过Redis缓存起来;
2.第二个是FulfillmentService,也就是订单履行系统,它也需要一份订单数据,借此实现订单履行的功能;
3.第三个是ElasticSearch搜索引擎系统,它也需要一份订单数据,可以支持前台用户、或者是后台运营快速查询订单信息;
4.第四个是传统数据仓库系统,它也需要一份订单数据,支持对订单数据的分析和挖掘。
当然,为了获得一份订单数据,这些系统可以定期去订单服务查询最新的数据,也就是拉模式,但是拉模式有两大问题:
1.一个是拉数据通常会有延迟,也就是说拉到的数据并不实时;
2.如果频繁拉的话,考虑到外围系统众多(而且可能还会增加),势必会对订单数据库的性能造成影响,严重时还可能会把订单数据库给拉挂。
所以,当企业规模到了一定阶段,还是需要考虑数据分发技术,将业务数据同步分发到对数据感兴趣的其它服务。
除了上面提到的一些数据分发场景,其实还有很多其它场景,例如:
1.第一个是数据复制(replication)。
为了实现高可用,一般要将数据复制多分存储,这个时候需要采用数据分发。
2.第二个是支持数据库的解耦拆分。
在单体数据库解耦拆分的过程中,为了实现不停机拆分,在一段时间内,需要将遗留老数据同步复制到新的数据存储,这个时候也需要数据分发技术。
3.第三个是实现CQRS,还有去数据库Join。
这两个场景我后面有单独文章解释,这边先说明一下,实现CQRS和数据库去Join的底层技术,其实也是数据分发。
4.第四个是实现分布式事务。
这个场景我后面也有单独文章讲解,这边先说明一下,解决分布式事务问题的一些方案,底层也是依赖于数据分发技术的。
5.其它还有流式计算、大数据BI/AI,还有审计日志和历史数据归档等场景,一般都离不开数据分发技术。
总之,波波认为,数据分发,是构建现代大规模分布式系统、微服务架构和异步事件驱动架构的底层基础技术。
双写?
对于数据分发这个问题,乍一看,好像并不复杂,稍有开发经验的同学会说,我在应用层做一个双写不就可以了吗?
比方说,请看上图右边,这里有一个微服务A,它需要把数据写入DB,同时还要把数据写到MQ,对于这个需求,我在A服务中弄一个双写,不就搞定了吗?
其实这个问题并没有那么简单,关键是你如何才能保证双写的事务性?
请看上图左边的代码,这里有一个方法updateDbThenSendMsgInTransaction,这个方法上加了事务性标注,也就是说,如果抛异常的话,数据库操作会回滚。
我们来看这个方法的执行步骤:
第一步先更新数据库,如果更新成功,那么result设为true,如果更新失败,那么result设为false;
第二步,如果result为true,也就是说DB更新成功,那么我们就继续做第三步,向mq发送消息
如果发消息也成功,那么我们的流程就走到第四步,整个双写事务就成功了。
如果发消息抛异常,也就是发消息失败,那么容器会执行该方法的事务性回滚,上面的数据库更新操作也会回滚。
初看这个双写流程没有问题,可以保证事务性。
但是深入研究会发现它其实是有问题的。
比方说在第三步,如果发消息抛异常了,并不保证说发消息失败了,可能只是由于网络异常抖动而造成的抛异常,实际消息可能是已经发到MQ中,但是抛异常会造成上面数据库更新操作的回滚,结果造成两边数据不一致。
模式一:
事务性发件箱(TransactionalOutbox)
对于事务性双写这个问题,业界沉淀下来比较实践的做法,其中一种,就是采用所谓事务性发件箱模式,英文叫TransactionalOutbox。
据说这个模式是eBay最早发明和使用的。
事务性发件箱模式不难理解,请看上图。
我们仍然以订单Order服务为例。
在数据库中,除了订单Order表,为了实现事务性双写,我们还需增加了一个发件箱Outbox表。
Order表和Outbox表都在同一个数据库中,对它们进行同时更新的话,通过数据库的事务机制,是可以实现事务性更新的。
下面我们通过例子来展示这个流程,我们这里假定OrderService要添加一个新订单。
首先第一步,OrderService先将新订单数据写入Order表,然后它再向Outbox表中写入一条订单新增记录,这两个DB操作可以包在一个DB事务里头,也就是可以实现事务性写入。
然后第二步,我们再引入一个称为消息中继MessageRelay的角色,它负责定期Poll拉取Outbox中的新数据,然后第三步再Publish发送到MQ。
如果写入MQ确认成功,MessageRelay就可以将Outbox中的对应记录标记为已消费。
这里可能会出现一种异常情况,就是MessageRelay在将消息发送到MQ时,发生了网络抖动,实际消息可能已经写入MQ,但是MessageRelay并没有得到确认,这时候它会重发,直到明确成功为止。
所以,这里也是一个AtLeastOnce,也就是至少交付一次的消费语义,消息可能被重复投递。
因此,MQ之后的消费方要做消息去重或幂等处理。
总之,事务性发件箱模式可以保证,对Order表的修改,然后将对应事件发送到MQ,这两个动作可以实现事务性,也就是实现数据分发的事务性。
注意,这里的MessageRelay角色既可以是一个独立部署的服务,也可以和OrderService住在一起。
生产实践中,需要考虑MessageRelay的高可用部署,还有监控和告警,否则如果MessageRelay挂了,消息就发不出来,然后,依赖于消息的各种消费方也将无法正常工作。
TransactionalOutbox参考实现~KillbillCommonQueue
事务性发件箱的原理简单,实现起来也不复杂,波波这边推荐一个生产级的参考实现。
这个实现源于一个叫killbill的项目,killbill是美国高朋(GroupOn)公司开源的订阅计费和支付平台,这个项目已经有超过8~9年的历史,在高朋等公司已经有不少落地案例,是一个比较成熟的产品。
killbill项目里头有一些公共库,单独放在一个叫killbill-commons的子项目里头,其中有一个叫killbillcommonqueue,它其实是事务性发件箱的一个生产级实现。
上图有给出这个queue的github链接。
Killbillcommonqueue也是一个基于DB实现的分布式的队列,它上层还包装了EventBus事件总线机制。
killbillcommonqueue的总体设计思路不难理解,请看上图:
在上图的左边,killbillcommonqueue提供发送消息API,并且是支持事务的。
比方说图上的postFromTransaction方法,它可以发送一个BusEvent事件到DBQueue当中,这个方法还接受一个数据库连接Connection参数,killbillcommonqueue可以保证对事件event的数据库写入,和使用同一个Connection的其它数据库写入操作,发生在同一个事务中。
这个做法其实就是一种事务性发件箱的实现,这里的发件箱存的就是事件event。
除了POST写入API,killbillcommonqueue还支持类似前面提到的MessageRelay的功能,并且是包装成EeventBus+Handler方式来实现的。
开发者只需要实现事件处理器,并且注册订阅在EventBus上,就可以接收到DBQueue,也就是发件箱当中的新事件,并进行消费处理。
如果事件处理成功,那么EvenbBus会将对应的事件从发件箱中移走;如果事件处理不成功,那么EventBus会负责重试,直到处理成功,或者超过最大重试次数,那么它会将该事件标记为处理失败,并移到历史归档表中,等待后续人工检查和干预。
这个EventBus的底层,其实有一个Dispatcher派遣线程,它负责定期扫描DBQueue(也就是发件箱)中的新事件,有的话就批量拉取出来,并发送到内部EventBus的队列中,如果内部队列满了,那么DispatherThread也会暂停拉取新事件。
在killbillcommonqueue的设计中,每个节点上的Dispather线程只负责通过自己这个节点写入的事件,并且在一个节点上,Dispather线程也只有一个,这样才能保证消息消费的顺序性,并且也不会重复消费。
Reaper机制
killbillcommonqueue,其实是一个基于集中式数据库实现的分布式队列,为什么说它是分布式队列呢?
请看上图,killbillcommonqueue的设计是这样的,它的每个节点,只负责消费处理从自己这个节点写入的事件。
比方说上图中有蓝色/黄色和绿色3个节点,那么蓝色节点,只负责从蓝色节点写入,在数据库中标记为蓝色的事件。
同样,黄色节点,只负责从黄色节点写入,在数据库中标记为黄色的事件。
绿色节点也是类似。
这是一种分布式的设计,如果处理容量不够,只需按需添加更多节点,就可以实现负载分摊。
这里有个问题,如果其中某个节点挂了,比方说上图的蓝色节点挂了,那么谁来继续消费数据库中蓝色的,还没有来得及处理的事件呢?
为了解决这个问题,killbillcommonqueue设计了一种称为reaper收割机的机制。
每个节点上都还住了一个收割机线程,它们会定期检查数据库,看有没有长时间无人处理的事件,如果有,就抢占标记为由自己负责。
比方说上图的右边,最终黄色节点上的收割机线程抢到了原来由蓝色节点负责的事件,那么它会把这些事件标记为黄色,也就是由自己来负责。
收割机机制,保证了killbillcommonqueue的高可用性,相当于保证了事务性发件箱中的MessageRelay的高可用性。
KillbillPersistentBus表结构
基于killbillcommonqueue的EventBus,也被称为killbillPersistentBus。
上图给出了它的数据库表结构,其中bus_events就是用来存放待处理事件的,相当于发件箱,主要的字段包括:
1.event_json,存放json格式的原始数据。
2.creating_owner,记录创建节点,也就是事件是由哪个节点写入的。
3.processingowner,记录处理节点,也就是事件最终是由哪个节点处理的;通常由creatingowner自己处理,但也可能被收割,由其它节点处理。
4.processing_state,当前的处理状态。
5.error_count,处理错误计数,超过一定计数会被标记为处理失败。
当前处理状态主要包括6种:
1.AVAILABL