若何解决微做事的数据一致性分发问题?_数据_事宜 智能问答

以下文章来源于波波微课 ,作者架构师杨波

先容

系统架构微做事化往后,根据微做事独立数据源的思想,每个微做事一样平常具有各自独立的数据源,但是不同微做事之间难免须要通过数据分发来共享一些数据,这个便是微做事的数据分发问题。
Netflix/Airbnb等一线互联网公司的实践[参考附录1/2/3]表明,数据同等性分发能力,是构建疏松耦合、可扩展和高性能的微做事架构的根本。

本文阐明分布衰落做事中的数据同等性分发问题,运用处景,并给出常见的办理方法。
本文紧张面向互联网分布式系统架构师和研发经理。

为啥要分发数据?场景?

我们还是要从详细业务场景出发,为啥要分发数据?有哪些场景?在实际企业中,数据分发的场景实在是非常多的。
假设某电商企业有这样一个订单做事Order Service,它有一个独立的数据库。
同时,周边还有不少系统须要订单的数据,上图给出了一些例子:

一个是缓存系统,为了提升订单数据的访问性能,我们可以把频繁访问的订单数据,通过Redis缓存起来;第二个是Fulfillment Service,也便是订单履行系统,它也须要一份订单数据,借此实现订单履行的功能;第三个是ElasticSearch搜索引擎系统,它也须要一份订单数据,可以支持前台用户、或者是后台运营快速查询订单信息;第四个是传统数据仓库系统,它也须要一份订单数据,支持对订单数据的剖析和挖掘。

当然,为了得到一份订单数据,这些系统可以定期去订单做事查询最新的数据,也便是拉模式,但是拉模式有两大问题:

一个是拉数据常日会有延迟,也便是说拉到的数据并不实时;如果频繁拉的话,考虑到外围系统浩瀚(而且可能还会增加),势必会对订单数据库的性能造成影响,严重时还可能会把订单数据库给拉挂。

以是,当企业规模到了一定阶段,还是须要考虑数据分发技能,将业务数据同步分发到对数据感兴趣的其它做事。
除了上面提到的一些数据分发场景,实在还有很多其它场景,例如:

第一个是数据复制(replication)。
为了实现高可用,一样平常要将数据复制多分存储,这个时候须要采取数据分发。
第二个是支持数据库的解耦拆分。
在单体数据库解耦拆分的过程中,为了实现一直机拆分,在一段韶光内,须要将遗留老数据同步复制到新的数据存储,这个时候也须要数据分发技能。
第三个是实现CQRS,还有去数据库Join。
这两个场景我后面有单独文章阐明,这边先解释一下,实现CQRS和数据库去Join的底层技能,实在也是数据分发。
第四个是实现分布式事务。
这个场景我后面也有单独文章讲解,这边先解释一下,办理分布式事务问题的一些方案,底层也是依赖于数据分发技能的。
其它还有流式打算、大数据BI/AI,还有审计日志和历史数据归档等场景,一样平常都离不开数据分发技能。

总之,波波认为,数据分发,是构建当代大规模分布式系统、微做事架构和异步事宜驱动架构的底层根本技能。

双写?

对付数据分发这个问题,乍一看,彷佛并不繁芜,稍有开拓履历的同学会说,我在运用层做一个双写不就可以了吗?比方说,请看上图右边,这里有一个微做事A,它须要把数据写入DB,同时还要把数据写到MQ,对付这个需求,我在A做事中弄一个双写,不就搞定了吗?实在这个问题并没有那么大略,关键是你如何才能担保双写的事务性?

请看上图左边的代码,这里有一个方法updateDbThenSendMsgInTransaction,这个方法上加了事务性标注,也便是说,如果抛非常的话,数据库操作会回滚。
我们来看这个方法的实行步骤:

第一步先更新数据库,如果更新成功,那么result设为true,如果更新失落败,那么result设为false;

第二步,如果result为true,也便是说DB更新成功,那么我们就连续做第三步,向mq发送

如果发也成功,那么我们的流程就走到第四步,全体双写事务就成功了。

