软件服务架构的一些感悟
早想着要写一篇博客,但由于各种原因(其实因为懒),迟迟没有动笔。今日下决心,写写关于软件服务架构的一点感悟。
三层架构
从读大学开始,老师就讲三层架构。后来的项目实施基本上也都是三层架构。对于小型项目,业务逻辑相对简单的项目,三层架构是快速迭代的利器。随着项目的迭代,功能越来越多,业务逻辑越来越复杂,业务开发团队越来越庞大,单体的三层架构就逐渐暴露出它的不足。因为这样的项目是一个高内聚、高耦合的项目,一个类、一个方法可能被多处引用,给维护带来了极大的不方便,要修改一个方法、修改一个字段,可能会影响到所有引用它的方法。如果项目中还存在包引用、dll引用,可能还会导致包名冲突、命名空间冲突。这种情况下,我们会去想,如何降低业务复杂度?答案是拆分服务,微服务化。开发团队的壮大对于高效的管理也是一个问题,微服务化后,原先业务团队被拆分成多个微服务团队,也降低了管理的难度。
微服务架构
(图1) | (图2) |
对于微服务如何划分,粒度多粗多细,每个团队有每个团队的划分法。从我个人的经历来看,无外乎一种细粒度划分,一种粗粒度划分。
图1,展示了细粒度的服务划分通常的一个结构。对于一个复杂的业务,如果服务划分过细(这样的服务通常绝对禁止跨库访问),业务逻辑层必然要对服务进行组装,不管是RPC的调用方式,还是Rest的方式,此时的业务逻辑层仍然是一个高内聚、高耦合的模块。对于一个需要快速迭代的产品,这样的架构快速不起来。比如一个下单服务,业务逻辑层的负责团队需要等待商品服务相应的接口、订单服务相应的接口、库存服务相应的接口等下单涉及到的接口准备就绪,才能开始编写服务。
图2,展示课粗粒度的服务划分通常的结构。这时,一个微服务接口是一个粗粒度的接口,微服务与微服务之间不允许相互调用,而允许跨库访问,降低了服务之间的依赖,这样的的微服务是一个高内聚、低耦合的模块。还是以下单服务来举例,此时负责订单服务的团队,编写服务的时候,不必等待商品团队、库存团队、用户团队(收货地址)等其他团队,他们自己可以快速着手开发下单服务。负责库存服务的团队,只写他们关心的服务,比如商品的采购、入库。而订单团队需要的库存扣减操作接口,订单团队自给自足。(这里引申一个问题,从DDD的思想来看,一个Domain的边界,到底在哪里?拿库存来说,一切有关库存的操作都写到这个Domain叫做DDD?如果只有订单模块会扣减库存,退单后会加回库存,这样的接口还写在库存服务模块?这类接口属于订单领域还是库存领域?)
微服务,不仅能够降低业务复杂度、开发团队管理难度,而且由于微服务的特性,使得部署软件的资源能更合理高效的应用,降低资源成本。
高并发
(图3)
软件并发量逐渐提高,不管是三层架构、还是微服务,优化的途径都差不多,读写分离-》加缓存-》分库分表。上方所示图2到图3,展示了利用一些数据访问中间件(Sharding-JDBC、Macat、Atlas&&)实现分库分表的架构。
重构
对于一个潜在的可能存在高并发场景的项目,如何能在遇到高并发的时候,从容地重构一个项目?我有一些浅见,供大家讨论:
(临时搭建的项目,凑合看吧)
如上图所示,项目划分了两个Package,一个Product,一个Membership,请注意,ProductController里面引用了Membership这个Package里的UserService,换句话说,就是ProductController依赖了Membership这个Package里的UserService。假如我们约定,不允许跨Package依赖,那么Product这个Package里有需要用到User相关的服务的时候,自己写到Product这个Package里。那么当我要进行服务拆分的时候,只需要把Product这个Package单独打包成一个jar,即可拆分完成。这样约定,方便从三层架构重构到上图2的架构,对将来可能的分库分表没有任何影响。
如果觉得不允许跨Package调用代码不能复用,造成了代码冗余,也可以采用下面的项目结构:
Service以下层(包含Service)仍然安装单体应用的开发模式开发,拆分时,只需要将Controller层拆开打包成jar即可,后续的维护要求拆分后的团队各自维护各自的代码,而不是继续一个团队维护底层。
如果业务进一步复杂,采用图2所示架构也会遇到问题。比如库存数据,如果除了订单服务之外还有其他的服务都去修改库存数据的话,在没有统一日志或者这样的服务数量比较多的情况下,想要知道这个库存数据是怎么来的,会非常困难。这个时候只能重构成图1的架构。采用图1的架构,需要业务专家来定义接口,使其尽可能地适应多的使用场景。