vlambda博客
学习文章列表

读书笔记《developing-microservices-with-node-js》从巨石到微服务

Chapter 3. From the Monolith to Microservices

在我的职业生涯中,我曾在很多不同的公司工作过,主要是金融服务业,而我工作过的所有公司都遵循以下相同的模式:

  1. 一家公司是由几个具有良好领域知识的人成立的:保险、支付、信用卡等等。

  2. 公司发展壮大,需要快速满足新的业务需求(监管、大客户要求愚蠢的事情等等),而这些需求是匆忙建立的,几乎没有计划。

  3. 该公司经历了另一个发展阶段,其中业务交易被明确定义并且难以维护的单一软件建模不佳。

  4. 由于最初对软件的构建方式施加了限制,该公司增加了员工人数,这导致了增长的痛苦和效率的损失。

本章不仅是关于如何避免之前的流程(不受控制的有机增长),而且也是关于如何使用微服务对新系统进行建模。本章是本书的灵魂,我将尝试在几页中综合我的经验,在Chapter 4中设置要遵循的原则在 Node.js 中编写您的第一个微服务,我们将使用在前几章。

First, there was the monolith


现代企业软件的巨大百分比(我估计约为90%)是按照单体方法构建的。

在单个容器中运行并具有明确定义的开发生命周期的大型软件组件,这完全违背了敏捷的原则,尽早交付并经常交付 (https ://en.wikipedia.org/wiki/Release_early,_release_often),如下:

  • 尽早交付越早失败,越容易恢复.如果您在一个软件组件上工作了两年然后发布,那么存在偏离原始需求的巨大风险,这些需求通常是错误的并且每隔几天就会更改一次。

  • 经常交付经常交付,利益相关者了解进度并可以看到软件中快速反映的变化。错误可以在几天内修复,改进很容易识别。

公司 构建大型软件组件,而不是构建协同工作的小型软件组件,因为这是很自然的事情,如下所示:

  1. 开发商有新的需求。

  2. 他在服务层的现有类上构建了一个新方法。

  3. 该方法通过 HTTP、SOAP 或任何其他协议在 API 上公开。

现在,将它乘以您公司的开发人员数量,您将获得称为有机增长< /strong>。有机增长是一种不受控制和无计划的软件系统在业务压力下没有足够的长期规划的增长,这是不好的。

How to tackle organic growth?

解决有机增长所需的第一件事是确保公司的业务和 IT 保持一致。通常,在大公司中,IT 不被视为业务的核心部分。

组织外包他们的 IT 系统,牢记价格而不是质量,以便构建这些软件组件的合作伙伴专注于一件事:按时交付 并根据规范,即使它们不正确。

这产生了一个不太理想的生态系统,无法通过现有问题的有效解决方案来响应业务需求。领导 IT 的人几乎不了解系统是如何构建的,并且通常忽略软件开发的复杂性。

幸运的是,随着 IT 系统成为全球 99% 企业的驱动力,这是一种不断变化的趋势,但我们需要更明智地构建它们。

解决有机增长的第一个措施是让 IT 和业务利益相关者协同工作:教育非技术利益相关者是成功的关键。

如果我们回到几个大版本模式。我们能做得更好吗?

我们当然可以。将工作划分为可管理的软件工件,这些工件对单个、定义明确的业务活动进行建模并赋予它一个实体。

现阶段不需要是微服务,但保持逻辑在一个分离的、定义明确的、易于测试的和解耦的模块中,将为我们在应用程序的未来变化中提供巨大的优势。

让我们考虑以下示例:

读书笔记《developing-microservices-with-node-js》从巨石到微服务

在这个保险系统中,你可以看到有人很着急。 SMS 和电子邮件发件人虽然都是通信渠道,但它们的性质非常不同,您可能希望它们以不同的方式行事。

调用服务分为以下两个高级实体:

  • 新业务新客户在他们报名

  • 续保在保单生效时收到短信的现有客户准备更新

在某些时候,系统需要发送短信和电子邮件,有人创建了处理所有第三方通信的通信服务实体。

一开始看起来是个好主意。短信或电子邮件归根结底只是一个渠道,通信机制将 90% 相同,我们可以重用大量功能。

如果我们突然想要集成一个第三方服务来处理所有物理岗位?

如果我们想添加一份每周向客户发送一次的时事通讯,其中包含我们认为客户感兴趣的信息,会发生什么?

该服务将变得失控,测试、发布和确保 SMS 代码的更改不会影响以任何形式发送电子邮件将变得更加困难。

这是有机增长,在这种情况下,它与称为 康威定律 的定律有关,该定律指出 如下:

任何设计系统(此处定义比信息系统更广泛)的组织都不可避免地会产生其结构是组织通信结构副本的设计。

在这种情况下,我们陷入了陷阱。我们正在尝试在单个软件组件上建模通信,该组件可能太大且太复杂,无法快速响应新的业务需求。

让我们看一下下面的图表:

读书笔记《developing-microservices-with-node-js》从巨石到微服务

现在,我们已经 将每个通信渠道封装在其自己的服务(稍后on,将被部署为微服务),我们将为未来的通信渠道做同样的事情。

这是击败有机增长的第一步:创建具有明确边界和单一职责的细粒度服务——做一些小事,但要做好跨度>。

How abstract is too abstract?

我们的大脑 无法处理复杂的机制。抽象能力是最新的人类智能获取之一。

在上一节的示例中,我给出了一些好的东西,这会让世界上一半的程序员感到不安:消除我们系统的抽象跨度>。

抽象能力是我们多年来学习的东西,与智能不同,它可以被训练。不是每个人都能达到相同的抽象级别,如果我们将某些行业所需的特定和复杂的领域知识与高级抽象相结合,我们就有了灾难的完美秘诀。

在构建软件时,我一直试图遵循的黄金法则之一(并且 try 是正确的词,因为我总是发现对它的强烈反对)是避免过早抽象。

有多少次 次你发现自己处于一个只有一组简单要求的角落:构建一个程序来解决X。但是,您的团队开始预测 X 的所有可能变化,甚至不知道它们是否合理。然后,一旦软件投入生产,利益相关者之一就会带来您无法想象的 X 的变体(因为需求不是甚至正确)现在,让这个变体工作将花费你几天的时间和大规模的重构。

避免这个问题的方法很简单:在没有至少三个用例的情况下避免抽象

不要考虑通过不同类型的通道发送数据的可能性,因为它可能不会发生,并且您正在通过不必要的抽象损害当前功能。一旦你至少有一个其他的沟通渠道,就该开始考虑如何更好地设计这两个软件组件,当第三个用例出现时,重构。

请记住,在构建微服务时,它们应该足够小,以便在一个 sprint(大约两周)内重写,因此在如此短的时间内拥有一个工作原型的好处是值得冒一次必须重写它的风险。需求更加具体:向利益相关者展示的东西是确定需求的最快方法。

Seneca 在这方面做得很好,通过模式匹配,我们可以在不影响现有调用代码的情况下扩展给定微服务的 API:我们的服务对扩展开放,但对修改关闭(SOLID 原则),因为我们正在添加功能而不影响现有服务。我们将在 Chapter 4, 编写你的第一个微服务中看到更完整的示例在 Node.js 中

Then the microservices appeared


微服务将继续存在。如今,公司更加重视软件的质量。如上一节所述,尽早交付并经常交付是软件开发成功的关键。

微服务通过模块化和专业化帮助我们尽快满足业务需求。可以在几天内轻松进行版本控制和升级的小软件,它们易于测试,因为它们具有明确而小的目的(专业化)并且以与系统的其余部分隔离的方式编写(模块化)。

不幸的是, 发现上述情况并不常见。通常,大型软件系统不是以易于识别的模块化或专业化方式构建的。一般规则是构建一个什么都做的大软件组件,模块化很差,所以我们需要从最基础的开始。

让我们从编写一些代码开始,如下所示:

module.exports = function(options) {

  var init = {}

  /**
   * Sends one SMS
   */
  init.sendSMS = function(destination, content) {
    // Code to send SMS
  }

  /**
   * Reads the pending list of SMS.
   */
  init.readPendingSMS = function() {
    // code to receive SMS
    return listOfSms;
  }

  /**
   * Sends an email.
   */
  init.sendEmail = function(subject, content) {
    // code to send emails
  }

  /**
   * Gets a list of pending emails.
   */
  init.readPendingEmails = function() {
    // code to read the pending emails
    return listOfEmails;
  }

  /**
   * This code marks an email as read so it does not get
   * fetch again by the readPendingEmails function.
   */
  init.markEmailAsRead = function(messageId) {
    // code to mark a message as read.
  }

  /**
   * This function queues a document to be printed and
   * sent by post.
   */
  init.queuePost = function(document) {
    // code to queue post
  }

  return init;
}

如您所见,这个模块可以很容易地称为通信服务,而且很容易猜到 它在做什么。它管理电子邮件、SMS 和邮政通信。

这可能是 太多了。这项服务被视为失控,因为人们将不断添加与通信相关的方法。这是单体软件的关键问题:有界上下文跨越不同的领域,从功能和维护的角度影响我们软件的质量。

如果你是一名软件开发人员,就会马上提出一个危险信号:这个模块的凝聚力很差。

虽然它可能会奏效一段时间,但我们现在正在改变我们的心态。我们正在构建可以隔离的小型、可扩展和自主的组件。这种情况下的内聚力很差,因为模块做了太多不同的事情:电子邮件、SMS 和邮寄。

如果我们添加另一个沟通渠道,例如 Twitter 和 Facebook 通知,会发生什么?

服务变得失控。您最终会得到一个难以重构、测试和修改的巨大模块,而不是拥有小的功能性软件组件。我们来看看下面的 SOLID 设计原则,在第2章微服务中解释在 Node.js – Seneca 和 PM2 替代方案中

  • 单一职责原则 模块做的事情太多。

  • 打开扩展,关闭修改:模块需要修改 添加新功能并可能更改通用代码。

  • Liskov Substitution:我们将再次跳过这个。

  • 接口隔离:我们模块中没有指定任何接口,只是任意一组函数的实现。

  • 依赖注入:没有依赖注入。该模块需要由调用代码构建。

如果我们没有测试,事情会变得 更复杂。

因此,让我们使用 Seneca 将其拆分为各种小模块。

首先,电子邮件模块(email.js)如下:

module.exports = function (options) {

  /**
   * Sends an email.
   */
  this.add({channel: 'email', action: 'send'}, function(msg, respond) {
    // Code to send an email.
    respond(null, {...});
  });

  /**
   * Gets a list of pending emails.
   */
  this.add({channel: 'email', action: 'pending'}, function(msg, respond) {
    // Code to read pending email.
    respond(null, {...});
  });

  /**
   * Marks a message as read.
   */
  this.add({channel: 'email', action: 'read'}, function(msg, respond) {
    // Code to mark a message as read.
    respond(null, {...});
  });
}

SMS 模块(sms.js)如下:

module.exports = function (options) {

  /**
   * Sends an email.
   */
  this.add({channel: 'sms', action: 'send'}, function(msg, respond) {
    // Code to send a sms.
    respond(null, {...});
  });

  /**
   * Receives the pending SMS.
   */
  this.add({channel: 'sms', action: 'pending'}, function(msg, respond) {
    // Code to read pending sms.
    respond(null, {...});
  });
}

最后,post 模块(post.js)如下:

module.exports = function (options) {

  /**
   * Queues a post message for printing and sending.
   */
  this.add({channel: 'post', action: 'queue'}, function(msg, respond) {
    // Code to queue a post message.
    respond(null, {...});
  });
}

下图显示了模块的新结构:

读书笔记《developing-microservices-with-node-js》从巨石到微服务

现在,我们拥有三个模块。这些模块中的每一个都做一件特定的事情,而不会相互干扰;我们创建了高内聚模块。

让我们运行前面的代码,如下:

var seneca = require("seneca")()
      .use("email")
      .use("sms")
      .use("post");

seneca.listen({port: 1932, host: "10.0.0.7"});

就这么简单,我们创建了一个 IP 10.0.0.7 绑定的服务器,它在 1932 端口上侦听传入请求.如您所见,我们没有引用任何文件,我们只是按名称引用了模块;塞内卡将完成其余的工作。

让我们运行它 并验证 Seneca 是否已加载插件:

node index.js --seneca.log.all | grep DEFINE

此命令将输出类似于以下行的内容:

读书笔记《developing-microservices-with-node-js》从巨石到微服务

如果您还记得 第 2 章Node.js 中的微服务 - Seneca 和 PM2 替代方案,Seneca 默认加载几个插件:basic, transport, webmem-store,这使得 Seneca 可以开箱即用,而无需进行配置。显然,正如我们将在 Chapter 4 中看到的,在 Node.js 中编写您的第一个微服务。 js,配置是必要的,例如,mem-store 只会将数据存储在内存中,而不会在执行之间持久化。

除了标准插件,我们可以看到 Seneca 还加载了三个额外的插件:emailsmspost,这是我们创建的插件。

如您所见,一旦您了解了框架的工作原理,用 Seneca 编写的服务就很容易理解。在这种情况下,我以插件的形式编写了代码,以便它可以被不同机器上的不同 Seneca 实例使用,因为 Seneca 有一个透明的传输机制,允许我们快速重新部署和扩展我们单体应用程序的部分作为微服务,如下:

  • 新版本可以轻松测试,因为电子邮件功能的更改只会影响电子邮件的发送。

  • 它很容易扩展。正如我们将在下一章中看到的那样,复制服务就像配置新服务器并将我们的 Seneca 客户端指向它一样简单。

  • 它也易于维护,因为该软件更易于理解和修改。

Disadvantages

借助微服务,我们 解决了现代企业中最大的问题,但这并不意味着它们没有问题。微服务通常会导致难以预见的不同类型的问题。

第一个也是最令人担忧的一个是运营开销,它可能会破坏使用微服务所获得的好处。当您设计一个系统时,您应该始终牢记一个问题:如何实现自动化?自动化是解决这个问题的关键。

微服务的第二个缺点是应用程序的不一致性。一个团队可能会考虑一些可以在另一个团队中被禁止的良好实践(尤其是在异常处理方面),这会在团队之间增加一层额外的隔离层,这可能不利于团队内工程师的沟通。

最后但同样重要的是,微服务引入了更大的通信复杂性,可能导致安全问题。我们现在不必控制单个应用程序及其与外部世界的通信,而是面对许多相互通信的服务器。

Splitting the monolith

考虑到您公司的营销部门已决定开展一项激进的电子邮件 活动,该活动将需要 可能会损害正常的日常电子邮件发送过程的容量。在压力下,电子邮件将被延迟,这可能会给我们带来问题。

幸运的是,我们已经按照上一节中的说明构建了我们的系统。高内聚和低耦合插件形式的小型 Seneca 模块。

然后,实现它的解决方案很简单:在多台机器上部署电子邮件服务(email.js):

var seneca = require("seneca")().use("email");
seneca.listen({port: 1932, host: "new-email-service-ip"});

另外,创建一个指向它的 Seneca 客户端,如下所示:

var seneca = require("seneca")()
      .use("email")
      .use("sms")
      .use("post");
seneca.listen({port: 1932, host: "10.0.0.7"});

// interact with the existing email service using "seneca"

var senecaEmail = require("seneca").client({host: "new-email-service-ip", port: 1932});

// interact with the new email service using "senecaEmail"

从现在 开始,senecaEmail 变量将联系远程调用 act 时的服务,我们将实现我们的目标:扩展我们的第一个微服务

Problems splitting the monolith – it is all about the data

数据存储可能存在问题。如果您的应用程序失控多年,数据库也会这样做,而到现在,有机增长将使其变得困难处理数据库中的重大变化。

微服务应该照顾自己的数据。将数据保留在服务本地是确保系统在发展过程中保持灵活性的关键之一,但这可能并不总是可行的。例如,金融服务尤其受到面向微服务架构的主要弱点之一的影响:缺乏事务性。当软件组件处理金钱时,它需要确保数据在每次操作后保持一致而不是最终一致。如果客户将钱存入金融公司,那么持有账户余额的软件需要与银行持有的资金一致,否则账户对账会失败。不仅如此,如果您的公司是受监管的实体,它可能会对业务的连续性造成严重问题。

在使用微服务和金融系统时,一般的经验法则是保留一个不那么微的服务来处理所有的钱,并为系统的辅助模块创建微服务,例如电子邮件、短信、用户注册、以此类推,如下图所示:

读书笔记《developing-microservices-with-node-js》从巨石到微服务

正如您在上图中所见,支付将是一个大型微服务而不是小型服务这一事实,它只对运营方面有影响,有没有什么能阻止我们像之前看到的那样对应用程序进行模块化。从 ATM 取款必须是原子操作(成功或失败没有中间状态)这一事实不应该决定我们如何在应用程序中组织代码,允许我们模块化服务,而是跨越所有交易范围他们。

Organizational alignment


在一家公司,软件是基于微服务构建的,每个利益相关者都需要参与决策。

微服务是一个巨大的范式转变。通常,大型组织倾向于以非常老式的方式构建软件。每隔几个月就需要几天时间来完成质量保证QA< /span>) 阶段和几个小时的部署。

