vlambda博客
学习文章列表

读书笔记《developing-microservices-with-node-js》Node.js中的微服务-Seneca和PM2替代品

Chapter 2. Microservices in Node.js – Seneca and PM2 Alternatives

在本章中,您将主要了解两个框架,SenecaPM2,以及为什么它们对构建微服务很重要。我们还将了解这些框架的替代方案,以便大致了解 Node.js 生态系统中正在发生的事情。在本章中,我们将重点关注以下主题:

Need for Node.js


在之前的章节中,我提到我过去并不是Node.js 的忠实粉丝。这样做的原因是我还没有准备好应对 JavaScript 正在经历的标准化水平。

浏览器中的 JavaScript 很痛苦。跨浏览器兼容性一直是个问题,缺乏标准化也无助于减轻痛苦。

然后 Node.js 出现了,由于它的非阻塞特性,很容易创建高度可扩展的应用程序(我们将在本章后面讨论),而且它也很容易学习,因为它基于 JavaScript,一个很好的已知语言。

如今,Node.js 是全球众多公司的首选,同时也是需要服务器非阻塞特性的方面的首选,例如 Web 套接字。

在本书中,我们将主要(但不仅是)使用 Seneca 和 PM2 作为构建和运行微服务的框架,但这并不意味着替代方案不好。

市场上的替代品很少,例如 restifyExpress用于构建应用程序和 forevernodemon 来运行它们。但是,我发现 Seneca 和 PM2 是构建 微服务的最合适组合原因如下:

  • PM2 在应用程序部署方面非常强大

  • Seneca 不仅是一个构建微服务的框架,它还是一个重塑我们对面向对象软件的认知的范式

我们将在本书各章的几个示例中使用 Express,我们还将讨论如何将 Seneca 作为中间件集成到 Express 中。

但是,在此之前,让我们讨论一些有关 Node.js 的概念,这将有助于我们理解这些框架。

Installing Node.js, npm, Seneca, and PM2

Node.js 相当容易安装。根据您的系统,有一个安装程序 可用,可以安装 Node.js 和 npm Node Package Manager)一个相当简单的任务。只需双击它并按照的说明进行操作。在编写本书时,有适用于 Windows 和 OSX 的安装程序。

但是,高级用户,尤其是 DevOps 工程师,需要安装 Node.js 和 a>npm 来自源代码或二进制文件。

Note

Node.js 和 npm 程序都捆绑在一个包中,我们可以从 Node.js 网站(源代码或二进制文件)下载各种平台:

https://nodejs.org/en/download/

对于 Chef 用户,一种流行的用于构建服务器的配置管理软件,可用的选项很少,但最流行的是以下配方(对于不熟悉 Chef 的用户,< /a>recipe 基本上是一个通过 Chef 在服务器中安装或配置软件的脚本):

https://github.com/redguide/nodejs

在编写本书时,有适用于 Linux 的二进制文件。

Learning npm

npm 是一个 Node.js 自带的软件,可以让你从 Internet 中提取依赖项,而不用担心它们的管理。它还可用于维护和更新依赖项,以及从头开始创建项目。

您可能知道,每个节点应用程序都带有一个 package.json 文件。该文件描述了项目的配置(依赖、版本、常用命令等)。让我们看看下面的例子:

{
  "name": "test-project",
  "version": "1.0.0",
  "description": "test project",
  "main": "index.js",
  "scripts": {
  "test": "grunt validate --verbose"
  },
  "author": "David Gonzalez",
  "license": "ISC"
}

该文件本身是不言自明的。文件中有一个有趣的部分——scripts

在本节中,我们可以指定用于运行不同操作的命令。在这种情况下,如果我们从终端运行 npm test,npm 将执行 grunt validate --verbose

Node 应用程序通常与执行以下命令一样容易运行:

node index.js

在项目的根目录中,考虑引导文件是 index.js。如果不是这种情况,您可以做的最好的事情是在 package.jsonscripts 部分添加一个小节,如下:

"scripts": {
  "test": "grunt validate --verbose"
  "start": "node index.js"
},

如您所见,现在我们有两个命令执行同一个程序:

node index.js
npm start

使用 npm start 的好处非常明显——一致性。无论您的应用程序多么复杂,npm start 都会始终运行它(如果您正确配置了 scripts 部分)。

让我们在一个干净的项目上安装 Seneca 和 PM2。

首先,安装 Node.js 后,在终端的新文件夹中执行 npm init。您应该会收到类似于下图的提示:

读书笔记《developing-microservices-with-node-js》Node.js中的微服务-Seneca和PM2替代品

npm 会要求您提供一些参数来配置您的项目,一旦您完成,它会编写一个 package.json 文件,其内容类似于前面的代码。

现在我们需要安装依赖项; npm 会为我们做这件事。只需运行以下命令:

npm install --save seneca

现在,如果您再次检查 package.json,您可以看到有一个名为 <包含 Seneca 条目的代码 class="literal">dependencies:

"dependencies": {
  "seneca": "^0.7.1"
}

这意味着从现在开始,我们的应用程序可以 require Seneca 模块并且 require() 函数将能够找到它。 save 标志有几种变体,如下所示:

  • save:这会将依赖项保存在 dependencies 部分。它在整个开发生命周期中都可用。

  • save-dev:这会将依赖项保存在 devDependencies 部分。它仅在开发中可用,不会部署到生产中。

  • save-optional:这会添加一个依赖项(例如 save),但如果找不到依赖项,则让 npm 继续.由应用程序来处理缺少这种依赖关系。

