读书笔记《developing-microservices-with-node-js》在Node.js中编写您的第一个微服务
它涵盖以下内容:
编写微服务
调整微服务规模
创建 API
将 Seneca 与 Express 集成
使用 Seneca 存储数据
在本章中,我们将编写一个基于微服务的完整(几乎)简单的电子商务解决方案。从概念的角度来看,完整意味着完整,但由于显而易见的原因,它不会是完整的(作为生产准备),因为我们可能需要几本书来处理所有可能的流程。
我们不会深入到 UI,因为它与本书的主题无关。我们要做的是一个微服务,它将聚合所有其他微服务,创建一个供 使用的前端 API 单页应用程序 (SPA),使用任何现代 JavaScript 框架构建。
在本章中,我们将开发以下四个微服务:
构建前面的四个微服务,我们将开发前面章节中讨论的概念,以便在本章结束时,我们将能够识别出最常见的陷阱。请记住,本书的目标不是让您成为微服务或 Node.js 专家,而是为您提供自学所需的工具,并让您了解最佳设计原则和最常见的陷阱.
我们看一下部署图:
此图显示了我们公司(黄色方块)如何从现实世界中隐藏我们的一些微服务,并将其他一些微服务暴露给不同的网络,如下所示:
Tip
如您所见,没有用户或人员管理,但是有了这四个微服务,我们将能够开发微服务架构的核心概念。 Seneca 带有一个非常强大的数据和传输插件系统,可以轻松地将 Seneca 与不同的数据存储和传输系统一起使用。
对于我们所有的微服务,我们将使用 MongoDB 作为存储。 Seneca 带有一个开箱即用的内存数据库插件,可让您立即开始编码,但存储是暂时的:它不会在调用之间保留数据。
产品经理是我们系统的核心。我知道你在想什么:微服务应该是小(micro)和分布式(没有中心点),但是你需要在某个地方设置概念中心,否则你最终会导致系统碎片化和可追溯性问题(我们会谈论它)之后)。
使用 Seneca 构建双 API 相当容易,因为它与 Express 的集成非常简单。 Express 将用于暴露 UI 的一些功能,例如编辑产品、添加产品、删除产品等。它是一个非常方便的框架,易于学习,并且与 Seneca 集成得很好。它也是用于 web 应用程序的 Node.js 的事实上的标准,因此可以轻松找到有关可能问题的信息。
它还将通过 Seneca TCP(Seneca 中的默认插件)公开一个私有部分,以便我们的内部微服务网络(特别是 UI)能够访问我们目录中的产品列表。
产品经理将变得小而有凝聚力(它只会管理产品),并且具有可扩展性,但它将拥有在我们的电子商务中处理产品所需的所有知识。
我们需要做的第一件事是定义我们的 Product Manager 微服务,如下所示:
我们的产品将是一个具有四个字段的数据结构:name、category 、说明和价格。正如你所看到的,它有点简单,但它会帮助我们理解微服务的复杂世界。
我们的产品管理微服务将使用 MongoDB (https://www.mongodb.org/)。 Mongo 是一个面向文档的无模式数据库,它允许极大的灵活性来存储诸如产品之类的数据(归根结底,就是文档)。它也是 Node.js 的一个不错的选择,因为它存储 JSON 对象,这是为 JavaScript ( JSON 代表 JavaScript Object Notation),因此看起来是完美的配对。
如果您想了解更多信息,MongoDB 网站上有很多有用的信息。
让我们开始编写我们的函数。
为了获取产品,我们进入数据库并将完整的产品列表直接转储到界面中。在这种情况下,我们不会创建任何分页机制,但总的来说,对数据进行分页是避免数据库(或应用程序,但主要是数据库)性能问题的好习惯。
让我们看看下面的代码:
我们已经在 Seneca 中有一个模式,它返回我们数据库中的所有数据。
products.list$()
函数将接收以下两个参数:
查询条件
接收错误和结果对象的函数(记住错误优先回调方法)
Seneca 使用 $
符号来识别 list$
、save$< 等关键功能/code> 等等。关于对象属性的命名,只要您使用字母数字标识符,您的命名 就不会发生冲突。
我们将 done
函数从 seneca.add()
方法传递给 list$
方法。这是因为 Seneca 遵循错误优先的回调方法。换句话说,我们正在为以下代码创建一个快捷方式:
按类别获取与获取完整产品列表非常相似。唯一的区别是现在 Seneca 操作将采用一个参数来按类别过滤产品。
让我们看看代码:
大多数高级开发人员现在首先想到的问题之一是 这不是注入攻击的完美场景吗? Seneca 足够聪明来防止它,所以我们不需要担心它,只需避免将字符串与用户输入连接。
如您所见,唯一显着的区别是传递的名为 category
的参数,它被委托给 Seneca 数据抽象层,该层将根据我们使用的存储生成适当的查询。在谈论微服务时,这非常强大。如果你还记得,在前面的章节中,我们总是把耦合看作是万恶之源,现在我们可以确定它是,并且Seneca 以一种非常优雅的方式处理它。在这种情况下,框架提供了不同存储插件必须满足才能工作的契约。在前面的示例中,list$
是该合约的一部分。如果您明智地使用 Seneca 存储,则将您的微服务切换到新的数据库引擎(您是否曾经试图将部分数据移动到 MongoDB 上?)是一个配置问题。
通过ID获取商品是最必要的方法之一,也是比较棘手的方法。从编码的角度来看并不棘手,如下所示:
棘手的部分是如何生成 id
。 id
的生成是与数据库的接触点之一。 Mongo 创建一个哈希来表示一个合成 ID;然而,MySQL 通常会创建一个自动递增的整数来唯一标识每条记录。鉴于此,如果我们想在我们的一个应用程序中将 MongoDB 切换到 MySQL,我们需要解决的第一个问题是如何将类似于以下内容的哈希映射到序数:
e777d434a849760a1303b7f9f989e33a
在 99% 的情况下,这没问题,但我们需要小心,尤其是在存储 ID 时,如果您回想一下前面的章节,数据应该是每个微服务的本地数据,这可能意味着更改数据类型一个实体的 ID,需要更改所有其他数据库中的引用 ID。
添加产品是微不足道的。我们只需要创建数据并将其保存在数据库中:
在这个方法中,我们使用了一个来自 Seneca 的助手 products.data$(false)
。当数据返回给调用方法时,这个助手将允许我们检索实体的数据,而不需要关于命名空间(区域)、实体名称和基本名称的所有元数据。
产品 的删除通常由 id
完成:我们针对要删除的特定数据通过主键,然后将其删除,如下所示:
在这种情况下,如果出现问题,我们不会返回除错误之外的任何内容,因此调用此操作的端点可以假定没有错误的响应是成功的。
这是一个有趣的场景。在编辑产品之前,我们需要通过 ID 获取它,我们已经这样做了。因此,我们在这里所做的是依靠已经存在的操作来通过 ID 检索产品、复制数据并保存它。
这是 Seneca 引入的代码重用的好方法,您可以将调用从一个操作委托给另一个操作,并在包装器操作中使用结果。
正如我们之前商定的那样,产品经理将有两个面孔:一个将通过 TCP 使用 Seneca 传输暴露给其他微服务,另一个将暴露给其他微服务以 REST 方式通过 Express(用于创建 Web 应用程序的 Node.js 库)公开。
让我们将所有东西连接在一起:
现在让我们解释一下代码:
我们创建了一个 Seneca 插件。这个插件可以在不同的微服务中重用。该插件包含我们之前描述的微服务所需的所有方法定义。
上述代码描述了以下两个部分:
前几行 连接到 Mongo。在这种情况下,我们指定 Mongo 是一个本地数据库。我们通过使用名为 mongo-store 的插件 —https://github.com/rjrodger/seneca-mongo-store,由 Seneca 的作者 Richard Rodger 编写。
第二部分对我们来说是新的。如果您以前使用过 JQuery,这听起来可能很熟悉,但基本上
seneca.ready()
回调所做的是处理 Seneca 之前可能没有连接到 Mongo 的事实调用开始流入其 API。seneca.ready()
回调是用于将 Express 与 Seneca 集成的代码所在的位置。
以下是我们应用的 package.json
配置:
正如我们在上一节中看到的那样,此代码片段提供了以下三个 REST 端点:
/products/fetch
/products/edit
/products/delete
让我们解释一下。
首先,我们要做的是告诉 Seneca 执行 role:web
动作,指示配置。此配置指定为所有 URL 使用 /products
前缀,并使用匹配的 {area: "product", action : "*"}
模式。这对我们来说也是新的,但它是一种向 Seneca 指定它在 URL 中执行的任何操作的好方法,它将具有处理程序的隐式 area: "product"
.这意味着 /products/fetch
端点将对应于 {area: 'products', action: 'fetch'}
模式.这可能有点困难,但一旦你习惯了它,它实际上真的很强大。它不会强制 use
按照惯例将我们的操作与我们的 URL 完全耦合。
在配置中, 属性映射指定了可以通过端点执行的 HTTP 操作:fetch 将允许 GET
,edit 将允许
PUT
,而 delete 将只允许 DELETE
。这样,我们可以控制应用程序的语义。
其他所有内容您可能很熟悉。创建一个 Express 应用程序并指定使用以下两个插件:
JSON 正文解析器
塞内卡网络插件
这是所有的了。现在,如果我们向 Seneca 操作列表添加一个新操作以便通过 API 公开它,那么唯一需要做的就是修改 map 属性以允许 HTTP 方法。
尽管我们构建了一个非常简单的微服务,但它捕获了您在创建 CRUD (创建读取更新删除)应用程序。我们还毫不费力地从 Seneca 应用程序中创建了一个小型 REST API。我们现在需要做的就是配置基础设施(MongoDB),然后我们就可以部署我们的微服务了。
电子邮件是每个公司都需要做的事情。我们需要与客户沟通,以便发送通知、账单或注册电子邮件。
在我以前工作过的公司中,电子邮件总是会出现诸如电子邮件未发送或发送两次、将错误的内容发送给错误的客户等问题。看起来很可怕,像发送电子邮件这样简单的事情可能会变得如此复杂。
一般来说,电子邮件通信是编写微服务的首选。想一想:
电子邮件只做一件事
电子邮件做得很好
电子邮件保留自己的数据
这也是康威定律如何在不被注意的情况下进入我们的系统的一个很好的例子。我们设计我们的系统来模拟我们公司的现有通信,因为我们受到的约束。
回到基础。我们如何发送电子邮件?我不是在谈论我们用于发送电子邮件的网络协议或可接受的最小标头是多少?
我说的是从业务的角度来看我们需要发送电子邮件:
标题
内容
目标地址
这就是一切。我们本可以走得更远,谈论确认、安全电子邮件、密件抄送等。但是,我们遵循精益方法:从最小的可行产品开始,并以此为基础进行构建,直到达到预期的结果。
我不记得有一个项目,其中电子邮件发送不是一个有争议的部分。选择用于发送电子邮件的产品最终与系统紧密耦合,并且很难无缝替换它。但是,微服务可以拯救我们。
正如我之前提到的,虽然这听起来很容易,但发送公司电子邮件最终可能会变得一团糟。因此,我们首先需要明确的是我们的最低要求:
我们如何呈现电子邮件?
呈现电子邮件是否属于电子邮件操作的绑定上下文?
我们是否创建另一个微服务来呈现电子邮件?
我们是否使用第三方来管理电子邮件?
我们是否出于审核目的存储已发送的电子邮件?
对于这个微服务,我们将使用 Mandrill。 Mandrill 是一家允许我们发送公司电子邮件、跟踪已发送电子邮件并创建可在线编辑的电子邮件模板的公司。
我们的微服务将如下面的代码所示:
我们有两种模式:一种使用模板,另一种发送请求中包含的内容。
如您所见,我们在此处定义的所有内容都是与电子邮件相关的信息。 Mandrill 的术语并没有渗入其他微服务在我们的电子邮件发送中看到的内容。我们正在做的唯一妥协是模板。我们将模板 渲染委托给电子邮件发件人,但这没什么大不了的,因为即使我们离开 Mandrill,我们也需要以某种方式呈现内容。
我们稍后会回到代码。
Mandrill 相当易于使用,设置起来应该不成问题。但是,我们将使用测试模式,以便我们可以确保电子邮件不会发送,并且我们可以访问 API我们所有的需求。
我们需要做的第一件事是在 Mandrill 上创建一个帐户。只需在 https://mandrillapp.com 使用您的电子邮件进行注册,您应该能够访问它,如以下屏幕截图所示:
现在我们已经创建了一个需要进入测试模式的帐户。为此,只需单击右上角的电子邮件,然后从菜单中选择 打开测试模式 选项.左侧的 Mandrill 菜单现在将变为橙色。
接下来,我们需要创建一个 API 密钥。此键是 Mandrill API 使用的登录信息。只需点击 Settings 和 SMTP & API Info 并添加一个新密钥(不要忘记将密钥标记为测试密钥的复选框)。它现在应该类似于以下屏幕截图:
关键是你现在需要的一切。让我们测试一下 API:
通过这几行代码,我们成功地测试了 Mandrill 是否已启动并运行,并且我们有一个有效的密钥。该程序的输出应该是非常类似于以下JSON的:
现在一切都准备好了。我们有一个工作密钥和我们的界面。剩下的唯一事情就是创建代码。我们将使用 Mandrill API 的一小部分,但如果你想制作 < /a>其他功能的使用,你可以在这里找到更好的描述:https://mandrillapp .com/api/docs/
我们来看看下面的代码:
第一种方法在不使用模板的情况下发送消息。我们只是从我们的应用程序中获取 HTML 内容(和一些其他参数)并通过 Mandrill 传递它。
如您所见,我们与外部世界只有两个接触点:传入的参数和我们操作的返回。他们俩都有明确的合同,与Mandrill无关,但是数据呢?
在错误中,我们返回 e.name
,假设它是一个代码。在某些时候,有人最终会根据这个错误代码来分支流程。在这里,我们有一种叫做数据耦合的东西;我们的软件组件不依赖于合同,但它们确实依赖于发送的内容。
现在,问题是:我们如何解决它? 我们不能。至少不是以一种简单的方式。我们需要假设我们的微服务并不完美,它有缺陷。如果我们为电子邮件切换提供商,我们将需要重新访问调用代码以检查潜在的耦合。
在软件的世界里,在我之前从事的每一个项目中,总是有很大的推动力,试图使代码尽可能通用,试图猜测未来,这通常就像假设你的微服务不会是完美的。有一点总是引起我的注意:我们为完美付出了巨大的努力,但我们几乎忽略了我们将失败的事实,我们对此无能为力。软件经常失败,我们需要为此做好准备。
稍后,我们将看到一种将人性因素纳入微服务的模式:断路器。
如果 Mandrill 因 unsigned 原因拒绝电子邮件,请不要感到惊讶。这是因为他们无法验证我们发送电子邮件的域(在这种情况下,一个不存在的虚拟域)。如果我们希望 Mandrill 实际处理电子邮件(即使我们处于测试模式),我们只需要通过添加一些 配置来验证我们的域它。
发送电子邮件的第二种方法是从模板发送电子邮件。在这种情况下,Mandrill 提供了一个灵活的 API:
为方便起见,由于本书篇幅有限,我们将只使用全局变量。
我们来看看下面的代码:
现在我们可以在 Mandrill 中创建我们的模板(并让其他人来管理它们)并且我们可以使用它们来发送电子邮件。再次,我们是专业的。我们的系统专门用于发送电子邮件,您将电子邮件的创建留给其他人(可能是营销团队中知道如何与客户交谈的人)。
数据存储在本地:不是真的(它存储在 Mandrill 中),但从设计的角度来看,它是
我们的微服务内聚性很好:它只发送电子邮件;它只做一件事,而且做得很好
微服务大小是否正确:几分钟就能看懂,没有不必要的抽象,改写还算方便
当我们前面谈到 SOLID 设计原则时,我们总是跳过 L,它代表 Liskov Substitution。基本上,这意味着软件必须在语义上正确。对于 示例,如果我们编写一个处理一个抽象类的面向对象程序,则该程序必须能够处理所有子类。
回到 Node.js,如果我们的服务能够处理发送普通电子邮件,那么在不修改现有功能的情况下应该很容易扩展和添加功能。
从日常生产运营的角度来思考;如果向您的系统添加了新功能,您最不想做的就是重新测试现有功能,或者更糟糕的是,将该功能交付到生产环境中,从而引入一个没人知道的错误。
让我们创建一个用例。我们想将同一封电子邮件发送给两个收件人。尽管 Mandrill API 允许调用代码执行此操作,但我们并未考虑潜在的 CC。
因此,我们将在 Seneca 中添加一个允许我们执行此操作的新操作,如下所示:
我们已指示 Seneca 接受在参数列表中包含 cc
的调用,并在发送 API 中使用 Mandrill CC 发送它们。如果我们想使用它,调用代码的以下签名将会改变:
签名将更改为以下代码:
如果你没记错的话,模式匹配会尝试匹配最具体的输入,这样如果一个动作匹配的参数多于另一个,调用将被定向到它。
这就是 Seneca 的亮点:我们可以称之为 动作的多态性,因为我们可以用不同的参数定义相同操作的不同版本,最终会做些稍微不同的事情,如果我们 100% 确定这是正确的做法,我们就可以重新利用代码(请记住,微服务强制执行无共享方法:重复代码可能不如耦合两个动作那么糟糕)。
这是电子邮件微服务的 package.json
:
当你设计一个系统时,我们通常会考虑现有组件的可替换性;例如,在 Java 中使用持久化技术时,我们倾向于倾向于标准(JPA),这样我们就可以在不太多的情况下替换底层实现努力。
微服务采用相同的方法,但它们隔离了问题,而不是努力实现简单的可替换性。如果您阅读前面的代码,在 Seneca 操作中,我们没有做任何事情来隐藏我们使用 Mandrill 发送电子邮件的事实。
正如我之前提到的,电子邮件虽然看起来很简单,但最终总会带来问题。
想象一下,我们想用一个普通的 SMTP 服务器(例如 Gmail)替换 Mandrill。我们不需要做任何特别的事情,我们只需更改实现并推出我们微服务的新版本。
该过程就像应用以下代码一样简单:
对于外部世界,我们最简单的电子邮件发件人版本现在是通过 Gmail 使用 SMTP 来传递我们的电子邮件。
正如我们将在本书后面看到的那样,在微服务网络中交付相同接口的新版本相当容易。只要我们尊重接口,实现应该是无关紧要的。
我们甚至可以使用这个新版本推出一台服务器并向其发送一些流量,以便在不影响所有客户的情况下验证我们的实施(换句话说,包含故障)。
我们已经在本节中了解了如何编写电子邮件发件人。我们已经研究了一些示例,说明一旦业务需要新功能或者我们认为我们的供应商不足以满足我们的技术要求,我们的微服务如何能够快速适应新需求。
订单管理器是一个 微服务,用于处理客户通过 UI 下达的订单。您可能还记得,我们不会使用现代可视化框架创建复杂的单页应用程序,因为它超出了本书的范围,但我们将提供 JSON 接口以便能够构建前端稍后。
订单管理器引入了一个有趣的问题:这个微服务需要访问有关产品的信息,例如名称、价格、可用性等。但是,它存储在产品经理微服务中,那么我们该怎么做呢?
恢复订单
创建订单
删除现有订单
恢复订单时,选项将很简单。通过主键恢复订单。我们可以扩展它以通过不同的标准恢复订单,例如价格、日期等,但我们将保持简单,因为我们希望专注于微服务。
删除现有订单时,选项也很明确:使用 ID 删除订单。同样,我们可以选择更高级的删除标准,但我们希望保持简单。
当我们试图创建订单时,问题就出现了。在我们的小型微服务架构中创建订单意味着向客户发送一封电子邮件,指定我们正在处理他们的订单以及订单的详细信息,如下所示:
产品数量
每个产品的价格
总价
订单 ID(以防客户需要解决订单问题)
我们如何恢复产品详细信息?
如果您在本章的 Micromerce – the big picture 部分看到我们的图表,订单管理器将仅从 UI 调用,这将是负责恢复产品名称、价格等。我们可以在这里采用以下两种策略:
订单经理致电产品经理并获取详细信息
UI 调用产品经理并将数据委托给订单经理
这两个选项都是完全有效的,但在这种情况下,我们选择第二种:UI 将收集生成订单所需的信息,并且只有在所需的所有数据都可用时才会调用订单管理器。
现在回答这个问题:为什么?
一个简单的原因:容错。我们来看看下面这两个选项的时序图:
在第一个视图中,有一个很大的区别:调用的深度;而在第一个示例中,我们有两个深度级别(UI 调用订单管理器,后者调用产品管理器)。在第二个示例中,我们只有一个深度级别。我们的架构有一些直接的影响,如下所示:
我们使用这种架构而不是两级深度这一事实并不意味着它不适合另一种情况:如果您正在创建面向微服务的架构,则需要提前计划网络拓扑,例如这是最难改变的方面之一。
在某些情况下,如果我们想要非常灵活,我们可以使用带有发布者/订阅者技术的消息队列,我们的微服务可以订阅不同类型的消息并发出 其他服务被不同的服务使用,但这可能会使我们需要部署的基础设施复杂化以避免单点故障。
如您所见,代码没有什么复杂的。唯一有趣的一点是 create 操作中缺少的代码。
到目前为止,我们假设我们所有的微服务都运行在同一台机器上,但这远非理想。在现实世界中,微服务是分布式的,我们需要使用某种传输协议将消息从一个服务传送到另一个服务。
Seneca 以及 Seneca 背后的公司 nearForm 已经为我们和围绕它的开源社区解决了这个问题。
作为模块化系统,Seneca 嵌入了插件的概念。默认情况下,Seneca 附带一个捆绑的 插件以使用 TCP 作为协议,但创建新的传输插件并不难。
Note
在写这本书时,我自己创建了一个:https://github。 com/dgonzalez/seneca-nservicebus-transport/
使用这个插件,我们可以通过 NServiceBus(一种基于 .NET 的企业总线)路由 Seneca 消息,从而改变我们的客户端和服务器的配置。
让我们看看如何配置 Seneca 以指向另一台机器:
默认情况下,Seneca 将使用默认插件进行传输,正如我们在 Chapter 2 中看到的,Node.js 中的微服务 – Seneca 和 PM2 替代方案,是 tcp
,我们已经指定它指向 8080
端口上的 class="literal">192.168.0.2 主机。
就这么简单,从现在开始,当我们在 senecaEmailer
上执行一个 act 命令时,transport 会将消息发送到 e-mailer 并接收响应。
让我们看看剩下的代码:
如您所见,我们正在接收包含所有所需数据的产品列表,并将它们传递给电子邮件程序以呈现电子邮件。
如果我们更改电子邮件程序所在的主机,我们需要在这里做的唯一更改是 senecaEmailer
变量的配置。
即使我们改变了 频道的性质(例如,我们甚至可以编写一个插件来通过 Twitter 发送数据),该插件也应该照顾它的特殊性并且对应用程序是透明的。
在前面 部分的示例中,我们构建了一个微服务,该微服务调用另一个微服务以解析它收到的调用。但是,需要牢记以下几点:
如果电子邮件程序关闭会发生什么?
如果配置错误并且电子邮件程序没有在正确的端口上工作,会发生什么情况?
我们可能会抛出 what ifs 几页。
人类是不完美的,他们构建的东西也是如此,软件也不例外。人类也不善于识别逻辑流中的潜在问题,软件往往是一个复杂的系统。
在其他语言中,处理异常几乎是正常的,但在 JavaScript 中,异常是一件大事:
如果在 Java 中的 Web 应用程序中出现异常,它会终止当前的调用堆栈,并且 Tomcat(或您使用的容器)向客户端返回错误
如果 Node.js 应用程序中出现异常,应用程序将被终止,因为我们只有一个线程执行应用程序
如您所见,Node.js 中几乎每个回调都有一个错误的第一个参数。
在谈论微服务时,这个错误尤其重要。你想要有弹性。电子邮件发送失败的事实并不意味着订单无法处理,但电子邮件可以稍后由重新处理数据的人手动发送。这就是我们所说的最终一致性;我们考虑到我们的系统在某些时候我们的系统会崩溃的事实。
在这种情况下,如果发送电子邮件有问题,但我们可以将订单存储在数据库中,调用代码(在这种情况下为 UI)应该有足够的信息来决定客户是否收到了致命消息或只是一个警告:
您的订单已准备好处理,但我们可能需要两天时间才能将包含订单详细信息的电子邮件发送给您。感谢您的耐心等待。
通常,即使我们无法完成请求,我们的应用程序也会继续工作,这通常更多的是业务而非技术决策。这是一个重要的细节,因为在构建微服务时,康威定律正在推动我们这些技术人员对现有业务流程和部分成功图进行建模完全符合人性。如果您无法完成任务,请在 Evernote(或类似工具)中创建提醒,并在阻止程序解决后返回。
这比以下内容好得多:
发生了一些事情,但我们不能告诉你更多(当我在某些网站上遇到普遍故障时,我有时会想到) .
我们将这种处理错误的方式称为系统退化:它可能不是 100% 正常工作,但即使它的少数功能不可用,它仍然可以工作,而不是一般故障。
如果您想一想,有多少次 Web 服务调用回滚了您的大公司系统中的完整事务,只是因为它无法访问甚至可能不重要的第三方服务?
在本节中,我们构建了一个微服务,它使用另一个微服务来解决来自客户的请求:订单经理使用电子邮件来完成请求。我们还谈到了弹性以及它在我们的架构中的重要性,以便提供最好的服务。
到目前为止,我们已经构建了独立的微服务。他们有一个特定的目的并处理我们系统的一个特定部分:电子邮件发送、产品管理和订单处理,但现在我们正在构建一个微服务,其唯一目的是促进微服务之间的通信。
现在我们将构建一个与他人交互的微服务,它是客户的正面。
当我在计划本章的内容时,并没有像这样的服务。然而,仔细想想,如果不展示一些关于 API 聚合的概念,这些概念在前端很容易展示,这一章就不一样了微服务。
考虑可扩展性。在处理 HTTP 流量时,有一个流量金字塔。前端的点击量比后端多。通常,为了到达后端,前端需要处理来自前端的以下几个请求:
阅读表格
验证它
可以看到,有很多逻辑需要前端去处理,所以软件忙的时候不难看出容量问题。如果我们正在使用微服务,并且我们以正确的方式使用它,那么扩大或缩小规模应该是一个自动过程,只需几次点击(或命令)即可触发。
到目前为止,我们几乎总是在单个服务器上测试代码。这对于测试来说很好,但是当我们构建微服务时,我们希望它们是分布式的。因此,为了实现它,我们需要向 Seneca 指明如何到达服务:
我们所做的是创建三个 Seneca 实例。它们就像服务器之间的通信管道。
让我们解释一下代码:
Seneca 默认使用传输插件 TCP。这意味着 Seneca 将监听服务器上的 /act
URL。例如,当我们创建 senecaEmailer
时,Seneca 将指向的 URL 是 http://192.168.0.2:8080/act< /代码>。
我们实际上可以用 curl 来验证它。如果我们执行以下命令行,将 <valid Seneca pattern>
替换为有效的 Seneca 命令,我们应该得到来自服务器的 JSON format,这将是动作的 done
函数中的第二个参数:
让我们看一个简单的例子:
如果我们运行这个程序,我们可以从终端看到以下输出:
这意味着 Seneca 正在监听端口 3000
。让我们测试一下:
这应该在终端中打印出与以下代码非常相似的内容:
前面的代码是我们的终端和 Seneca 服务器之间的 TCP/IP 对话,最后一行是响应的结果。
因此,我们之前在拥有三个不同的 Seneca 实例上所取得的成就是配置我们的微服务网络; Seneca 将为我们在网络上传输消息。
以下流程 图描述了单个 API 如何在后端使用不同的微服务(基本上是不同的 Seneca 实例)隐藏多个 Seneca 服务器:
现在,让我们看一下微服务的骨架:
我们实际上已经将调用其他微服务的 功能留待以后讨论。现在我们将重点关注代码的表达方式:
我们正在创建一个新插件。该插件名为
api
(包装插件的函数名称为api
)。该插件必须执行以下三个操作:
列出所有产品
通过 ID 获取产品
创建订单
这三个操作将调用两个不同的微服务:产品经理和订单经理。我们稍后会回到这个话题。
到这里,一切都是众所周知的,但是插件的初始化函数呢?
乍一看,它看起来像黑魔法:
让我们解释一下:
Seneca 将调用
init: <plugin-name>
操作来初始化插件。通过
prefix
参数,我们正在监听/api
路径下的 URL。我们正在指示 Seneca 通过固定一个基本公共参数将 URL 映射到操作。在这种情况下,我们所有的
seneca.add(..)
都包含一个名为area
的参数,其中ui
值。我们还要求 Seneca 路由包含action
参数的调用(无论值如何,这就是我们使用*
的原因) 以便它会忽略未指定action
参数的调用。
以下参数 (map
) 指定匹配中允许的方法。
参数匹配是如何完成的?
area
参数是隐式的,因为我们已将其与 ui
值固定在一起。
action
参数需要存在。
URL 必须以 /api
开头,因为我们指定了前缀。
因此,有了这些信息,/api/products
将对应于 {area: "ui", action: "products"}
行动。同理,/api/createorder
会对应{area: "ui", action:"createorder"}
行动。
Productbyid
参数有点特殊。
现在,虽然它并不简单,但这看起来要容易得多。
让我们回到将提供功能的 Seneca 操作:
我们实际上使用了我们在前几章中讨论过的所有内容,但是我们在 Seneca 语义方面向前迈进了一步。
我们创建了一个功能非常有限的 API,但通过它们,我们将不同微服务的功能聚合为一个。
需要考虑的一个细节是创建订单操作(最后一个)中嵌套调用的数量。在这种情况下,我们仅从一个产品中创建订单以简化代码,但是如果我们嵌套了太多的非阻塞操作调用等待等待回调中的响应,我们最终会得到一个使您的程序难以阅读的代码金字塔。
它的解决方案是重构如何获取数据和/或重新组织匿名函数,避免内联。
另一种解决方案是使用 promises 库,例如 Q 或 Bluebird (http://bluebirdjs.com/) 允许我们通过 Promise 链接方法流:
通过这种方式,我们没有构建大量回调,而是很好地将调用链接到方法并添加错误处理程序以避免异常冒泡。
微服务很棒,我们通过用几百行 代码编写一个相当容易理解的小系统来证明这一点。
它们也很棒,因为它们允许我们在发生故障时做出反应:
如果电子邮件微服务停止工作会发生什么?
如果订单处理器停止工作会发生什么?
我们能从这种情况中恢复过来吗?
客户看到了什么?
这些问题在单体系统上是无稽之谈。电子邮件程序可能是应用程序的一部分。发送电子邮件的失败意味着一般错误,除非它是专门处理的。与订单处理器相同。
但是,我们面向微服务的架构呢?
即使客户没有收到电子邮件,电子邮件发送者未能发送几封电子邮件的事实并不能阻止订单的处理。这就是我们所说的性能或服务降级;系统可能会更慢,但某些功能仍然可以使用。
订单经理呢?嗯……我们仍然可以进行与产品相关的调用,但我们将无法处理任何订单……这可能仍然是一件好事。
订单经理负责发送电子邮件而不是 UI 微服务这一事实并非巧合;我们只想在成功事件上发送确认销售的电子邮件,而在其他任何情况下我们都不想发送成功电子邮件。
在上一节中,我们讨论了发生故障时的系统降级,但是在 IT 工作多年的每个人都知道,系统在大多数失败情况下不会突然失败。
最常见的事件是超时;服务器忙了一段时间,导致请求失败,给我们的客户带来糟糕的用户体验。
我们如何解决这个特殊问题?
我们可以用断路器解决这个问题,如下图所示:
断路器是一种机制,可防止请求到达可能导致我们的应用程序行为异常的不稳定服务器。
正如您在前面的架构中看到的那样,断路器具有以下三种状态:
闭合:电路闭合;请求到达目的地。
开路:电路开路;请求没有通过断路器并且客户端收到错误。系统将在一段时间后重试通信。
HalfOpen:电路再次测试服务,如果没有错误到达它,请求可以再次流动,并且断路器是关闭。
通过这种简单的机制,我们可以防止错误通过我们的系统级联,避免灾难性故障。
理想情况下,电路 breaker 应该是异步的。这意味着即使没有请求,每隔几秒/毫秒,系统也应该尝试重新建立与故障服务的连接,以继续正常运行。
断路器也是提醒支持工程师的理想场所。根据我们系统的性质,无法访问给定服务的事实可能意味着一个严重的问题。您能想象一家无法通过 SMS 服务发送双因素验证码的银行吗?无论我们多么努力,它总会在某个时刻发生。所以,做好准备吧。
塞内卡很棒。它使开发人员能够将一个简单而小的想法转化为一段代码,其中的连接点不做任何假设,只是事实。动作具有明确的输入,并为您提供通过回调给出答案的界面。
有多少次,您发现您的团队为应用程序的类结构而苦苦挣扎,只是为了以一种不错的方式重用代码?
Seneca 专注于简单。我们不是在建模对象,而只是使用对对象具有极强内聚性和幂等性的一小部分代码来建模系统的一部分,这一事实使我们的生活变得更加轻松。
Seneca 让我们的生活变得轻松的另一种方式是通过可插入性。
如果您回顾我们在本书中编写的代码,首先会发现插件的便利性。
它们为相互关联的一堆动作(它看起来像一个类吗?)提供了正确的封装级别。
我总是尽量不要过度设计解决方案。很容易陷入过早的抽象,为我们不知道在大多数情况下是否会发生的未来准备代码。
我们没有意识到我们花费了多长时间来维护设计过度的功能,并且每次有人更改它们周围的代码时都需要对其进行测试。
Seneca 避免(或至少不鼓励)这种类型的设计。将 Seneca 的行动想象成一张小纸条(如便利贴),您需要在其中写下上周发生的事情。你需要很聪明地知道在里面放什么,如果内容太密集,可能会把它分成另一个便利贴。
Seneca 好的另一点是可配置性。正如我们之前所见,Seneca 带有许多用于数据存储和传输的集成。
Seneca 的一个重要方面是传输协议。正如我们现在所知道的,默认传输是通过 TCP 进行的,但是我们可以使用消息队列来做到这一点吗?结构如下图:
我们可以。它已经完成并维护。
如果您查看插件的代码(它看起来很复杂,但实际上并非如此),您可以在几秒钟内发现魔法发生的地方:
Seneca 正在使用 Seneca 操作来委派消息的传输。虽然看起来有点递归,但是很厉害!
一旦你了解了 Seneca 和选择的传输协议是如何工作的,你就有资格为 Seneca 编写一个传输插件。
Note
当我开始学习为了写这本书,我还写了一个传输插件来使用NServiceBus (http://particular.net/)。
NServiceBus 是一个有趣的想法,它允许您连接多个存储和符合 AMPQ 的系统并将它们用作客户端。例如,我们可以在 SQL Server 表中写入消息,并在它们通过 NServiceBus 路由后从队列中使用它们,对消息的历史具有即时审计功能。
前面章节中的所有代码都依赖于回调。只要您的代码不将它们嵌套在三个以上的级别上,回调就很好。
但是,还有一种更好的方式来管理 JavaScript 的异步特性:promises< /跨度>。
看看下面的代码:
前面的 代码是使用 Promise 的 JQuery 片段示例。
一个承诺,根据它的定义是:
一个人会做某事或某件事会发生的声明或保证。
就是这样。如果您看到前面的代码,$.when
会返回一个 Promise。我们不知道函数的效果需要多长时间,但是我们可以保证,一旦它准备好,完成
将被执行。它看起来与回调非常相似,但请看下面的代码:
不要试图执行它,这只是一个假设的例子,但我们在那里做的是链式承诺;这使得代码是垂直的,而不是最终形成一个金字塔形的程序,这更难阅读,如下图所示:
默认情况下,Seneca 不是一个面向 Promise 的框架,但是(总是有一个但是)使用 Bluebird,这是 JavaScript 中最著名的 Promise 库之一,我们可以promisify Seneca,如下:
这将创建 act
函数的承诺版本及其用法,如下所示:
最后一个片段中的一个重要细节;而不是接收带有以下两个参数的回调:
一个错误
结果
我们链接了以下两种方法:
Then:当 promise 被解决时执行
Catch:如果在解决promise时出现错误,就会执行这个
这种类型的构造允许我们编写以下代码:
这段代码正在处理我们以前从未讨论过的事情:门执行器超时。当 Seneca 在某些情况下无法到达目的地时会发生这种情况,并且可以通过如前所示的 Promise 轻松处理。 then
部分永远不会被执行,因为该函数只会在出现错误时被调用。
对于 JavaScript 中的 Promise,现在市场上有一些整合良好的选项。如今,我的首选是 Bluebird (https://github.com/petkaantonov/bluebird ) 因为它的简单性。 Q 是 AngularJS 使用的另一个选项(最流行的 SPA 框架),但对于日常使用,它看起来更复杂比蓝鸟。
调试 Node.js 应用程序与调试任何其他应用程序非常相似。 WebStorm 或 IntelliJ 等 IDE 提供了一个传统的调试器,您可以每当应用程序到达给定行时,安装断点并停止执行。
如果您购买其中一个 IDE 的许可证,这是完美的选择,但是有一个免费的替代方案,对于 Google Chrome 的用户,node-inspectornode-inspector 。
Node-inspector 是一个 npm 包,它几乎可以让 Chrome 调试器调试 Node.js 应用程序。
让我们看看它是如何工作的:
我相信世界上每个开发人员都非常熟悉这张图片:它是显示我们代码的 Chrome 调试器。正如您在第一行中看到的那样,以蓝色突出显示的那一行,应用程序在第一条指令中停止,因此我们可以通过单击行号来放置断点,如下图所示:
正如你在前面的图片中看到的,我们已经在9< /跨度>。现在我们可以使用控制面板浏览变量的代码和值:
如果您曾经调试过应用程序,则顶部的控件不言自明:
第一个按钮称为播放,它允许应用程序运行到下一个断点
Step over 执行当前文件中的下一行
Step into 进入下一行,深入调用堆栈,以便我们可以看到调用层次结构
Step out 是 step into 的逆过程
禁用断点将阻止程序在断点处停止
异常暂停,顾名思义,将导致程序停止异常(在尝试捕获错误时非常有用)
如果我们点击播放,我们可以看到脚本将如何停止在行9< /span> 在下图中: