如何解决微服务的数据聚合Join问题?
单库Join问题
有后端开发经验的同学应该了解,对于传统SQL数据库,我们通常以正规化(normalization)的⽅式来建模数据。正规化的好处是数据冗余少,不⾜之处是数据聚合Join会⽐较⿇烦。实际Join的时候,需要将⼏张相关表,通过主键和外键关系才能Join起来。我们知道,Join是⼀种开销⽐较⼤的SQL运算,当数据量少的时候,这种开销通常OK。但是随着企业规模逐渐变⼤,数据库中的数据量也会越变越⼤,相应地,Join的开销也会越来越⼤。于是,Join变慢的问题就会越来越突出,通常表现为⽤户的查询慢,严重时,复杂的Join可能会导致数据库繁忙不响应甚⾄宕机。之前我在上家公司⼯作的时候,就曾经经历过⼏次复杂Join造成DB宕机的事故。可以说,单库Join性能慢的问题,是⽬前很多⽹站的普遍痛点问题。所以,去数据库Join,是很多企业当前正在做的数据库优化⼯作之⼀。
分布式聚合Join的问题
在分布式的微服务时代,数据聚合Join的问题并没有消失,它变成了另外⼀种形式。请看上图,假设有
两个基础领域服务,⼀个叫customer-service,另外⼀个叫order-service。根据微服务有界上下⽂和职责单⼀的原则,customer-service只负责客户数据,order-service只负责订单数据。但是前端业务需要⼀个order-history-API,这个API⽀持查询⽤户的历史订单,它既要提供⽤户详细信息,也要提供⽤户的历史订单信息。为此,我们需要引⼊这样⼀个order-history-API服务,它同时去调⽤order-service和customer-service,获得数据后再在本地进⾏聚合Join,然后再对外提供聚合好的客户+订单历史数据。总体上,上图的order-history-API做的事情,就是所谓的分布式聚合Join。这个API还有两个专门的称谓,⼀个叫Aggregator聚合服务,另外⼀个叫BFF服务,BFF是Backend for Frontend的简称,它的主要⼯作也就是聚合Join。
在中⼤规模互联⽹系统中,分布式聚合Join⾮常常见。基本上你上任何⼀个⼤⼚的⽹站,⽐⽅说天猫,京东,或者美团,携程等,它们的⽹站页⾯上的数据,⼤部分都是通过后台的分布式聚合服务聚合出来的。所以,聚合服务层(或者称BFF层),是现代互联⽹和微服务架构中普遍存在的⼀个架构层次。
在⼤部分场景下,分布式聚合服务可以满⾜需求,并且它还具有实时性和强⼀致性的好处,但是它同时也引⼊了新的问题:
1. ⼀个是N + 1问题。有的时候,为了获得A和B服务的聚合数据,可能A只需要调⽤⼀次,但是B却需要调⽤N次才能获取完整数据。这
个就是软件开发领域臭名昭著的N + 1问题,它通常是性能杀⼿。
2. 第⼆个问题是数据量的问题。聚合服务需要把A和B的部分数据都加载到本地内存,然后才能进⾏聚合运算。当访问量⼤的时候,聚合
服务会占⽤⼤量内存开销,严重时可能会造成内存被撑爆。
3. 第三个问题就是随着后台基础领域服务的数据量越来越⼤,总体聚合服务的性能也会随着越变越慢。需要特别说明的是,如果不做缓
存的话,这种分布式聚合,对于每个请求都是会重复执⾏和运算的,也就是会有⼤量的频繁和重复的聚合运算,会⽩⽩消耗⼤量CPU/内存等资源。
Denormalize + Materialize the View
企业实践表明,当互联⽹公司的体量规模发展到⼀定的阶段,为了解决分布式聚合Join慢的问题(或者是为了解决传统SQL数据库Join慢的问题),它们通常会采⽤另外⼀种称为数据分发+预聚合的新⽅式。
怎么理解这种⽅式呢?我再举个例⼦,请看上⾯的图。我们这⾥也有两个基础领域服务,⼀个是商品服务item-service,另外⼀个是订单反馈服务order-feedback-service。但是前端业务需要⼀个商品反馈服务item-feedback-service,它的数据是由item-service和order-feedback-service聚合的结果。为了实现这个order-feedbak-service,我们可以⽤前⾯的聚合(或者说BFF服务)来实现,但是那种做法可能每次查询的开销较⼤,性能⽆法满⾜要求。为了解决性能问题,我们可以改⽤,⽐⽅说事务性发件箱技术,或者CDC变更数据捕获技术,也就是基于数据分发+预聚合的思路来实现这个服务。当item-service或者order-feedback-service有数据变更的时候,我们把它们的变更,通过数据分发技术,分发到item-feedback-service这个聚合+查询服务。item-feedback-service可以根据本地已有的数据,加上发送过来的变更数据,实时/或者近实时的聚合计算出商品反馈数据,并存⼊本地数据库缓存起来。这个就是数据分发+预聚合的思路。
这个⽅式和前⾯的聚合层BFF⽅式是有本质区别的。前⾯的⽅式是每次请求都要触发重复计算的,⽽这⾥的⽅式是⼀次性预先聚合好,并且缓存起来,后⾯的查询都是查询的缓存数据,所以这是⼀个提前预聚合的思路。
细⼼的学员会发现,这个⽅式其实就是反正规化(denormalize)的⽅式。它把原来正规化的需要聚合Join的数据,通过反正规化⽅式预先聚合并缓存,这样可以⼤⼤加快后续的查询。另外,学过数据库的同学应该知道,数据库当中有物化视图(Materialized View)这样⼀个概念,它本质上也是⼀种预聚合
的思路。物化视图把底层的若⼲张表,以反正规化的⽅式,实时地聚合起来,提供⽅便查询的视图View。并且,当底层数据表发⽣变更的时候,物化视图也可以实时同步这些变更(相当于实时聚合Join)。现在你应该明⽩,我们这⾥所讲的数据分发+预聚合⽅式,其实它的思想和物化视图是相同的,只不过我们这⾥讲的是分布式的物化视图。
实时预聚合能够⼤⼤提升查询的性能,但是它的技术门槛也⽐较⾼。当数据变更发⽣的时候,或者说当变更数据流过来的时候,你就需要对数据流进⾏实时运算。这个计算越实时,查询的实时性就越好,当然,所需要的技术门槛也越⾼。之前我们提到过的Kafka Stream,它就是⽀持实时流式聚合的⼀个开源产品。
CQRS模式
上⾯讲的数据分发+预聚合的⽅式,在互联⽹领域还有⼀个更时髦的名称,叫CQRS,英⽂全称是Command/Query Responsibility Segregation,翻译成中⽂是命令/查询职责分离模式。
这个模式的总体形态,如上图所⽰。CQRS的左边是Command命令端,这⼀端通常只负责写⼊。CQRS的右边是Query查询端,这⼀端通常只负责读取。底层⼀般是数据分发技术,⽐如事务性发件箱、CDC还有MQ,它们将命令端的变更数据,实时或者近实时地同步到查询端。
写⼊端的数据存储,通常采⽤传统SQL数据库。⽽查询端则可以根据需要选择最适合的存储机制,⽐如说如果通过KV键查询的话,可以采⽤Redis或者Cassandra;通过关键字查询的话,可以采⽤ElasticSearch。当然,还可以引⼊离线批处理Hadoop,甚⾄是实时计算平台Spark/Flink等。不管查询端采⽤何种存储技术,它们的⽬标都是提升查询的规模化和性能。
总体上,从命令端到查询端,数据的流动变化过程,就是⼀个反正规化,适合各种快速查询需求的过程。在三层应⽤时代,为了提⾼查询性能,我们通常采⽤数据库的读写分离技术。到了微服务时代,这个技术的思路仍然适⽤,只不过它向上提升到服务层,演变成CQRS模式了。所以也可以说,CQRS是服务层的读写分离技术。
值得⼀提的是,合理应⽤CQRS技术,可以⼤⼤提升查询的性能,同时提升企业数据规模化的能⼒。但是对于CQRS/CDC这类技术,它们的技术门槛不低,⼀般⼩公司可能玩不起,只有到⼀定体量的公司才会考虑。后续,我会介绍⼀些CDC/CQRS技术在前沿⼤⼚,⽐如Netflix的落地案例。
CQRS和最终⼀致性
采⽤CQRS模式以后,客户从命令端写⼊数据,然后变更数据分发到查询端,查询端再聚合⽣成查询
视图,这中间难免会有⽹络和聚合计算延迟,所以这个模式并不保证写⼊和查询数据的强⼀致性,⽽是演变成最终⼀致性。
最终⼀致性会带来UI更新的问题。举个例⼦,如PPT所⽰,⽤户通过UI到Order订单服务创建⼀个新订单,这个订单落到订单服务的数据库中,然后订单服务在返回⽤户响应的同时,后台再异步发消息到Order Query订单查询服务,然后订单查询服务收到消息,就去做聚合更新订单视图的⼯作,这个⼯作可能需要耗费⼀定的时间。如果新视图在被更新之前,⽤户⼜通过UI来查询新订单数据,那么他可能会查不到数据。也就是说,CQRS的最终⼀致特性,会引⼊⼀定的时间差,⽽且这个时间差还是不确定的。
另外,考虑到⽹络的不稳定和不可靠,数据分发组件可能会因为⽹络等因素⽽重发数据(At least Once语义),所以,查询端⼀般需要对数据进⾏去重或者做幂等处理。
CQRS和UI更新策略
为了解决最终⼀致性带来的时间差问题,业界通常有三种实践的UI更新策略,请看上图:
1. 第⼀种策略是乐观更新。UI在发出请求后,马上更新UI,页⾯反应已经更新的数据状态。⽐⽅说你
点赞了某社交⽹站上的视频或图
⽚,页⾯马上会显⽰⼀颗红⼼。然后页⾯后台再通过ajax等⽅式查询更新结果,如果确认更新成功,那就不需要做什么;如果确认更新失败,只需将页⾯状态回滚即可。这种⽅式仅适⽤于⼀些简单的场景。
2. 第⼆种策略是采⽤拉模式。UI向命令端发出请求时,请求中带上版本号,然后通过ajax等⽅式不断轮询查询端,并检查更新后的视图
的版本号是否和请求的版本号⼀致,直到版本号匹配为⽌,也就是等到视图明确更新成功或失败为⽌。
3. 第三种策略是采⽤发布订阅模式。UI向命令端发出请求,同时通过websocket等⽅式订阅在查询端,查询端更新好视图,通过发消息
通知UI更新页⾯展⽰。
架构2005 VS 2016
本⽂的最后,我们再来补充⼀张图,这张图表达的是从2005到2016,互联⽹⽹站架构发⽣的巨⼤变化。这张图来⾃ThoughtWorks的⼀篇⽂章,我做了适当的改编。
这张图还是⽐较容易看懂的,所以具体细节我这⾥不展开。我这⾥重点提⼀下2016年的⽹站架构的⼏个显著特点:
1. 第⼀个当然是中间引⼊了微服务。微服务可以⽤不同语⾔栈开发,⽽且可以拥有独⽴的数据存储。
2. 第⼆个是微服务前端引⼊了BFF聚合服务层,实现实时和强⼀致性的聚合Join。
3. 第三个特点是后台引⼊了CQRS/CDC/⼤数据/AI等技术。这些技术引⼊的主要⽬标,说⽩了,⽆⾮就是对数据库中的数据(包括变更分布式和微服务的关系
数据),进⾏聚合或者再加⼯计算,⽣成能够进⼀步产⽣业务价值的读视图,再通过微服务或者BFF服务等⽅式暴露给⽤户。如果把下半部分逆时针旋转90度,就是⼀个典型的CQRS模式图。
从总体架构上看,2016年的⽹站架构和2005年相⽐,最⼤的区别是2016年的⽹站架构是⼀个更⼤规模的读写分离架构。另外,⽀持2016年⽹站架构的底层技术,和本⽂所讲的内容,包括微服务架构,数据分发技术,CDC,还有BFF聚合服务等等,都是密切相关的。所以波波认为,理解本⽂的内容,是理解现代⽹站架构的⼀个基础。
课程推荐

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。