saas系统从简单到复杂的演化过程

1,301 阅读17分钟

前言

相信很多朋友在工作中都会遇到ToB的项目,而其中最耳熟能详的代表就是saas系统。
虽然目前的行情各种各样的行业saas系统数不胜数,但其中的核心思维和项目演进的过程其实看上去都是大同小异。
本文,我将根据个人的一些经验,分享自己对于saas的一些看法,希望能给大家在工作中一些帮助。

微服务基础

目前,大部分的saas系统都是基于微服务的架构上开发的,当然一些早期的项目也是在MVC上做的。
由于微服务在应用架构层面是比较灵活的,所以非常切合saas系统。
什么是应用架构层面?直白点,就是那些所谓的大中台、小中台、业务中台、领域模型等等一系列专有名词。
相信大家对于这些词汇已经耳熟能详,但其实就是针对比较统一的业务进行系统拆分,比如支付、OA、短信、客服、库存等等。
这样做,缺点自然非常明显,现在稍微有点规模的公司,就是动不动几十上百的微服务节点。
但为什么大家还是趋之如骛,优点也非常明显,开发者可以将精力更聚焦到具体的业务领域。为什么需要提到这些,当然是和接下来要讲的saas有着不少联系。

权衡

首先,saas系统如何盈利?saas作为一种软件产品,对公司来说永远离不开的就是盈利。但saas和传统的资源型产品不同的是,它更多的是输出软件服务。
成本大部分集中在人力成本(主要就是开发者的人力成本)和服务资源成本(服务器、域名、带宽等)。所以合理的降低人力成本和服务资源成本,将带来更多的盈利。
人力成本降低主要是人员数量和人员质量的减少,但这样容易导致系统的不稳定性增加,saas因为它的业务复杂性和技术要求,对开发者的要求也会比一般传统软件高不少。
服务资源成本就是针对业务实现的不断优化,企图以最少的服务器资源满足业的需求。但同时也需要更有能力和经验的开发者去支撑。
所以,从成本角度来说,在不浪费人力资源的情况下,降低服务器资源成为了saas系统降低成本的最常用途径。
但是说点题外话,有些公司喜欢裁员,好一点的公司(和规模没关系)能明确知道人力是否过剩。
但很多公司的人资部门可能不清楚这点,只是看到了人力成本过高,不了解自身业务需求的人力到底是多少。
这样很容易进入一种恶性循环,裁员导致系统不稳定性增加-》问题增加,客户流失-》收入降低-》继续裁员 的死循环中,很多做saas系统的公司就倒在了这种循环中。
那么回到正题,在saas的角度,拆分业务领域,其实就是为了更好的降低成本,不管是人还是服务器资源。当然,不排除有的公司越拆越贵的。
不同的业务需要投入的人力和服务器资源是不相同的,为了更好的细分出成本来,拆分业务领域是非常有必要的。
但是拆分的具体方案非常考验公司架构能力,拆分过多或过少都会导致成本的增加,所以大家在这种公司可能会遇到,时不时的业务合并或拆分,其实就是在尝试,寻求业务和成本的最优解(有些为了刷KPI的不做考虑)。

从简单到复杂

大部分的saas系统看上去都是比较复杂的,但同时大部分saas系统开始都比较简单,之所以变复杂,是因为业务(这个慢慢说)。
那么,我们开始去了解saas系统如何从简单到复杂的过程(排除业务上的复杂性,只考虑共通性)。
最开始,我们要的其实非常简单,采用微服务架构,网关、配置和注册中心、具体业务中心。
网关的具体功能就是分发请求、登录授权、黑白名单。
配置和注册中心一般nacos就行。
业务中心,就是saas能够提供的具体软件服务。
同时需要区分几个环境:开发、测试、预上线、线上

基础服务

大体方向如上面步骤,我们来优化下细节。

个性化

saas系统之所以如此复杂,就是因为个性化。接入的供应商越多,需求越多样。