如果发抛非常,也便是发失落败,那么容器会实行该方法的事务性回滚,上面的数据库更新操作也会回滚。

初看这个双写流程没有问题,可以担保事务性。
但是深入研究会创造它实在是有问题的。
比方说在第三步,如果发抛非常了,并不担保说发失落败了,可能只是由于网络非常抖动而造成的抛非常,实际可能是已经发到MQ中,但是抛非常会造成上面数据库更新操作的回滚,结果造成两边数据不一致。

模式一:事务性发件箱(Transactional Outbox)

对付事务性双写这个问题,业界沉淀下来比较实践的做法,个中一种,便是采取所谓事务性发件箱模式,英文叫Transactional Outbox。
听说这个模式是eBay最早发明和利用的。
事务性发件箱模式不难明得,请看上图。

我们仍旧以订单Order做事为例。
在数据库中,除了订单Order表,为了实现事务性双写,我们还需增加了一个发件箱Outbox表。
Order表和Outbox表都在同一个数据库中,对它们进行同时更新的话,通过数据库的事务机制,是可以实现事务性更新的。

下面我们通过例子来展示这个流程,我们这里假定Order Service要添加一个新订单。

首先第一步,Order Service先将新订单数据写入Order表,然后它再向Outbox表中写入一条订单新增记录,这两个DB操作可以包在一个DB事务里头,也便是可以实现事务性写入。

然后第二步,我们再引入一个称为中继Message Relay的角色,它卖力定期Poll拉取Outbox中的新数据,然后第三步再Publish发送到MQ。
如果写入MQ确认成功,Message Relay就可以将Outbox中的对应记录标记为已消费。
这里可能会涌现一种非常情形,便是Message Relay在将发送到MQ时,发生了网络抖动,实际可能已经写入MQ,但是Message Relay并没有得到确认,这时候它会重发,直到明确成功为止。
以是,这里也是一个At Least Once,也便是至少交付一次的消费语义,可能被重复投递。
因此,MQ之后的消费方要做去重或幂等处理。

总之,事务性发件箱模式可以担保,对Order表的修正,然后将对应事宜发送到MQ,这两个动作可以实现事务性,也便是实现数据分发的事务性。

把稳,这里的Message Relay角色既可以是一个独立支配的做事,也可以和Order Service住在一起。
生产实践中,须要考虑Message Relay的高可用支配,还有监控和告警,否则如果Message Relay挂了,就发不出来,然后,依赖于的各种消费方也将无法正常事情。

Transactional Outbox参考实现 ~ Killbill Common Queue

事务性发件箱的事理大略,实现起来也不繁芜,波波这边推举一个生产级的参考实现。
这个实现源于一个叫killbill的项目,killbill是美国高朋(GroupOn)公司开源的订阅计费和支付平台,这个项目已经有超过8~9年的历史,在高朋等公司已经有不少落地案例,是一个比较成熟的产品。
killbill项目里头有一些公共库,单独放在一个叫killbill-commons的子项目里头,个中有一个叫killbill common queue,它实在是事务性发件箱的一个生产级实现。
上图有给出这个queue的github链接。

Killbill common queue也是一个基于DB实现的分布式的行列步队,它上层还包装了EventBus事宜总线机制。
killbill common queue的总体设计思路不难明得,请看上图:

在上图的左边,killbill common queue供应发送API,并且是支持事务的。
比方说图上的postFromTransaction方法,它可以发送一个BusEvent事宜到DB Queue当中,这个方法还接管一个数据库连接Connection参数,killbill common queue可以担保对事宜event的数据库写入,和利用同一个Connection的其它数据库写入操作,发生在同一个事务中。
这个做法实在便是一种事务性发件箱的实现,这里的发件箱存的便是事宜event。

