SaaS化是企业实现价值最大化的关键走向,也是技术积累沉淀的有效证明。为了使得SaaS能力快速铺设,我们基于市场成熟的多租户业务设计了SaaS底座,可以赋能给各个应用,快速获得SaaS能力。
本底座宗旨是为了满足三个特性:易用性,可复用,可扩展。在保证高质量代码的基础上,对核心业务进行合理的抽象,基于SPI的方式提供更高的可定制化,基于微服务化让接入变得更加高效解耦。
基本目标是做到三点:租户隔离、无缝切换、租户支持个性化定制。最高隔离级别的数据库实例隔离成本太高,pass,于是我们选用次隔离级别的方案,也就是基于Schema进行隔离。每个租户的每个应用都有自己独立的数据库,应用间共享租户信息。每个租户拥有一个数据库账号,只可以操作自己拥有的数据库,这样保证了隔离性的同时又有很大的可定制性。对于切换业务来说,基于请求上下文 + 动态数据源也可以无缝切换。
本小节将会确定系统使用的技术框架和技术,对于部分技术会进行详细描述。
web框架虽然不是一个系统的核心所在,却是系统性能瓶颈的根本所在。
建议选型为:基于Netty实现,底层由Reactor支持的Spring Webflux 。经过大量的调研和最佳实践的分析,NIO是比BIO更优秀的解决方案,实现了多路复用I/O技术,大大成倍的提升吞吐量。做SaaS平台需要尽可能地压榨硬件性能,避免性能浪费。因此异步开发模式是首选。
其次,为了更高的兼容性,针对老项目的改造,我们还需要基于Tomcat和BIO模型实现。两种实现方式松耦合,可按需引入。
JDK必须保证为1.8或以上。如果可以的话建议使用最新LTS版本1.17。许多主流框架将在下个版本废弃JDK1.8,使用新版JDK也是未来趋势。尽管从1.9开始,Java废弃了J2EE组件,但是在新一代的Reactor技术栈上,我们不需要J2EE的相关支持,所以没有任何兼容性问题(已测试)。而J2EE的后继框架——Jakarta EE,Tomcat10默认使用方案,也可以完美替代传统API。
数据库引擎使用Mysql即可。
数据库驱动依旧推荐使用JDBC,异步模式的R2DBC并未完全成熟。使用JDBC的好处很多,其功能相当的全,几乎涵盖了所有数据库使用场景,且针对不同的数据库有非常高的兼容性。
由于基于JDBC,所以数据库连接池我们可选的依旧有Druid和HikariCp,两者都是非常优秀的连接池组件。
而ORM框架依旧使用最熟悉的MybatisPlus即可。有个非常好的消息是,除了开源社区活跃着很多优秀的Mybatis-Reactive框架,Mybatis官方也开始重视异步化版本改造,相信在不久的将来,我们无需改造原项目,就能在Webflux中用上全异步的mybatis了。
底座同时支持Standalone单体运行和微服务运行两种需求。
微服务使用灵活性最高的Spring Cloud,可以快速整合多个Spring Boot项目。基于Spring Cloud Alibaba还可以快速拥有可视化配置管理,差量实时同步,热更新等特性。此外,基于Spring Cloud的微服务架构能够更加灵活地进行扩展,可以同时兼容新旧架构,可具有更高的普适性。
使用性能非常高的Apache MinIO完成OSS的搭建,无论是稳定性和吞吐量以及未来趋势都优于FastDFS,HDFS。MinIO提供的用户级别权限、策略控制,能够在多租户环境下更精准地进行控制,此外,通过开发插件的形式可以完成诸如配额管理等更深层次的应用。
单机文件模式下考虑使用目录隔离。
系统普遍使用Redis作为缓存,对于多租户环境来说,需要更多中间临时变量的分布式缓存,这一点Redis可以发挥更大价值,此外,Jwt token的黑名单也会使用到Redis。Redis还是计数性业务的核心提供者。
建议选用RabbitMQ作为消息对接中间件。在租户创建应用时,必须初始化应用才能让应用可用,而初始化的步骤就包括资源分配、数据初始化、缓存初始化等操作,其中可能涉及到大量IO,创建动作需要放入队列以进行平峰处理,使CPU的负载处于均衡状态。此外,一些消息通知,归档操作等异步非及时性且阻塞时间较长的业务也都需要放入消息队列。
考虑到底座的通用性,需要将框架进行多种情况的适配,以方便系统整合。我们底层将基于通用框架进行实现,例如洞头村后台架构,其使用的框架是多个后台自研产品使用的架构,源于数据中台,进过长期不断地迭代和业务的扩充,已经相当成熟。
如果公司的任意项目想要接入SaaS能力,在改动量最小的情况下,仅需要启动底座项目,同时在原系统中引入底座的SDK,即可以最小的代价完成项目转换。
此时底座支持Standalone的模式运行。接入SDK后,使用少量配置和底座进行关联,SDK会在Runtime阶段对原系统服务进行拦截代理,基于REST通信的方式调用租户。
正常的SaaS化改造都建议使用微服务架构,因SaaS化业务会不断扩充,业务繁杂度会越来越大, 松耦合的架构才是项目发展的长足之道。
本方案部署底座时,需要先创建好租户相关表,并配置好系统表和租户表字段映射。
手动修改用户架构相关代码,修改用户拦截器,增加多租户环境上下文拦截器,这些修改可以预先封装到SDK中,在修改时可以事半功倍。此时底座建议使用微服务模式运行,因项目本身改动较大,为了日后的业务扩充和代码的可维护性,强烈建议使用微服务架构,本模式依然支持Standalone进行接入,以满足复杂多变的项目需求。
作为通用底座,扩展性是系统设计好坏的最直观评判标准。扩展性一般体现在业务扩展性和开发扩展性两个方面。
微服务底座,指本底座提供一套微服务基础环境,应用系统可以直接进行少量修改,引入公共Jar就可以接入SaaS支持,日后可以直接使用该环境进行服务的扩充。
微服务底座基于微服务本身的特性提供业务扩展性,后续可以以增加服务的形式扩展核心底座能力。
底座从开始架构便决定使用标准的模块化开发,每个模块都作为独立的完整功能,并且提供SPI,供用户动态替换自己的实现,拥有丰富底层能力的同时提供不错的扩展性和自定义性。
模块化是基于Java语言本身的特性,以及代码架构上的一些策略提供的开发扩展性。
本小节着重阐明SaaS底座中的核心模块,也是核心能力所在。这部分代码均以最高标准交付,要求高效且稳定。
动态多数据源作为SaaS平台的基石,需要以最高标准进行编写和测试。动态多数据源模块我们在实现数据中台时就已经实现了一版,并且经过几次的重大迭代,已经趋于稳定,但是仍然存在着一些问题,如某些数据源正常连接,却在某些特定条件下超时的问题。
在本次开发过程中,将基于原模块,进行自下而上的梳理和重构,优化数据库连接池底层参数,并提升代码质量,增加异常处理,并实现基于租户上下文的无感数据源切换。
多数据源切换在BIO环境中是基于ThreadLocal线程局部变量来实现的,在NIO环境下并不适用,相应的,需要增加ContextWrite的实现方式。
动态多数据源其实利用了最基本的代理模式。
动态多数据源包含两层实现:多数据源和动态数据源。
原本项目中存在的主数据源是固定的数据库实例和用户,而多数据源则是除了主数据源外,增加了若干扩展数据源,可以在代码里利用注解和上下文代码切换,这种实现方式叫做多数据源。
而动态数据源,则是系统中仅有一个动态数据源,或仅有一个主数据源和一个动态数据源,数据中台采用的就是第二种。动态数据源之所以称之为动态,是因为数据源并非配置在代码文件或在配置文件中写死,而是通过数据库维护或者通过其他动态方式如三方接口进行维护,在切换时,也是通过变量或动态表达式(Spring EL或Groovy)来确定切换的数据源。底层一般是一个存储了多个连接池的Map,以数据源key作为键,以连接池实例作为值。在上下文中进行解析切换。
所以综上所述,动态数据源≠多数据源,可能仅有一个配置的数据源,而动态多数据源,就是多数据源+动态数据源的组合体,也就是数据中台实现的方案,包括主数据源(平台数据源),扩展数据源(采集、治理、资产数据源)。
在SaaS化底座中,我们也需要实现动态多数据源,类似数据中台,但是不一样的是,我们不需要在代码中显式切换,而是由中间件或框架AOP进行无感切换,大大降低代码耦合度。
BIO环境中,实现动态数据源非常简单,但是在当下主流的NIO环境中则需要考虑的比较全面,需要完善的异常处理链路以及正确的订阅链路。这部分工作需要进一步梳理,可放在后面进行实现。
SaaS平台不光要考虑数据的存储和隔离,还要考虑用量和配额,这关系到系统负载和实际收入的比率,以及最终影响定价的规则。
存储配额,即文件以及相关归档的配额。类似于百度云,每个租户拥有的存储空间在申请时就已经固定,超过配额将无法继续产生新的文件。基于Minio的用户体系,我们需要扩展开发出一套配额管理逻辑。
Mysql数据库本身不支持存储配额,但是支持运行时配额。我们可以基于类似的语句限制运行时配额。
CREATE USER 'francis'@'localhost' IDENTIFIED BY 'frank'
-> WITH MAX_QUERIES_PER_HOUR 20
-> MAX_UPDATES_PER_HOUR 10
-> MAX_CONNECTIONS_PER_HOUR 5
-> MAX_USER_CONNECTIONS 2;
对于存储配额而言,我们有两个方案可选,一是使用旧版本的Mysql Quota Daemon守护进程,可限制mysql的磁盘空间,超过后的insert将被revert;二是自行开发守护逻辑,插入时利用钩子判定文件系统的大小和配额大小的关系,来决定是否舍弃操作。
SaaS系统还有个最常见的配额,也就是用户数配额。购买较低级别的套餐往往比较优惠的同时,带有着种种限制,用户在线数就是其一,这个工作非常简单,只要在缓存里计数,当前租户下超过多少用户在线即拒绝新用户的登录。
实现配额管理,主要就是实现租户在系统使用过程中的行为和访问控制,我们统一实现一个模块,提供核心的管理器——QuotaControlManager。基于QuotaControlManager,由配置类QuotaConfigProvider(配额配置提供者)提供适配和定制化。
配额管理是一个全局的功能,各部分限制在各个模块进行埋点处理。
存储配额由存储模块作为入口,QuotaControlManager进行控制。数据库配额由JDBC作为入口,QuotaControlManager以SQL拦截器的角色进行控制。用户配额以用户鉴权模块作为入口,QuotaControlManager限制整体在线用户数。
我们常说的数据库管理引擎,包括了数据库管理、数据表管理、表元数据管理、用户权限管理和其他一些数据库操作如视图、存储过程、事件等维护操作。
为了满足租户的创建过程,我们需要实现数据库的动态管理、用户的动态管理以及表、数据的初始化。在程序的角度来说,就是实现一套简单的DBMS管理工具,按模块划分,可以根据业务进行编排。
综上,我们需要实现这样一个数据库管理引擎:支持数据库核心管理操作,支持各种管理操作的编排。
基于JDBC提供的通用api,完成一套包含元数据读写,Schema读写,元数据比对,SQL生成器,语法解析器等功能的模块。目前已经出具自研Demo,可以进行调试。
表管理,支持高亮搜索,鼠标悬浮操作:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Uv2IBOZc-1675244012028)(/Users/wangyu/Library/Application Support/typora-user-images/image-20220726090902851.png)]
模型设计,支持修改表名和备注,包括字段管理、索引管理、表关联管理、SQL预览等:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WyfTJTit-1675244012029)(/Users/wangyu/Library/Application Support/typora-user-images/image-20220726091243530.png)]
封装数据库操作,基于代码进行编排,支持整体成功和整体回滚,并做好任务监控和消息队列的推入、消费。
主要的业务是,基于上面维护的元数据新建数据库后进行表结构初始化和数据初始化。
多租户环境最重要的就是不同租户登录到同一套服务端,能够用户无感知地连接到所属租户的数据库以及文件存储,并且正确读写,展示视图。
这一功能对于不同的应用,有着不同的处理方式。
对于我们即将开展的低代码开发平台,是以应用作为界限进行划分的,对应的,每个应用都具有独立的访问地址,这个独立的体现,可以是分配的二级到三级域名,也可以是路径上下文。总之所有能够在请求标识应用的唯一性即可。其调用流程如图:
对应的,对于大部分业务平台,是不需要区分应用的。这时我们就需要以租户为界限进行划分。对此一般的处理方式为,每个用户登录账号都具有统一的后缀,如某个租户的编号为example,该租户下的成员user1要登录系统,需要以 user1@example进行登录,这种模式适用于所有多租户系统。其调用流程如下:
多租户环境下,用户体系不再是简单的单一体系,而是多种维度的不同体系。用户体系架构按作用域可划分为平台级用户和租户级用户,按功能可划分为管理员用户和普通用户。
平台级用户,特指平台管理员。在多租户环境下,租户的开通、维护、升降配额、注销等,都由平台管理员完成。平台管理员是SaaS系统中权限最高的用户,具有所有资源的访问权限。平台级用户不包括普通用户,在SaaS环境下,平台级普通用户没有意义。
租户级用户分为租户管理员和租户用户。
租户管理员的账号由平台管理员分配,其权限限定在平台管理员指定的范围内,不可越界赋权。租户管理员可以增加其他租户管理员账号,或者增加租户用户账号。租户管理员拥有租户级别的最高权限,可以维护租户信息,管理租户级配置,维护租户人员组织架构。
租户用户作为终端用户使用的账户,由租户管理员创建和管理,不可更改租户级配置,仅具有个人设置权限。
对于低代码平台,一个租户可订阅多个应用,多个应用间可以实现租户用户的共享。同一个租户的不用应用间共享组织架构,人员信息,可以快速实现类似单点登录,数据共享等功能。
本章节将详细讲述租户架构的设计,将从租户生命周期、营销定价和配额,租户和应用的关系三个小节进行描述。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r234Cxjp-1675244012030)(/Users/wangyu/Desktop/租户创建.png)]
租户从产生到销毁,分为五个状态,如图:
租户由平台管理员统一开通,分配账号,并初始化租户资源。
平台管理员注册租户信息,完成租户信息的维护后,可以分配租户管理员账号。
租户完成注册后,会初始化租户数据库账号、数据库Schema、文件系统账号、文件系统Bucket,初始化过程在后台静默异步完成。资源初始化完毕后,此时可以将租户管理员账号发送给相应人员,告知其登录以进行租户管理操作。
租户未订阅前,仅支持租户管理员登录进行系统体验,此时平台为演示模式,配额最小化,部分管理模块设置为最小权限。
SaaS平台一般会制定几种定价方案,客户以订阅的方式对服务进行租用,才正式转为租户。
一般分为线上收费和线下收费两种方案,线上收费需要相关增值电信资质,且需要电子签章等功能以完成合同签订,收费后可交由系统自动完成租户功能的开通,适合外地和国际客户订购使用。线下收费不需要资质,需要线下和客户商谈签订合同,然后由平台管理员手动分配账号并告知,适合本地客户。
订阅后的租户可以获得订购的相应功能,由系统初始化资源后,即可立即使用。
租户在使用过程中如果需要升级配额,如用户体量增加,或业务需求增加,现有配置无法满足,则可以付费升级订阅。升级订阅后,如涉及到新功能模块的增加,需要动态扩展数据库,并完成数据迁移。
租户租用的服务分为终身和有限期两种。终身服务不涉及到续费操作,仅涉及升降配。但是对于有限制订阅来说,租户在订阅到期后一定时间内未完成续费,正在使用的系统资源将被停止,运行数据将由系统进行归档化处理,此时归档数据将在一定期限内进行保存,超时后将彻底删除。在归档化数据尚未清理之前,租户可以用过付费重新恢复服务。
如果客户不再需要租用服务,则需要进行租户销毁工作。此外,租户过期且归档后超过时限,也会触发租户销毁动作。虽然系统中使用逻辑删除,但是销毁后的租户账号原则上不可恢复,仅供大数据分析使用。
待讨论。
在低代码平台这种多租户多应用模式下,租户可以创建应用,此时租户的运作模式和上述情况稍有不用。
本章节主要描述底座用于提高性能的一些技术设计。
系统使用多种缓存提高性能,以JVM为内,中间件为外,逐步分为内存缓存,数据库缓存,Redis缓存,磁盘缓存四种缓存。
原则上,使用缓存的目的是为了加快高频数据的访问速度,可以缩短调用链路,减少系统消耗,以空间换时间。但是,缓存绝对不可滥用,过多的缓存会增加不必要的内存压力,所以我们必须着眼于系统的高频数据。为此,我们需要基于LRU算法组织我们的代码。
动态数据源环境下,有一个特殊的“缓存”,即数据源缓存,基于Map在内存中对数据库连接池进行缓存和全生命周期的管理,可以保证基本的连接复用。
在多租户环境下,最高频的无非就是租户信息、用户信息、权限信息,这些缓存一般都放在Redis中,可以由分布式系统共享访问。
数据库缓存是目前所有项目普遍使用的缓存,由Mybatis框架提供,也称为一二级缓存。本质上也是内存缓存,基于LRU实现,可以保障高频同参查询的快速响应。
磁盘缓存一般用于文件系统的任务以及异常关闭恢复先前状态。磁盘缓存的目的是灾备使用,并非为了提高性能。
因SaaS系统服务的对象不再是一个客户,而是任意多个客户,所以其CPU闲时率将大大降低。为了避免不定时出现的高负载情况,我们有必要引入消息队列进行处理。在租户初始化、消息通知、文件归档、接口回调等场景,都可以使用消息队列完成,此外,在租户消耗尽其并发配额后,可以将用户请求压入队列,等待并发线程释放后再继续执行。
SaaS平台架构最主要的一点,就是必须在设计之初就支持横向扩展,增加算力。
一般情况下,我们使用Nginx的负载均衡完成七层负载就可以满足大部分系统的负载情况了。这种模式下,是一台Nginx机器对外提供入口,负载多台机器负责输送数据到Nginx所在机器。但不得不说,该模式有一个最大的弊端,就是流量入口都集中在了Nginx端,遇到访问高峰期,单独的机器很难扛得住大量IO和网络带宽的损耗。
在稍微健壮的架构中,我们使用四层负载均衡结合七层负载均衡完成。即四层负载均衡(LVS集群)对外提供服务(虚IP),内部RS再根据区域、业务编程式地再进行七层负载均衡的流量转发(Nginx、HAProxy)。四层代理直接在传输层进行转发,直接建立源端和目的端的TCP连接,性能要显著优于七层(应用层)负载。
下图是LVS的DR(Direct Routing,直接路由)模式图。
以下是常见的四层+七层负载架构:
虽然Nginx和HAProxy也支持四层代理,但是性能要远低于LVS,LVS基于操作系统级别控制,是最常用的四层代理方案。
有条件的话还是建议使用支持负载均衡的硬件,如F5,其封装的物理端口和底层模块能够最高效实现甚至是二层三层负载均衡,达到真正的高效。基于对外的VIP(虚IP)提供对外服务,对内通过MAC地址(二层负载)、IP地址(三层负载)进行流量转发。
综上,因硬件负载均衡价格昂贵,我们经常采用软件负载均衡,即纯Nginx负载模式,或基于LVS的DR、NAT模式做四层转发,结合Nginx转发后端流量完成负载均衡。
微服务模式依赖于SpringCloud,天生就是分布式架构。我们一般使用微服务专用的Spring Cloud Loadbalancer,可以以编程式的方式控制Ribbon,且和Spring Cloud无缝兼容。新版本的Nacos也将依赖从Netflux Ribbon换成了Spring自家“亲儿子”Spring Cloud LoadBalancer,所以我们可以放心使用。
在Gateway的上层,我们还可以基于四层负载均衡器LVS进行上层负载,提升性能。
根据实际需要设计。
需要提到的是,目前Spring Native和GraalVM盛行,毫秒级启动项目,超低内存占用,线上微服务环境建议尝试。
第一章我们梳理需求的时候已经明确,对于大部分场景来说,基于模块化开发的方式已经足够能满足开发需要。如果在高吞吐量或者高度解耦的系统中,可以使用服务的方式进行引入。为了快速完成系统对接,我们需要实现SDK部分。
系统整合架构图如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yE22QDvw-1675244012032)(/Users/wangyu/Downloads/系统架构图 (1)].svg)
为了实现代码的复用,我们需要预先定义好一套标准化的Java Interface,完成核心调用逻辑的编排,具体的实现交给对应的模块去做。该思想取自Spring Boot核心源码。
该模块主要定义了以下内容:
该模块作为saas-api的代码实现部分,可被要对接的系统以及服务模块引用。
模块包括:
该模块是一个单独的springboot项目,可以直接启动,要对接的系统引入saas-api和saas-connector,通过Rest代理的方式可以直接集成SaaS化改造模块,可通过拦截系统请求,配置用户注入模块完成无缝切换。
saas-service依赖saas-core模块,配合少量Spring框架代码完成工作,后续可以扩展SpringCloud微服务集成模块。
该模块提供了Rest Proxy的能力,自动将核心Interface代理到独立服务模块,并集成了核心的过滤器以及上下文处理逻辑。系统可以简单通过配置文件指向独立服务模块完成接入,无代码侵入性,剩下的就是修改少量自己的代码以完成改造。
该模块作为可替换实现模块,除了core中的公共逻辑外,实现了不同web服务器的适配,对应着BIO模型架构(J2EE Servlet)和NIO模型架构(Netty Reactor)。使用时,需要根据需要选择:
本章节我们简单描述下集成思路,大家可以集思广益,考虑一下每一步的正确性,以及是否缺失一些考虑。
开始前,选择适合自己项目架构的core实现,系统将会提供基于Servlet的实现和基于Webflux的实现两种方案,以同时兼容tomcat系应用和netty系应用。
@Hired
注解进行租用标记EntityAdaptor
,将JDBC控制权转移到框架中,并与框架代码进行自动集成@Configuration
类上添加注解@EnableSaaS
以启用saas化支持待设计。
后续设计。
将租户管理、配置管理、系统运行态势、租户运行态势等功能集合为一个管理端,独立的一套用户体系和系统架构。支持同时管理当前局域网内的多个SaaS应用。支持多个SaaS应用时,能够手动维护多个应用的信息,包括ip、端口、名称、版本。初始化SQL模板也支持统一集中管理。SQL模板也需要支持版本控制。
支持多个应用时,需要考虑租户上下文缓存的更新逻辑。需要使用消息中间件处理缓存。
对租户进行升降配,支持两种生效方式。
选择立即生效,当前有效订阅将立即失效,修改过期时间为当前时间。接下来创建新的订阅并设置为有效,开始时间为当前时间,结束时间取决于用户选择。这里用户有两种选择:
(1)原过期时间:读取原订阅过期时间直接赋值。原过期时间不能小于当前时间,否则界面应不可选择。
(2)指定过期时间:可以选择今天之后的任意时间作为新的过期时间,可用于改期
选择下个周期生效,新的订阅不会立即生效,而是等到当前有效订阅过期后自动生效。注意,此方式仅在修改了套餐的前提下才可选,否则将置灰。
如10月1日进行操作,套餐A在10月20日过期,选择新的套餐B,并选择下个周期生效,则套餐A的有效时间截止10月20日自动失效,同时10月20日当天生效新的套餐,新的套餐的开始时间即为上个套餐的失效时间,结束时间可以有两种选择:
(1)开通周期:可选择新套餐的开通周期数,开始时间为当前套餐的失效时间,结束时间则为开始时间+周期数
上述7.1小节所述的两种生效方式,其中立即生效能够指定过期时间,下个周期生效仅能指定开通周期(同开通租户逻辑),但是最终影响的都是新订阅的开始和结束时间。建议实现时,在DTO提供对应配置项,然后在后台逻辑中根据业务计算新订阅的开始结束时间。