让我们继续 PM2。虽然它可以用作库,但 PM2 主要是一个命令工具,就像任何 Unix 系统中的 lsgrep 一样。 npm 在安装命令行工具方面做得很好:

npm install –g pm2

-g 标志指示 npm 全局安装 PM2,因此它在系统中可用,而不是在应用程序中。这意味着 当上一个命令完成时,pm2 可以作为控制台中的命令使用。如果你在终端运行pm2 help,你可以看到PM2的帮助。

Our first program – Hello World

Node.js 中最有趣的概念之一就是简单。只要你熟悉 JavaScript,你可以在几天内学习 Node.js,并在几周内掌握它。 Node.js 中的代码往往比其他语言更短、更清晰:

var http = require('http');

var server = http.createServer(function (request, response) {
  response.writeHead(200, {"Content-Type": "text/plain"});
  response.end("Hello World\n");
});

server.listen(8000);

前面的代码创建了一个服务器,它在 8000 端口上侦听请求。不信,打开浏览器,在导航栏输入http://127.0.0.1:8000,如下图:

读书笔记《developing-microservices-with-node-js》Node.js中的微服务-Seneca和PM2替代品

让我们解释一下代码:

  • 第一行加载 http 模块。通过require()指令,我们要求节点加载http模块,并将这个模块的导出赋值给< code class="literal">http 变量。导出语言元素是 Node.js 从模块内部向外部世界公开函数和变量的方式。

  • 脚本中的第二个构造创建 HTTP 服务器。 http 模块创建并公开了一个名为 createServer() 的方法,该方法接收一个函数(请记住 JavaScript 将函数视为第一级对象以便它们可以作为其他函数参数传递)作为参数,在 Node.js 世界中,称为 回调。回调是作为对事件的响应而执行的操作。在这种情况下,事件是脚本 接收到 HTTP 请求。由于其线程模型,Node.js 大量使用回调。您的应用程序将始终在单个线程上执行,以便在等待操作完成时不会阻塞应用程序线程,并防止我们的应用程序看起来停滞或挂起。否则,您的程序将无法响应。我们将在 第 4 章中再次讨论这一点, 在 Node.js 中编写您的第一个微服务

  • 在下一行中,server.listen(8000) 启动服务器。从现在开始,每次我们的服务器收到请求,http.createServer() 函数上的回调都会被执行。

就是这个。简单是 Node.js 程序的关键。该代码让您无需编写大量的 类、方法和配置对象就可以直奔主题更简单:编写一个服务于请求的脚本。

Node.js threading model

Node.js 编写的程序是单线程的。这样做的影响是相当显着的;在前面的示例中,如果我们有一万个并发请求,它们将被 Node.js 事件循环排队并满足(将在 第 4 章用 Node.js 编写你的第一个微服务< /span> 和 第 6 章测试和记录 Node.js 微服务)一一。

乍一看,这听起来不对。我的意思是,现代 CPU 由于其多核特性,可以处理多个并行请求。那么,在一个线程中执行它们有什么好处呢?

这个问题的答案是 Node.js 旨在处理异步处理。这意味着在发生读取文件等缓慢操作时,Node.js 不会阻塞线程,而是允许线程继续满足其他事件,然后节点的控制进程将执行与事件关联的方法,处理响应。

继续前面的例子,createServer() 方法接受一个回调,该回调将在 HTTP 请求的事件中执行,但同时,线程可以自由地继续执行其他操作.

这个模型中的问题是 Node.js 开发人员所说的回调地狱。代码变得复杂,因为作为对阻塞操作的响应的每个操作都必须在回调中进行处理,就像前面的示例一样;用作 createServer() 方法参数的函数就是一个很好的例子。

Modular organization best practices

大型项目的源代码组织总是有争议的。不同的开发人员对如何排序源代码有不同的方法来避免混乱。

