从整体到微服务的过渡:历史和实践

在本文中,我将讨论我所从事的项目如何从大型整体变为一组微服务。

该项目的历史始于很久以前,即2000年初。最初的版本是用Visual Basic 6编写的。随着时间的流逝,很显然,将来很难用这种语言进行开发,因为IDE和语言本身开发得很差。 在2000年代后期,决定改用更有前途的C#。 新版本是在对旧版本进行改进的同时编写的,逐渐在.NET上有越来越多的代码。 C#后端最初专注于服务体系结构,但是在开发过程中,使用了具有逻辑的共享库,并且在单个过程中启动了服务。 事实证明,该应用程序称为“服务整体”。

此捆绑包的少数优点之一是服务能够通过外部API相互调用的能力。 过渡到更正确的服务以及将来的微服务体系结构存在明显的先决条件。

我们在2015年左右开始了分解工作。 我们还没有达到理想的状态-大型项目的某些部分很难被称为整体,但是它们看起来也不像微服务。 但是,进展是可观的。
我将在文章中谈论他。



目录内容




现有解决方案的体系结构和问题


最初,体系结构如下所示:UI是一个单独的应用程序,整体部分是用Visual Basic 6编写的,.NET中的应用程序是一组与相当大的数据库一起工作的相关服务。

先前解决方案的缺点

单点故障
我们有一个单点故障:.NET应用程序在一个进程中运行。 如果任何模块崩溃,则整个应用程序将失败,因此您必须重新启动它。 由于我们正在为不同的用户自动化大量的进程,由于其中一个失败,某些进程可能在一段时间内无法工作。 而且由于软件错误,冗余也无济于事。

改进阵容
此缺陷相当有组织性。 我们的应用程序有很多客户,他们都希望尽快完成它。 以前,不可能并行执行此操作,并且所有客户都排队。 此过程对业务造成负面影响,因为他们需要证明自己的任务很有价值。 开发团队花了很多时间来组织这个阵容。 这花费了大量的时间和精力,结果产品无法像他本来那样迅速地改变。

资源使用不当
当将服务放在一个过程中时,我们总是在服务器之间完全复制配置。 我们希望将负载最大的服务分开放置,以免浪费资源,并获得对部署方案的更灵活管理。

很难引进现代技术
所有开发人员都熟悉的问题:希望将现代技术引入该项目,但没有可能性。 有了大型的整体解决方案,对当前库的任何更新,更不用说过渡到新的库,都变成了一项不平凡的任务。 需要很长时间才能证明团队领导者带来的奖金比花在神经上的还要多。

发行困难
这是最严重的问题-我们每两个月发布一次发行。
尽管进行了测试和开发人员的努力,但每个发行版都对银行造成了真正的灾难。 Business知道在一周开始时某些功能对他不起作用。 开发人员知道他们正在等待一周的严重事件。
每个人都有改变这种状况的愿望。

微服务期望


根据可用性交付组件。 由于溶液的分解和各个过程的分离,在组件可用时将其交付。

小食品队。 这很重要,因为大型团队难以处理旧的整体。 这样的团队被迫按照严格的程序工作,但是我想要更多的创造力和独立性。 只有小团队可以负担得起。

在单独的流程中隔离服务。 理想情况下,我想隔离在容器中,但是用.NET Framework编写的大量服务只能在Windows下运行。 现在,.NET Core上有服务,但是到目前为止,它们很少。

部署灵活性。 我想根据需要而不是代码的力量来组合服务。

使用新技术。 这对任何程序员都很有趣。

过渡问题


当然,如果很容易将一个整体拆分成微服务,那么您就不必在会议上谈论它并撰写文章。 在这个过程中,有很多陷阱,我将描述影响我们的主要陷阱。

第一个问题是大多数独角兽所特有的:业务逻辑的一致性。 编写整体时,我们想重用我们的类,以免编写多余的代码。 而且,当切换到微服务时,这将成为一个问题:所有代码都紧密连接在一起,并且很难分离服务。

