读书笔记《developing-microservices-with-node-js》部署微服务
部署通常是 软件开发生命周期(SDLC )派对。 DevOps 将在开发和系统管理之间缺少一个 联系点在未来几年内解决。 SDLC不同阶段修复bug的成本如下图所示:
尽早失败是我在精益方法论中最喜欢的概念之一。在变更管理领域,在软件生命周期的不同阶段 修复错误的成本称为< strong>成本变化曲线。
粗略地说,修复生产中的错误估计需要花费 150 倍的资源,而在获取需求时修复它的成本。
不管数字是多少,这在很大程度上取决于我们使用的方法和技术,我们从中吸取的教训是,我们可以通过及早发现错误来节省大量时间。
从持续集成到持续交付,流程应尽可能自动化,其中 as much as possible 表示 100%。请记住,人类是不完美的,在执行手动重复任务时更容易出错。
PM2 是一个非常强大的工具。无论我们处于 开发的哪个阶段,PM2 总能提供一些东西。
在软件开发的这个阶段,部署是 PM2 真正闪耀的地方。通过一个 JSON 配置文件,PM2 将管理一个应用程序集群,以便我们可以轻松地在远程服务器上部署、重新部署和管理应用程序。
PM2 称为一组 应用生态系统。每个生态系统都由 JSON 文件描述,生成它的最简单方法是执行以下命令:
这应该输出类似于以下代码的内容:
ecosystem.json
文件的内容会有所不同,取决于 PM2 的版本,但是这个文件是什么 contains 是 PM2 集群的骨架:
此文件包含为两个环境配置的两个应用程序。我们将修改这个骨架以使其适应我们的需求,对我们整个生态系统进行建模,用 第 4 章,在 Node.js 中编写你的第一个微服务。
但是,现在,让我们解释一下配置:
如您所见,在这两个环境之间,除了我们在开发中配置一个环境变量和我们部署应用程序的文件夹这一事实之外,没有重大变化。
在第4章中,在 Node.js 中编写您的第一个微服务,我们编写了一个简单的电子商务以便 显示微服务中的不同概念和常见。
现在,我们将学习如何使用 PM2 部署它们。
为了使用 PM2 部署软件,我们需要做的第一件事是配置远程机器和本地机器,使其能够使用 SSH 与公共/私钥架构。
方法很简单,如下:
生成一个 RSA 密钥
安装到远程服务器
我们开始做吧:
这应该会产生类似于以下输出的内容:
现在,如果我们转到 前面输出中指示的文件夹,我们可以找到以下两个文件:
pm2_rsa
:第一个pm2_rsa
是您的私钥。从名称中可以看出,没有人应该有权访问此密钥,因为他们可能会在信任此密钥的服务器中窃取您的身份。pm2_rsa.pub
:pm2_rsa.pub
是您的公钥。该密钥可以交给任何人,以便使用非对称加密技术,他们可以验证您的身份(或您所说的身份)。
我们现在要做的是将公钥复制到远程服务器,这样当我们的本地机器 PM2 尝试与服务器通信时,它知道我们是谁,让我们无需密码即可进入 shell:
最后一步是将您的私钥注册为本地计算机中的已知身份:
就是这样。
从现在开始,每当您以用户身份youruser
SSH 到远程服务器时,您无需输入密码即可进入shell。
完成此配置后,只需将任何应用程序部署到此服务器即可:
虚拟化一直是过去几年最大的趋势之一。虚拟化使 工程师能够在不同的软件实例之间共享硬件。 Docker 并不是真正的虚拟化软件,但在概念上是相同的。
使用纯虚拟化解决方案,新操作系统运行在位于现有操作系统(主机操作系统)之上的管理程序之上。运行完整的操作系统意味着我们可能会消耗几 GB 的硬盘驱动器,以便将完整的堆栈从内核复制到文件系统,这通常会消耗大量资源。虚拟化解决方案的结构如下图所示:
使用 Docker,我们只 复制文件系统和二进制文件,因此无需在不需要的地方运行完整的操作系统堆栈。 Docker 镜像通常是几百兆字节,而不是千兆字节,而且它们非常轻量级,因此,我们可以在同一台机器上运行一些容器。之前使用Docker的结构如下图所示:
借助 Docker,我们还消除了软件部署的最大问题之一,即 > 配置管理。
我们正在切换一个复杂的按环境配置管理,我们需要担心应用程序如何部署/配置到一个容器中,该容器基本上就像一个可以安装在任何支持 Docker 的机器上的软件包。
目前唯一支持 Docker 的操作系统是 Linux,因为 Docker 需要利用高级内核功能,迫使 Windows 和 Mac 用户在 Linux 上运行虚拟机,以支持运行 Docker 容器。
Docker 提供了一种非常强大且熟悉的容器配置方式(对于开发人员而言)。
您可以基于现有镜像(互联网上有数千个镜像)创建 容器,然后通过添加新软件包来修改镜像以满足您的需求或更改文件系统。
一旦我们对它感到满意,我们就可以使用新版本的镜像来使用类似于 Git 的版本控制系统来创建我们的容器。
但是,我们需要先了解 Docker 是如何工作的。
正如前面提到的,Docker 需要一个虚拟机来提供对 Mac 和 Windows 的支持,因此在这些系统上的安装可能会有所不同。在您的系统上安装 Docker 的最佳方法是访问官方网站并按照 步骤操作:
https://docs.docker.com/engine/installation/
目前,这是一个非常活跃的项目,因此您可以预期每隔几周就会发生变化。
默认情况下,Docker 没有镜像。我们可以通过在终端上运行 docker images
来验证这一点,这将产生与以下屏幕截图非常相似的输出:
这是一个空列表。本地计算机中没有存储图像。我们需要做的第一件事是搜索图像。在这种情况下,我们将使用 CentOS 作为我们创建图像的基础。 CentOS 与 Red Hat Enterprise Linux 非常接近,后者似乎是业界最扩展的 Linux 发行版之一。他们提供了强大的支持,并且 Internet 上有大量信息可用于解决问题。
让我们搜索一个 CentOS 镜像,如下所示:
如您所见, 有很多基于 CentOS 的图像,但只有第一个是官方的。
此图像列表来自 Docker 中称为 Registry 的 世界。 Docker Registry 是一个简单的图像存储库,可供公众使用。您还可以运行自己的注册表,以防止您的图像进入通用注册表。
在前面的屏幕截图中的表格中有一个列应该立即引起您的注意, 列称为 明星。此列表示用户对给定图像的评分。我们可以使用 -s
标志,根据用户给图像的星数缩小搜索范围。
如果您运行以下命令,您将看到评分为 1000 星或更多星的图像列表:
为了将 CentOS 映像提取到本地机器,我们需要运行以下命令:
命令完成后,如果我们再次运行 Docker 映像,我们可以看到 centos 现在出现在以下列表中:
正如我们之前指定的,Docker 不使用完整的镜像,而是使用它的简化版本,只虚拟化操作系统的最后几层。你可以清楚地看到它,因为图像的大小甚至不到 200 MB,对于完整版的 CentOS,最多可以达到几 GB。
这将产生以下输出:
可以看到,终端的提示变成了类似root@debd09c7aa3b
的东西,表示我们在容器里面。
从现在开始,我们运行的每一个命令都将在一个包含的 CentOS Linux 版本。
Docker 中还有另一个有趣的命令:
如果我们在新终端中运行此命令(不退出正在运行的容器),我们将得到以下输出:
这个输出是不言自明的;这是查看 Docker 容器中发生的情况的一种简单方法。
在 Docker 世界中,镜像是给定容器的配置。我们可以将图像用作 模板来运行我们想要的任意数量的容器,但是首先,我们需要保存在上一节中所做的更改。
如果您是软件开发人员,您可能熟悉 CVS、Subversion 或 Git 等控制版本系统。 Docker 的构建考虑了他们的理念——可以将容器视为可版本化的软件组件,然后可以提交更改。
为此,请运行以下命令:
此命令将显示过去运行的容器列表,如下图所示:
在我的情况下,容器很少,但在这种情况下有趣的是第二个;这是 Node.js 的安装位置。
现在,我们需要提交容器的状态,以便使用我们的更改创建一个新图像。我们通过运行以下命令来做到这一点:
让我们解释一下命令:
-a
标志表示作者。在这种情况下,dgonzalez
。下面的参数是
container id
。如前所述,第二个容器具有相应的 ID62e7336a4627
。第三个参数是赋予新图像的名称和图像标签的组合。当我们处理大量图像时,标记系统可能非常强大,因为识别它们之间的微小变化会变得非常复杂。
这可能需要几秒钟,但完成后,命令的输出必须与下图非常相似:
这表明我们的列表中有一个安装了软件的新图像。再次运行docker images
,输出会确认,如下图所示:
为了运行基于新镜像的容器,我们可以运行以下命令:
这将 让我们能够访问容器中的 shell,我们可以通过运行 node 来确认已安装 Node.js -v
,应该输出 Node 的版本,在本例中为 4.2.4。
现在,是时候在容器中部署 Node.js 应用程序了。为了做到这一点,我们要需要从我们本地的中暴露代码a>machine 到 Docker 容器。
正确的做法是在 Docker 机器上挂载一个本地文件夹,但首先,我们需要创建要在容器内运行的小应用程序,如下所示:
这是一个简单的应用程序,使用 Express 基本上将 Hello Earth!
呈现到浏览器中。如果我们从终端运行它并访问 http://localhost:80/hello
,我们可以看到结果。
现在,我们将在容器内运行它。为了做到这一点,我们将在 Docker 容器中挂载一个本地文件夹作为卷并运行它。
Docker 来自系统管理员和开发人员的经验,他们最近融入了一个叫做 DevOps 的角色,这个角色介于两者之间。在 Docker 之前,每家公司都有自己部署应用程序和管理配置的方式,因此对于如何以正确的方式做事没有达成共识。
现在有了 Docker,这些公司就有了一种方法来提供统一的部署。无论您的业务是什么,一切都简化为构建容器、部署应用程序并在适当的机器上运行容器。
假设应用程序位于 /apps/test/
文件夹中。现在,为了将它暴露给容器,我们运行以下命令:
如您所见, 看到,Docker 可以通过 参数变得非常冗长,但让我们解释一下他们,如下:
如果一切正常,我们应该可以访问容器内的系统提示符,如下图所示:
如图所示,有一个名为 /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 终端启动时显示,如下图所示:
可以看到,IP是192.168.99.100
,所以为了访问我们的应用,我们需要访问http://192.168 .99.100:9000/
网址。
如果您记得,在前面的章节中,一个最重要的概念是自动化。使用微服务时,自动化是关键。您可能需要操作几十台服务器,而不是操作一台服务器,以至于您的日常任务几乎都被预订满了。
Docker 设计者在允许用户从一个名为 Dockerfile
的文件中编写的脚本创建容器时考虑到了这一点。
如果您曾经从事过 C 或 C++ 编码工作,即使在大学期间,您也可能对 Makefiles
很熟悉。 Makefile
文件是一个脚本,开发人员在其中指定自动构建软件组件的步骤。这是的一个例子:
前面的 Makefile
包含要执行的任务和依赖项的列表。例如,如果我们在包含 Makefile
文件的同一文件夹中执行 make clean
,它将删除可执行文件和所有以 o
结尾的文件。
Dockerfile
与 Makefile
不同,它不是任务和依赖项的列表(尽管概念相同),它是一个从头开始构建容器到就绪状态的指令列表。
让我们看一个例子:
前面这几行代码足以构建一个安装了 Node.js 的容器。
让我们在下面解释它:
首先,我们选择基础镜像。在这种情况下,它是我们之前使用的
centos
。为此,我们使用FROM
命令,然后使用图像的名称。MAINTAINER
指定创建容器的人的姓名。在这种情况下,它是David Gonzalez
。RUN
,顾名思义,运行一个命令。在这种情况下,我们使用了两次:一次将存储库添加到yum
,然后安装 Node.js。
Dockerfile 可以 包含许多不同的命令。他们的 文档非常清楚,但让我们看一下最常见的(除了之前看到的):
如您所见, 域特定语言(Dockerfiles 的 ="strong">DSL) 非常丰富,您几乎可以构建所需的每个系统。 Internet 上有数百个示例可以构建几乎所有内容:MySQL、MongoDB、Apache 服务器等等。
强烈建议通过Dockerfiles
来创建容器,因为它可以作为脚本在未来复制和更改容器,并且可以自动部署我们的软件无需太多人工干预。
我们都知道 Node.js 是一个以单线程方式运行应用程序的平台;那么,为什么我们不使用多线程来运行应用程序,这样我们才能获得多核处理器的好处呢?
Node.js 建立在一个名为 libuv 的库之上。这个库抽象了系统调用,为使用它的程序提供了一个异步接口。
我来自 非常重的 Java 背景,并且在那里,一切都是 同步的(除非您使用某种非阻塞库进行编码),并且如果您向数据库发出请求,一旦数据库回复数据,线程就会被阻塞并恢复。
这通常工作得很好,但它提出了一个有趣的问题:阻塞的线程正在消耗可用于服务其他请求的资源。 Node.js的事件循环如下图所示:
默认情况下,JavaScript 是一种事件驱动的语言。它执行配置事件处理程序列表的程序,这些事件处理程序将对给定事件做出反应,之后,它只是等待动作发生。
让我们看一个非常熟悉的例子:
那么JavaScript代码如下:
如您所见,这是一个非常简单的示例。显示按钮和 JavaScript 代码片段的 HTML,使用 JQuery,在单击按钮时显示警告框。
这是关键:当按钮被点击时。
点击一个按钮是一个事件,通过事件循环处理该事件JavaScript 使用 JavaScript 中指定的处理程序。
归根结底,我们只有一个线程执行事件,我们从不谈论 JavaScript 中的并行性,正确的词是并发。因此,更简洁地说,我们可以说 Node.js 程序是高度并发的。
您的应用程序将始终只在一个线程中执行,我们在编码时需要牢记这一点。
如果您一直在使用 Java 或 .NET 或使用线程阻塞技术设计和实现的任何其他语言/框架,您可能已经观察到 Tomcat 在运行应用程序时会产生许多线程来侦听请求。
在 Java 世界中,这些线程中的每一个都是,称为 workers,并且他们负责从头到尾处理来自给定用户的请求。 Java 中有一种类型的数据结构可以利用它。它被称为 ThreadLocal 并将数据存储在本地线程中,因此以便以后可以恢复。这种类型的存储是可能的,因为启动请求的线程也负责完成它,如果线程正在执行任何阻塞操作(例如读取文件或访问数据库),它会一直等待直到完成。
这通常没什么大不了的,但是当您的软件严重依赖 I/O 时,问题可能会变得很严重。
另一个支持 Node.js 非阻塞模型的要点是缺少上下文切换。
当 CPU 将一个线程切换到另一个线程时,寄存器和内存的其他区域中的所有数据都被堆叠起来,并允许 CPU 与一个新进程切换上下文,该新进程有自己的数据要放入其中在那里,如下图所示:
此操作 需要时间,而此时间未被应用程序使用。它只是迷路了。在 Node.js 中,您的应用程序仅在一个线程中运行,因此在运行时没有这种上下文切换(它仍然存在于后台,但对您的程序隐藏)。在下图中,我们可以看到当 CPU 切换线程时在现实世界中会发生什么:
至此,您知道 Node.js 应用程序是如何工作的,当然,有些读者可能会有一个 问题,即如果应用程序在单线程上运行,那么现代多核处理器会发生什么?
在回答这个问题之前,我们先来看看下面的场景。
在我上高中的时候,CPU 发生了一次重大的技术飞跃:分割。
这是在指令级引入并行性的第一次尝试。您可能知道,CPU 解释汇编指令,每条指令都由多个阶段组成,如下图所示:
在 Intel 4x86之前,CPU每次执行一条指令,所以从上图中的指令模型来看,任何CPU每次只能执行一条指令六个 CPU 周期。
然后,细分开始发挥作用。通过一组中间寄存器,CPU 工程师设法并行化指令的各个阶段,以便在最佳情况下,CPU 能够每个周期(或几乎)执行一条指令,如下图所示:
这种技术改进带来了更快的 CPU,并为原生硬件多线程打开了大门,从而催生了可以执行大量 并行处理的现代 n 核处理器任务,但是当我们运行 Node.js 应用程序时,我们只使用一个内核。
如果我们不集群我们的应用程序,与其他利用 CPU 多核的平台相比,我们的性能将会严重下降。
然而,这一次我们很幸运,PM2 已经允许您集群 Node.js 应用程序以最大限度地利用您的 CPU。
此外,PM2 的重要方面之一是它允许您在不停机的情况下扩展应用程序。
让我们以集群模式运行一个简单的应用程序:
这次我们使用 Node.js 的原生 HTTP 库来处理传入的 HTTP 请求。
现在我们可以从终端运行应用程序,看看它是如何工作的:
虽然它不输出任何内容,但我们可以 curl 到 http://localhost:3000/
URL 以查看服务器如何响应,如下图所示:
如您所见, 看到,Node.js 管理了所有 HTTP 协商,并且还设法使用 进行回复,我们在这里are!
短语,因为它是在代码中指定的。
这个服务很琐碎,但它是更复杂的Web服务工作的原理,所以我们需要对Web服务进行集群以避免瓶颈。
Node.js 有一个名为 cluster
的库,它允许我们以编程方式对应用程序进行集群,如下所示:
就个人而言,我 发现使用 PM2 等特定软件来实现有效的集群要容易得多,因为在尝试处理集群实例时代码会变得非常复杂我们的应用程序。
鉴于此,我们可以通过 PM2 运行应用程序,如下所示:
PM2 中的 -i
标志,如您在命令的输出中所见,用于指定我们想要用于我们的应用程序的内核数量。
如果我们运行 pstree
,我们可以看到系统中的进程树,并检查 PM2 是否只为我们的应用程序运行一个进程,如下图所示:
在这种情况下,我们只在一个进程中运行应用程序,因此它将被分配在 CPU 的一个内核中。
在这种情况下,我们 没有利用运行应用程序的 CPU 的多核功能,但我们仍然可以从自动重启应用程序中获益如果我们的算法出现一个异常。
现在,我们将使用 CPU 中的所有可用内核运行我们的应用程序,以便最大限度地利用它,但首先,我们需要停止集群:
现在,我们可以使用 CPU 的所有内核重新运行应用程序:
PM2 已经 设法猜测了我们计算机中的 CPU 数量,在我的例子中,这是一个具有四个内核的 iMac,如下图所示:
在pstree
中可以看到,PM2在OS层面启动了四个线程,如下图所示:
在集群应用程序时,关于应用程序应该使用的内核数量有一个不成文的规则,这个数字是内核数量减一。
这个数字背后的原因 是因为操作系统需要一些 CPU 能力,因此如果我们在应用程序中使用所有 CPU,一旦操作系统开始运行对于其他一些任务,它将强制进行上下文切换,因为所有内核都会很忙,这会减慢应用程序的速度。
有时,集群我们的应用程序是不够的,我们需要水平扩展我们的应用程序.
有多种方法可以水平扩展应用程序。如今,借助亚马逊等云提供商,每个提供商都实施了自己的解决方案,并具有许多功能。
我首选的实现负载平衡的方法之一是使用 NGINX。
NGINX 是一个 网络服务器,重点关注并发性和低内存使用.它也非常适合 Node.js 应用程序,因为非常不鼓励从 Node.js 应用程序中提供静态资源。主要原因是避免应用程序由于可以使用其他软件(例如 NGINX(这是专业化的另一个示例))更好地完成的任务而承受压力。
但是,让我们关注负载平衡。下图展示了 NGINX 如何作为负载均衡器工作:
正如您在上图中 看到的,我们有两个 PM2 集群负载均衡通过 NGINX 的一个实例。
我们需要做的第一件事是了解 NGINX 如何管理配置。
在 Linux 上,NGINX 可以通过 yum
、apt-get
或任何其他包管理器安装。它也可以从源代码构建,但推荐的方法是使用包管理器,除非您有非常具体的要求。
默认情况下,主配置文件为/etc/nginx/nginx.conf
,为 如下:
这个文件非常简单,它指定了工作人员的数量(记住,服务请求的进程)、错误日志的位置、工作人员当时可以激活的连接数,最后是 HTTP 配置。
最后一行是最有趣的:我们通知 NGINX 使用 /etc/nginx/sites-enabled/*.conf
作为潜在的配置文件。
使用此 配置,.conf 结尾的文件indexterm"> 指定的文件夹将成为 NGINX 配置的一部分。
如您所见,那里已经存在一个默认文件。将其修改为如下所示:
这是我们构建负载均衡器所需的所有配置。让我们在下面解释它:
upstream app
指令正在创建一组名为app
的服务。在该指令中,我们指定了两个服务器,如上图所示。server
指令向 NGINX 指定它应该监听来自端口80
的所有请求并将它们传递给上游组称为app
。
现在,NGINX 如何决定将请求发送到哪台计算机?
在这种情况下,我们可以指定用于分散负载的策略。
默认情况下,NGINX 在没有专门配置平衡方式时,使用 Round Robin 。
要记住的一件事是,如果我们使用循环,我们的应用程序应该是无状态的,因为我们不会总是在同一台机器上运行,所以如果我们将状态保存在服务器中,它可能不会出现在以下调用中.
轮询是将负载从工作队列分配给多个工作人员的最基本方法;它会旋转它们,以便每个节点都获得 相同数量的请求。
还有其他机制可以分散负载,如下所示:
Least connected,正如其 名称所示,将请求发送到最少连接的节点,在所有节点之间平均分配负载:
IP hashing 是一种有趣的负载分配方式。如果您曾经使用过任何 Web 应用程序,那么几乎所有应用程序中都会出现会话的概念。为了记住用户是谁,浏览器向服务器发送一个 cookie,服务器在内存中存储了用户的身份以及他/她需要/可以被该给定用户访问的数据。另一种类型的负载平衡的问题是我们不能保证总是访问同一台服务器。
对于 示例,如果我们使用 Least connected 作为平衡策略,我们可能会在第一次加载时点击 服务器,但随后在后续重定向中点击不同的服务器,这将导致用户无法显示正确的信息第二台服务器不知道用户是谁。
使用 IP 散列,负载均衡器将为给定 IP 计算散列。这个哈希会以某种方式产生一个从 1 到 N 的数字,其中 N< /span> 是服务器的数量,然后,只要他们保持相同的 IP,用户将始终被重定向到同一台机器。
我们还可以对负载均衡应用权重,如下所示:
这将以这样的方式分配负载,对于每六个请求,五个将被定向到第一台机器,一个将被定向到第二台机器。
一旦我们选择了首选的负载平衡方法,我们可以重新启动 NGINX 以使更改生效,但首先,我们要验证它们,如下图所示:
如您所见, 可以看到,配置测试真的很有帮助,可以避免配置 灾难。
一旦 NGINX 通过 configtest
,保证 NGINX 能够restart/start/reload
没有任何语法问题,如如下:
重新加载将优雅地等待旧线程完成,然后重新加载配置并使用新配置路由新请求。
如果你有兴趣了解 NGINX,我发现以下 NGINX 的官方文档 很有帮助:
健康检查是负载均衡器上的一项重要活动。如果其中一个节点出现严重的硬件故障并且无法处理 更多请求,会发生什么情况?
在这种情况下,NGINX 带有两种类型的健康检查:passive 和 active 。