某些语言(例如 Java 或 C#)将代码组织在包中,以便我们可以在包中找到相关的源代码文件。例如,如果我们正在编写一个任务管理器软件,在 com.taskmanager.dao 包中,我们可以期望找到实现 数据访问对象DAO ) 模式以访问数据库。同理,在 com.taskmanager.dao.domain.model 包中,我们可以找到我们应用程序中所有代表模型对象(通常是表)的类。

这是 Java 和 C# 中的 约定。如果您是 C# 开发人员,并且开始处理现有项目,则只需几天时间即可习惯代码的结构方式,因为该语言会强制执行源代码的组织。

Javascript

JavaScript 最初设计为在浏览器中运行。该代码应该嵌入到 HTML 文档中,以便 文档对象模型 (DOM) 可以被操纵来创建动态效果。看看下面的 例子:

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Title of the document</title>
</head>
<body>
  Hello <span id="world">Mundo</span>
  <script type="text/javascript"> document.getElementById("world").innerText = 'World'; </script>
</body>
</html>

如您所见,如果您在浏览器上加载此 HTML,则 span 标记内的文本与 idworld 在页面加载时被替换。

在 JavaScript 中,没有依赖管理的概念。 JavaScript 可以从 HTML 中分离到它自己的文件中,但是(目前)没有办法将 JavaScript 文件包含到另一个 JavaScript 文件中。

这导致了一个大问题。当项目包含数十个 JavaScript 文件时,资产管理变得更像一门艺术而不是工程工作。

导入 JavaScript 文件的顺序变得很重要,因为浏览器会在找到 JavaScript 文件时执行它们。让我们对上一个例子中的代码重新排序来演示一下,如下:

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Title of the document</title>
  <script type="text/javascript"> document.getElementById("world").innerText = 'World'; </script>
</head>
<body>
  Hello <span id="world">Mundo</span>

</body>
</html>

现在,将此 HTML 保存在 index.html 文件中,并尝试在任何浏览器中加载它,如如下图所示:

读书笔记《developing-microservices-with-node-js》Node.js中的微服务-Seneca和PM2替代品

在这种情况下,我使用了 Chrome,控制台在第 7 行显示 Uncaught TypeError: Cannot set property 'innerText' of null 错误。

为什么会这样?

正如我们之前解释的,浏览器执行找到的代码,事实证明,当浏览器执行 JavaScript 时,world 元素还不存在。

幸运的是,Node.js 使用一种非常优雅和标准的方法解决了依赖加载问题。

SOLID design principles

在谈论微服务时,我们总是在谈论模块化,而模块化总是归结为 为以下(SOLID< /strong>) 设计原则:

  • 单身 责任原则

  • 打开扩展,关闭修改

  • Liskov 替换

  • 接口隔离

  • Dependency inversion(控制反转和依赖注入)

您希望将代码组织在模块中。模块是代码的集合,可以做一些简单的事情,比如操作字符串,而且它做得很好。您的模块包含的函数(或类、实用程序等)越多,它的内聚性就越低,我们正在努力避免这种情况。

在 Node.js 中,每个 JavaScript 文件默认都是一个模块。我们也可以将文件夹用作模块,但让我们关注文件:

function contains(a, b) {
  return a.indexOf(b) > -1;
}

function stringToOrdinal(str) {
  var result = ""
  for (var i = 0, len = str.length; i < len; i++) {
    result += charToNumber(str[i]);
  }
  return result;
}

function charToNumber(char) {
  return char.charCodeAt(0) - 96;
}

module.exports = {
  contains: contains,
  stringToOrdinal: stringToOrdinal
}

前面的代码表示 Node.js 中的一个有效模块。在这种情况下,模块包含三个函数,其中两个函数暴露在模块外部。

在 Node.js 中,这是通过 module.exports 变量完成的。无论你分配给这个变量的什么东西都会被调用代码看到,这样我们就可以模拟模块上的私有内容,例如在这种情况下的 charToNumber() 函数。

所以,如果我们想使用这个模块,我们只需要require()它,如下:

var stringManipulation = require("./string-manipulation");
console.log(stringManipulation.stringToOrdinal("aabb"));

这应该输出 1122

让我们回到 SOLID 原则,看看我们的模块长什么样:

  • 单一职责原则:我们的模块只处理字符串

  • 对扩展开放,对修改关闭:我们可以添加更多功能,但是我们拥有的功能是正确的,它们可以用来构建新功能在模块中

  • Liskov 替换:我们将跳过这个,因为模块的结构与实现这一原则无关

  • Interface segregation:JavaScript 不是像 Java 或 C# 这样的接口元素的语言,但是在这个模块中,我们暴露了接口,并且module.exports 变量将充当调用代码的契约,我们实现中的更改不会影响模块的调用方式

  • 依赖倒置:这是我们失败的地方,不是完全失败,但足以重新考虑我们的方法

在这种情况下,我们需要 模块,与之交互的唯一方法是通过全局范围。如果在模块内部,我们想与来自外部的数据进行交互,唯一可能的选择是在需要模块之前创建一个全局变量(或函数),然后假设它总是在那里。

全局变量是 Node.js 中的一个大问题。您可能知道,在 JavaScript 中,如果在声明变量时省略 var 关键字,它会自动成为全局变量。

再加上有意的全局变量会在模块之间创建数据耦合(耦合是我们不惜一切代价要避免的),这是找到更好的方法来为我们的微服务(或一般而言)定义模块的原因)。

让我们重构代码如下:

function init(options) {
  
  function charToNumber(char) {
    return char.charCodeAt(0) - 96;
  }
  
  function StringManipulation() {
  }
  
  var stringManipulation = new StringManipulation();
  
  stringManipulation.contains = function(a, b) {
    return a.indexOf(b) > -1;
  };
  
  stringManipulation.stringToOrdinal = function(str) {
    var result = ""
    for (var i = 0, len = str.length; i < len; i++) {
      result += charToNumber(str[i]);
    }
    return result;
  }
  return stringManipulation;
}

module.exports = init;

这看起来有点复杂,但是一旦你习惯了它,好处是巨大的:

  • 我们可以给模块传递配置参数(比如调试信息)

  • 避免了全局范围的污染,就好像所有东西都包装在一个函数中,并且我们强制执行 use strict 配置(这避免了没有 var 有编译错误)

  • 参数化模块可以轻松模拟行为和数据以进行测试

在本书中,我们将编写大量代码来从微服务的角度对系统进行建模。我们将尽可能地保持这种模式,以便我们可以看到好处。

我们将用于构建微服务的库之一,Seneca,遵循这种模式,以及可以找到的大量库在互联网上。

Seneca – a microservices framework


Seneca 是一个 构建微服务的框架,由 nearForm 的创始人兼首席技术官 Richard Rodger 编写,该咨询公司帮助其他公司使用 Node.js 设计和实施软件. Seneca 是关于简单的,它通过一个复杂的模式匹配接口连接服务,该接口从代码中抽象出传输,因此编写高度可扩展的软件相当容易。

让我们停止讨论,看看一些例子:

