从接口设计看分层微服务架构

1,255 阅读7分钟

故事的背景来源于,今天产品提了一个需求:对某个模块更新了一种文案,理论上来说,其实这是一个 k-v 结构,比如我有一个 getTexts 接口,返回如下:

[
    { title: '你好', desc: '世界' },
    { title: '效果不错', desc: '追加一条' } // 本次追加
]

那么直观感受是,我们接入了这个接口,接下来定义完 render 的结构,无论服务端怎么变,是不是都能渲染出来。

这似乎是理所当然的——产品也是这么认为的,这里甚至不用提及系统架构中的分层,因为抽象就似乎这么简单。

现在我们的困难来了,如果有多个端需要做同样的事情呢——比如我们就有 App、PC、H5、小程序,按照最简单的架构来看,似乎会变成这样:

接口架构.drawio.png

当然,作为一个职业的开发人员,大家自然不至于做出课设级别的简单设计,至少你会知道,直连 DB 的应该只有一个服务,稍微改改这个设计:

接口架构-第 2 页.drawio.png

当然,这一个微服务的底层可能还会有好几个微服务,问题就来了,我的系统到底是怎么拆分,才能最终做到独立、解耦,同时又能满足一次变更、多端生效呢。

我们从图中的接口说起,也有人会把这一层称为 BFF,它的本质是面向视图服务的,也就是说让前端的逻辑尽可能的少,这样可以把前端做的更加轻量,避免重逻辑的操作,这很好理解,从逻辑、解耦、提速和统一的角度,我们都能找到这样拆分的理由。

但是他的下游是由一个个微服务组成的,而不是我们图中所示的只有简单的一条线,现实系统中的链路往往复杂很多,这又应该怎么拆分呢?

关于拆分,可以阅读下:微服务拆分之道,讲的挺清晰,简单总结一下,最下游的服务是基于领域模型进行拆分的,比如「用户服务」、「信息服务」、「支付服务」、「订单服务」等等。同时,我们也会基于服务重要性、QPS 等角度考虑去进行更细粒度的切割,并且在维护成本、运维成本中取得一个最终的平衡点。

但如果光按照领域模型切割完服务,才是一个服务的开始,更重要的是,我这个服务应该去做哪些事情?

拿一个获取用户订单来说,那么首先我必然需要拿到用户,并且鉴权通过后,我才能执行后续的逻辑,吐给上游服务订单,既然如此,我是不是应该调用用户服务去进行鉴权呢?——从单一职责的角度上来看,其实不用,理论上,应该是上游鉴权并且告诉你 userId,然后直接去拉指定 userId 的信息,否则一个「订单服务」的所有接口极有可能都是需要鉴权的,在一个上游服务调用多个微服务的时候,就会造成「用户服务」的严重读放大,同时平白无故的增加了链路成本。

无论是代码还是服务,我们都应该奉行高内聚低耦合的原则进行。但是如果这样,可能会导致服务之间的关联性变弱,过于散装,所以我们会在最底层的查询类服务上游再去进行一些聚合,行程上游服务,这也就是为什么微服务架构的链路存在一定复杂性,而不是上图中的简单三层结构。

接下来回到原来的话题,现在产品能够实现他提一个需求改变所有端的想法了吗:似乎是可以的,毕竟只要微服务变更,接口侧只是对微服务进行的一些拼装裁剪,上面那个 case 本质只是数据多了一条,没什么本质的变化。

但是新的问题来了,虽然这个特性最终可能是多端生效的,但是过程中产品可能要对某一段进行一些实验,比如 App 去做一些 AB 来验证想法。

这里我们的关键词有两个,一个是 App,一个是 AB;当我们提到平台时,我们会更倾向于在客户端或者接口层去进行实验桶的分配,然后在对不同的情况进行数据处理。而如果是平台无关,则可以考虑将实验逻辑进一步下沉到下游服务中,这样才更能保证多端一致。

结合需求来看,我们常见的需求有两种:

  1. 新增 feature,需要前端调整 UI 的同时后端新增数据字段
  2. 策略调整,前端不变,但是后端数据会变得不一样的场景

对于场景 1,毫无疑问,应该新增个字段,来减少对现有服务的干扰,但也有一些情况是,本来 enum 是两个值,现在有了三种值该怎么办,这种情况上游服务从一开始就应该做好防御策略,避免数据变更导致的兼容性问题。

对于场景 2,如果产品预期多端一致,是完全可以进行多端一致处理的,做到在接口层不变更的情况下,微服务下发数据修正,App 试验期间,如果用户命中实验桶,可以将实验标传给下游服务,下游服务返回实验版本,但在结束实验后,需要将代码的缺省版本更新,让它在所有端都生效。

但是对于服务端来说,可能有的顾虑是:如果各端因为数据变更出了异常,我是不是要背锅,于是始终不敢更新缺省版本,而让各端传入之前支持的实验标——其实开发并不是一项自闭工程,还是需要一些协调沟通的,不用假设。

如果 App 端只有某些版本才支持,其实应该在接口层基于版本进行兼容性处理,而不是一路无脑透传,接口层其实是一个脏活累活,也并不是无脑透传这么愉快的。

其实对于链路的每一层,都秉持 less is more 的理念,如果长期把持着实验标,对于下游服务的可维护性也造成了一定挑战,比如这次在接入一个接口的时候,我需要传十几个 options 才能保持和 App 一致,但有些并不是 App 正在进行的实验,而是已经结束后保留的选项,虽然最终接口的提供方也没能给我解释每一个选项,但是简单看名字,有的接口是决定了:是否出现某一个字段,这种裁剪能力应该由上游去做才对,也就是说,或许我们的微服务拆分就是有问题的(甚至还见到了底层微服务下发十六进制色值、下发裁剪后图片而不是原图的情况)。

我们再回去看看产品本身的诉求:多端一致。其实是不是只要把接口出入参简单化,逻辑清真化,多多沟通,就能解决的问题呢?

对于业务来说,本身没有银弹,只有结合业务后的合理的设计,但是根本依旧是,我们不仅要实现需求,还得想想怎么能更好的实现需求,能够走在产品前面,设计出一个可扩展、易使用的服务。

当然,本文本来觉得没啥好讲的,也很低端,没想到这两周接接口接的痛不欲生,同时也终于发现了为什么我们的多端永远对不齐的其中一个问题点,于是有了这篇文章。