从既有系统到微服务架构
微服务近年来可谓炙手可热,合理的使用微服务架构可以解耦系统、提供更好的软件伸缩性以及提高组织的敏捷性。然而现实中较少有项目一开始就会选择使用微服务架构,绝大多数新项目在最初都会务实地从更容易掌控的单体架构起步构建,如果最终发现单体架构复杂到影响了团队的开发效率及软件的伸缩性等方面时,才会开始考虑逐步将系统往微服务架构做演进。
现实中任何软件架构都是诸多trade-off的结果,想要获得微服务架构所带来的好处也就意味着有能力承担它所带来的的副作用。Martin Fowler就曾在MicroservicePrerequisites一文中指出实施微服务所需要的先决条件,他用“个子是否够高”形象地比喻了微服务所需的技能门槛。
而对于既有系统,还需要一种务实的演进方法和实施策略,使得能够伴随着恰当的代码重构,逐步积累能力和完善基础设施,最终平稳的将其演进到微服务架构。
本文总结了一些从既有系统到微服务演进之路上会遇到的问题和解决策略。文中使用“既有系统”而非“遗留系统”,是因为遗留系统给人一种即将退出生命周期、行将就木的感觉,而我们则希望把精力投入到还有长远商业价值的系统上,通过合理的微服务演进让其具有持续的生命力。
演进策略
本文推荐的从既有系统到微服务的一种务实安全的演进策略是:自上向下分析,自下向上重构,逐步完善配套。
所谓“自上向下分析”,主要包含以下步骤:
-
整体演进路线规划:
- 梳理既有系统的领域模型,设计合理的内部服务边界,按照优先级和依赖关系规划演进路线;
-
服务治理方案设计:
- 按照优先级,为新服务定义职责,接口,与既有系统的交互方式以及跨服务的集成测试方案;
- 定义新服务的打包、测试、发布、部署、集成方式,目标是能够为其构建独立的代码库和持续交付流水线;
-
代码解耦设计和重构:
- 分析属于新服务的独立代码以及和既有系统耦合的代码,从物理打包和逻辑代码重构两层面解决耦合问题;
- 针对不同的解耦策略,制定不同的测试策略,完善自动化测试以支撑对应的代码重构工作。
所谓“自下向上重构”,指的是按照前面的分析设计结果,从代码重构开始,自下向上按照优先级和一定节奏持续进行服务化改造。
而“逐步完善配套”,指的是随着服务化的开展,逐步完善代码库管理,多流水线集成,并逐步按需引入服务治理框架,积累微服务需要的技术和工具能力。
上述过程是一个迭代的过程:通过适度的分析和设计,规划出具体的落地工作,然后通过小步增量的实践迅速获得成果和反馈,在过程中逐步培养人的能力、完善支撑微服务架构的工程实践。
自上向下设计
明确目标和约束
对既有系统做微服务化解耦,需要对不同解耦方向能获得的收益和存在的约束做到心中有数。见过一些组织在做微服务拆分时只强调可以获得的片面好处,忽略了对组织更有益的其它潜在价值,或者低估了微服务化带来的问题。这往往会导致不合理的服务边界划分或者错误的优先级排序。
沿着不同的边界划分,目的是为了不同的价值目标:
-
沿着系统内不同的变化原因和变化频率做服务划分
通过隔离不同的变化方向,减少特性开发之间的干扰,使能小的独立交付团队。通过独立代码库、独立流水线,独立的开发、测试、交付和运维过程,提高交付效率和响应速度。 -
沿着不同的资源使用边界做服务划分
通过将不同资源占用特征的服务进行隔离,使能独立的水平弹缩,优化资源使用效率和提升业务响应能力。 -
沿着不同性能路径边界做服务划分
通过将性能核心路径作为独立服务进行隔离,可以为性能核心路径使用不同的技术栈以及做各种极致的性能优化;另一方面避免各种改动影响到关键路径的性能下降(例如被动引入更多的异步交互等)。
由于服务划分会为系统引入新内部边界,所以必须考虑如下的约束:
-
数据一致性约束:服务划分后可能带来数据一致性变弱的问题,需要考虑是否可以接受;
-
性能约束:服务划分后带来的潜在性能下降,需要考虑如何度量以及承受程度;
-
容错性约束:服务划分为系统内部引入更多的分布式故障点,需要能够为其找到可接受的容错设计;
-
耦合关系约束:服务划分会放大系统的耦合问题,所以需要考虑沿着系统的松耦合边界进行服务划分,避免服务间复杂的交互或者联动修改。
在开始可以按照理想的价值目标去划分微服务边界,然后再接受每一项约束的挑战,最终的服务划分方案往往是一个在目标和约束之间逐渐平衡后的结果。
避免过度设计陷阱
对既有系统的微服务改造设计往往会陷入“架构设计陷阱”。过于详尽的分析和设计反而常常会阻碍微服务的拆分,经常得到一个“成本很大,困难很多”的论证。
对于这种情况,建议采用 快速启动、增量交付、大胆实验、小心求证 的原则。即快速构建目标,通过敏捷和精益软件开发的方式快速实践,通过反馈进行快速学习,在行动中解决各种问题。
具体的实践过程中:
- 有了基本的分析后,快速成立试点团队作为探索者进行解耦验证,尽早获得反馈;
- 虽然快速启动,但是短期目标要明确,通过迭代的增量交付来规避风险;
- 在实践过程中逐步按需对修改影响较大的特性补充和完善自动化测试;
- 对有较大风险的代码修改,可以先拷贝一份在新的服务内做实验,获得足够反馈后再择机合入原代码库;
- 可以借助工具分析代码的依赖关系。曾经在一个项目我们通过部署doxygen和graphviz来可视化代码的依赖关系和解耦进展,取得了不错的效果。
微服务设计
关于微服务设计的方方面面已经有很多优秀的书和文章了,例如《微服务设计》就是一本不错的教材。即便如此到任何一个具体的领域,仍有很多困难和挑战,需要领域专家和软件工程师们密切配合去解决。
使用领域驱动设计(DDD)方法可以帮助所有参与者重新梳理业务并达成共识。通过识别业务的界定上下文和聚合根,可以为如何划分服务提供参考。可以尝试组织DDD Workshop,但是要清楚这只是一项工具,而且有时成本并不小。DDD是一个演进式的过程,更多的工作需要随着深入业务和代码,通过实践收集反馈迭代式的进行。
现实情况中,负责系统架构演进的人员都是对业务和设计现状比较熟悉的专家,一种高效的做法是从当前的数据模型直接入手。分析每一张表和每项字段所支撑的业务,将业务按照数据的内聚性进行分类,然后以此作为服务划分的起点。可以假设已经将数据按照新的服务边界重新分库分表,然后尝试基于此重新构建每条业务流程,并在过程中解决由于数据拆分而出现的各种问题。该做法适合对微服务架构有经验的人和领域专家合作完成,这样能够对出现的各种问题找到不偏颇的解决方案。
天下没有免费的午餐,有时为了得到微服务的好处,是需要做一些妥协的。例如数据模型中某一实体的不同属性具有不同的业务内聚度,所以同一概念的不同属性数据被分到了不同服务中,但是这些数据在某些场景下要保持同步(例如需要被整体删除或修改等)。最常见的解决方案是选择一个稳定的服务作为对该实体的权威拥有者,其它服务通过某种手段(例如消息队列)和该服务对齐各种实例操作。这意味着业务要能接受最终一致性,还得接受在某些异常场景下数据一直没有同步成功而上报的告警。
在设计服务的集成方式时,需要站在业务角度去识别谁是更稳定的服务。依据“向稳定方向依赖”的原则,我们只会让不稳定的服务去调用稳定服务的API,而反过来稳定的服务最好通过消息队列发布事件。那些需要跨越多个服务去获取数据的服务,一般可以通过监听事件和缓存与系统解耦,但这并非适合所有场景。在某些场景下由于业务的一致性和性能限制,我们确实需要往回退,把某些服务进行合并。这就是个不断的头脑风暴,然后再在各种选择中做trade-off,最终获得平衡的过程。
对于缺乏经验的团队可以从较容易拆分的服务做起。例如一个web服务端可以先把路由和基本鉴权拆分出来,交给API Gateway负责;然后再把各种报表和统计等一致性与性能要求相对低的拆分出来,最后再尝试切分其它业务处理。
一旦服务拆分出来,就可以根据业务特征重新优化数据模型并选择更适合的数据库。另外服务的API设计也是有技巧的:应用接口隔离原则,需要API能独立完成功能,又要粒度相对小可以灵活组合。这方面亚马逊各种AWS服务的API设计就是不错的样例。
自下向上重构
得到了可行的服务划分方案,接下来就需要实际操作代码,将新服务的代码与既有系统进行解耦,为独立的服务代码库和流水线做好准备。
目录/包结构调整
软件的包结构一般和构建软件的组织结构以及建模方式有关。一般复杂系统同时存在着两个大的变化方向:技术维度和业务维度,而软件的包结构往往只能反映其中的一个维度。当组织结构以软件的技术维度进行划分,那么系统的包结构也基本上会以此划分,这时业务维度的变化往往会映射到系统的每一个包上。反过来也是一样!衡量哪种包结构合理,往往是看当前哪个是主要的变化方向。对主要的变化方向进行拆包隔离,可以降低代码变化之间的互相影响程度。
如果按照变化方向进行包的拆分,就会发现系统中应该存在很多小的包,最后每个服务是一堆原子的小包组合。这本质上是将系统重新进行合理模块化的过程。Adam Drake在文章Enough with the microservices中就直接指出微服务架构应该先从良好的模块化重构做起,大多数时候当模块化做好了甚至会发现很多问题已经得到解决了。
然而既有系统的模块合理化调整很难仅通过重新拆包达成!因为代码是有逻辑的,模块化的逻辑边界不可能刚刚好落在代码文件边界。大多数情况下都需要先对某一个代码文件进行拆分,对某一个类或者函数进行重构,对某一段逻辑进行重新设计,然后才能重新得到一个一致的逻辑和物理边界,支撑继续的拆包工作。
之前见过一个组织通过拆包进行系统解耦,他们把新服务和既有系统共享的所有代码拆分成很多小的共享包。这样做后看似每个服务在构建和流水线是独立性的,但是问题在于那些共享包的代码量并不小而且包含很多耦合的业务逻辑,新的修改经常导致新服务和既有系统一起升级更新。
可以先对新服务建立独立的目录,然后尝试把属于新服务的代码逐渐往独立目录中迁移,在这一过程中识别出新服务和既有系统耦合的代码,然后一边重构,再一边继续调整目录和包结构,最后使得新服务和既有系统在物理和逻辑上同时解耦。
代码重构
软件重构目的是为了解耦新服务和既有系统之间的共享代码。共享代码一般分为如下几种形式:
-
共同依赖的组件或者类,这又分为如下几种情况:
- 稳定的基础功能代码。例如编解码库,加解密等等。这些代码可以按照功能发布成独立组件,供每个服务自行决定使用。
- 服务间接口和消息定义。这类代码可以划分到独立的库中,尽量保持向前兼容,由接口的消费方自由选择依赖的版本。服务间的API和消息定义在本质上是契约的共享,可以使用契约描述文件代替共享代码,使用时自动从契约描述生成代码,这对于不同技术栈的服务会比较友好。
- 不合理编码导致的耦合。例如耦合了所有功能的大而全的单例类,一般是一些全局配置类或者是“创建一切”的工厂类等。这种情况需要对原有设计进行重构,对大而全的类进行拆分,将属于不同服务的代码拆分到不同的类中,由各个服务领回属于自己的代码。
-
共同继承的接口或者类,这又分为如下几种情况:
- 继承是为了组合:需要将继承的公共处理重构为支撑组件,由不同的服务根据需要自行选择组合和使用方式。
- 继承是为了面向接口编程,这时接口往往是为了配合某些公共业务处理而做的抽象。这些公共处理可以按照以下几种情况进行重构:
- 接口背后的公共处理包含了复杂的业务逻辑,优先考虑将该公共处理变为一个服务。这时需要将继承接口上的同步调用变为服务间的消息接口。
- 接口背后的公共处理简单或者并不稳定,可以考虑按照“Replication Over Reuse”的原则,由每个服务自行实现,减少服务间的代码共享。
- 接口背后的公共处理复杂,但是包含的业务逻辑相对稳定,如果不能将其独立为服务(例如由于性能原因),可以将其打包成公共组件,由每个服务自行组合使用。
从既有系统到微服务演进,在具体的落地中会发现最基础的工作主要是代码重构。而能否很好的实施代码重构是一个体现团队基本软件技能素质的过程,需要团队提升软件设计、代码重构、自动化测试方面的能力。
逐步完善配套
随着自下向上的重构,新服务的代码逐渐解耦到独立的目录或者包中,这时就可以按需补齐服务化所欠缺的服务治理机制和各种工程实践。在服务代码不具备独立性的时候开始尝试搭建各种服务治理机制和工程流水线,往往会引入很多偶发复杂度,对工具提出一些不切实际的要求。
服务治理
微服务作为面向服务架构当下能够流行,原因之一在于随着技术的进步各种服务治理工具都可以廉价获得。服务注册发现、API网关、消息队列、负载均衡、服务监控、集群运维等每种需求都可以在网络上找到一批的开源工具,而团队则需要根据自己的现状进行合理的选择。有经验的团队可以把各种不同的治理工作交给最合适的工具去做,而对于缺乏经验的团队来说可以先从某一工具入手累积经验。曾经有一个团队在开始不想引入过多工具复杂性,先选择使用redis做缓存和消息队列,随后又使用redis做分布式配置以及服务的注册与发现等等。后来随着能力提升,转而使用etcd替代redis做服务的注册发现,使用kafka做消息队列。
对服务治理工具的选择要避免陷入选择困难症。每个团队都会觉得自己的业务特殊,开源工具总是不能满足自己的所有要求。带着这种想法很容易裹足不前,一再浪费架构重构的合适时机。精益的做法是,先找到业界普遍使用的工具,一边使用一边解决问题,一旦开始了很多问题在实践中总能迎刃而解。对于一些注重性能的系统,不可避免的需要对开源组件在特定业务场景下进行优化定制,也最好先开始使用然后在实践中确定优化的方向。
持续交付
“服务有自己独立代码库和交付流水线,可以避免交付过程中的互相干扰,提高交付速度和质量”,遗憾的是上述描述其实是个伪命题!
真正减少团队干扰、提高交付速度和质量的是“正确的解耦”本身。独立的代码库和流水线会将架构约束显示化,让团队成员难以犯错。但是如果过早的对不成熟或者不稳定的架构边界进行固化,反而会降低团队的效率,让后续架构调整变得困难。另外在系统没有合理解耦的情况下,独立的代码库和流水线只会让交互变得更复杂,导致对构建和发布工具提出一堆不合理的要求。
但是如果服务之间确实已经正交拆分,代码边界和架构边界一致并且是稳定的,这时独立的代码库和流水线就可以降低团队在交付流水线上的互相干扰和排队,此时就值得为新的服务建立独立的代码库和自动化流水线。考虑到服务之间的集成,往往需要多级流水线,这时选择一款支持pipeline的持续集成工具是必要的。Jenkins2.x以及GoCD是此类产品的代表。
适应的组织结构和文化
康威定律经常被拿来说明组织结构和系统架构之间的互相作用关系。在对既有系统的服务化重构中,软件架构和团队结构同步进行调整会让整个过程更加顺畅。曾经有一个系统最初按照技术维度划分团队,后来为了提高响应市场的速度把团队按照不同的业务类型进行了调整。重新划分后的团队开始发现他们会经常修改同一公共组件,这时他们自发的对该组件进行了解耦,将其中和业务相关的逻辑各自领了回去,然后将剩下的稳定逻辑下沉到了基础设施中。
除了匹配的组织结构,还需要团队逐渐调整自己的文化。专门的测试人员和运维人员在微服务架构下必然成为瓶颈,需要改变传统的细分工的文化。团队每个成员都要有意愿和能力承担起服务的测试和运维工作,这需要组织从文化建设到考核方式做对应的调整。
总结
对于既有系统做微服务演进,一旦第一个服务改造成功,后续的服务借助前面的成功经验和已有的基础实施,会更加的容易拆分。不过第一个服务的拆分确实需要投入比较大的决心和精力,本文给出了一些建议,归根结底总结起来就是:以精益的方式开展,以代码解耦为核心,以服务化技能做武装,以组织结构和文化调整做基础!