var seneca = require( 'seneca' )()

seneca.add({role: 'math', cmd: 'sum'}, function (msg, respond) {
  var sum = msg.left + msg.right
  respond(null, {answer: sum})
})

seneca.add({role: 'math', cmd: 'product'}, function (msg, respond) {
  var product = msg.left * msg.right
  respond( null, { answer: product } )
})

seneca.act({role: 'math', cmd: 'sum', left: 1, right: 2}, console.log)
    seneca.act({role: 'math', cmd: 'product', left: 3, right: 4}, console.log)

如您所见,代码是不言自明的:

将代码写入我们在本章前面创建的项目中的一个名为 index.js 的文件中(记住我们安装了 Seneca 和 PM2),然后运行以下命令:

node index.js

输出将类似于下图:

读书笔记《developing-microservices-with-node-js》Node.js中的微服务-Seneca和PM2替代品

我们稍后会讨论这个输出,以便准确解释它的含义,但是如果您习惯于企业应用程序,您几乎可以猜到发生了什么。

最后两行是两个服务的响应:第一行执行 1+2,第二行执行 3*4

在最后两行中显示为第一个单词的 null 输出对应于广泛的模式 在 JavaScript 中使用:错误第一个回调。

让我们用一个代码示例来解释它:

var seneca = require( 'seneca' )()

seneca.add({role: 'math', cmd: 'sum'}, function (msg, respond) {
  var sum = msg.left + msg.right
  respond(null, {answer: sum})
})

seneca.add({role: 'math', cmd: 'product'}, function (msg, respond) {
  var product = msg.left * msg.right
  respond( null, { answer: product } )
})

seneca.act({role: 'math', cmd: 'sum', left: 1, right: 2}, function(err, data) {
  if (err) {
    return console.error(err);
  }
  console.log(data);
});
seneca.act({role: 'math', cmd: 'product', left: 3, right: 4}, console.log);

前面的代码用更合适的方法重写了对 Seneca 的第一次调用。不是将所有内容都转储到控制台中,而是处理来自 Seneca 的响应,这是一个回调,其中第一个参数是错误,如果发生错误(null 否则),第二个参数是从微服务返回的数据。这就是为什么在第一个示例中,null 是控制台的第一个输出。

在 Node.js 的世界中,使用回调是很常见的。回调是一种向程序指示发生了某些事情的方法,在结果准备好处理之前不会被阻塞。塞内卡也不例外。它严重依赖回调来处理对服务调用的响应,当您考虑部署在不同机器上的微服务时,这更有意义(在前面的示例中,所有内容都运行在 同一台机器),尤其是因为网络延迟可能是软件设计中的一个因素。

Inversion of control done right

控制的反转是现代软件中必须的。它与依赖注入一起出现。

控制反转可以定义为一种委托创建或调用组件和方法的技术,这样您的模块就不需要知道如何构建通常获得的依赖项通过依赖注入

Seneca 并没有真正使用依赖注入,但它是控制反转的完美示例。

我们来看看下面的代码:

var seneca = require('seneca')();
seneca.add({component: 'greeter'}, function(msg, respond) {
  respond(null, {message: 'Hello ' + msg.name});
});
seneca.act({component: 'greeter', name: 'David'}, function(error, response) {
  if(error) return console.log(error);
  console.log(response.message);
});

这是最基本的 Seneca 示例。从企业软件的角度来看,我们可以在这里区分两个组件:生产者(Seneca.add())和消费者(Seneca.act ())。如前所述,Seneca 没有像现在这样的依赖注入系统,但 Seneca 是围绕控制反转原则优雅地构建的。

Seneca.act() 函数中,我们没有显式调用持有业务逻辑的组件;取而代之的是,我们要求 Seneca 通过使用接口(在本例中为 JSON 消息)为我们解析组件。这是控制反转。

Seneca 非常灵活:没有关键字(集成除外),也没有必填字段。它只是由模式匹配 引擎使用的关键字和值的组合,称为 Patrun

Pattern matching in Seneca

模式匹配是可用于微服务的最灵活的软件模式之一。

与网络地址或消息相反,模式相当容易扩展。让我们借助以下示例对其进行解释:

var seneca = require('seneca')();
seneca.add({cmd: 'wordcount'}, function(msg, respond) {
  var length = msg.phrase.split(' ').length;
  respond(null, {words: length});
});

seneca.act({cmd: 'wordcount', phrase: 'Hello world this is Seneca'}, function(err, response) {
  console.log(response);
});

这是一项计算句子中单词数量的服务。正如我们之前看到的,在第一行中,我们为 wordcount 命令添加了处理程序,在第二行中,我们向 Seneca 发送了一个计算单词数的请求一句话。

如果你执行它,你应该得到类似于下图的东西:

读书笔记《developing-microservices-with-node-js》Node.js中的微服务-Seneca和PM2替代品

到目前为止,您应该能够理解它的工作原理,甚至可以对其进行一些修改。

让我们扩展模式。现在,我们要跳过简短的单词,如下所示:

var seneca = require('seneca')();

seneca.add({cmd: 'wordcount'}, function(msg, respond) {
  var length = msg.phrase.split(' ').length;
  respond(null, {words: length});
});

seneca.add({cmd: 'wordcount', skipShort: true}, function(msg, respond) {
  var words = msg.phrase.split(' ');
  var validWords = 0;
  for (var i = 0; i < words.length; i++) {
    if (words[i].length > 3) {
      validWords++;
    }
  }
  respond(null, {words: validWords});
});

