vlambda博客
学习文章列表

读书笔记《developing-microservices-with-node-js》部署微服务

Chapter 8. Deploying Microservices

在本章中,我们将部署微服务。我们将使用不同的技术为读者提供为每项工作选择正确工具所需的知识。首先,我们将使用 PM2 及其部署功能在远程服务器中运行应用程序。然后,我们将围绕最先进的部署平台之一的 Docker 以及围绕容器的整个生态系统进行游戏。在本章中,我们将展示如何尽可能高地自动化所有部署。

Concepts in software deployment


部署通常是 软件开发生命周期SDLC )派对。 DevOps 将在开发和系统管理之间缺少一个 联系点在未来几年内解决。 SDLC不同阶段修复bug的成本如下图所示:

读书笔记《developing-microservices-with-node-js》部署微服务

此图显示了修复错误的成本,具体取决于 SDLC 的阶段

尽早失败是我在精益方法论中最喜欢的概念之一。在变更管理领域,在软件生命周期的不同阶段 修复错误的成本称为< strong>成本变化曲线

粗略地说,修复生产中的错误估计需要花费 150 倍的资源,而在获取需求时修复它的成本。

不管数字是多少,这在很大程度上取决于我们使用的方法和技术,我们从中吸取的教训是,我们可以通过及早发现错误来节省大量时间。

从持续集成到持续交付,流程应尽可能自动化,其中 as much as possible 表示 100%。请记住,人类是不完美的,在执行手动重复任务时更容易出错。

Continuous integration

持续集成CI)是集成工作的实践每天(或一天多次)从不同的分支机构验证这些更改不会通过运行集成和单元测试来破坏现有功能。

CI 应该 使用与我们稍后将在预生产和生产中使用的相同的基础架构配置自动化,所以如果有任何缺陷,它可以被捕获早期的。

Continuous delivery

持续交付CD)是一种软件工程方法,旨在构建小型、可测试且 易于部署的功能块, 可以交付随时无缝。

这就是我们使用微服务的目标。同样,我们应该推动交付过程自动化,因为如果我们手动执行,我们只是在寻找问题。

从微服务的角度来看,部署自动化是关键。我们需要解决拥有几十个服务而不是几台机器的开销,或者我们会发现自己维护服务云而不是为我们的公司增加价值。

Docker 是我们这里最好的盟友。使用 Docker,我们将部署新软件的麻烦减少到在不同环境中移动文件(容器),我们将在本章后面看到。

Deployments with PM2


PM2 是一个非常强大的工具。无论我们处于 开发的哪个阶段,PM2 总能提供一些东西。

在软件开发的这个阶段,部署是 PM2 真正闪耀的地方。通过一个 JSON 配置文件,PM2 将管理一个应用程序集群,以便我们可以轻松地在远程服务器上部署、重新部署和管理应用程序。

PM2 – ecosystems

PM2 称为一组 应用生态系统。每个生态系统都由 JSON 文件描述,生成它的最简单方法是执行以下命令:

pm2 ecosystem

这应该输出类似于以下代码的内容:

[PM2] Spawning PM2 daemon
[PM2] PM2 Successfully daemonized
File /path/to/your/app/ecosystem.json generated

ecosystem.json 文件的内容会有所不同,取决于 PM2 的版本,但是这个文件是什么 contains 是 PM2 集群的骨架:

{
  apps : [

    {
      name      : "My Application",
      script    : "app.js"
    },

    {
      name      : "Test Web Server",
      script    : "proxy-server.js"
    }
  ],

*/
  deploy : {
    production : {
      user : "admin",
      host : "10.0.0.1",
      ref  : "remotes/origin/master",
      repo : "[email protected]:the-repository.git",
      path : "/apps/repository",
      "post-deploy" : "pm2 startOrRestart ecosystem.json --env production"
    },
    dev : {
      user : "devadmin",
      host : "10.0.0.1",
      ref  : "remotes/origin/master",
      repo : "[email protected]:the-repository.git",
      path : "/home/david/development/test-app/",
      "post-deploy" : "pm2 startOrRestart ecosystem.json --env dev",
    }
  }
}

此文件包含为两个环境配置的两个应用程序。我们将修改这个骨架以使其适应我们的需求,对我们整个生态系统进行建模,用 第 4 章在 Node.js 中编写你的第一个微服务

但是,现在,让我们解释一下配置:

  • 我们有一系列应用程序(apps)定义了两个应用程序:API 和 WEB

  • 如您所见,我们为每个应用程序设置了一些配置参数:

    • name:这是应用程序的名称

    • script:这是应用的启动脚本

    • env:这些是PM2要注入系统的环境变量

    • env_<environment>:与 env 相同,但根据环境量身定制

  • deploy键下默认生态系统中定义了两个环境,如下:

    • 生产

    • 开发

如您所见,在这两个环境之间,除了我们在开发中配置一个环境变量和我们部署应用程序的文件夹这一事实之外,没有重大变化。