除了POST写入API,killbill common queue还支持类似前面提到的Message Relay的功能,并且是包装成EeventBus + Handler办法来实现的。
开拓者只须要实现事宜处理器,并且注册订阅在EventBus上,就可以吸收到DB Queue,也便是发件箱当中的新事宜,并进行消费处理。
如果事宜处理成功,那么EvenbBus会将对应的事宜从发件箱中移走;如果事宜处理不堪利,那么EventBus会卖力重试,直到处理成功,或者超过最大重试次数,那么它会将该事宜标记为处理失落败,并移到历史归档表中,等待后续人工检讨和干预。
这个EventBus的底层,实在有一个Dispatcher叮嘱消磨线程,它卖力定期扫描DB Queue(也便是发件箱)中的新事宜,有的话就批量拉取出来,并发送到内部EventBus的行列步队中,如果内部队列满了,那么Dispather Thread也会停息拉取新事宜。

在killbill common queue的设计中,每个节点上的Dispather线程只卖力通过自己这个节点写入的事宜,并且在一个节点上,Dispather线程也只有一个,这样才能担保消费的顺序性,并且也不会重复消费。

Reaper机制

killbill common queue,实在是一个基于集中式数据库实现的分布式行列步队,为什么说它是分布式行列步队呢?请看上图,killbill common queue的设计是这样的,它的每个节点,只卖力消费处理从自己这个节点写入的事宜。
比方说上图中有蓝色/黄色和绿色3个节点,那么蓝色节点,只卖力从蓝色节点写入,在数据库中标记为蓝色的事宜。
同样,黄色节点,只卖力从黄色节点写入,在数据库中标记为黄色的事宜。
绿色节点也是类似。
这是一种分布式的设计,如果处理容量不足,只需按需添加更多节点,就可以实现负载分摊。

这里有个问题,如果个中某个节点挂了,比方说上图的蓝色节点挂了,那么谁来连续消费数据库中蓝色的,还没有来得及处理的事宜呢?为理解决这个问题,killbill common queue设计了一种称为reaper收割机的机制。
每个节点上都还住了一个收割机线程,它们会定期检讨数据库,看有没有永劫光无人处理的事宜,如果有,就抢占标记为由自己卖力。
比方说上图的右边,终极黄色节点上的收割机线程抢到了原来由蓝色节点卖力的事宜,那么它会把这些事宜标记为黄色,也便是由自己来卖力。

收割机机制,担保了killbill common queue的高可用性,相称于担保了事务性发件箱中的Message Relay的高可用性。

Killbill PersistentBus表构造

基于killbill common queue的EventBus,也被称为killbill PersistentBus。
上图给出了它的数据库表构造,个中bus_events便是用来存放待处理事宜的,相称于发件箱,紧张的字段包括:

event_json,存放json格式的原始数据。
creating_owner,记录创建节点,也便是事宜是由哪个节点写入的。
processingowner,记录处理节点,也便是事宜终极是由哪个节点处理的;常日由creatingowner自己处理,但也可能被收割,由其它节点处理。
processing_state,当前的处理状态。
error_count,处理缺点计数,超过一定计数会被标记为处理失落败。

当前处理状态紧张包括6种:

AVAILABLE,表示待处理IN_PROCESSING,表示已经被dispatcher线程取走,正在处理中PROCESSED,表示已经处理REMOVED,表示已经被删除FAILED,表示处理失落败REPEATED,表示被其它节点收割了

除了bus_events待处理事宜表,还有一个对应的bus-events-history事宜历史记录表。
不管成功还是失落败,终极,事宜会被写入历史记录表进行归档,作为事后审计或者人工干预的依据。

上图下方给出了数据库表的github链接,你可以进一步参考学习。

Killbill PersistentBus处理状态迁移

上图给出了killbill PersistentBus的事宜处理状态迁移图。

刚开始事宜处于AVAILABLE待处理状态;之后事宜被dispatcher线程拉取,进入IN_PROCESSING处理中状态;之后,如果事宜处理器成功处理了事宜,那么事宜就进入PROCESSED已经处理状态;如果事宜处理器处理事宜失落败,那么事宜的缺点计数会被增加1,如果缺点计数还没有超过最大失落败重试阀值,那么事宜就会重新进入AVAILABLE状态;如果事宜的缺点数量超过了最大失落败重试阀值,那么事宜就会进入FAILED失落败状态;如果卖力待处理事宜的节点挂了,那么到达一定的韶光间隔,对应的事宜会被收割进入REAPED被收割状态。

