27 Services: Great And Small

面向服务的架构和微服务架构变得越来越火了,导致流行的原因有以下几点:
1.服务之间似乎是高度解耦的,但是我们将会看到这只对了一半。
2.服务看似支持独立开发与部署,同样,我们将会看到这也只对了一半。

Service Architecture?

首先我们来思考一个问题:使用服务,从本质上来讲,本身就是一种架构,这显然是错误的。
一个系统的架构,是由分隔高层策略和底层细节,并遵循依赖规则的那些边界所定义的。仅仅是对应用行为做拆分的服务,只不过是代价高昂的函数调用,在架构意义上并不一定重要。这并不说所以后服务都必须具备架构意义。无论是否遵循依赖规则,将功能按照进程与平台拆分成服务,通常都会来巨大好处。
只是服务本身并不能定义架构
一个很有用的类比是函数的组织方式:单体系统或基于组件的系统,其架构是由那些跨越架构边界、遵循依赖规则的函数调用所定义的。而系统中其他许多函数,仅仅是把不同行为分开,并不具备架构上的重要性。
服务也是如此。毕竟,服务只不过是跨进程、跨平台的函数调用。这些服务中,一部分具备架构意义,另一部分则没有。本章我们关心的,是前者。

Service Benefits?

解耦的谬误
将系统拆分为服务的一大所谓好处,是服务之间会强解耦。毕竟,每个服务都运行在不同的进程,甚至不同的处理器中;因此这些服务无法访问彼此的变量。此外,每个服务的接口还必须被良好定义。
这其中确实有一定道理 —— 但道理并不大。没错,服务在单个变量层面是解耦的。然而,它们仍然可能通过处理器内或网络中的共享资源产生耦合。更重要的是,它们会通过共享的数据而强耦合。
例如,如果在服务之间传递的数据记录中新增了一个字段,那么每一个处理该字段的服务都必须修改。这些服务还必须对字段中数据的含义达成高度一致。因此,这些服务与数据记录强耦合,并因此间接彼此耦合。
至于接口被良好定义,这一点固然没错 —— 但函数同样如此。服务接口并不比函数接口更正式、更严谨、定义得更好。
显然,这一优势在某种程度上只是一种假象。
独立开发与部署的谬误
服务的另一项所谓优势是,它们可以由专门的团队拥有和维护。该团队可以负责编写、维护和运维服务,作为 DevOps 策略的一部分。人们假定这种开发与部署的独立性具备可扩展性。人们认为,大型企业系统可以由几十个、几百个甚至几千个可独立开发和部署的服务构建而成。系统的开发、维护和运维可以在数量相当的独立团队之间划分。
这种看法有一定道理 —— 但也只是部分正确。首先,历史表明,大型企业系统既可以用单体架构和基于组件的系统构建,也可以用基于服务的系统构建。因此,服务并不是构建可扩展系统的唯一选择。
其次,解耦的谬误意味着服务并不总能被独立开发、部署和运维。在它们因数据或行为而耦合的程度内,开发、部署和运维就必须进行协调。

The Kitty Problem?

我们用之前的出租车聚合平台系统来举例说明这两个谬误。你应该还记得,这个系统会整合一座城市里的多家出租车服务商,并允许乘客叫车。我们假设乘客会根据一系列条件选择车辆,比如接单时间、费用、车型档次、司机经验等。
为了让系统具备可扩展性,我们决定用大量微小的微服务来构建。我们把开发人员拆成许多小团队,每个团队负责开发、维护和运维少量对应的服务。
图 27.1 展示了我们虚构的架构师们如何划分服务来实现这个应用:
TaxiUI 服务:负责与乘客交互,乘客通过移动设备叫车。
TaxiFinder 服务:检查各个出租车供应商的可用车辆,找出符合条件的候选车辆,并把这些信息存入与该用户关联的短期数据记录中。
TaxiSelector 服务:根据用户设定的费用、时间、车型档次等条件,从候选车辆中选出最合适的出租车。
TaxiDispatcher 服务:接收选中的车辆,并真正下发叫车指令。
现在假设这个系统已经稳定运行了一年多。开发人员一直在愉快地开发新功能,同时维护和运维这些服务。
直到某一天,市场部门突然召开会议,宣布他们计划在这座城市推出一项小猫配送服务。用户可以下单,让小猫配送到家或公司。
公司会在城市里建立多个小猫集散点。当有人下单小猫配送时,系统会选择附近的出租车,从集散点取猫,再送到指定地址。
已经有一家出租车供应商同意参与这项计划,其他供应商可能陆续加入,也可能拒绝。
当然,有些司机可能对猫过敏,这类司机绝对不能被分配到小猫配送任务。同样,有些乘客也对猫过敏,那么过去 3 天内送过小猫的车辆,就不应该派给这类乘客。
现在再看那张服务结构图。要实现这个新功能,有多少服务需要修改?
答案是:全部都要改。
很明显,小猫配送这个功能的开发和部署必须进行非常谨慎的统一协调。
换句话说:这些服务其实是高度耦合的,根本无法独立开发、独立部署、独立维护。

