架构重构-单体到微服务
随着系统复杂度的上升,我们需要能够更好的对其进行管理。通常我们会考虑将大单体拆分为分布式系统,以减少单个单元的复杂度。但同时会引入分布式系统带来的问题,比如通讯的性能损耗、可能的异常、分布式事务、更复杂的调用链等。随着云原生和微服务技术的成熟,微服务架构成了一个较好的选择。微服务架构如何建设和优化是一个大的话题,这里不做过多的展开,主要讨论从单体到微服务的一些主要变化和需要注意的点。
微服务架构的优势和挑战
- 业务拆分带来的影响:由于微服务基于领域划分的设计要求,一般会有更清晰的业务边界。单个服务更易于理解和修改,但整体更难理解,服务间关系不易于管理。
- 从本地代码依赖到远程服务依赖的影响:远程服务依赖会有更清晰的接口定义,会促进服务优化管理对外提供的能力,从而使服务自身的设计更清晰。远程依赖会导致接口契约被破坏时更难被发现,同时会带来性能降低、数据一致性保障困难、异常处理困难、链路长排查问题困难等问题。
- 服务可独立发展的影响:服务独立后,可以选择与业务更匹配的技术,有利于业务建设和技术创新。同时保持统一的技术规范和技术栈变得困难,对于非功能性的技术要求如安全问题处理、监控告警、链路追踪会带来更大的挑战。过多的技术栈和中间件会削弱整体技术的持续深入,可能导致对部分技术掌握不够而出问题甚至失控。同时对部署也会带来更多的挑战,特别是私有化部署的场景(我们后面会专题讨论)。
- 团队协作的影响:微服务一般由独立的团队维护,而且对外接口相对清晰,因此团队需要优化业务和技术时更加的灵活。团队间协作更多会采用甲乙方模式,关联业务的变更和发布会更灵活,但对甲方的质量要求也更高以降低集成成本甚至取消集成动作。由于业务服务只专注于业务处理,因此公共的业务和基础服务团队需要更好的支撑业务服务,避免业务团队需要了解公共服务的细节,同时需要屏蔽对第三方依赖和基础设施的变化。
架构重构的主要关注点
- 服务划分原则:建议基于领域驱动设计(DDD)的方法对服务进行划分,可以按产品分别进行领域建模,然后确定产品间的依赖关系,提炼公共的支撑域和通用域。业务的核心域、独有的支撑域由业务团队负责,公共的支撑域和通用域由公共业务团队负责。对于公共的技术框架、技术组件、中间件、PaaS平台可以于公共技术团队负责。关于服务的颗粒度,建议根据业务规模和团队规模来确定,尽量粗一点,一方面避免一个团队维护太多的服务,一次迭代需要修改多个服务来回切换,内部还要设计很多服务接口;另一方面避免需要处理强分布式事务,分布式数据一致性的保障也是有成本的,会降低系统的性能和可用性。
- 服务依赖管理:在开发架构上,需要显性的定义服务依赖关系,也就是说通过代码的扫描,可以方便的得出服务依赖的关系图。微服务一般通过RPC框架来进行服务间的通讯,可以通过RPC接口定义的引用来确定依赖关系,例如GPRC的ProtoBuf。设计上应该避免循环依赖,否则可能对服务的异常恢复带来麻烦。
对于服务依赖的显性化,建议使用服务依赖声明文件,就像对外部代码包的依赖申明一样。构建工具必须依赖声明文件才能正确构建,这样就能避免场景遗漏。同时建议在依赖声明中明确区分强依赖(没有则无不正常工作)、弱依赖(没有可以正常工作,只有部分功能开启时才需要),这样有利于确定依赖程度,以最小的成本部署服务最小集合。
- 强化架构规范和约束能力:微服务架构会大大提高团队的自由度,因此公共规范更容易被破坏(有意或无意),因此应该尽量在架构、工具层面来进行约束,实在不行再考虑流程约束(成本高,效果差)。例如对象存储为实现按业务+租户隔离的规范,提供的SDK存储必须指定业务模块和租户编码参数,由SDK来生成具体的存储路径。工具方面可以参考上面提示的服务依赖显性化,不显示声明依赖,则代码无法通过构建流水线构建。
- 公共业务平台建设:经过多业务发展阶段公共业务的提炼,有助于微服务架构公共服务建设,如果之前的公共模块抽象得不好,公共逻辑散落在各个产品中,会严重阻碍微服务转型。相对于产品系统间有限的交互,系统和公共业务的交互会更多、更复杂。微服务化后,由模块代码的依赖变为远程的服务依赖,部分交互逻辑可能需要重新设计方案。尽量将原来细粒度的多次交互重新设计为粗粒度的少量交互,以减少分布式带来的性能和稳定性影响。
- 公共技术平台建设
- CI/CD:需要基于微服务的特点,重新设计CI/CD流水线。在大单体架构中,所有人都基于一套代码,所以需要更多的协调和避免冲突的约束。到微服务架构时,团队的独立性更强,可以充分解耦团队间对代码的依赖。同时需要在流水线中加入代码、质量、安全相关的表态扫描和质量卡点,避免基础技术规范和要求被破坏。
- 可观测性:微服务架构可观测性建设的重要性毋庸置疑,相比于单体架构,需要更完善的链路追踪建设,否则系统出现异常一段一段排查非常困难和耗时;同是需要更完善的度量监护和告警,且具备便捷的配置能力,消息能够直达运维和业务团队;由于微服务具备更多的基础设施和调用过程,日志需要更良好的规划,以方便日志的检索和存储优化,否则日志量可能过大,导致查询性能差,存储成本高。
- 集成平台:在微服务架构下,建议考虑将集成能力由之前的SDK方式升级为服务方式。升级后可以保留原有的SDK(部分接口可能需要调整),连接到集成平台的服务,然后由集成平台来适配第三方。好处是当第三方有调整或接入新的第三方时,通常只需要修改或添加适配服务,而不需要所有依赖的业务服务。当第三方提供新能力时,在做好兼容的情况下,也只有使用新能力的服务需要升级SDK。很多第三方服务费用并不包含在SaaS产品的订阅费中,而是按量收费(如短信、电子合同),并需要提供线上充值、使用范围管理、消费记录和统计等管理功能。用统一的集成服务也更利于统一的管理功能设计与维护。
- 开放平台:在单体模式下,对外开放的接口可能会单独部署一个站点,但一般还是本地调用,使用微服务后,开放平台可以完全独立出来,和业务服务的交互使用远程调用。开放平台集中建设开放领域内的能力,例如不同的鉴权方式、限流、计费、开放文档、Mock服务、接口调试、调用链路和日志等。
- PaaS平台:前面提到的支撑业务个性化的PaaS平台,在多业务发展阶段可能就会产生多个业务共用的情况,到本阶段可以进一步抽象独立成不同的PaaS平台。
- 任务调度平台:在微服务架构下,原来基于进程/线程的服务调度方式可以升级为基于服务的任务调度。通过使用容器化部署,可以进行更好的资源隔离、获取方便和快速的系统弹性能力,从而降低任务管理的复杂度。
- 大数据平台:在单体架构下的数据清洗方式可能无法适应微服务架构,一方面随着服务㰽,需要整合的数据更分散、逻辑更复杂;另一方面随着业务的发展数据量越来越大。此时需要考虑使用大数据相关的技术来应对微服务架构的数据分析和报表需求。
- 运维平台:微服务带来的基础设施规模增大,无疑会增加运维难度。特别是对于业务服务所需要的资源、在业务变化时的调整及自动伸缩、异常时限流、熔断的策略等方面,需要对业务有足够的了解。这对运维和业务的协作会带来很高的沟通和执行成本,会降低异常和故障恢复的速度。因此在微服务有必要建设运维平台,在设定底线约束的基础上,将运维能力开放给业务开发团队,同时业务团队对自己业务的运维共同承担责任。这会极大的加强业务团队和运维的合作,同时会让系统尽量处于较好的资源/业务平衡。
架构重构的路径
从单体到微服务的架构和重构充满了风险和挑战,单体规模越大,之前的代码设计越糟糕,则困难也会越多。网上也有很多讨论这种迁移的方案和经验,本次我们重点讨论一下重构的策略和路径,不展开具体的技术细节。
-
首先使用微服务架构构建一个产品:我们要使用微服务构建一个产品来构建微服务基础设施以及获取相关的经验。为了降低过程的复杂度,最好是用于构建一个全新的产品。如果没一个机会,也最好是一个相对独立的模块。除了微服务构建和运行的基础设施外,还需要考虑与现有系统的打通。
-
使用代理服务与现有系统连接:建议使用代理服务与现有系统打通,一方面可以简化微服务与有原有系统的交互,另一方面在原有功能重构到微服务的过程中,可以减少对依赖服务的影响。系统交互的示意图如下:
- 逐步构建公共技术平台:在微服务开始阶段,可以使用上面提到的代理服务的方式使用公共的技术能力,比如第三方集成、任务调度等。当微服务架构成型,在开始大规模迁移前,需要完成我们上面提到的公共技术平台的建设,避免业务和原有单体有过多的耦合。平台建设初期可以只实现必要的功能,随着业务的迁移需求进行逐步的完善。系统交互示意图如下:
-
使用绞杀者模式:在基于服务代理的方式下,可以应用绞杀都模式来完成单体应用的逐步迁移。示意图如下:
由于公共业务的依赖方较多,部分和业务关联紧密的公共业务让依赖方统一修改调用接口可能会比较麻烦,可以根据实际情况将功能逐步迁移到微服务,在单体中保留入口(代理到微服务),随着业务的迁移,在单体中的依赖会越来越少,再选择时机完成彻底的迁移。
1、 在微服务建立新功能(子模块4)
2、将原有功能迁移到微服务(子模块1、2)
3、完成微服务迁移
-
及时清理单体中的废弃代码:虽然这不是一个需要的动作,但遗留的无效代码会影响后续迁移对影响的分析和设计(比如想废弃原有的任务调度系统,发现有一堆任务还在跑,就不得不分析这些任务的有效性),从长远影响看,及时的清理是非常值得去做的一件事情。
-
制定迁移节奏和计划:整体的技术和业务规划可能会有很多变化,但还是应该为迁移节奏制定一个有效的整体计划,业务团队可以考虑整体计划来规划自己的迁移节奏,如果没有这个计划,业务团队可能会一直推迟迁移的启动时间,整体架构的重构可能会拖很长的时间(几年、十几年、或无法完成),这会导致团队不得不长期维护两套架构。
架构重构是一项危险而艰难的工作,投入大而且耗时比较长。一方面我们我们需要从长远角度出发进行规划,另一方面也要考虑投入产出比,以及平衡好重构对业务发展的影响。同时我们也可以看到,前期单体系统的设计对微服务重构的影响非常大。比如业务间不应该有基于数据库等存储依赖(或尽量少),产品、模块间的交互应该通过明确定义的接口等设计可以降低系统的耦合。而高耦合设计和低质量的代码会导致重构风险大、成本高、时间长,这会严重降低架构重构的信心和动力。因此良好的设计和高质量的代码是架构有序演进重要保障。