Deploying microservices with PM2

第4章中,在 Node.js 中编写您的第一个微服务,我们编写了一个简单的电子商务以便 显示微服务中的不同概念和常见

现在,我们将学习如何使用 PM2 部署它们。

Configuring the server

为了使用 PM2 部署软件,我们需要做的第一件事是配置远程机器和本地机器,使其能够使用 SSH 与公共/私钥架构。

方法很简单,如下:

  • 生成一个 RSA 密钥

  • 安装到远程服务器

我们开始做吧:

ssh-keygen -t rsa

这应该会产生类似于以下输出的内容:

Generating public/private rsa key pair.
Enter file in which to save the key (/Users/youruser/.ssh/id_rsa): /Users/youruser/.ssh/pm2_rsa
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in pm2_rsa.
Your public key has been saved in pm2_rsa.pub.
The key fingerprint is:
eb:bc:24:fe:23:b2:6e:2d:58:e4:5f:ab:7b:b7:ee:38 [email protected]
The key's randomart image is:
+--[ RSA 2048]----+
|                 |
|                 |
|                 |
|    .            |
|   o    S        |
|    o   ..       |
|   o o..o.       |
|  . +.+=E..      |
|   oo++**B+.     |
+-----------------+

现在,如果我们转到 前面输出中指示的文件夹,我们可以找到以下两个文件:

  • pm2_rsa:第一个 pm2_rsa 是您的私钥。从名称中可以看出,没有人应该有权访问此密钥,因为他们可能会在信任此密钥的服务器中窃取您的身份。

  • pm2_rsa.pubpm2_rsa.pub 是您的公钥。该密钥可以交给任何人,以便使用非对称加密技术,他们可以验证您的身份(或您所说的身份)。

我们现在要做的是将公钥复制到远程服务器,这样当我们的本地机器 PM2 尝试与服务器通信时,它知道我们是谁,让我们无需密码即可进入 shell:

cat pm2_rsa.pub | ssh youruser@yourremoteserver 'cat >> .ssh/authorized_keys'

最后一步是将您的私钥注册为本地计算机中的已知身份:

ssh-add pm2_rsa

就是这样。

从现在开始,每当您以用户身份youruser SSH 到远程服务器时,您无需输入密码即可进入shell。

完成此配置后,只需将任何应用程序部署到此服务器即可:

pm2 deploy ecosystem.json production setup
pm2 deploy ecosystem.json production

第一个 命令将配置适应应用程序所需的一切。第二个命令将实际部署应用程序本身,就像我们之前配置的那样。

Docker – a container for software delivery


虚拟化一直是过去几年最大的趋势之一。虚拟化使 工程师能够在不同的软件实例之间共享硬件。 Docker 并不是真正的虚拟化软件,但在概念上是相同的。

使用纯虚拟化解决方案,新操作系统运行在位于现有操作系统(主机操作系统)之上的管理程序之上。运行完整的操作系统意味着我们可能会消耗几 GB 的硬盘驱动器,以便将完整的堆栈从内核复制到文件系统,这通常会消耗大量资源。虚拟化解决方案的结构如下图所示:

读书笔记《developing-microservices-with-node-js》部署微服务

虚拟机环境的层图

使用 Docker,我们只 复制文件系统和二进制文件,因此无需在不需要的地方运行完整的操作系统堆栈。 Docker 镜像通常是几百兆字节,而不是千兆字节,而且它们非常轻量级,因此,我们可以在同一台机器上运行一些容器。之前使用Docker的结构如下图所示:

读书笔记《developing-microservices-with-node-js》部署微服务

Docker 的层图

借助 Docker,我们还消除了软件部署的最大问题之一,即 > 配置管理

我们正在切换一个复杂的按环境配置管理,我们需要担心应用程序如何部署/配置到一个容器中,该容器基本上就像一个可以安装在任何支持 Docker 的机器上的软件包。

目前唯一支持 Docker 的操作系统是 Linux,因为 Docker 需要利用高级内核功能,迫使 Windows 和 Mac 用户在 Linux 上运行虚拟机,以支持运行 Docker 容器。

Setting up the container

Docker 提供了一种非常强大且熟悉的容器配置方式(对于开发人员而言)。

您可以基于现有镜像(互联网上有数千个镜像)创建 容器,然后通过添加新软件包来修改镜像以满足您的需求或更改文件系统。

一旦我们对它感到满意,我们就可以使用新版本的镜像来使用类似于 Git 的版本控制系统来创建我们的容器。

但是,我们需要先了解 Docker 是如何工作的。

Installing Docker

正如前面提到的,Docker 需要一个虚拟机来提供对 Mac 和 Windows 的支持,因此在这些系统上的安装可能会有所不同。在您的系统上安装 Docker 的最佳方法是访问官方网站并按照 步骤操作:

https://docs.docker.com/engine/installation/

目前,这是一个非常活跃的项目,因此您可以预期每隔几周就会发生变化。

Choosing the image