上图有一个通过API触发进入的REMOVED移除状态,这个是给关照行列步队用的,用户可以通过API移除对应的关照。
顺便提一下,除了事宜/行列步队,Killbill queue也是支持关照行列步队(或者说延迟行列步队)的。

模式二:变更数据捕获(Change Data Capture, CDC)

对付事务性双写这个问题,业界沉淀下来比较实践的做法,个中第二种,便是所谓的变更数据捕获,英文称为Change Data Capture,简称CDC。

变更数据捕获的事理也不繁芜,它利用了数据库的事务日志记录。
一样平常数据库,对付变更提交操作,都记录所谓事务日志Transaction Log,也称为提交日志Commit Log,比方说MySQL支持binlog,Postgres支持Write Ahead log。
事务日志可以大略理解为数据库本地的一个文件行列步队,它记录了按韶光顺序发生的对数据库表的变更提交记录。

下面我们通过例子来展示这个变更数据捕获的流程,我们这里假定Order Service要添加一个新订单。

第一步,Order Service将新订单记录写入Order表,并且提交。
由于这是一次表变更操作,以是这次变更会被记录到数据库的事务日志当中,个中内容包括发生的变更数据。

第二步,我们还须要引入一个称为Transaction Log Miner这样的角色,这个Miner卖力订阅在事务日志行列步队上,如果有新的变更记录,Miner就会捕获到变更记录。

然后第三步,Miner会将变更记录发送到MQ行列步队。
同之前的Message Relay一样,这里的发送到MQ也是At Least Once语义,可能会被重复发送,以是MQ之后的消费者须要做去重或者幂等处理。

总之,CDC技能同样可以担保,对Order表的修正,然后将对应事宜发送到MQ,这两个动作可以实现事务性,也便是实现数据分发的事务性。

把稳,这里的CDC一样平常是一个独立支配的做事,生产中须要做好高可用支配,并且做好监控告警。
否则如果CDC挂了,也就发不出来,然后,依赖于的各种消费方也将无法正常事情。

CDC开源项目(企业级)

当前,有几个比较成熟的企业级的CDC开源项目,我这边网络了一些,供大家学习参考:

第一个是阿里开源的Canal,目前在github上有超过1.4万颗星,这个项目在海内用得比较多,之前在拍拍贷的实时数据场景,Canal也有不少成功的运用。
Canal紧张支持MySQL binlog的增量订阅和消费。
它是基于MySQL的Master/Slave机制,它的Miner角色是通过伪装成Slave来实现的。
这个项目的利用文档相比拟较完善,建议大家一步参考学习。
第二个是Redhat开源的Debezium,目前在github上有超过3.2k星,这个项目在国外用得较多。
Debezium紧张是在Kafka Connect的根本上开拓的,它不仅支持mysql数据库,还支持postgres/sqlserver/mongodb等数据库。
第三个是Zendesk开源的Maxwell,目前在github上有超过2.1k星。
Maxwell是一个轻量级的CDC Deamon,紧张支持MySQL binlog的变更数据捕获和处理。
第四个是Airbnb开源的SpinalTap,目前在github上有两百多颗星。
SpinalTap紧张支持MySQL binlog的变更捕获和处理。
这个项目的星虽然不多,但是它是在Airbnb SOA做事化过程中,通过实践落地出来的一个项目,值得参考。

对付上面的这些项目,如果你想生产利用的话,波波推举的是阿里的Canal,由于这个项目毕竟是海内大厂阿里落地出来,而且在海内已经有不少企业落地案例。
其它几个项目,你也可以参考研究。

学习参考 ~ Eventuate-Tram

既然谈到这个CDC,这里有必要提到一个人和一本书,这个人叫Chris Chardson,他是美国的老一辈的技能大牛,曾今是第一代的Cloud Foundry项目的创始人(后来Cloud Foundry被Pivotal所收购)。
近几年,Chris Chardson开始转战微做事领域,这两年,他还专门写了一本书,叫《微做事设计模式》,英文名是《Microservices Patterns》。
这本书紧张是讲微做事架构和设计模式的,内容还不错,是我推举大家阅读的。

