在第2章中,我们探讨了如何思考迁移微服务架构这件事。具体而言,我们探讨了微服务是否是个好主意,还探讨了如果微服务是一个好主意的话,我们应该如落地新架构并确保我们朝着正确的方向前进。
我们讨论了一个良好的服务看起来是什么样子的,还讨论了为什么较小的服务可能对我们更好。但是,可能已经存在大量不遵循这些模式的应用了,我们如何处理这一事实呢?我们如何在不需要进行大规模重写的情况下来拆解这些单体应用呢?
在本章的其余部分,我们将探讨各种迁移模式和技巧,以便为采用微服务架构而提供帮助。我们将研究那些适用于如下场景的模式:
- 供应商提供的黑盒软件
- 遗留系统
- 计划继续维护并发展的单体应用
但是,要使增量部署能够正常工作,我们必须确保我们可以继续开发并使用现有的单体软件。
记住,我们希望增量迁移。我们希望逐步迁移到微服务架构,从而使我们能够从迁移过程中得到学习,并在需要时改变主意。
改变单体应用,还是不改变?
在迁移过程中,首先要考虑的是:我们是否计划(或能够)修改现有的单体。
如果有能力修改现有的系统,就可以为使用各种模式来修改系统提供最大的灵活性。但是,在某些情况下,这会是一个严格的约束条件,从而导致我们没有这种机会来修改现有的系统。现有系统可能是没有源代码的供应商产品,也可能是用我们不再具备对应能力的技术来实现的。
同时,也许还有一些较软的驱动因素让我们不愿修改现有的系统。当前的单体架构可能处于非常糟糕的状态,以至于变更成本太高了。因此,我们想减少损失并重新开始(尽管我在第二章详述了迁移需要考虑的事情,我仍担心人们会轻易得出这个结论)。不愿修改当前单体的另一种可能是:还有很多其他的人也会开发该单体系统,我们担心对单体的修改会影响他们的工作。某些模式——例如稍后将探讨的抽象分支模式(Branch by Abstraction pattern)——可以缓解这些问题,但我们仍然会给出如下结论:对单体的修改会给其他人带来太大的影响。
源代码丢失也是一种令人难忘的场景。我曾经与一些同事一起来帮忙扩展一个计算量繁重的系统。该系统的底层计算由我们提供的一个C lib库
执行。我们的工作是集合各种输入,将它们传递到该库,从该库取回计算结果并存储结果。这个库本身千疮百孔。内存泄漏和极其低效的API设计只是导致问题的两个主要原因。我们索要该库的源代码数月之久,以便我们可以修复这些问题。但是,我们被拒绝了。
多年以后,我遇见了那个项目的发起人(sponsor)。于是我问他:为什么不让我们修改底层库。直到那时,他才终于承认是他们把源代码弄丢了。这太难堪了,因此他不想告诉我们!不要让这种情况发生在我们身上。
因此,希望我们处于如下的场景:可以使用并修改现有单体系统的代码库。但是,如果我们不具备这个条件,这是否意味着我们陷入困境之中了?完全相反,此时,很多模式可以帮助我们。我们很快就会简要介绍其中的部分模式。
剪切,复制,还是重写
一张取自网络的斗图
即使可以访问单体的现有代码,但是当我们开始把功能迁移到新的微服务时,如何处理现有的代码也并不总是一目了然。我们应该按原样移动代码,还是重新实现功能?
如果现有的单体代码已经被充分拆分了,则可以通过移动代码来节省大量时间。这里的关键是要了解我们想从单体中复制代码,并且至少在现阶段,我们不想从单体中删除此功能。为什么要这么做呢?因为让该功能在单体中保留一段时间会为我们提供更多选择。这可以为我们提供一个回滚点,或者为我们提供并行运行两种实现版本的机会。接下来,一旦我们对迁移感到满意,我们就可以从单体中删除该功能。
重构单体
我观察到,通常,应用单体的现有代码到新的微服务的最大的障碍是:现有的代码库没有围绕业务领域概念来组织。技术分类更为突出(例如,想一下我们所看到的所有Model,View,Controller包名)。当我们尝试移动业务领域的功能时,这可能会很困难:现有的代码库与该业务分类不匹配,因此,即使找到要移动的代码都可能会是问题!
如果确实沿着业务领域边界重新组织了现有的单体架构,那么我强烈推荐Michael Feathers的《修改代码的艺术》一书。在该书中,Michael定义了接缝(seam)的概念:接缝是指程序中的特殊的点,在这些点上,无需做任何修改就可以改变程序的行为。本质而言,可以围绕要修改的代码段定义接缝,对该接缝进行新的实现,并在变更完成后用新的实现替换旧有的实现。Michael把使用接缝来安全的工作的技术作为帮助清理代码库的一种方式。
虽然Michael的接缝的概念通常可以应用于许多范围,但接缝的概念确实与界定的上下文非常吻合。因此,尽管《修改代码的艺术》没有直接引用领域驱动设计的概念,但是我们也可以使用该书中的技术,并按照书中的这些原则组织我们的代码。
模块化的单体?
一旦我们开始了解现有的代码,显然,接下来值得考虑的事情是:采用新识别出的接缝,并开始把接缝抽取为单独的模块,从而使单体成为模块化的单体。我们仍然只有一个部署单元,但是该部署单元由多个静态链接的模块组成。这些模块的确切形式取决于我们的底层技术栈——对于Java而言,模块化单体将由多个JAR文件组成;对于Ruby应用而言,模块化的单体则可能是Ruby gems的集合。
正如我们在本书开始时简要提到的那样,将单体拆分为可以独立开发的模块可以带来很多好处,并且可以避免微服务架构的许多挑战。模块化的单体还是许多组织的最佳选择。我已经与多个团队进行了交谈,他们已经开始将其单体拆分为模块化的单体,以最终转向微服务架构,然而却发现模块化的单体已经解决了大多数的问题!
增量重写
我总是倾向于先尝试挽救现有的代码,然后再诉诸于重新实现功能。我在上一本书Building Microservices中给出的建议也遵循了这个思路。有时,团队会发现他们从这项工作中获得了足够的利益,并认识到他们最开始时就不需要微服务!
然而,我必须接受的是,实际上,我发现很少有团队采用重构单体的方法来迁移到微服务。取而代之、似乎更常见的是:一旦团队确定了新创建的微服务的职责,他们便会对该功能进行全新的实现。
但是,如果我们开始重新实现功能,我们是否会面临重蹈大规模重写(big bang rewrites)覆辙的危险?迁移的关键是确保我们一次只重写一小部分功能,并定期将重新实现的功能交付给客户。如果重新实现服务行为的工作需要几天或几周,那就没问题了。如果时间表开始看起来有几个月了,那么需要重新检查自己的方法。