配置中心

第一个问题就是个性化配置,针对每个客户,某种功能,有的客户需要,有的不需要,每个客户展示内容也不相同。
所以需要有一个配置中心,而一个灵活的客户配置中心应该是怎样的呢?
首先,从作用域范围分类,分别是系统层级、客户层级、账号层级。
系统层级就是最顶层配置,基本放的都是默认值之类的,比如需要给某些客户增加特定功能,默认的系统层级配置为未生效,指定客户层级配置为生效,
这样就能区分出需要使用该功能的客户和不需要使用该功能的客户。然后是账号层级,为什么需要账号层级?
账号层级应该是最细粒度的层级,多数情况是一个客户可能在系统开通多个账号,比如子公司等业务,针对每个账号也需要不同的配置
另外,配置需要区分应用,每个应用的配置肯定是不一样的。
然后从传递方式可以分为向上传递或向下传递,所谓的向上向下传递,就是指三个层级相同的配置key同时配置时,应该取哪一层级的配置。
一般情况下都是取粒度更细的配置,当然,也不排除向上取。
举两个例子:
1.向上传递(取粒度更小的配置),比如有这么一个需求,系统可以配置个性化的logo,
大部分小客户(个体工商户、小门店等)可能没有这种需求,他们只想要一个可以操作的系统,没有心思去搞logo之类的,那么系统就需要给一个默认的图片。
根据客户类型,也需要配置不同的图片样式等。但是中大型客户有自己完善的信息,他们能自己配置logo图片等。
而针对这类客户,还可能存在子公司等情况,而子公司也会有自己的个性化配置,但子公司如果未配置会默认为上层公司的配置。
所以这种配置就需要向上传递,取细粒度最小的那个配置。
2.向下传递(取细粒度更大的配置),比如说业务中需要发送短信功能,会根据客户的付费情况分配每月能够发送的短信条数。
最底层的账号配置成100w。但它的上一层级为了控制资源,想要限制他的短信条数,给它配置为50w,那么发送短信时就需要获取层级最上面的那个配置50w去做限制。
好了,从这里的需求就能看出,非saas系统和saas系统在配置上的差异化。
而这里只是列举了三层结构,但真实的情况是,子公司下面还会有子公司,所以真实的业务配置是一个无限向下的树形结构。
那么配置中心的数据结构在saas系统的演化过程就是:
简单配置 -》 固定层级配置(两层、三层等)-》多层配置(无限延展的树形结构)
如果一上来就设计成最复杂的多层结构,应该怎么去定义我们的数据结构呢?
首先,我们需要暴露出一个接口,支持修改配置,需要的字段有组织机构id、配置的key、对应的值、传递方式(每个key可以写死)
新增和保存比较简单,只需要将上面的信息入库即可,但是获取配置信息的时候就会比较麻烦。
这里的查询需要结合组织机构树来查询,根据组织id、层级关系和传递方式获取对应的配置信息。
而组织机构树的结构一般记录的信息就是机构id、上层机构id两个信息。但是,为了提高查询效率,也可以采用空间换时间的思路,冗余存储每层关系。
比如以下简单的一个组织机构:

微信截图_20240509095340.png

正常来说,在表中的树状结构数据会记录成这样(sys_org)
img_1.png

