《架构整洁之道》第 27 章 服务:宏观与微观

2023-06-15 13:34:02 浏览数 (1)

面向服务的架构,以及微服务架构,近年来非常流行,其中的原因如下:

  • 服务之间似乎是强隔离的,下文会说到,并不完全是这样。
  • 服务被认为是支持独立开发和部署的,下文也会说到,并不完全是这样。

面向服务的架构

首先我们需要反对“只要使用了服务,就等于有了一套架构”这种思想。这显然是错误的。

架构设计的任务就是找到高层策略和低层细节之间的架构边界,同时保持这些边界遵守依赖关系规则。所谓服务,本身只是一种比函数调用成本稍高的,分割应用程序行为的一种形式,与系统架构无关。

当然,并不是说所有的服务都应该具有系统架构上的意义。有时候,用服务这种形式来隔离不同平台或进程中的程序行为,本身就很重要,不管他们是否遵守依赖关系规则。我们只是认为,服务本身并不能完全代表系统架构。

为了理解上面的区别,我们用函数的组织形式来做个类比。

不管是单体程序,还是多组件程序,系统架构都是由,那些跨越架构边界的关键函数调用来定义的,并且整个架构必须遵守依赖关系规则。系统中许多其他的函数虽然也起到隔离行为的效果,但它们不一定是具有架构意义的。

服务带来的好处

下面将会针对那些所谓的好处,一个一个地来批驳。

解耦合的谬论

很多人认为拆分服务的一个重要好处就是服务之间的解耦。毕竟每个服务都是以一个进程来运行的,甚至可能在不同服务器上。因此服务之间不能访问彼此的变量,其次服务之间的接口一定是充分定义的。

从一定程度上来说,这个是对的。但是服务之间还是可能会通过某种共享资源,导致强耦合。

例如,如果给服务之间传递的数据记录中,增加了一个新字段,那么每个需要操作这个字段的服务都必须做出相应的变更,服务之间必须对这条数据的解读达成一致。因此,这些服务实际上都强耦合这条数据结构,因此它们之间是彼此耦合的。

再来说说服务能很好的定义接口,它确实能很好的定义接口。但函数也能做到这一点。事实上,服务的接口和普通函数相比,并没有比后者更正式,更严谨,也没有更好,所以这点好处根本不算什么。

独立开发的谬论

人们认为另一个使用服务的好处就是,服务可以由不同的团队负责部署和运维。这可以让团队采用devops混合的形式来编写,维护以及运维各自的服务,这种开发和部署独立性被认为是可扩展的。这种观点认为大型系统可以由几十上百个上千个独立开发部署的服务组成。整个系统的研发,运维就可以用同等量级的团队来共同完成。

这种理念有一些的道理,但也仅仅是一些。无数历史事实证明,大型系统一样可以采用单体模式,或者组件模式来构建,不一定非要服务化。因此服务化并不是构建大型系统的唯一选择。

其次因为服务并不能很好的解耦,所以他们的开发,部署,运维,也必须彼此协调进行。

运送猫咪的难题

我们用之前举过的出租车调度例子,再来说明这两个谬论。该系统会负责统一调度某城市中的多个出租车提供商,用户集中在系统下单,下单时可筛选司机经验,豪华程度,时间,价格等条件。

我们希望该系统是可以扩展的,所以大量的采用了微服务架构。然后将团队划分为多个小团队,每个团队都负责开发,运维相应小数量(通常是1个人负责1~n个服务,少数情况多个人负责一个服务)的微服务。

TaxiUI负责和用户打交道,TaxiFinder负责调度不同的TaxiSupplier服务来获取可用车辆信息,并且找出可用出租车以作为推荐项。这些推荐项会短期被固化成一条数据记录,与用户信息挂钩。TaxiSelector负责根据用户的筛选条件进行筛选,最后将筛选结果传递给TaxiDispatcher服务,由该服务进行派单。

现假设该系统已经运行了一年,研发团队在持续开发新功能,维护着所有服务。

现在要加上一个新功能,猫咪送达服务,让用户可以下单,要求将他们自己的猫咪送到自己的家里或办公室。在此之前,猫咪将会被统一送到公司的集散地,下单后,出租车将会去集散地取猫,并送到指定地点。

有些出租车提供商会参加这个新功能计划,也有些不会参加。有些司机对猫咪过敏,所以不能派他们去运猫,有些乘客也会对猫过敏,所以他们下单时必须避免三天内运过猫的车。

现在我们再来看这个系统架构图,数一数多少个服务需要变更?答案是全部。显然,为了增加运送猫咪的功能,所有服务都需要变更,而且这些服务之间还需要协调。

换句话说,这些服务在事实上,是全部耦合的。并不能做到真正的独立开发,部署和维护。

这就是所谓的横跨型变更问题,它是所有的软件系统都要面对的问题,无论是服务化的还是非服务化的。而这种按功能切分的服务架构,在这种变更中是最脆弱的。

对象化是救星

如果采用组件化的系统架构,如何解决这个难题?通过对SOLID设计原则的仔细考虑,我们应该一开始就设计出一系列多态化的类,以应对未来的扩展。

在下图中,我们可以看到,设置了架构边界,同时遵循了依赖关系原则。可以看到,之前的服务都被抽象成了基类,每次特定行程的逻辑,被抽离到单独的Rides组件中。运送猫咪的新功能被放到了Kittens组件中。这两个组件覆盖了原始组件中的抽象基类。这种设计模式被称为模板方法模式,或策略模式

实现功能的类,也都是由UI控制下的工厂创建出来的。显然如果我们新增加猫咪功能,TaxiUI组件就必须要变更,但其他组件就不需要变更了。这里只需要系统在运行时就会自动动态加载它们。这样一来,运送猫咪功能,就和其他部分实现了解耦,可以实现独立开发和部署了。

基于组件的服务

那么问题来了,服务化能做到这一点吗?答案是肯定的,服务并不一定必须是小型的单体程序。服务也可以按照SOLID原则来设计,按照组件架构来部署。回顾上面的,可以清晰的看出,架构,和服务不能完全划等号。

横跨型变更

现在我们应该可以明白了,系统的架构边界事实,并不落在服务之间,而是穿透所有服务,在服务内部以组件的形式存在。

为了处理大型软件都会遇到的横跨型变更,我们必须在服务内部采用遵守依赖关系原则的组件设计方式。总之,服务边界并不能代表架构边界,服务内部的组件边界才是。

本章小结

虽然服务化有助于提升系统的扩展性和研发性,但是服务并不能代表整个系统的架构设计。系统的架构是由系统内部的架构边界,以及边界的依赖关系所定义的,与系统中的各个组件调用和通信无关。

一个服务可能是一个单独的组件,以架构边界的形式隔开。一个服务也可能是由多个组件组成,其中的组件以架构边界的形式互相隔离。极端情况下,客户端和服务端甚至可能会由于耦合得过于紧密,而不具有系统架构意义上的隔离性。

0 人点赞