Objects to the rescue

在基于组件的架构中,我们该如何解决这个问题?只要认真遵循 SOLID 设计原则,就会引导我们创建一组可多态扩展的类,用来处理新功能。
图 27.2 展示了这种策略。图中的这些类大致对应图 27.1 里的那些服务。但请注意其中的边界,也要注意依赖关系遵循了依赖规则。
原先服务中的大部分逻辑都被保留在对象模型的基类里。但是,那些专门用于打车业务的逻辑被提取到了 Rides(打车)组件中。而新增的小猫配送功能则被放到了 Kittens(小猫)组件中。这两个组件通过模板方法或策略模式这类模式,重写了原始组件里的抽象基类。
再次注意:Rides 和 Kittens 这两个新组件都遵循依赖规则。并且,实现这些功能的类是由工厂创建的,控制权在 UI 手里。
很明显,在这种设计下:当要实现小猫配送功能时,只有 TaxiUI 需要修改,其他所有部分都不需要改动。我们只需要向系统中添加一个新的 jar、Gem 或 DLL 文件,并在运行时动态加载即可。
因此,小猫功能是解耦的,可以独立开发、独立部署。

Component-based Services

那么一个很明显的问题就是:我们能否对服务也做同样的事情?答案当然是:可以!
服务并不一定要做成小型单体。相反,服务完全可以遵循 SOLID 原则进行设计,并拥有组件化结构,这样就能在不修改服务内部已有组件的前提下,添加新的组件。
你可以把 Java 中的服务想象成一个或多个 jar 包里的一组抽象类。把每一个新功能或功能扩展,想象成另一个 jar 包,里面包含扩展了前面 jar 包中抽象类的实现类。
这样一来,部署一个新功能就不再需要重新部署整个服务,而只需要把新的 jar 包添加到服务的加载路径中即可。
换句话说,添加新功能符合开闭原则(Open-Closed Principle)。
图 27.3 的服务结构图展示了这种结构:服务依然像以前一样存在,但每个服务内部都采用了组件化设计,允许以新的派生类的形式添加新功能。这些派生类都位于它们自己的组件内部。
图 27.3 每个服务都拥有内部组件化设计,使得新功能可以作为新的派生类被添加。

Cross-Cutting Concerns

我们从中得到的结论是:架构边界并不位于服务与服务之间。恰恰相反,这些边界贯穿服务内部,将服务划分成不同的组件。
要处理所有重要系统都必须面对的横切关注点问题,服务在设计时,内部就必须具备遵循依赖规则的组件化架构,正如图 27.4 所示。
服务本身并不定义系统的架构边界;
真正定义系统架构边界的,是服务内部的组件。

Conclusion

尽管服务对于系统的可扩展性和可开发性很有用,但服务本身并不是具有架构意义的核心元素。一个系统的架构,是由系统内部绘制的边界,以及跨越这些边界的依赖关系所定义的。架构不是由组件之间通信和运行的物理机制所定义的。
一个服务可能只是单个组件,完全被一条架构边界包围。反过来,一个服务也可能由多个被架构边界分隔开的组件组成。在极少数情况下(我们希望是极少数),客户端与服务之间可能耦合严重,以至于完全不具备任何架构意义。

posted @ 2026-03-23 10:27  cyusouyiku  阅读(10)  评论(0)    收藏  举报