在开始工作时,该存储库有500多个项目和70万行代码。 这是一个相当大的解决方案,也是第二个问题 。 无法简单地将其分成微服务。

第三个问题是缺乏必要的基础设施。 实际上,我们参与了将源代码手动复制到服务器的工作。

如何从整体转向微服务


微服务分配

首先,我们立即确定自己,微服务的分离是一个反复的过程。 始终要求我们并行进行业务任务开发。 我们将如何在技术上执行此操作已经是我们的问题。 因此,我们正在为迭代过程做准备。 如果您有大型应用程序,它将无法正常工作,并且不准备从一开始就进行重写。

我们使用什么方法隔离微服务?

第一种方法是将现有模块作为服务移植。 在这方面,我们很幸运:已经有可用于WCF协议的正式服务。 它们被张贴在单独的程序集中。 我们分别移动了它们,并在每个程序集中添加了一个小型启动器。 它是使用出色的Topshelf库编写的,它使您可以将应用程序既作为服务又作为控制台运行。 由于此解决方案中不需要其他项目,因此调试非常方便。

服务根据业务逻辑进行连接,因为它们使用通用程序集并使用通用数据库。 很难称它们为纯形式的微服务。 但是,我们可以在不同的过程中分别发行这些服务。 这已经可以减少它们之间的相互影响,减少了并行开发和单点故障的问题。

使用主机进行构建只是Program类中的一行代码。 我们将Topshelf隐藏在帮助程序类中。

namespace RBA.Services.Accounts.Host { internal class Program { private static void Main(string[] args) { HostRunner<Accounts>.Run("RBA.Services.Accounts.Host"); } } } 

隔离微服务的第二种方法:创建微服务以解决新问题。 如果整体不同时生长,那么这已经非常好了,这意味着我们正在朝着正确的方向前进。 为了解决新问题,我们尝试提供单独的服务。 如果有这样的机会,那么我们将创建更多的“规范”服务来完全控制其数据模型(一个单独的数据库)。

与许多其他公司一样,我们从认证和授权服务开始。 他们是完美的。 它们是独立的,通常,它们具有单独的数据模型。 他们自己不与整体互动,只有他转向他们解决一些问题。 在这些服务上,您可以开始过渡到新的体系结构,在它们之上调试基础结构,尝试一些与网络库相关的方法,等等。 在我们的组织中,没有团队无法提供身份验证服务。

隔离我们使用的微服务的第三种方法是我们特有的。 这使业务逻辑脱离了UI层。 我们有主要的桌面UI应用程序,它与后端一样,都是用C#编写的。 开发人员会定期犯错,并在UI的逻辑部分上执行应该在后端存在并可以重用的逻辑。

如果从UI部分的代码看一个真实的示例,您会看到该解决方案的大多数包含真实的业务逻辑,这在其他过程中很有用,不仅用于构建UI表单。



真正的UI逻辑只有最后两行。 我们将其传输到服务器,以便我们可以重用它,从而减少了UI并实现了正确的体系结构。

隔离微服务的第四种最重要的方法是减少处理中的现有服务,从而减少了整体性。 当我们按原样取出现有模块时,对于开发人员来说结果并不总是令人满意,并且从创建功能之日起的业务流程就可能过时。 由于重构,我们可以支持新的业务流程,因为业务需求在不断变化。 我们可以改善源代码,消除已知缺陷,创建更好的数据模型。 有很多优点。

加工服务部门与有限环境的概念密不可分。 这是面向主题设计的概念。 它表示一个域模型部分,其中唯一定义一种语言的所有术语。 以保险和票据的上下文为例。 我们有一个整体应用程序,因此有必要使用保险中的帐户。 我们希望开发人员在另一个程序集中找到现有的“ Account”类,并从“ Insurance”类中进行链接,然后我们将获得一个有效的代码。 将尊重DRY原则,通过使用现有代码来完成任务将更快。