但为了查询效率,也会新增加一张表(sys_org_node)
img_4.png
这样做的好处是什么?如果我想查询某个节点向上的树状结构,就是org_id = #{目标节点id}。
如果想查询节点下关联的所有节点,条件为parent_org_id = #{目标节点id}。
但这样做的缺点也比较明显,就是多了很多数据。
说完数据层级,另一个问题就是具体的配置信息应该存在哪里?
为什么会有这个问题,因为越到后面你会发现需要配置的东西越来越多,而value也越来越千变万化。
简单的数字、字符串、图片地址、json、甚至是一大段js文本。
直接存数据库?
开始可以,但慢慢的就会达到瓶颈,这里我们以最终演化结果来说,最好的办法是缓存+mongodb+异步广播消息。
先说mongodb,主要是用来做持久化的,相对数据库,mongodb对文本和json支持的更好,所以更适合用来存储。
缓存很好理解,自然是为了查询效率,对于我们的配置中心来说,并不知道一项配置的调用情况。
最极端的时候,可能每个方法都会去获取一次,所以这里不得不考虑高并发的情况。
但是使用缓存的情况也是有讲究的,这里可以考虑本地缓存,为什么不使用redis?
redis同样可以使用,但是分布式redis架构多了一层io,如果redis出现异常,那么影响范围可能比较广泛。
另外,如果配置的值过大,也会出现头痛的大key问题(感兴趣的可以去看一下大key的危害),当然解决的办法可以考虑字符集压缩或拆分key等方式。
所以使用本地缓存是一个比较不错的选择方案,当然,也可以本地缓存 + redis相结合的灵活方案。
本地缓存一般有Guava Cache、EhCache、Caffeine这些比较成熟的方案,不过由于功能简单,也可以自己写,其实就是一个ConCurrentHashMap。
本地缓存也并非完美,本地缓存的问题是数据不一致,mongodb和本地缓存、多个节点的缓存都可能存在数据不一致。
那么数据不一致的原因是什么?
下面列几个常见的原因:
1.广播的消息未收到
2.临近的多次操作导致更新的值不是最新值
3.节点重启
4.节点更新失败
第一个问题,个别节点消息未收到怎么处理?消息丢失在MQ是比较常见的问题,如何保证每一个服务都明确收到消息呢。
其实很简单,分发消息前,我们需要获取注册中心需要发送的应用节点的当前注册的服务个数,当每个节点修改完成后,需要发送消息到配置中心,更新修改数量。
而2、3两个问题,可以根据缓存修改数据方式处理,每次获取到消息时,需要删除本地缓存数据。获取配置数据时,需要去获取最新的配置信息。
当然,更新的时候需要加本地锁(synchronized 或 lock)。
第4个问题,当更新失败时,需要在配置中心执行一个定时任务,判断更新成功的数量小于注册数量时进行预警。
我们先看下流程图:

微信截图_20240509171119.png 以上,就是配置中心的实现思路,基本可以满足大部分个性化配置需求。
然后是最后一点,更新的粒度,也就是key的选择。这里不能一概而论,需要根据实际业务选择。
比较好的更新策略是 租户code + 组织机构id + 具体需要更新的key的值。缓存结构也可以按这种方式存储成双层map。

常见问题

问题一:OOM
OOM是比较常见的问题,发生这个问题的原因主要是配置内容越来越多,内容越来越大。
因为是加载到本地内存,稍有不注意,一些堆内存配置过小的应用就容易出现内存溢出。
解决方式为:
1.配置预演,在配置中心增加新配置时,按应用计算加载到应用中最大配置的内存大小,并结合运维,设定合理的堆配置。
2.动静结合,动静结合就是将访问频繁的配置通过本地内存更新的方式获取,不频繁的配置直接从配置中心接口获取。
3.更换存储方式,这样做主要是处理大内容配置,一般的大内容配置都是js文本、图片流、视频流、大段中英文描述内容等。
像这种大内容,一般后台需要使用的都比较少,基本是展示在前台,所以可以转换成文件存储,配置时只配置文件地址。
问题二:个别节点更新不上
个别节点可能因为某种原因始终更新不到最新的应用配置
解决方式为:
根据前面提到的重试预警机制,鉴别出具体哪个节点出现问题,如果不能手动恢复,可以将流量从这个节点剔除,做安全退出处理。

数据隔离

saas系统比较核心的一个问题就是数据隔离。

上下文