seneca.act({cmd: 'wordcount', phrase: 'Hello world this is Seneca'}, function(err, response) {
  console.log(response);
});

seneca.act({cmd: 'wordcount', skipShort: true, phrase: 'Hello world this is Seneca'}, function(err, response) {
  console.log(response);
});

如您所见,我们为 wordcount 命令添加了另一个处理程序,并带有一个额外的 skipShort 参数。

这个处理程序现在跳过所有三个或更少字符的单词。如果执行上述代码,输出类似于下图:

读书笔记《developing-microservices-with-node-js》Node.js中的微服务-Seneca和PM2替代品

第一行 {words: 5} 对应于第一个动作调用。第二行 {words: 4} 对应于第二个调用。

Patrun – a pattern-matching library

Patrun 也是由 Richard Rodger 编写的。 Seneca 使用它来执行模式匹配并决定哪个服务应该响应调用。

Patrun 使用 最接近的match 方法来解决呼叫。让我们看看下面的例子:

读书笔记《developing-microservices-with-node-js》Node.js中的微服务-Seneca和PM2替代品

在上图中,我们可以看到三种模式。这些等价于上一节示例中的 seneca.add()

在这种情况下,我们注册了 xy 变量。现在,让我们看看 Patrun 是如何进行匹配的:

  • {x: 1} ->A:这与 A 匹配 100%

  • {x: 2} ->:不匹配

  • {x:1, y:1} -> B:与 B 100% 匹配;它也与 A 匹配,但 B 更好匹配——二选二对一

  • {x:1, y:2} -> C:与 C 100% 匹配;同样,它也与 A 匹配,但 C 更多具体的

  • {y: 1} ->:不匹配

如您所见,Patrun(和 Seneca)总是会得到最长的匹配。这样,我们可以很容易地通过具体化匹配来扩展更抽象模式的功能。

Reusing patterns

在前面的示例中,为了跳过少于三个字符的单词,我们没有重复使用单词统计功能。

在这种情况下,很难按原样重用函数;尽管问题听起来非常相似,但解决方案几乎没有重叠。

但是,让我们回到添加两个数字的示例:

var seneca = require( 'seneca' )()

seneca.add({role: 'math', cmd: 'sum'}, function (msg, respond) {
  var sum = msg.left + msg.right
  respond(null, {answer: sum})
});

seneca.add({role: 'math', cmd: 'sum', integer: true}, function (msg, respond) {
  this.act({role: 'math', cmd: 'sum', left: Math.floor(msg.left), right: Math.floor(msg.right)},respond);
});

seneca.act({role: 'math', cmd: 'sum', left: 1.5, right: 2.5}, console.log)

seneca.act({role: 'math', cmd: 'sum', left: 1.5, right: 2.5, integer: true}, console.log)

如您所见,代码发生了一些变化。现在,接受整数的模式依赖于基本模式来计算数字的总和。

Patrun 总是尝试用以下两个维度匹配它可以找到的最接近和最具体的模式:

  • 最长的匹配链

  • 图案的顺序

它总是试图找到最合适的,如果有歧义,它会匹配找到的第一个模式。

通过这种方式,我们可以依赖已经存在的模式来构建新的服务。

Writing plugins

插件是基于Seneca的应用程序的重要组成部分。正如我们在 第 1 章中讨论的,微服务架构,API 聚合是构建应用程序的完美方式。

Node.js 最流行的框架是围绕这个概念构建的:将小软件组合起来创建一个更大的系统。

Seneca 也是围绕此构建的; Seneca.add() 原理为拼图添加了一个新的部分,因此最终的 API 是不同小软件部分的混合。

Seneca 更进一步,实现了一个有趣的插件系统,以便可以将通用功能模块化并抽象为可重用的组件。

以下示例是最小的 Seneca 插件:

function minimal_plugin( options ) {
  console.log(options)
}

require( 'seneca' )()
  .use( minimal_plugin, {foo:'bar'} )

将代码写入 minimal-plugin.js 文件并执行:

node minimal-plugin.js

此执行的输出应类似于下图:

读书笔记《developing-microservices-with-node-js》Node.js中的微服务-Seneca和PM2替代品

在 Seneca 中,在启动时会加载一个插件,但我们看不到它,因为默认的日志级别是 INFO。这意味着 Seneca 不会显示任何 DEBUG 级别信息。为了了解 Seneca 在做什么,我们需要获取更多信息,如下:

node minimal-plugin.js –seneca.log.all

这会产生 巨大的输出。这几乎是 Seneca 内部发生的所有事情,这对于调试复杂的情况非常有用,但在这种情况下,我们要做的是显示插件列表:

node minimal-plugin.js --seneca.log.all | grep plugin | grep DEFINE

它将产生类似于下图的内容:

读书笔记《developing-microservices-with-node-js》Node.js中的微服务-Seneca和PM2替代品

让我们分析一下前面的输出:

  • basic:这个插件包含在主要的 Seneca 模块中,并提供一小组基本的实用操作模式。

  • transport:这是传输插件。到目前为止,我们只在同一台机器上执行了不同的服务(非常小而简洁),但是如果我们想要分发它们呢?这个插件将帮助我们,我们将在以下部分中看到如何做到这一点。

  • web:在 第 1 章微服务架构,我们提到微服务应该提倡将连接它们的管道保持在一个广泛使用的标准之下用过的。 Seneca 默认使用 TCP,但创建 RESTful API 可能会很棘手。这个插件有助于做到这一点,我们将在下一节中看到如何做到这一点。

  • mem-store:Seneca 自带了一个数据抽象层,这样我们就可以处理不同地方的数据存储:Mongo、SQL 数据库等。开箱即用,Seneca 提供了一个内存存储,因此它可以正常工作。

  • minimal_plugin:这是我们的插件。所以,现在我们知道 Seneca 能够加载它。