结果,事实证明帐户和保险的上下文是关联的。 当出现新需求时,此连接将干扰开发,从而增加本来已经很复杂的业务逻辑的复杂性。 要解决此问题,您需要在代码中的上下文之间找到边界,并删除它们之间的冲突。 例如,在保险方面,中央银行的20位帐号和开户日期很有可能就足够了。

为了将这些有限的上下文彼此分离,并开始从整体解决方案中提取微服务,我们使用了一种方法,例如在应用程序内创建外部API。 如果我们知道某个模块应该成为微服务,并且在流程中有所改变,那么我们会通过外部调用立即调用该逻辑,该逻辑属于另一个有限的上下文。 例如,通过REST或WCF。

我们自己决定不要避免需要分布式事务的代码。 在我们的案例中,事实证明遵守此规则非常容易。 当真正需要硬分布式事务时,我们还没有遇到过这样的情况-模块之间的最终一致性就足够了。

考虑一个具体的例子。 我们有一个乐队的概念-传送带,它处理“应用程序”的本质。 他轮流创建客户,帐户和银行卡。 如果成功创建了客户和帐户,并且创建卡失败,则应用程序不会进入“成功”状态,而是保持在“未创建卡”状态。 将来,后台活动将对其进行收集并结束。 该系统在一段时间内处于不一致状态,但这总体上适合我们。

但是,如果出现需要连续保存部分数据的情况,我们很可能会扩大服务范围,以便在一个过程中进行处理。

让我们考虑一个微服务分配的例子。 如何相对安全地将其投入生产? 在此示例中,我们有系统的单独部分-薪水服务模块,我们要制作微服务的代码部分之一。



首先,我们通过重写代码来创建微服务。 我们改进了一些不适合我们的观点。 我们实现了客户的新业务要求。 我们将添加到UI和Gateway API后端之间的捆绑包,以提供呼叫转移。



接下来,我们将该配置释放,但处于试验状态。 我们的大多数用户仍然使用旧的业务流程。 对于新用户,我们正在开发该过程不再包含的单片应用程序的新版本。 实际上,我们有大量的整体和微服务以试点的形式工作。



通过成功的试验,我们了解到新配置确实可行,我们可以从方程式中删除旧的整体,而将新配置替换为旧解决方案。



总体而言,我们几乎使用了所有现有方法来分离整体代码。 所有这些都使我们能够减少应用程序部分的大小,并将它们转移到新的库中,从而获得更好的源代码。

使用数据库


可以比源代码更糟糕地划分数据库,因为它不仅包含当前方案,还包含累积的历史数据。

像许多其他数据库一样,我们的数据库还有另一个重要的缺点-它的庞大规模。 该数据库是根据整体的复杂业务逻辑设计的,并且在各种受限上下文的表之间积累了链接。

在我们的案例中,除了所有麻烦(大型数据库,许多关系,有时表之间的边界有时难以理解)之外,许多大型项目也出现了问题:使用共享数据库模板。 数据是通过视图,复制和复制从表中获取的,并传送到需要此复制的其他系统中。 结果,我们无法以单独的方案取出这些表,因为它们被积极使用。

这种分离有助于我们在代码中分解成有限的上下文。 通常,我们可以很好地了解如何在数据库级别分解数据。 我们了解哪些表与一个有限的上下文有关,哪些与另一个有限的上下文有关。

我们应用了两种全局方式对数据库进行分区:对现有表进行分区以及对处理进行分区。

如果数据结构质量高,满足业务需求并适合所有人,则分离现有表是一种很好的方法。 在这种情况下,我们可以选择单独模式中的现有表。

当业务模型发生了很大变化并且表格不再完全满足我们时,需要一个处理部门。

分开现有的表。 我们需要确定我们将分开的内容。 没有这些知识,将一无所获,在这里,代码中有限上下文的分离将为我们提供帮助。 通常,如果可以理解源代码中上下文的边界,那么很清楚应该在部门中列出哪些表。

