
从毕业上工地搬砖,到今天也有八年了,时光荏苒,岁月如梭,有时候颇有种 “一入侯门深似海,从此萧郎是路人” 的恍惚。
这些年维护过老旧系统,下线过系统,也从 0 到 1 建立过新系统,经历了从单体过渡到微服务的整个过程,陆续分享一些浅薄的思考,权当对这个人生阶段的记录。
本来想写到一篇里的,感情波折 + 力有未逮,就拆为 3 篇,陆续填坑吧。
- 🎯从单体到微服务,Part 1:代码管理变化
- 从单体到微服务,Part 2:构建模式变化
- 从单体到微服务,Part 3:运行模式变化
从一个巨石应用拆分为多个小一些的领域服务,一般会同时把代码库也拆分为多个,一个领域服务对应一个代码库,方便独立演化,好处不再赘述,说说在现实中会碰到的几个问题:
💣 分支爆炸
巨石应用只有一个代码库,现在拆分为 10 个领域服务,那么有 10 个代码库,每个代码库至少得有 3 个分支( 开发分支、预发布/提测分支、发布/生产分支,如果采用 Feature 分支模式进行开发或者同时有多个版本在维护,还会再多几个分支,产生的分支交互关系很复杂,需要小心 ),10×3 就是 30 个分支。
且一个服务代码库的 3 个分支不是各自独立的,一定是围绕一个机制 —— 通常是从开发到测试再到生产的软件研发流程 ,让代码变更在 3 个分支之间有序流转。同一时间,不同的服务,必然处在软件研发流程的不同环节,也即处在分支流转的不同阶段,这也会给分支管理增加负担。
最理想的情况下,对领域服务的拆分是正交的,此时一个需求也可能需要同时变更 2 个服务,也就是让 6 个分支产生了一定的关联。现实中,显然不可能对服务拆分、需求作这么理想的假设,甚至落地服务拆分的现实路径就是渐进拆分。
这就棘手了,而且分支爆炸暗示了环境爆炸 —— 各服务在多大程度上独享/共享运行环境,1 个代码库的 3 个分支是否对应到不同环境等。
相关的管理压力需要借助组织进行分解、重组,繁琐的手工操作必须要借助工具进行自动化管理。
💣 版本号爆炸
每一次发版,都需要根据【版本号定义】出一个版本号,最常用的是由 3 个数字组成的语义版本号 [X].[Y].[Z],除了 [Z],其他两个位置的数字变化都可能表明新版本做了不兼容的调整,暗示了 2 个风险:
- 当前版本需要的运行环境发生了变化,需要对运行环境进行适配调整,影响环境管理方,比如升级了 jdk;
- 当前版本暴露给外部使用的接口( Restful API、AbstractClass、Interface… )发生了变化,需要使用方进行适配调整,影响服务或包的使用方。
服务每一次发版就有一个新的版本号,由于服务在一定尺度上和其他服务存在耦合,各服务的版本号一定存在兼容关系,所有服务的兼容关系放在一起就形成了一个兼容关系网,当某个服务有新版本发布时,不能不关心这个版本和其他服务已发布版本的兼容关系,如果这个版本做出了不兼容的修改,其他服务未同步进行调整,就可能引发故障。
考虑一种场景,某天 A 服务运行过程中发现问题,需要将 V5 版本回退到 V3 版本:B 服务 当前的 v9 版本依赖于 A 服务 V4 版本新增的特性 F,这时候 A 服务就不能回退到 V3 —— 会丢失 V4 版本新增的特性 F,只能直接修复问题,前进到 A 服务的 V6 版本。或者,当 A 服务回退到 V3 版本,它依赖于 B 服务的 v8 版本,这就要求 B 服务一起回退。
这个场景表明,想回退一个服务,必须将其他服务一并考虑进来,小心分析其版本兼容关系。魔鬼藏在细节里,如果兼容关系复杂、服务设计不合理或者管理不善,秒级回退就会沦为一个笑话,可行的只有前进这一个选项。
一剂良药:自动化+模块化
有人说我用 Gradle 对代码进行了模块化,但 Gradle 的模块化比较糙,它只是对代码文件的物理组织结构进行模块化,无法限制一个模块内的实现伸手使用另一个模块的内部实现细节( 容易造成复杂失控的依赖关系 ),缺少了对模块边界的管控 —— 模块应只允许外部调用其对外暴露的接口( 参见 Java 9 Jigsaw ),其内部实现细节对外隐藏。
并且模块的世界也应是一个有序的世界,比如符合层次化原则、不能有循环依赖等。对边界的合理规划和控制应作为一个设计原则,贯穿软件的任何尺度,从方法、类、包,到模块、服务、系统边界。当边界从混乱变得有序,通过判断边界上的契约是否发生了变化,就可以对兼容关系进行自动化管理了。
💣 依赖爆炸
开发也要站在行业的肩膀上,不引用第三方工具包,几乎做不出任何功能,依赖和复用是一个硬币的两面( 关于复用,详见前作:代码复用)。
巨石应用拆分后的每一个服务都有其依赖树,树里的包绝大部分是行业提供的第三方包,且依赖还在继续分化,朝着颗粒度更细、可复用性更高的方向发展( 可复用性高 ≠ 被复用率高 )—— 从行业整体来看是有益的。
假设巨石应用生产环境以前有 100 个依赖包( 基础 60 + 特性 40 ),现在拆分为 10 个领域服务后,每个服务可能有 70 个依赖包,生产环境至少会有 700 个依赖包。由于各个服务在一定程度上是独立演进的,不同服务、不同开发人员的本地环境、不同的测试环境、不同的生产环境,这些现实因素的存在使得同一个类型的包,必然存在不同的合法版本。
怎么协调和二方、三方包的依赖关系,怎么协调各个服务、各个环境依赖包使用和升级的步伐,怎么引进、退出、替代依赖包,都是需要考虑的问题。
The End
任何种类的问题都需要适配人有限的认知能力,才能被人处理,所以可以从人的认知能力出发,基于 2 个维度( 有序性、复杂性 )将问题空间划分为 4 个象限进行认知:有序简单、无序简单、有序复杂、无序复杂,从左到右,问题的认知和处理难度不断提高,直至无法被认知、无法被处理。

科学家和高级神棍工作在无序复杂象限,专业工作者工作在有序复杂象限,其他人工作在剩余两个象限。问题所处的象限不是固定的,大体是随着新理论新发现而从复杂变得简单、从无序变得有序,是一个不断流动的过程。
上述的爆炸问题属于有序复杂问题,对于有序的问题,只要摸清规则,就可以大胆将其自动化,不再要求人参与到 B、C、D、E 4 个中间环节,直接构造出从 A 到 F 的( 人造 )黑盒规则,将大量重复繁琐的工作交给机器去完成,让人和机器各展所长。
0 条评论