数据隔离的第一步是请求上线文配置,一般在登录成功后绑定到token上,内容必须包含租户code和组织机构id。
每次请求的时候在每个服务上利用拦截器(WebMvcConfigurer)将对应的信息设置到当前线程(ThreadLocal)的上下文中。
请求完成后,注意在afterCompletion方法里面将线程上的信息清除掉。
因为线程池的存在,线程是重复利用的,如果不清理,下一次的请求可能获取到上一次用户的上下文信息。

简单数据隔离

简单数据隔离就是在业务表中直接增加字段,标识属于哪个租户的数据。
为了在业务编码上无感知,可以利用mysql提供的拦截器,在执行sql时动态的将租户筛选条件带上。类比其他的中间件入mongodb、es、redis也是类似。
简单的数据隔离策略只适用于数据量比较小,且业务不是很复杂的场景,一般常见于saas系统开发初期,用的资源少、逻辑简单。

一般数据隔离

一般的数据隔离方式就是数据分表,业务表后缀带上分割标识,一般以租户code区分。数据库上同样可以利用拦截器,替换指定表表名执行sql。
当然也可以直接引入分库分表工具,如shardingJDBC等。分库分表工具不仅可以动态修改sql,也能更好的对表进行新增、修改等操作,手动运维有时候容易出错。
这种方式,就比较适合有一定数据规模的saas系统了。

较为完善的数据隔离

大部分saas系统可能在第二种数据隔离方式下就已经算是到头了,但也不乏一些比较大型的saas系统。
他们的数据隔离方式更加复杂,基本是分库 + 分表两种方式结合使用。
如何分库?
针对一个比较大的客户,他们的业务量可能是几十甚至上百家中小客户的总和,这种客户单纯的分表已经满足不了需求,一般就会分库。
多个中小客户分在一个库里,大客户单独成库。
当然,在不断前进的时间轴里,个别中小客户也会成为大客户,这时候如果我们有幸在这个变化中,就会经历分表到分库的数据迁移过程,以及服务迁移过程。
这时,光站在分库分表策略角度,你就会发现,分库分表策略将变得非常复杂,而且复杂的程度是呈指数式增长。
因为这种策略上,需要包含每个客户的分库逻辑和分表逻辑、单个大客户的分表逻辑和分库逻辑(大客户就算分了库,也可能再分表)。不管是维护还是开发,都会有一定的难度。
个人认为比较好的处理方式是流量数据隔离,既然从分库分表策略上比较复杂,可以直接选择在流量接入点就做流量切分。
比如存在一个大客户和几十个小客户,在请求到达网关和内部feign调用时,动态的修改请求需要访问的应用服务,将相同应用分成两部分,
一部分应用叫大客户应用,一部分叫集群中小应用,提供的功能完全一样,但访问的节点永远隔离。
这样,不管哪个服务,只需要关注分表策略,分库策略在流量上就已经动态隔离。
同时,这样也带来以下一些优点:
1.资源细分,更方便做资源优化。大客户本来的并发和数据量就比较大,可以分配更多的资源给他们。
小客户可能需要的资源微乎其微,如果绑在一起,将节约不少成本。
2.内测,如果一次大版本更新,就算经历好几轮测试,估计也没人会笃定一定不会出现大的纰漏。
如果是在比较大的公司,一个小bug就可能会导致很大的损失。
这时候如果是上线一部分企业,流量和数据又是完全隔离的,即使出现问题也影响不到大部分其他客户。这样做能将损失尽量减少。

公共数据

说了数据隔离,一般的业务中也会有不需要数据隔离的部分。
比如登录的时候,在没登陆之前是不知道当前用户应该归属于哪个租户的。
针对这种情况,如果是数据,就会将这些部分单独抽离出,比如用户、短信、支付、审批等。
由此我们就看到了各种集合的中台或者领域模型。

其他

saas系统和一般的微服务应用最大的区别就是数据隔离、个性化配置以及对资源的掌控。
其他的什么高并发、链路跟踪、ELK、限流熔断等等都和一般的微服务没什么区别,这里就不详细说了。