想象一下,我们有一个解决方案,其中两个整体模块与一个数据库交互。 我们需要确保只有一个模块与分开的表的一部分进行交互,而另一个模块则开始通过API与之交互。 对于初学者来说,仅通过API进行输入就足够了。 这是一个必要条件,因此我们可以讨论微服务的独立性。 阅读链接可以一直保留,直到出现大问题为止。



下一步,我们已经可以选择一个与可分离表一起使用的代码部分,无论是否处理成单独的微服务,都可以在单独的进程容器中运行它。 这将是一个单独的服务,它与Monolith数据库以及与该数据库没有直接关系的那些表进行通信。 整体仍与可拆卸部分相互作用以进行读取。



稍后,我们将删除此连接,也就是说,从分离的表中读取单片应用程序的数据也将被传输到API。



接下来,我们从常规数据库中选择仅适用于新微服务的表。 我们可以将表放置在单独的模式中,甚至放置在单独的物理数据库中。 微服务和整体数据库之间存在读取连接,但是没有什么可担心的,在这种配置下它可以生存很长时间。



最后一步是完全删除所有连接。 在这种情况下,我们可能需要从主数据库迁移数据。 有时我们想在几个数据库中重用一些从外部系统复制的数据或目录。 我们定期遇到这个问题。



加工部门。 此方法与第一种方法非常相似,只是顺序相反。 我们立即有了一个新的数据库和一个新的微服务,可以通过API与整体交互。 但与此同时,仍然存在一组我们将来希望删除的数据库表。 我们将不再需要它,在新模型中我们替换了它。



为了使该计划生效,我们很可能需要一个过渡期。

有两种可能的方法。

首先 :我们复制新数据库和旧数据库中的所有数据。 在这种情况下,我们有数据冗余,同步可能会出现问题。 但是,然后我们可以接受两个不同的客户。 一个将使用新版本,另一个将使用旧版本。

第二 :我们根据某些业务特征共享数据。 例如,在我们的系统中,有5种产品存储在旧数据库中。 作为新业务任务的第六部分,我们放入了一个新数据库。 但是我们需要网关API,该API可以同步这些数据并向客户显示在何处以及采取什么措施。

两种方法都有效,根据情况选择。

在确保一切正常之后,可以禁用用于旧数据库结构的整体部分。



最后一步是删除旧的数据结构。



综上所述,我们可以说我们的数据库存在问题:与源代码相比,使用它比较困难,分离起来更困难,但这是可以做到的。 我们发现了一些可以相当安全地完成此操作的方法,但是,与源代码相比,对数据犯错误更容易。

使用源代码


这就是我们开始分析整体项目时的源代码图。



它可以有条件地分为三层。 这是启动的模块,插件,服务和个人活动的一层。 实际上,这些是整体解决方案中的切入点。 它们全都与Common层紧密结合。 它具有在服务和许多连接之间共享的业务逻辑。 每个服务和插件最多使用10个或更多的通用程序集,具体取决于它们的大小和开发人员的良心。

我们很幸运,我们拥有可以单独使用的基础结构库。

有时会出现一种情况,即某些Common对象实际上并不属于该层,而是基础结构库。 这是通过重命名决定的。

最关心的是有限的环境。 过去是3-4个上下文在一个Common程序集中混合在一起,并且在同一业务功能内互相使用。 有必要了解这可以在哪里划分,在什么边界划分以及下一步如何将这种分离映射到源代码程序集中。

我们为代码分离过程制定了一些规则。

首先 :我们不再希望在服务,活动和插件之间共享业务逻辑。 他们希望在微服务框架内使业务逻辑独立。 另一方面,在理想情况下,微服务被视为完全独立存在的服务。 我认为这种方法有些浪费,并且很难实现,因为例如在任何情况下C#中的服务都将通过标准库进行连接。 我们的系统是用C#编写的,尚未使用其他技术。 因此,我们决定可以负担得起使用通用技术组件的费用。 最主要的是它们没有业务逻辑的任何片段。 如果您在使用的ORM上有一个方便的包装器,那么将其从服务复制到服务是非常昂贵的。