当一家公司选择实施面向微服务的架构时,方法会完全改变:小团队致力于自己构建、测试和部署的小功能。团队只做一件事(一个微服务,或者更现实一点,其中一些)并且做得很好(他们掌握了构建软件所需的领域和技术知识)。

这些就是通常所说的跨职能团队。具有构建高质量软件组件所需知识的少数人的工作单元。

标记团队必须掌握理解业务需求所需的领域知识也很重要。

这是我在职业生涯中工作过的大多数公司都失败的地方(在我看来)。开发人员被认为是堆砌砖的人,他们神奇地理解了业务流程,而之前没有接触过它们。如果一个开发人员在一周内交付 X 数量的工作,十个开发人员将交付 10X。这是错误的。

构建微服务的跨职能团队中的人员必须掌握(不仅知道)特定领域的知识,以便提高效率并将康威定律及其影响因素纳入系统以改变业务流程的工作方式。

在谈论微服务中的组织一致性时,自治是关键。团队需要自治才能在构建微服务时保持敏捷,这意味着在团队内部保持技术权威,如下所示:

  • 使用的语言

  • 规范标准

  • 用于解决问题的模式

  • 选择用于构建、测试、调试和部署软件的工具

这是一个重要的部分,因为这是我们需要定义公司如何构建软件以及可能引入工程问题的地方。