Charis Chardson还专门开拓了一个叫Eventuate-Tram的开源项目(这个项目也有商业版),其余他的微做事书里头也详细先容了这个项目。
这个项目可以说是一个大集成框架,它不仅实现了DDD领域驱动开拓模式,CQRS命令查询职责分离模式,事宜溯源模式,还实现了Saga事务状态机模式。
当然,这个项目的底层也实现了CDC变更数据捕获模式。

波波认为,Charis的项目,作为学习研究还是有代价的,但是暂不建议生产级利用,由于他的东西不是一线企业落地出来的,紧张是他个人开拓的。
至于说Charis的项目能否在一线企业落地,还有待韶光的进一步考验。

Transactional Outbox vs CDC

好的,前面我先容理解决数据的事务性分发的两种落地模式,一种是事务性发件箱模式,其余一种是变更数据捕获模式,这两种模式实在各有利害,为了帮助大家做选型决策,我这边对这两种模式进行一个比较,请看上面的比较表格:

首先比较一下繁芜性,事务性发件箱相比拟较大略,大略做法只须要在数据库中增加一个发件箱表,然后再启一个Poller线程拉和发就可以了。
CDC技能相比拟较繁芜,须要你深入理解数据库的事务日志格式和协议。
其余Miner的实现也不大略,要担保不丢,如果生产支配的话,还要考虑Miner的高可以支配,还有监控告警等环节。
第二个比较的是Polling延迟和开销。
事务性发件箱的Polling是近实时的,同时如果频繁拉数据库表,难免会有性能开销。
CDC是比较实时的,同时它不侵入数据库和表,以是它的性能开销相对小。
第三个比较的是运用侵入性。
事务性发件箱是有一定的运用侵入性的,运用在更新业务数据的同时,还要单独发送。
CDC对运用是无侵入的,由于它拉取的是数据库事务日志,这个和运用是不直接耦合的。
当然,CDC和事务性发件箱模式并不排斥,你可以在运用层采取事务性发件箱模式,同时仍旧采取CDC到数据库去捕获和发件箱中的对应的事务日志。
这个方法对运用有一定的侵入性,但是通过CDC可以得到较好的数据同步性能。
第四点是适用场合。
事务性发件箱紧张适用于中小规模的企业,由于做法比较大略,一个开拓职员也可以搞定。
CDC则紧张适用于中大规模互联网企业,最好有独立框架团队卖力CDC的管理和掩护。
像Netflix/Airbnb这样的一线互联网公司,也是在中后期才引入CDC技能的[参考附录1/2/3]。
Single Source of Truth

前面我解答了如何办理微做事的数据同等性分发问题,也给出了可落地的方案。
末了,我特殊解释在实践中进行数据分发的一个原则,叫Single Source of Truth,翻成中文便是单一真实数据源。
它的意思是说,你要实现数据分发,目标做事可以有很多,但是一定要把稳,数据的主人只能有一个,它是数据的威信记录系统(canonical system of record),其它的数据都是只读的,非威信的拷贝(read-only, non-authoritative copy)。

换句话说,任何时候,对付某类数据,它主人该当是唯一的,它是Single Source of Truth,只有它可以修正数据,其它的做事可以得到数据拷贝,做本地缓存也没问题,但是这些数据都是只读的,不能修正。

只有遵照这条原则,数据分发才能正常事情,不会产生不一致的情形。

结论Netflix和Airbnb等一线互联网公司的实践证明,企业要真正实现疏松耦合、可扩展和高性能的微做事架构,那么底层的数据分发同步能力是非常关键的。
数据分发技能,大略的可以采取事务性发件箱模式来实现,重量级的可以考虑变更数据捕获CDC技能来实现。
事务性发件箱可以参考Killbill Queue的实现,CDC可以参考阿里的Canal等开源产品来实现。
最大略的双写也是实现数据分发的一种办法,但是为了担保同等性,须要引入后台校验补偿程序。
末了,数据分发/同步的原则是:确保单一真实数据源(Single Source of Truth)。
系统中数据的主人该当只有一个,只有主人可以写入数据,其它都是只读拷贝。