我们的团队热衷于主题设计,因此“洋葱架构”对我们而言是完美的。 我们服务的基础不是数据访问层,而是具有域逻辑的程序集,该域逻辑仅包含业务逻辑并且没有基础结构连接。 同时,我们可以独立修改域程序集以解决与框架相关的问题。

在这个阶段,我们遇到了第一个严重的问题。 该服务原本是指一个域程序集,我们想使逻辑独立,并且这里的DRY原理强烈干扰了我们。 为了避免重复,开发人员希望重用相邻程序集中的类,结果,域开始重新相互通信。 我们分析了结果,并确定问题可能还出在源代码存储设备领域。 我们有一个大型存储库,其中包含所有源代码。 整个项目的解决方案很难在本地计算机上组装。 因此,为项目的各个部分创建了单独的小型解决方案,没有人禁止向其中添加任何ommon或domain程序集并重新使用它们。 唯一不允许我们执行此操作的工具是审阅代码。 但有时他也坠毁了。

然后,我们开始切换到具有单独存储库的模型。 业务逻辑已不再从一种服务流向另一种服务,而域已真正变得独立。 有限的上下文得到更清晰的支持。 我们如何重用基础架构库? 我们将它们分配到一个单独的存储库,然后将它们放置在Artifactory中放入的Nuget包中。 进行任何更改后,组装和发布将自动进行。



我们的服务开始以与外部基础包相同的方式引用内部基础包。 我们从Nuget下载外部库。 为了与放置这些软件包的Artifactory一起工作,我们使用了两个软件包管理器。 在小型存储库中,我们还使用了Nuget。 在具有多种服务的存储库中,我们使用了Paket,它提供了模块之间更多的版本一致性。



因此,通过处理源代码,略微更改体系结构和共享存储库,我们使我们的服务更加独立。

基础设施问题


切换到微服务的大多数缺点与基础架构有关。 您将需要自动化部署,您将需要用于基础结构的新库。

在环境中手动安装

最初,我们将解决方案手动安装在环境中。 为了使该过程自动化,我们创建了CI / CD管道。 我们选择了持续交付流程,因为从业务流程的角度来看,对于我们而言,持续部署尚不可接受。 因此,通过按钮执行操作并自动进行测试。



我们使用Atlassian,Bitbucket存储源代码,并使用Bamboo进行汇编。 我们喜欢用Cake编写汇编脚本,因为它是相同的C#。 现成的软件包到达Artifactory,Ansible自动到达测试服务器,然后可以立即对其进行测试。



单独记录


一次,整体的想法之一是提供联合测井。 我们还需要了解如何处理磁盘上的各个日志。 日志以文本文件的形式写给我们。 我们决定使用标准的ELK堆栈。 我们没有通过提供程序直接写给ELK,而是决定将文本日志定稿,并在其中记录跟踪ID作为标识符,并添加服务名称,以便可以对这些日志进行解析。



使用Filebeat,我们有机会从服务器收集日志,然后进行转换,并使用Kibana在UI中构建请求,并观察调用在服务之间的进行情况。 跟踪ID对此有很大帮助。

测试和调试相关服务


最初,我们并不完全了解如何调试开发的服务。 使用整体程序,一切都很简单,我们在本地计算机上运行了它。 最初,他们尝试对微服务执行相同的操作,但是有时要完全启动一个微服务,您需要运行其他多个微服务,这很不方便。 , , , . , prod. , , . , , .

, production- . , .

Specflow. NUnit Ansible. , . - . , , Jira.

, . JMeter, — InfluxDB, — Grafana.

?


-, «». , production-, -. 1,5 , , .

. , , . .

. , .

, . , . Scrum-. , .


  • . , , , . .
  • . , , . , , , Scrum.
  • — . . . legacy, , .

    : . . , , , , , , , — , . . , , .

    PS ( ) – .
    .

Source: https://habr.com/ru/post/zh-CN458404/


All Articles