我们写的插件什么都不做。现在,是时候写一些有用的东西了:

function math( options ) {

  this.add({role:'math', cmd: 'sum'}, function( msg, respond ) {
    respond( null, { answer: msg.left + msg.right } )
  })

  this.add({role:'math', cmd: 'product'}, function( msg, respond ) {
    respond( null, { answer: msg.left * msg.right } )
  })

}

require( 'seneca' )()
  .use( math )
  .act( 'role:math,cmd:sum,left:1,right:2', console.log )

首先,请注意在最后一条指令中,act() 遵循不同的格式。我们没有传递字典,而是传递一个与第一个参数具有相同键值的字符串,就像我们对字典所做的那样。它没有任何问题,但我首选的方法是使用 JSON 对象(字典),因为它是一种结构化数据而不会出现语法问题的方法。

在前面的 示例中,我们可以看到代码是如何构建为插件的。如果我们执行它,我们可以看到输出类似于以下输出:

读书笔记《developing-microservices-with-node-js》Node.js中的微服务-Seneca和PM2替代品

在 Seneca 中需要注意的一件事是如何初始化插件。包装插件的函数(在前面的例子中,math() 函数)被设计为同步执行,它被称为 定义函数。如果您还记得 上一章,Node.js 应用程序是单线程的。

要初始化插件,您需要添加一个特殊的 init() 操作模式。每个插件都会按顺序调用此操作模式。 init() 函数必须调用它的响应回调而不出错。如果插件初始化失败,则 Seneca 退出 Node.js 进程。当出现问题时,您希望您的微服务快速失败(并大声尖叫)。所有插件必须在执行任何操作之前完成初始化。

让我们看一个如何通过以下方式初始化插件的示例:

function init(msg, respond) {
  console.log("plugin initialized!");
  console.log("expensive operation taking place now... DONE!");
  respond();
}

function math( options ) {

  this.add({role:'math', cmd: 'sum'}, function( msg, respond ) {
    respond( null, { answer: msg.left + msg.right } )
  })

  this.add({role:'math', cmd: 'product'}, function( msg, respond ) {
    respond( null, { answer: msg.left * msg.right } )
  })
  
  this.add({init: "math"}, init);
}
require( 'seneca' )()
  .use( math )
  .act( 'role:math,cmd:sum,left:1,right:2', console.log )

然后,在执行 这个文件之后,输出应该和下图非常相似:

读书笔记《developing-microservices-with-node-js》Node.js中的微服务-Seneca和PM2替代品

从输出中可以看出,初始化插件的函数被调用了。

Tip

Node.js 应用程序的一般规则是永远不要阻塞线程。如果您发现自己阻塞了线程,您可能需要重新考虑如何避免它。

Web server integration

第 1 章中,微服务架构,我们特别强调使用标准技术与您的微服务进行通信。

默认情况下,Seneca 使用 TCP 传输层,尽管它使用 TCP,但并不容易与之交互,因为决定执行方法的标准是基于从客户端发送的有效负载。

让我们深入研究最常见的用例:您的服务由浏览器上的 JavaScript 调用。虽然可以做到,但如果 Seneca 公开一个 REST API 而不是 JSON 对话框,这将更容易,这非常适合微服务之间的通信(除非你有超低延迟的要求)。

Seneca 不是一个网络框架。它可以被定义为一个通用微服务框架,所以像之前公开的那样围绕一个具体案例构建它并没有太大意义.

取而代之的是,Seneca 的构建方式使得与其他框架的集成相当容易。

Express 是在 Node.js 上构建 Web 应用程序时的第一个选项。在 Internet 上可以找到有关 Express 的大量示例和文档,这使得学习它的任务相当容易。

Seneca as Express middleware

Express是也是根据API组合的原则构建的。 Express 中的每个软件都称为中间件,它们链接在代码中以处理每个请求。

在这种情况下,我们将使用 seneca-web 作为Express 的中间件,因此一旦我们指定配置,所有 URL 都将遵循命名约定。

让我们考虑以下示例:

var seneca = require('seneca')()

seneca.add('role:api,cmd:bazinga',function(args,done){
  done(null,{bar:"Bazinga!"});
});
seneca.act('role:web',{use:{
  prefix: '/my-api',
  pin: {role:'api',cmd:'*'},

  map:{
    bazinga: {GET: true}
  }
}})
var express = require('express')
var app = express()
app.use( seneca.export('web') )
app.listen(3000)

这段代码不像前面的例子那么容易理解,但我会尽力解释它:

  • 第二行向 Seneca 添加了一个模式。我们对它非常熟悉,因为本书中的所有示例都是这样做的。

  • 第三条指令 seneca.act() 是魔法发生的地方。我们正在使用 role:api 模式和任何 cmd 模式 (cmd:*) 安装模式,以对 cmd:* 下的 URL 做出反应代码类="literal">/my-api。在这个例子中,第一个 seneca.add() 将回复 URL /my-api/bazinga,作为 /my-api/prefix 变量指定,bazinga 由 < seneca.add() 命令的 code class="literal">cmd 部分。

  • app.use(seneca.export('web')) 指示 Express 使用 seneca-web 作为中间件,根据配置规则执行动作。

  • app.listen(3000) 将端口 3000 绑定到 Express。