例如,我们可以查看编码标准,如下表所示:

  • 我们想在团队中保持相同的编码标准吗?

  • 我们是否希望每个团队都有自己的编码标准?

总的来说,我总是赞成 80% 的规则:80% 的完美对于 100% 的用例来说绰绰有余。这意味着放宽编码标准(它可以应用于其他领域)并允许一定程度的不完美/个性化,有助于减少团队之间的摩擦,也允许工程师快速赶上需要遵循的极少数重要规则例如日志记录策略或异常处理。

如果您的编码标准过于复杂,那么当团队试图将代码推送到超出其通常范围的微服务中时就会产生摩擦(请记住,团队拥有服务,但每个团队都可以为它们做出贡献)。

Summary


在本章中,我们讨论了构建面向拆分为微服务的单体应用程序的原则,具体取决于业务需求。如您所知,原子性一致性隔离耐用性ACID) 设计原则是我们在构建高质量软件时需要牢记的概念。

您还了解到,我们不能假设我们将能够从头开始设计一个系统,因此我们需要聪明地了解如何构建系统的新部分以及如何重构现有部分,以便我们实现满足业务需求和弹性所需的灵活性水平。

我们还简要介绍了单体设计的数据库,以及它们是如何将单体软件拆分为微服务时的最大痛点,因为通常需要关闭系统几个小时才能将数据拆分到本地数据库中.这个主题很可能是一本书,因为 NoSQL 数据库的新趋势正在改变数据存储的游戏。

最后,我们讨论了如何调整我们公司的工程师团队以提高效率,同时保持敏捷所需的灵活性和弹性,以及康威定律如何影响单体系统向面向微服务的转换架构。

在下一章中,我们将应用前三章讨论的所有原则,以及大量的常识来构建一个基于微服务的完整工作系统。