默认情况下,Docker 没有镜像。我们可以通过在终端上运行 docker images 来验证这一点,这将产生与以下屏幕截图非常相似的输出:

读书笔记《developing-microservices-with-node-js》部署微服务

这是一个空列表。本地计算机中没有存储图像。我们需要做的第一件事是搜索图像。在这种情况下,我们将使用 CentOS 作为我们创建图像的基础。 CentOS 与 Red Hat Enterprise Linux 非常接近,后者似乎是业界最扩展的 Linux 发行版之一。他们提供了强大的支持,并且 Internet 上有大量信息可用于解决问题。

让我们搜索一个 CentOS 镜像,如下所示:

读书笔记《developing-microservices-with-node-js》部署微服务

如您所见, 有很多基于 CentOS 的图像,但只有第一个是官方的。

此图像列表来自 Docker 中称为 Registry世界。 Docker Registry 是一个简单的图像存储库,可供公众使用。您还可以运行自己的注册表,以防止您的图像进入通用注册表。

Note

更多信息可以在以下链接中找到:

https://docs.docker.com/registry/

在前面的屏幕截图中的表格中有一个列应该立即引起您的注意, 列称为 明星。此列表示用户对给定图像的评分。我们可以使用 -s 标志,根据用户给图像的星数缩小搜索范围。

如果您运行以下命令,您将看到评分为 1000 星或更多星的图像列表:

docker search -s 1000 centos

Tip

请注意您选择的图像,没有什么可以阻止用户使用恶意软件创建图像。

为了将 CentOS 映像提取到本地机器,我们需要运行以下命令:

docker pull centos

产生的输出 将与下图非常相似:

读书笔记《developing-microservices-with-node-js》部署微服务

命令完成后,如果我们再次运行 Docker 映像,我们可以看到 centos 现在出现在以下列表中:

读书笔记《developing-microservices-with-node-js》部署微服务

正如我们之前指定的,Docker 不使用完整的镜像,而是使用它的简化版本,只虚拟化操作系统的最后几层。你可以清楚地看到它,因为图像的大小甚至不到 200 MB,对于完整版的 CentOS,最多可以达到几 GB。

Running the container

现在我们在本地机器上有了图像的副本,是时候 运行它:

docker run -i -t centos /bin/bash

这将产生以下输出:

读书笔记《developing-microservices-with-node-js》部署微服务

可以看到,终端的提示变成了类似root@debd09c7aa3b的东西,表示我们在容器里面。

从现在开始,我们运行的每一个命令都将在一个包含的 CentOS Linux 版本。

Docker 中还有另一个有趣的命令:

docker ps

如果我们在新终端中运行此命令(不退出正在运行的容器),我们将得到以下输出:

读书笔记《developing-microservices-with-node-js》部署微服务

这个输出是不言自明的;这是查看 Docker 容器中发生的情况的一种简单方法。

Installing the required software

让我们在容器中安装 Node.js:

curl --silent --location https://rpm.nodesource.com/setup_4.x | bash -

此命令 将拉取并执行 Node.js 的设置脚本。

这将产生与下图非常相似的输出:

读书笔记《developing-microservices-with-node-js》部署微服务

按照 说明进行操作,因为这将安装节点:

yum install -y nodejs

强烈 建议安装开发工具,因为一些模块的安装过程需要编译步骤。我们开始做吧:

yum install -y gcc-c++ make

命令完成后,我们就可以在容器内运行节点应用程序了。

Saving the changes

在 Docker 世界中,镜像是给定容器的配置。我们可以将图像用作 模板来运行我们想要的任意数量的容器,但是首先,我们需要保存在上一节中所做的更改。

如果您是软件开发人员,您可能熟悉 CVS、Subversion 或 Git 等控制版本系统。 Docker 的构建考虑了他们的理念——可以将容器视为可版本化的软件组件,然后可以提交更改。

为此,请运行以下命令:

docker ps -a

此命令将显示过去运行的容器列表,如下图所示:

读书笔记《developing-microservices-with-node-js》部署微服务

在我的情况下,容器很少,但在这种情况下有趣的是第二个;这是 Node.js 的安装位置。

现在,我们需要提交容器的状态,以便使用我们的更改创建一个新图像。我们通过运行以下命令来做到这一点:

docker commit -a dgonzalez 62e7336a4627 centos-microservices:1.0

让我们解释一下命令:

  • -a 标志表示作者。在这种情况下,dgonzalez

  • 下面的参数是container id。如前所述,第二个容器具有相应的 ID 62e7336a4627

  • 第三个参数是赋予新图像的名称和图像标签的组合。当我们处理大量图像时,标记系统可能非常强大,因为识别它们之间的微小变化会变得非常复杂。

这可能需要几秒钟,但完成后,命令的输出必须与下图非常相似:

读书笔记《developing-microservices-with-node-js》部署微服务

这表明我们的列表中有一个安装了软件的新图像。再次运行docker images,输出会确认,如下图所示:

读书笔记《developing-microservices-with-node-js》部署微服务

为了运行基于新镜像的容器,我们可以运行以下命令:

docker run -i -t centos-microservices:1.0 /bin/bash

这将 让我们能够访问容器中的 shell,我们可以通过运行 node 来确认已安装 Node.js -v,应该输出 Node 的版本,在本例中为 4.2.4。

Deploying Node.js applications

现在,是时候在容器中部署 Node.js 应用程序了。为了做到这一点,我们要需要从我们本地的中暴露代码a>machine 到 Docker 容器。

正确的做法是在 Docker 机器上挂载一个本地文件夹,但首先,我们需要创建要在容器内运行的小应用程序,如下所示:

var express = require('express');
var myApplication = express();

app.get('/hello', function (req, res) {
  res.send('Hello Earth!');
});

var port = 80;

app.listen(port, function () {
  console.log('Listeningistening on port '+ port);
});

这是一个简单的应用程序,使用 Express 基本上将 Hello Earth! 呈现到浏览器中。如果我们从终端运行它并访问 http://localhost:80/hello,我们可以看到结果。

现在,我们将在容器内运行它。为了做到这一点,我们将在 Docker 容器中挂载一个本地文件夹作为卷并运行它。

Docker 来自系统管理员和开发人员的经验,他们最近融入了一个叫做 DevOps 的角色,这个角色介于两者之间。在 Docker 之前,每家公司都有自己部署应用程序和管理配置的方式,因此对于如何以正确的方式做事没有达成共识。

现在有了 Docker,这些公司就有了一种方法来提供统一的部署。无论您的业务是什么,一切都简化为构建容器、部署应用程序并在适当的机器上运行容器。

假设应用程序位于 /apps/test/ 文件夹中。现在,为了将它暴露给容器,我们运行以下命令:

docker run -i -t -v /app/test:/test_app -p 8000:3000 centos-microservices:1.0 /bin/bash

如您所见, 看到,Docker 可以通过 参数变得非常冗长,但让我们解释一下他们,如下:

  • -i-t 标志 很熟悉给我们。它们 捕获输入并将输出发送到终端。

  • -v 标志 是新的。它指定来自本地计算机的卷以及将其安装在容器中的位置。在这种情况下,我们将本地机器上的 /apps/test 挂载到 /test_app 中。请注意冒号分隔本地和远程路径。

  • -p 标志 指定将在容器中公开远程端口的本地计算机上的端口。在这种情况下,我们通过Docker机器中的端口8000暴露容器中的端口3000,所以访问docker-machine:8000 最终将访问容器中的端口 3000

  • centos-microservices:1.0 是我们在上一节中创建的图像的名称和标签。

  • /bin/bash 是我们要在容器内执行的 命令。 /bin/bash 将使我们能够访问系统的提示。

如果一切正常,我们应该可以访问容器内的系统提示符,如下图所示:

读书笔记《developing-microservices-with-node-js》部署微服务

如图所示,有一个名为 /test_app 的文件夹,其中包含我们之前的应用程序,名为 small-script.js

现在,是时候访问应用程序了,但首先,让我们解释一下 Docker 的工作原理。

Docker 是用 Go 编写的,这是一种由 Google 创建的现代语言,将编译语言(如 C++)的所有优点与所有高-来自现代语言(如 Java)的级别功能。

学习起来相当容易,掌握起来也不难。 Go 的哲学是带来解释语言的所有好处,例如减少编译时间(完整的语言可以在一分钟内编译)到编译语言。

Docker 使用 Linux 内核中非常特殊的功能,强制 Windows 和 Mac 用户使用 虚拟机来运行 Docker 容器。这台机器以前叫boot2docker,但新版本叫Docker Machine ,其中包含更多 高级功能,例如在远程虚拟机中部署容器。对于此示例,我们将仅使用本地功能。

鉴于此,如果 您从位于 /test_app/ 文件夹中的容器内运行应用程序,并且您在 Linux 中,访问 http://localhost:8000/,进入应用程序就足够了。

当您使用 Mac 或 Windows 时,Docker 在 Docker Machine 或 boot2docker 中运行,因此 IP 由该虚拟机提供,在 Docker 终端启动时显示,如下图所示:

读书笔记《developing-microservices-with-node-js》部署微服务

可以看到,IP是192.168.99.100,所以为了访问我们的应用,我们需要访问http://192.168 .99.100:9000/ 网址。

Automating Docker container creation

如果您记得,在前面的章节中,一个最重要的概念是自动化。使用微服务时,自动化是关键。您可能需要操作几十台服务器,而不是操作一台服务器,以至于您的日常任务几乎都被预订满了。

Docker 设计者在允许用户从一个名为 Dockerfile 的文件中编写的脚本创建容器时考虑到了这一点。

如果您曾经从事过 C 或 C++ 编码工作,即使在大学期间,您也可能对 Makefiles 很熟悉。 Makefile 文件是一个脚本,开发人员在其中指定自动构建软件组件的步骤。这是的一个例子:

all: my-awesome-app

my-awesome-app: main.o external-module.o app-core.o
  g++ main.o external-module.o app-core.o -o my-awesome-app


main.o: main.cc
  g++ -c main.cc


external-module.o: external-module.cc
  g++ -c external-module.cc


app-core.o: app-core.cc
  g++ -c hello.cc

clean:
  rm *.o my-awesome-app

前面的 Makefile 包含要执行的任务和依赖项的列表。例如,如果我们在包含 Makefile 文件的同一文件夹中执行 make clean,它将删除可执行文件和所有以 o 结尾的文件。

DockerfileMakefile 不同,它不是任务和依赖项的列表(尽管概念相同),它是一个从头开始构建容器到就绪状态的指令列表。

让我们看一个例子:

FROM centos
MAINTAINER David Gonzalez
RUN curl --silent --location https://rpm.nodesource.com/setup_4.x | bash -
RUN yum -y install nodejs

前面这几行代码足以构建一个安装了 Node.js 的容器。

让我们在下面解释它:

  • 首先,我们选择基础镜像。在这种情况下,它是我们之前使用的 centos。为此,我们使用 FROM 命令,然后使用图像的名称。

  • MAINTAINER 指定创建容器的人的姓名。在这种情况下,它是 David Gonzalez

  • RUN,顾名思义,运行一个命令。在这种情况下,我们使用了两次:一次将存储库添加到 yum,然后安装 Node.js。

Dockerfile 可以 包含许多不同的命令。他们的 文档非常清楚,但让我们看一下最常见的(除了之前看到的):

  • CMD:这与类似,但实际上是在构建命令后执行。 CMD 是容器实例化后用于启动应用程序的命令。

  • WORKDIR:这个要和CMD一起使用code>,它是用来指定下一个CMD命令的工作目录的命令。

  • ADD:此 命令用于将文件从本地文件系统复制到容器实例文件系统。在前面的示例中,我们可以使用 ADD 将应用程序从主机复制到容器中,运行 npm install CMD 命令,然后使用 CMD 命令再次运行应用程序。它还可用于将内容从 URL 复制到容器内的目标文件夹。

  • ENV:这是用来设置环境变量的。例如,您可以通过将环境变量传递给容器来指定一个文件夹来存储上传的文件,如下所示:

    ENV UPLOAD_PATH=/tmp/
    
  • EXPOSE:这是用于向集群中的其余容器公开端口。

如您所见, 域特定语言Dockerfiles 的 ="strong">DSL) 非常丰富,您几乎可以构建所需的每个系统。 Internet 上有数百个示例可以构建几乎所有内容:MySQL、MongoDB、Apache 服务器等等。

强烈建议通过Dockerfiles来创建容器,因为它可以作为脚本在未来复制和更改容器,并且可以自动部署我们的软件无需太多人工干预。

Node.js event loop – easy to learn and hard to master


我们都知道 Node.js 是一个以单线程方式运行应用程序的平台;那么,为什么我们不使用多线程来运行应用程序,这样我们才能获得多核处理器的好处呢?

Node.js 建立在一个名为 libuv 的库之上。这个库抽象了系统调用,为使用它的程序提供了一个异步接口。

我来自 非常重的 Java 背景,并且在那里,一切都是 同步的(除非您使用某种非阻塞库进行编码),并且如果您向数据库发出请求,一旦数据库回复数据,线程就会被阻塞并恢复。

这通常工作得很好,但它提出了一个有趣的问题:阻塞的线程正在消耗可用于服务其他请求的资源。 Node.js的事件循环如下图所示:

读书笔记《developing-microservices-with-node-js》部署微服务

这是 Node.js 事件循环图

默认情况下,JavaScript 是一种事件驱动的语言。它执行配置事件处理程序列表的程序,这些事件处理程序将对给定事件做出反应,之后,它只是等待动作发生。

让我们看一个非常熟悉的例子:

<div id="target">
  Click here
</div>
<div id="other">
  Trigger the handler
</div>

那么JavaScript代码如下:

$( "#target" ).click(function() {
  alert( "Handler for .click() called." );
});

如您所见,这是一个非常简单的示例。显示按钮和 JavaScript 代码片段的 HTML,使用 JQuery,在单击按钮时显示警告框。

这是关键:当按钮被点击时

点击一个按钮是一个事件,通过事件循环处理该事件JavaScript 使用 JavaScript 中指定的处理程序。

归根结底,我们只有一个线程执行事件,我们从不谈论 JavaScript 中的并行性,正确的词是并发。因此,更简洁地说,我们可以说 Node.js 程序是高度并发的。

您的应用程序将始终只在一个线程中执行,我们在编码时需要牢记这一点。

如果您一直在使用 Java 或 .NET 或使用线程阻塞技术设计和实现的任何其他语言/框架,您可能已经观察到 Tomcat 在运行应用程序时会产生许多线程来侦听请求。

