混合架构

ToC

近些年来,后端的架构普遍的都是微服务架构,单体架构似乎被看作是一种过去式,老旧的代名词。

Amazon的一个运维团队将一个监控系统从微服务,Serverless架构改成单体后节约了 90% 的运营成本,这是原文

历史是不可以开倒车的,从纯单体到纯微服务可能只是一个极端到另一个极端?如果现在一味追求恢复单体架构也有点“反清复明”的味道,例如这篇抨击微服务架构的文章:Even Amazon can't make sense of serverless or microservices

原文摘抄:

Replacing method calls and module separations with network invocations and service partitioning within a single, coherent team and application is madness in almost all cases.

在使用某种架构的时候,还是需要仔细考虑,可以节约运营成本也是一种环保(

不过我还是希望有一个较低的试错成本,不至于将某个模块从单体切换成微服务,或者从微服务切换成单体需要完全重写,这也是本文提出的 “混合架构”。

微服务相比单体带来了什么?

更多的机会,更多的变革

大多数公司喜欢微服务,它可以拿来招更多人,让产品迭代得更快。

大多数开发者喜欢微服务,它看样子比单体高级,催生很多技术、框架,大牛们都在用。

现在告诉他们:你们太疯狂了,明明写个单体就行了,搞这些浪费人力,浪费财力!

所以企业就开始缩减招聘,减慢产品迭代了?开发者就回归原始,拿起祖传的代码了?不可能的!

如何做个“墙头草”?—— 混合架构

怎么写微服务?这和把大象装进冰箱一样,分三步:

  1. 划分服务,实现服务
  2. 把不同服务间原本函数调用换成远程调用
  3. 测试、部署

好,我也来这样做!不过,我先和"服务"划清界限。

第一步:为了有一个类似服务的结构,这样好把任务划分给开发者,所以把它称其为"领域",不过先说好了,这些领域不是服务,划分了多少个领域也不代表最后有多少个服务。领域里就不用管服务的东西了,不用写路由,不用写RPC服务的实现,想一想这个领域里该干什么,就用领域模型抽象出这些行为吧!

第二步:领域都写完了,还不把服务请出来就不行了吧!现在要想一想怎么划分服务了(也许你会觉得代码都写得差不多了才想起划分服务很奇怪,这个架构的意义就在于此——想怎么划分就怎么划分,试错成本低)。很直观的想法就是一个领域一个服务,但这不是固定的,如果把所有领域放到一起,那不就是单体了么?最后实现服务,对接各个领域。

第三步:测试、部署。

还有一个问题,领域层不能有服务相关的东西,需要统一函数调用和远程调用,函数调用,只需要传参数就行了,远程调用,还要知道远程服务端的地址。但不管怎么说,除了请求参数以外,像数据库连接,远程RPC服务连接都是依赖!这些依赖会在依赖注入中介绍如何注入。所以,我把这两个东西抽象成同一种类型:(Request) -> (Response),一种函数类型,不同领域间互相调用,传入这种类型就可以了。

(Request) -> (Response) 类型的具体实现,是函数调用还是RPC调用将直接决定部署一个服务还是两个,也许这是这个系统试错的时候最难改的地方:两个领域间原本是函数调用的实现,编译到一起并部署,现在需要划分,那就得为被调用方实现服务,在调用方添加被调用方的地址,把实现改成RPC调用,但如果后面发现还是把这两个领域合并到一起好,就得返工,删掉写好的RPC调用实现,换回原来的函数调用实现。也许这两种实现不是互斥的,可以同时实现函数调用和RPC调用,运行时判断使用哪种调用,这是混合架构的一种特色:你的微服务架构中的某个服务居然可以根据下一个服务的负载自动决定是自己完成下一个服务的逻辑还是远程调用下一个服务的逻辑,或者可以做一个前端,通过连线的方式来控制服务调用策略。

弊端之一:难以普及

事实上,在没有明确的服务间的 API 文档(例如 Thrift),一个开发团队的目标就不是很明确了,该如何抽象分配到他们手上的领域中的模型呢?

而定义了这些 API 文档之后,其实已经将服务划分好了,后续的开发必定会依赖这个服务划分,以至于上层服务划分的变更依旧会引起下层代码的变动/反工。

或许在数据面和控制面之间抽象出一层新的 API 形式,让数据面提供 API 的定义,可以使底层的数据面和上层控制面之间的控制关系反转(IoC, Inversion of Control)。这种 API 不像提供给用户端 API 那样变动多,不稳定,它应该具有十分稳定,适用性广的性质。

同时,这种 API 的设计失误造成的重构/反工的代价更多。

弊端之二:观测性下降

当函数调用和远程调用混合之后,一个请求的调用链路无法预测,这对管控面来说,链路都无法预测,观测性就下降了,甚至某些问题是随机发生的(一个链路出问题了,但极少数请求会走这个链路)。

测试的时候,需要对所有可能的链路进行测试,这也会增加过多成本。

为什么有领域层?

领域层(Domain)来自于领域驱动设计(Domain Driven Design),很多设计模式在不同人眼里都有些不同,我不是DDD设计的发明者,也不是DDD设计的大师,但我更愿意叫这种架构也为领域驱动设计,而不是“服务驱动设计”。

领域驱动设计有很多优点,读者可以上网搜一搜就会有一群人列出一二三四出来。但我实践上来看,这种设计的优点和缺点相当,它限制了逻辑不分散到各个地方,只出现在领域层,方便逻辑的重构,但同时也不可避免地要进行过多的类型体操,一个函数可以解决的事情要为其包裹上领域模型,还要注意自己的抽象是否合理,非常地耗时间,但也许是我的水平不够的原因吧(每天幻想着闭着眼就能写出最佳实践.jpg

但总的来说,我需要面向领域来代替面向服务,服务并不是驱动系统设计的主体。

结语

我不是系统设计的大师,这仅仅只是我在写代码的时候的一些想法,希望对你有帮助,或者能激发你的思考也是可以的。