蚂蚁SOFA系列(1) - 聊聊SOFA的模块化
作者:404,转载请注明出处。欢迎关注公众号:404P。
SOFA是蚂蚁自研的一套金融级分布式中间件,目前正在逐步向业界开源。SOFA的全称有两个,最早是Service Oriented Fabric Architecture,即面向服务的架构。随着2018年的开源,其全称改为Scalable Open Financial Architecture,即可扩展的开源金融架构。
SOFA技术栈包含了微服务架构体系的各类组件,主要包括RPC框架,服务注册中心,分布式链路追踪,Metrics监控度量等。
本文我们来聊聊SOFA的模块化。
一 什么是模块化
模块化在计算机领域是经常讨论的话题,在学校学编程语言的时候,教科书上说程序设计要遵循模块化原则。
模块化程序设计是指在进行程序设计时将一个大程序按照功能划分为若干小程序模块,每个小程序模块完成一个确定的功能,并在这些模块之间建立必要的联系,通过模块的互相协作完成整个功能的程序设计方法。
上面这段话引自百度百科,其实精炼下就是:高内聚和低耦合。
二 模块化思想演变
1 代码设计模块化
最早学编程的时候,实现一个功能,所有逻辑放到一个main函数里去,后来发现理不清了,就把main里面的逻辑抽成几个函数。这是模块化吗?是。这个模块化是最基本的代码设计能力,增强代码的可读性、可维护性和可扩展性。如下图,计算成绩的时候,没有把所有逻辑放在A中,而是分在B1和B2中,单独计算,最后A调用B1、B2来实现。
在这种简单的程序设计中,往往更注重逻辑的内聚性,只要内聚做好了,往往就是低耦合的。
2 业务领域模块化
在真实做一些项目的时候,业务系统比较复杂,要实现的功能很多。这个时候出现了横向和纵向的模块化设计。横向的就是分层设计,纵向的就是按照不通的业务领域来设计。
一个业务系统,横向的模块化切分主要分为三大层:Web层,Service层,DAL层。
在业务初期,功能往往都是写在一个业务系统的,比如订单模块Order、库存模块Stock。在maven中这些模就是不同的module,但运行都是在同一个JVM,同一个web容器中的。
这在种情况下,订单服务依赖于库存服务怎么办?订单模块的pom中引入库存模块的依赖。然后注入bean。
public class OrderService {
@Autowired
private StockService stockService;
}
这种横纵模块化思想随着业务的复杂开始进化了。订单、库存模块虽然在一个Service层,但属于明显不同的领域,已经被分成不同的模块了,具有很好的内聚性,而且要引用其它模块,必须引入pom依赖之后才能访问,具有不错的隔离型。
3 业务系统模块化的弊端
但是,这样设计的耦合性还是不够低,隔离性不够强。
程序员小胖:还不够强吗?订单模块和库存模块都在不同的module了,不费任何力气,就可以直接把源代码分成两个项目,由不同的团队来写了。
404P: 不够,现在的隔离性最多在开发层,能够相互隔离开发。运行时呢?
所有module的bean都在同一个spring context中,A模块可以任意引用B模块的bean,开发同学引入另一个module之后,不太清楚该module中哪些是对外提供的的接口,哪些bean是可以直接注入调用的,哪些Bean是内部Bean,不适合直接去注入的。
长期如此,一个模块的bean被不断地注入到另外一个模块被调用,那么其运行时的隔离性就差了。运行时没有做好隔离,是服务拆分的一大痛点。大概就是下图这个样子。
程序员小胖:请继续你的表演。
404P:随着业务的增长,必然会把Order和Stock拆分,成为不同的子业务系统。代码在不同的module,拆分开来很简单,但是拆分后两个业务系统是运行在不同的SpringContext中的。而bean的注入只在一个SpringContext有效,所以之前通过bean注入来实模块交互的地方需要梳理出来,变成系统之间的接口交互才能实现服务拆分。
程序员小胖:听你这么一说,有道理。模块化思想都是跟着架构思想走的啊。如果要考虑未来模块拆分成服务,就需要考虑好运行时隔离,也就是运行时的低耦合交互。
404P: 是的。看看SOFA怎么做的。
三 SOFA模块化
为了防止这种模块之间滥用bean注入来交互。SOFA启动后,会为每个moudule创建一个SpringContext,每个module运行在各自的SpringContext中。不同模块之间的bean无法直接引用,具备了较好的运行时隔离能力。
那么当Order模块想引用Stock模块的Bean,怎么办呢?
首先,需要Stock模块有发布对外的公共bean,通过如下声明式发布(也可以通过注解方式):
<sofa:service ref="stockBeanA" interface="com.alipay.sofa.StockBeanA"/>
那Order模块怎么引用Stock模块中的公开bean呢?通过如下声明方式引用:
<sofa:reference id="stockBeanA" interface="com.alipay.sofa.StockBeanA"/>
Stock模块的stockBeanA已经公开发布,并且Order模块已经引用,那么Order模块在编码的时候,就可以直接注入bean stockBeanA了。
这种方式,我们可以很清晰地看到一个模块公开了哪些服务,引用了哪些服务。模块之间的交互变得非常清晰。
四 SOFA模块拆分成微服务
当一个SOFA应用开始变得复杂,开发团队成员开始增多时,就需要进行服务化了。比如,Order模块和Stock模块,不再是运行在一个系统了,而是要变成Order系统和Stock系统了。这意味着这两个领域之间的交互从模块级别的交互上升到应用系统级别的交互了。
这种服务化拆分需要考虑两点:
(1)模块化的交互是在同一JVM内存中不同SpringContext之间的交互,拆分成两个应用系统后,应用系统之间交互必然是通过网络请求来交互,必然要考虑远程通信的问题。
(2)模块之间的服务发布和引用与应用系统之间的服务发布和引用是否有差异,需要改造?
SOFA考虑了以上两点,要将一个SOFA模块拆成微服务是非常便捷的。
Stock应用发布的时候,如下:
<sofa:service ref="stockBeanA" interface="com.alipay.sofa.StockBeanA">
<sofa:binding.bolt/>
</sofa:service>
Order应用引用Stock的服务时,如下
<sofa:reference id="stockBeanA" interface="com.alipay.sofa.StockBeanA">
<sofa:binding.bolt/>
</sofa:reference>
可以看出,就是添加了个属性 ,
<sofa:binding.bolt/>
表示服务之间的交互是通过sofa bolt远程调用框架来完成,发布和引用方式几乎没有变化。这种简易的服务化拆分,为蚂蚁架构在服务化演进的过程中带来了很大的便利。
五 结语
随着问题域的复杂性越来越高,模块之间的隔离边界也有更高的要求,本文从简单的例子,逐渐演变到服务拆分,从而引出SOFA的模块化。SOFA基于SpringContext作为模块隔离边界,充分降低了模块交互的耦合性,同时也为后续服务拆分提供了便利。
关于SOFA模块化的实现原理,将另起一文,欢迎关注下方公众号,更多思考,与你分享。