在 Java 世界中,这些线程中的每一个都是,称为 workers,并且他们负责从头到尾处理来自给定用户的请求。 Java 中有一种类型的数据结构可以利用它。它被称为 ThreadLocal 并将数据存储在本地线程中,因此以便以后可以恢复。这种类型的存储是可能的,因为启动请求的线程也负责完成它,如果线程正在执行任何阻塞操作(例如读取文件或访问数据库),它会一直等待直到完成。

这通常没什么大不了的,但是当您的软件严重依赖 I/O 时,问题可能会变得很严重。

另一个支持 Node.js 非阻塞模型的要点是缺少上下文切换。

当 CPU 将一个线程切换到另一个线程时,寄存器和内存的其他区域中的所有数据都被堆叠起来,并允许 CPU 与一个新进程切换上下文,该新进程有自己的数据要放入其中在那里,如下图所示:

读书笔记《developing-microservices-with-node-js》部署微服务

这是从理论角度显示线程中的上下文切换的图表。

此操作 需要时间,而此时间未被应用程序使用。它只是迷路了。在 Node.js 中,您的应用程序仅在一个线程中运行,因此在运行时没有这种上下文切换(它仍然存在于后台,但对您的程序隐藏)。在下图中,我们可以看到当 CPU 切换线程时在现实世界中会发生什么:

读书笔记《developing-microservices-with-node-js》部署微服务

这是从实际(显示死时间)的角度显示线程中的上下文切换的图表。

Clustering Node.js applications


至此,您知道 Node.js 应用程序是如何工作的,当然,有些读者可能会有一个 问题,即如果应用程序在单线程上运行,那么现代多核处理器会发生什么?

在回答这个问题之前,我们先来看看下面的场景。

在我上高中的时候,CPU 发生了一次重大的技术飞跃:分割。

这是在指令级引入并行性的第一次尝试。您可能知道,CPU 解释汇编指令,每条指令都由多个阶段组成,如下图所示:

读书笔记《developing-microservices-with-node-js》部署微服务

Intel 4x86之前,CPU每次执行一条指令,所以从上图中的指令模型来看,任何CPU每次只能执行一条指令六个 CPU 周期。

然后,细分开始发挥作用。通过一组中间寄存器,CPU 工程师设法并行化指令的各个阶段,以便在最佳情况下,CPU 能够每个周期(或几乎)执行一条指令,如下图所示:

读书笔记《developing-microservices-with-node-js》部署微服务

该图描述了在具有分段流水线的 CPU 中执行指令

这种技术改进带来了更快的 CPU,并为原生硬件多线程打开了大门,从而催生了可以执行大量 并行处理的现代 n 核处理器任务,但是当我们运行 Node.js 应用程序时,我们只使用一个内核。

如果我们不集群我们的应用程序,与其他利用 CPU 多核的平台相比,我们的性能将会严重下降。

然而,这一次我们很幸运,PM2 已经允许您集群 Node.js 应用程序以最大限度地利用您的 CPU。

此外,PM2 的重要方面之一是它允许您在不停机的情况下扩展应用程序。

让我们以集群模式运行一个简单的应用程序:

var http = require("http");
http.createServer(function (request, response) {
  response.writeHead(200, {
    'Content-Type': 'text/plain'
  });
  response.write('Here we are!')
  response.end();
}).listen(3000);

这次我们使用 Node.js 的原生 HTTP 库来处理传入的 HTTP 请求。

现在我们可以从终端运行应用程序,看看它是如何工作的:

node app.js

虽然它不输出任何内容,但我们可以 curl 到 http://localhost:3000/ URL 以查看服务器如何响应,如下图所示:

读书笔记《developing-microservices-with-node-js》部署微服务

如您所见, 看到,Node.js 管理了所有 HTTP 协商,并且还设法使用 进行回复,我们在这里are! 短语,因为它是在代码中指定的。

这个服务很琐碎,但它是更复杂的Web服务工作的原理,所以我们需要对Web服务进行集群以避免瓶颈。

Node.js 有一个名为 cluster 的库,它允许我们以编程方式对应用程序进行集群,如下所示:

var cluster = require('cluster');
var http = require('http');
var cpus = require('os').cpus().length;

// Here we verify if the we are the master of the cluster: This is the root process
// and needs to fork al the childs that will be executing the web server.
if (cluster.isMaster) {
  for (var i = 0; i < cpus; i++) {
    cluster.fork();
  }

  cluster.on('exit', function (worker, code, signal) {
    console.log("Worker " + worker.proces.pid + " has finished.");
  });
} else {
  // Here we are on the child process. They will be executing the web server.
  http.createServer(function (request, response) {
    response.writeHead(200);
    response.end('Here we are!d\n');
  }).listen(80);
}

就个人而言,我 发现使用 PM2 等特定软件来实现有效的集群要容易得多,因为在尝试处理集群实例时代码会变得非常复杂我们的应用程序。

鉴于此,我们可以通过 PM2 运行应用程序,如下所示:

pm2 start app.js -i 1
读书笔记《developing-microservices-with-node-js》部署微服务

PM2 中的 -i 标志,如您在命令的输出中所见,用于指定我们想要用于我们的应用程序的内核数量。

如果我们运行 pstree,我们可以看到系统中的进程树,并检查 PM2 是否只为我们的应用程序运行一个进程,如下图所示:

读书笔记《developing-microservices-with-node-js》部署微服务

在这种情况下,我们只在一个进程中运行应用程序,因此它将被分配在 CPU 的一个内核中。

在这种情况下,我们 没有利用运行应用程序的 CPU 的多核功能,但我们仍然可以从自动重启应用程序中获益如果我们的算法出现一个异常。

现在,我们将使用 CPU 中的所有可用内核运行我们的应用程序,以便最大限度地利用它,但首先,我们需要停止集群:

pm2 stop all

读书笔记《developing-microservices-with-node-js》部署微服务

PM2,停止所有服务后

pm2 delete all

现在,我们可以使用 CPU 的所有内核重新运行应用程序:

pm2 start app.js -i 0
读书笔记《developing-microservices-with-node-js》部署微服务

PM2 显示以集群模式运行的四个服务

PM2 已经 设法猜测了我们计算机中的 CPU 数量,在我的例子中,这是一个具有四个内核的 iMac,如下图所示:

读书笔记《developing-microservices-with-node-js》部署微服务

pstree中可以看到,PM2在OS层面启动了四个线程,如下图所示:

读书笔记《developing-microservices-with-node-js》部署微服务

在集群应用程序时,关于应用程序应该使用的内核数量有一个不成文的规则,这个数字是内核数量减一。

这个数字背后的原因 是因为操作系统需要一些 CPU 能力,因此如果我们在应用程序中使用所有 CPU,一旦操作系统开始运行对于其他一些任务,它将强制进行上下文切换,因为所有内核都会很忙,这会减慢应用程序的速度。

Load balancing our application


有时,集群我们的应用程序是不够的,我们需要水平扩展我们的应用程序.

有多种方法可以水平扩展应用程序。如今,借助亚马逊等云提供商,每个提供商都实施了自己的解决方案,并具有许多功能。

我首选的实现负载平衡的方法之一是使用 NGINX

NGINX 是一个 网络服务器,重点关注并发性和低内存使用.它也非常适合 Node.js 应用程序,因为非常不鼓励从 Node.js 应用程序中提供静态资源。主要原因是避免应用程序由于可以使用其他软件(例如 NGINX(这是专业化的另一个示例))更好地完成的任务而承受压力。

但是,让我们关注负载平衡。下图展示了 NGINX 如何作为负载均衡器工作:

读书笔记《developing-microservices-with-node-js》部署微服务

正如您在上图中 看到的,我们有两个 PM2 集群负载均衡通过 NGINX 的一个实例

我们需要做的第一件事是了解 NGINX 如何管理配置。

在 Linux 上,NGINX 可以通过 yumapt-get 或任何其他包管理器安装。它也可以从源代码构建,但推荐的方法是使用包管理器,除非您有非常具体的要求。

默认情况下,主配置文件为/etc/nginx/nginx.conf,为 如下:

user  nginx;
worker_processes  1;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;

events {
  worker_connections  1024;
}