如果您还记得本章前面的部分,seneca.act() 将函数作为第二个参数。在这种情况下,我们将公开 Express 使用的配置,以了解如何将传入请求映射到 Seneca 操作。

让我们测试一下:

读书笔记《developing-microservices-with-node-js》Node.js中的微服务-Seneca和PM2替代品

前面的代码比较密集,下面就从浏览器的代码来解释一下:

  • Express 收到一个由 seneca-web 处理的请求。

  • seneca-web 插件被配置为使用 /my-api/ 作为前缀,它与关键字 pin 绑定(参考前面代码中的 seneca.act())到包含role:api 模式,以及任何 cmd 模式 (cmd:*)。在这种情况下,/my-api/bazinga 对应于第一个(也是唯一的)seneca.add() 命令{role: 'api', cmd: 'bazinga'} 模式。

完全理解 Seneca 和 Express 之间的集成需要一段时间,但一旦清楚,API 可组合性模式提供的灵活性是无限的。

Express 本身很大 超出了本书的范围,但值得一看,因为它是一个非常流行的框架。

Data storage

Seneca 带有一个数据抽象层,允许您以通用方式与应用程序的数据进行交互。

默认情况下,Seneca 带有一个内存插件(如上一节所述),因此它可以开箱即用。

我们将在本书的大部分内容中使用它,因为不同的存储系统完全超出了范围,Seneca 将我们从它们中抽象出来。

Seneca 提供了一个简单的数据抽象层(对象关系映射(< span class="strong">ORM)) 基于以下操作:

  • load:通过标识符加载实体

  • save:这会创建或更新(如果您提供标识符)实体

  • list:列出与简单查询匹配的实体

  • remove:通过标识符删除实体

让我们构建一个插件来管理数据库中的员工:

module.exports = function(options) {
  this.add({role: 'employee', cmd: 'add'}, function(msg, respond){
    this.make('employee').data$(msg.data).save$(respond);
  });
  
  this.find({role: 'employee', cmd: 'get'}, function(msg, respond) {
    this.make('employee').load$(msg.id, respond);
  });
}

请记住,默认情况下,数据库位于内存中,因此我们现在无需担心表结构。

第一个命令将雇员添加到数据库中。第二个命令通过 id 从数据库中恢复员工。

请注意,Seneca 中的所有 ORM 原语都以美元符号 ($) 结尾。

正如你现在所看到的,我们已经从数据存储细节中抽象出来了。如果将来应用程序发生变化,我们决定使用 MongoDB 作为数据存储而不是内存存储,我们唯一需要注意的是处理 MongoDB 的插件。

让我们使用我们的员工管理插件,如下代码所示:

var seneca = require('seneca')().use('employees-storage')
var employee =  {
  name: "David",
  surname: "Gonzalez",
  position: "Software Developer"
}

function add_employee() {
  seneca.act({role: 'employee', cmd: 'add', data: employee}, function (err, msg) {
    console.log(msg);
  });
}
add_employee();

在前面的示例中,我们通过调用插件中公开的模式将员工添加到内存数据库。

在本书中,我们将看到有关如何使用数据抽象层的不同示例,但主要关注的是如何构建微服务,而不是如何构建微服务。处理不同的数据存储。

PM2 – a task runner for Node.js


PM2 是一个 生产流程管理器,有助于扩展节点。 js 向上或向下,以及负载平衡服务器的实例。它还确保进程持续运行,解决 Node.js 线程模型的副作用之一:未捕获的异常会杀死线程,进而杀死您的应用程序。

Single-threaded applications and exceptions

正如您之前所了解的,Node.js 应用程序在单线程中运行。这并不意味着 Node.js 不是并发的,它只是意味着您的应用程序在单个线程上运行,而其他一切都并行运行。

这意味着:如果异常冒出而没有得到处理,您的应用程序将终止

解决方案是密集使用诸如bluebird;它添加了成功和失败的处理程序,因此如果出现错误,异常不会 冒泡,杀死你的应用程序。

然而,也有一些我们无法控制的情况,我们称之为不可恢复的错误或错误 em>。最终,您的应用程序将由于处理不当的错误而死掉。在 Java 等语言中,异常并不是什么大问题:线程死了,但应用程序继续工作。

在 Node.js 中,这是一个大问题。在第一个实例中,使用 as forever 等任务运行器解决了这个问题.

它们都是任务运行器,当您的应用程序由于某种原因退出时,会再次重新运行它以确保正常运行时间。

考虑以下示例:

读书笔记《developing-microservices-with-node-js》Node.js中的微服务-Seneca和PM2替代品

helloWorld.js 应用程序现在由永远处理,如果应用程序终止,它将重新运行它。让我们杀死它,如下图所示:

读书笔记《developing-microservices-with-node-js》Node.js中的微服务-Seneca和PM2替代品

如您所见,forever 产生了一个具有 4903 PID 的不同进程。现在,我们发出一个 kill 命令(kill -9 4093),这是永远的输出,如下所示:

读书笔记《developing-microservices-with-node-js》Node.js中的微服务-Seneca和PM2替代品

虽然我们已经杀死了它,但我们的应用程序被永远重生了,没有任何停机时间(至少,明显的停机时间)。

正如你所看到的,forever 是非常基本的:它重新运行应用程序的次数与它被杀死的次数一样多。

