微服务的数据聚合Join_cn_hhaip的专栏-CSDN博客
文章目录
单库join
传统SQL数据库,通常正规化(normalization)的方式来建模数据。正规化的好处是 数据冗余少
,不足之处是数据聚合Join会比较麻烦,可能实际Join的时候,需要将几张相关表,通过主键和外键关系才能Join起来。我们知道,Join是一种开销比较大的SQL运算,当数据量少的时候,这种开销通常OK。但是随着企业规模逐渐变大,数据库中的数据量也会越变越大,相应地,Join的开销也会越来越大。于是,Join变慢的问题就会越来越突出,通常表现为用户的查询慢,严重时,复杂的Join可能会导致数据库繁忙不响应甚至宕机。之前我在上家公司工作的时候,就曾经经历过几次复杂Join造成DB宕机的事故。可以说,单库Join性能慢的问题,是目前很多网站的普遍痛点问题。所以,去数据库Join,是很多企业当前正在做的数据库优化工作之一。
优点:
- 数据冗余少
- 无需跨微服务查询,实现方便,不用考虑分布式问题
缺点:
- join多张表的时候一样比较麻烦(SQL运算大)
- join非常影响性能,随着数据量越来越大,传将越慢,对CPU和内存的占用越来越大,甚至影响其他业务正常进行或DB宕机
- join往往是数据库瓶颈之一
分布式微服务聚合join
分布式聚合join服务层的另两种名字 1、Aggregator聚合服务 2、BFF服务,BFF是Backend for Frontend的简称
在分布式的微服务时代,数据聚合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、具有实时性和强一致性的好处
2、聚合服务层使相同业务更加内聚,不同业务更低耦合
分布式聚合的缺点:
1、一个是 N+1问题
。有的时候,为了获得A和B服务的聚合数据,可能A只需要调用一次,但是B却需要调用N次才能获取完整数据。这个就是软件开发领域臭名昭著的N + 1问题,它通常是性能杀手。
2、第二个问题是数据量的问题。聚合服务需要把A和B的部分数据都加载到本地内存,然后才能进行聚合运算。当访问量大的时候,聚合服务会占用大量内存开销,严重时可能会造成内存被撑爆。
3、第三个问题就是随着后台基础领域服务的数据量越来越大,总体聚合服务的性能也会随着越变越慢。需要特别说明的是,如果不做缓存的话,这种分布式聚合,对于每个请求都是会重复执行和运算的,也就是会有大量的频繁和重复的聚合运算,会白白消耗大量CPU/内存等资源。
CQRS
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可以根据本地已有的数据,加上发送过来的变更数据,实时/或者近实时的聚合计算出商品反馈数据,并存入本地数据库缓存起来,这个就是数据分发+预聚合的思路。
坤同例子:
KA-VMC接收kafka的机器消息,然后通过http调用(可改成缓存)机器和商品信息提前组装好数据,最后保存到本服务业务库。然后通过数据分发技术,将数据分发不到不同的业务服务。
**注意:**这个方式和前面的聚合层BFF方式是有本质区别的。前面的方式是每次请求都要触发重复计算的,而这里的方式是一次性预先聚合好,并且缓存起来,后面的查询都是查询的缓存数据,所以这是一个提前预聚合的思路, 有一定的延迟
。
这个方式其实就是 反正规化(denormalize)
的方式,它把原来正规化的需要聚合Join的数据,通过反正规化方式预先聚合并缓存,这样可以大大加快后续的查询。另外,学过数据库的同学应该知道,数据库当中有 物化视图(Materialized View)
这样一个概念,它本质上也是一种预聚合的思路。物化视图把底层的若干张表,以反正规化的方式,实时地聚合起来,提供方便查询的视图View。并且,当底层数据表发生变更的时候,物化视图也可以实时同步这些变更(相当于实时聚合Join)。现在你应该明白,我们这里所讲的数据分发+预聚合方式,其实它的思想和物化视图是相同的,只不过我们这里讲的是 分布式的物化视图
。
注意实时预聚合能够大大提升查询的性能,但是技术门槛也比较高。当数据变更发生的时候,或者说当变更数据流过来的时候,你就需要对数据流进行实时运算。这个计算越实时,查询的实时性就越好,当然,所需要的技术门槛也越高。之前我们提到过的Kafka Stream,它就是支持实时流式聚合的一个开源产品。
CQRS模式
基于数据分发与聚合还有一个名称是CQRS(Command/Query Responsibility Segregation) ,它是将数据的写入和查询职责分离的一种模式
上面讲的数据分发+预聚合的方式,在互联网领域还有一个更时髦的名称,叫 CQRS
,英文全称是Command/Query Responsibility Segregation,翻译成中文是 命令/查询职责分离模式
。
这个模式的总体形态,如上图所示。CQRS的左边是Command命令端,这一端通常只负责写入。CQRS的右边是Query查询端,这一端通常只负责读取。底层一般是数据分发技术,比如事务性发件箱、CDC还有MQ,它们将命令端的变更数据,实时或者近实时地同步到查询端。
写入端的数据存储,通常采用传统SQL数据库(MySQL Oracle DB2)
。而 查询端则可以根据需要选择最适合的存储机制
,比如说如果通过KV键查询的话,可以采用Redis或者Cassandra;通过关键字查询的话,可以采用ElasticSearch。当然,还可以引入离线批处理Hadoop,甚至是实时计算平台Spark/Flink/KafkaStream等。不管查询端采用何种存储技术,它们的目标都是提升查询的规模化和性能。
总体上,从命令端到查询端,数据的流动变化过程,就是一个反正规化,适合各种快速查询需求的过程。在三层应用时代,为了提高查询性能,我们通常采用 数据库的读写分离技术
。到了微服务时代,这个技术的思路仍然适用,只不过它向上提升到服务层,演变成CQRS模式了。所以也可以说, CQRS是服务层的读写分离技术
(读服务和写服务)。
**注意:**合理应用CQRS技术,可以大大提升查询的性能,同时提升企业数据规模化的能力。但是对于CQRS/CDC这类技术,它们的技术门槛高,一般小公司可能玩不起,只有到一定体量的公司才会考虑,例:Netflix的落地案例。
CQRS和最终一致性
采用CQRS模式以后,客户从命令端写入数据,然后变更数据分发到查询端,查询端再聚合生成查询视图,这中间难免会有网络和聚合计算延迟,所以这个模式并不保证写入和查询数据的强一致性,而是演变成最终一致性。
最终一致性会带来UI更新的问题。举个例子,如PPT所示,用户通过UI到Order订单服务创建一个新订单,这个订单落到订单服务的数据库中,然后订单服务在返回用户响应的同时,后台再异步发消息到Order Query订单查询服务,然后订单查询服务收到消息,就去做聚合更新订单视图的工作,这个工作可能需要耗费一定的时间。如果新视图在被更新之前,用户又通过UI来查询新订单数据,那么他可能会查不到数据。也就是说,CQRS的最终一致特性,会引入一定的时间差,而且这个时间差还是不确定的。
**总结:**由于聚合的延迟性,CQRS没有强一致,可有最终一致性
**注意:**考虑到网络的不稳定和不可靠,数据分发组件可能会因为网络等因素而重发数据( At least Once
至少一次),所以,查询端一般需要对数据进行去重或者做 幂等
处理(如:kafka消息就可能重复,幂等不可避免)。
CQRS和UI(前端)更新策略
为了解决最终一致性带来的时间差问题,业界通常有三种实践的UI更新策略,请看上图:
第一种策略是 乐观更新
。UI在发出请求后,马上更新UI,页面反应已经更新的数据状态。
例:比方说你点赞了某社交网站上的视频或图片,页面马上会显示一颗红心。然后页面后台再通过ajax等方式查询更新结果,如果确认更新成功,那就不需要做什么;如果确认更新失败,只需将页面状态回滚即可。这种方式仅适用于一些简单的场景。
第二种策略是采用 拉模式
。UI向命令端发出请求时,请求中带上版本号,然后通过ajax等方式不断轮询查询端,并检查更新后的视图的版本号是否和请求的版本号一致,直到版本号匹配为止,也就是等到视图明确更新成功或失败为止。
第三种策略是采用 发布订阅模式
。UI向命令端发出请求,同时通过websocket等方式订阅在查询端,查询端更新好视图,通过发消息通知UI更新页面展示。
架构2005 VS 2016
图是从2005到2016,互联网网站架构发生的变化
重点提一下2016年的网站架构的几个显著特点:
第一个当然是中间引入了微服务。微服务可以用不同语言栈开发,而且可以拥有独立的数据存储。
第二个是 微服务前端引入了BFF聚合服务层,实现实时和强一致性的聚合Join
。
第三个特点是后台引入了 CQRS/CDC/大数据/AI
等技术。这些技术引入的主要目标,说白了,无非就是对数据库中的数据(包括变更数据),进行聚合或者再加工计算,生成能够进一步产生业务价值的读视图,再通过微服务或者BFF服务等方式暴露给用户。如果把下半部分逆时针旋转90度,就是一个典型的CQRS模式图。
从总体架构上看,2016年的网站架构和2005年相比,最大的区别是2016年的网站架构是一个更大规模的读写分离架构。
本文所讲的内容,包括 微服务架构
, 数据分发技术
, CDC
,还有 BFF聚合服务
等等是现代网站架构的一个基础。
参考:https://www.bilibili.com/read/cv7547597