http {
  include                     /etc/nginx/mime.types;
  default_type                application/octet-stream;

  log_format          main '$remote_addr - $remote_user [$time_local] "$request" '
     '$status $body_bytes_sent "$http_referer" '
     '"$http_user_agent" "$http_x_forwarded_for" '
     '$request_time';

  access_log                  /var/log/nginx/access.log  main;
  server_tokens               off;
  sendfile                    on;
  #tcp_nopush                 on;
  keepalive_timeout           65s;
  send_timeout                15s;
  client_header_timeout       15s;
  client_body_timeout         15s;
  client_max_body_size        5m;
  ignore_invalid_headers      on;
  fastcgi_buffers             16 4k;
  #gzip                       on;
  include                     /etc/nginx/sites-enabled/*.conf;
}

这个文件非常简单,它指定了工作人员的数量(记住,服务请求的进程)、错误日志的位置、工作人员当时可以激活的连接数,最后是 HTTP 配置。

最后一行是最有趣的:我们通知 NGINX 使用 /etc/nginx/sites-enabled/*.conf 作为潜在的配置文件。

使用此 配置,.conf 结尾的文件indexterm"> 指定的文件夹将成为 NGINX 配置的一部分。

如您所见,那里已经存在一个默认文件。将其修改为如下所示:

http {
  upstream app {
    server 10.0.0.1:3000;
    server 10.0.0.2:3000;
  }
  server {
    listen 80;
    location / {
      proxy_pass http://app;
    }
  }
}

这是我们构建负载均衡器所需的所有配置。让我们在下面解释它:

  • upstream app 指令正在创建一组名为 app 的服务。在该指令中,我们指定了两个服务器,如上图所示。

  • server 指令向 NGINX 指定它应该监听来自端口 80 的所有请求并将它们传递给上游组称为 app

现在,NGINX 如何决定将请求发送到哪台计算机?

在这种情况下,我们可以指定用于分散负载的策略。

默认情况下,NGINX 在没有专门配置平衡方式时,使用 Round Robin

要记住的一件事是,如果我们使用循环,我们的应用程序应该是无状态的,因为我们不会总是在同一台机器上运行,所以如果我们将状态保存在服务器中,它可能不会出现在以下调用中.

轮询是将负载从工作队列分配给多个工作人员的最基本方法;它会旋转它们,以便每个节点都获得 相同数量的请求

还有其他机制可以分散负载,如下所示:

  upstream app {
    least_conn;
    server 10.0.0.1:3000;
    server 10.0.0.2:3000;
  }

Least connected,正如其 名称所示,将请求发送到最少连接的节点,在所有节点之间平均分配负载:

  upstream app {
    ip_hash;
    server 10.0.0.1:3000;
    server 10.0.0.2:3000;
  }

IP hashing 是一种有趣的负载分配方式。如果您曾经使用过任何 Web 应用程序,那么几乎所有应用程序中都会出现会话的概念。为了记住用户是谁,浏览器向服务器发送一个 cookie,服务器在内存中存储了用户的身份以及他/她需要/可以被该给定用户访问的数据。另一种类型的负载平衡的问题是我们不能保证总是访问同一台服务器。

对于 示例,如果我们使用 Least connected 作为平衡策略,我们可能会在第一次加载时点击 服务器,但随后在后续重定向中点击不同的服务器,这将导致用户无法显示正确的信息第二台服务器不知道用户是谁。

使用 IP 散列,负载均衡器将为给定 IP 计算散列。这个哈希会以某种方式产生一个从 1 到 N 的数字,其中 N< /span> 是服务器的数量,然后,只要他们保持相同的 IP,用户将始终被重定向到同一台机器。

我们还可以对负载均衡应用权重,如下所示:

  upstream app {
    server 10.0.0.1:3000 weight=5;
    server 10.0.0.2:3000;
  }

这将以这样的方式分配负载,对于每六个请求,五个将被定向到第一台机器,一个将被定向到第二台机器。

一旦我们选择了首选的负载平衡方法,我们可以重新启动 NGINX 以使更改生效,但首先,我们要验证它们,如下图所示:

读书笔记《developing-microservices-with-node-js》部署微服务

如您所见, 可以看到,配置测试真的很有帮助,可以避免配置 灾难。

一旦 NGINX 通过 configtest,保证 NGINX 能够restart/start/reload 没有任何语法问题,如如下:

sudo /etc/init.d/nginx reload

重新加载将优雅地等待旧线程完成,然后重新加载配置并使用新配置路由新请求。

如果你有兴趣了解 NGINX,我发现以下 NGINX 的官方文档 很有帮助:

http://nginx.org/en/docs/

Health check on NGINX

健康检查是负载均衡器上的一项重要活动。如果其中一个节点出现严重的硬件故障并且无法处理 更多请求,会发生什么情况?

在这种情况下,NGINX 带有两种类型的健康检查:passiveactive

Passive health check

在这里,NGINX 被 配置为反向代理(就像我们在上一节中所做的那样)。它对来自上游服务器的某种类型的响应做出反应。

如果返回错误,NGINX会将节点标记为故障,将其从负载均衡中移除一段时间,然后重新引入它. 使用此策略,由于 NGINX 将不断从负载均衡器中删除节点,因此故障数量将大大减少。

有一些可配置的参数,例如 max_failsfail_timeout,我们可以在其中配置标记节点所需的失败次数无效或请求超时。

Active health check

主动健康检查与被动健康检查不同,主动向上游服务器发出连接以检查它们是否响应正确解决遇到的问题。

NGINX 中最简单的主动健康检查配置如下:

http {
  upstream app {
    zone app test;
    server 10.0.0.1:3000;
    server 10.0.0.2:3000;
  }
  server {
    listen 80;
    location / {
      proxy_pass http://app;
      health_check;
    }
  }
}

在这个配置文件中新增了两行,如下:

  • health_check:启用主动健康检查。默认配置是每五秒向 upstream 部分中指定的主机和端口发出一次连接。

  • zone app test:这是NGINX配置在启用健康检查时需要的。

有很多选项可以配置更具体的健康检查,所有选项都可以在 NGINX 配置中使用,可以组合以满足不同用户的需求。

Summary


在本章中,您学习了可用于部署微服务的各种技术。到目前为止,您已经知道如何构建、部署和配置软件组件,从而使我们能够同质化非常多样化的技术。本书的目的是为您提供开始使用微服务所需的概念,并使读者知道如何查找所需的信息。

就个人而言,我一直在努力寻找一本总结微服务生命周期各个方面的书,我真的希望这本书能涵盖这个空白。