还有另一个包叫做nodemon,这是最有用的一个用于开发 Node.js 应用程序的工具。如果它检测到它监视的文件中的更改(默认情况下,*.*),它会重新加载应用程序:

读书笔记《developing-microservices-with-node-js》Node.js中的微服务-Seneca和PM2替代品

现在,如果 我们修改 helloWorld.js 文件,我们 可以看到 nodemon 如何重新加载应用程序。为了避免编辑/重新加载周期并加快开发速度,这非常有趣。

Using PM2 – the industry-standard task runner

虽然,forever 看起来很有趣,但 PM2 是一个比 forever 更高级的任务运行器。使用 PM2,您可以在不停机的情况下完全管理您的应用程序生命周期,以及通过一个简单的命令来扩大或缩小您的应用程序。

PM2 还充当负载平衡器。

让我们考虑以下示例:

var http = require('http');

var server = http.createServer(function (request, response) {
  console.log('called!');
  response.writeHead(200, {"Content-Type": "text/plain"});
  response.end("Hello World\n");
});
server.listen(8000);
console.log("Server running at http://127.0.0.1:8000/");

这是一个相当简单的应用程序。让我们使用 PM2 运行它:

pm2 start helloWorld.js

这会产生类似于下图的输出:

读书笔记《developing-microservices-with-node-js》Node.js中的微服务-Seneca和PM2替代品

PM2 注册了一个名为 helloWorld 的应用。这个应用程序在 fork 模式下运行(也就是说,PM2 没有充当负载均衡器,它只是 fork 应用程序)并且操作系统的 PID 是 6858

现在,正如下面的 屏幕所示,我们将运行 pm2 show 0,它会显示与id 0 的应用,如下图所示:

读书笔记《developing-microservices-with-node-js》Node.js中的微服务-Seneca和PM2替代品

通过两个命令,我们成功地以非常复杂的方式运行了一个简单的应用程序。

从现在开始,PM2 将确保您的应用程序始终运行,因此如果您的应用程序死掉,PM2 将再次重新启动它。

我们还可以监控 PM2 正在运行的应用程序数量:

pm2 monit

这显示了以下输出:

读书笔记《developing-microservices-with-node-js》Node.js中的微服务-Seneca和PM2替代品

这是 PM2 监视器。在这种情况下,这完全是矫枉过正,因为我们的系统仅由 一个应用程序组成,该应用程序以 fork 模式运行。

我们还可以看到执行 pm2 logs 的日志,如下图所示:

读书笔记《developing-microservices-with-node-js》Node.js中的微服务-Seneca和PM2替代品

如您所见,PM2 感觉很稳定。只需很少的命令,我们就可以满足应用程序 90% 的监控需求。然而,这还不是全部。

PM2 还提供了一种无需停机即可重新加载应用程序的简单方法:

pm2 reload all

此命令可确保您的应用程序以零停机时间重新启动。 PM2 将为您排队传入的请求,并在您的应用再次响应后重新处理它们。有一个更细粒度的选项,您可以通过指定应用程序名称来指定仅重新加载某些应用程序:

pm2 reload helloWorld

对于那些与 Apache、NGINX、PHP-FPM 等战斗多年的人来说,这听起来很熟悉。

PM2 中另一个有趣的 特性是以集群模式运行您的应用程序。在这种模式下,PM2 会生成一个控制器进程和您指定的任意数量的工作程序(您的应用程序),以便您可以利用具有单线程技术(如 Node.js)的多核 CPU 的优势。

在此之前,我们需要停止正在运行的应用程序:

pm2 stop all

这将导致以下输出:

读书笔记《developing-microservices-with-node-js》Node.js中的微服务-Seneca和PM2替代品

PM2 会记住正在运行的应用程序,因此在集群模式下重新运行应用程序之前,我们需要通知 PM2 忘记您的应用程序,如下所示:

pm2 delete all
读书笔记《developing-microservices-with-node-js》Node.js中的微服务-Seneca和PM2替代品

我们准备好在集群模式下运行我们的应用程序:

pm2 start helloWorld.js -i 3
读书笔记《developing-microservices-with-node-js》Node.js中的微服务-Seneca和PM2替代品

PM2 充当主进程和三个工作人员之间的循环,以便他们可以同时处理 三个请求。我们还可以减少或增加我们的工人数量:

pm2 scale helloWorld 2

这将导致为同一个应用程序运行两个进程,而不是三个:

读书笔记《developing-microservices-with-node-js》Node.js中的微服务-Seneca和PM2替代品

如您所见,我们几乎不费吹灰之力就成功配置了我们的应用程序,以便为生产做好准备。

现在,我们可以保存 PM2 的状态,这样如果我们重新启动服务器,并且 PM2 作为守护程序运行,应用程序将自动启动。

PM2 有一个代码 API,因此我们可以编写一个 Node.js 程序来管理我们手动执行的所有步骤。它还有一种使用 JSON 文件配置服务的方法。我们将在 第 6 章中更深入地讨论这个问题, 测试和记录 Node.js 微服务,当我们研究如何使用 PM2 和 Docker 部署 Node.js 应用程序时。

Summary


在本章中,您学习了 Seneca 和 PM2 的基础知识,以便我们能够在 第 4 章在 Node.js 中编写您的第一个微服务,来自这本书。

我们还证明了上一章中介绍的一些概念实际上有助于解决现实世界的问题,并使我们的生活变得非常轻松。

在下一章中,我们将讨论如何拆分单体应用程序,为此我们需要了解本章中开发的一些概念。