TypeScript 微服务(二)

微服务设计与实践

原文:zh.annas-archive.org/md5/042BAEB717E2AD21939B4257A0F75F63

译者:飞龙

协议:CC BY-NC-SA 4.0

第四章:开始您的微服务之旅

微服务是企业中最具体的解决方案之一,可以快速、有效和可扩展地构建应用程序。然而,如果它们没有得到正确的设计或理解,错误的实施和解释可能导致灾难性或无法挽回的失败。本章将通过深入实际实施来开始我们的微服务之旅。

本章将以对购物微服务的描述开始,这是我们将在整个旅程中开发的微服务。我们将学习如何将系统切分为一组相互连接的微服务。我们将设计购物车微服务的整体架构,定义分层,添加缓存级别等。

本章将涵盖以下主题:

  • 购物车微服务概述

  • 购物车微服务的架构设计

  • 购物车微服务的实施计划

  • 模式设计和数据库选择

  • 微服务前期开发方面

  • 为购物车开发一些微服务

  • 微服务设计最佳实践

购物车微服务概述

在处理新系统时最重要的方面就是它的设计。一个糟糕的初始设计总是导致更多挑战的主要原因。与其之后抱怨、解决错误或应用补丁来掩盖糟糕的设计,总是明智的不要急于通过设计过程,花足够的时间,并拥有一个灵活的防错设计。这只能通过清楚地理解需求来实现。在本节中,我们将简要概述购物车微服务;我们需要通过微服务解决的问题;以及业务流程、功能视图、部署和设计视图的概述。

业务流程概述

我们的场景用例非常简单明了。以下流程图显示了我们需要转换为微服务的端到端购物流程。用户将商品添加到购物车,更新库存,用户支付商品,然后可以结账。基于业务规则,涉及了几个验证。例如,如果用户的支付失败,那么他们就不应该能够结账;如果库存不可用,那么商品就不应该被添加到购物车等等。看一下以下流程图:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

业务流程概述

功能视图

每个业务能力及其子能力都显示在一行中,这基本上构成了购物车微服务。一些子能力涉及到多个业务能力,因此我们需要管理一些横切关注点。例如,库存服务既用作独立流程,也用于用户结账产品。以下图显示了购物车微服务的功能视图:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

功能视图

该图将业务能力结合成一张图片。例如,库存服务说明有两个子功能——添加产品详情和添加产品数量和库存项目。这总结了库存服务的目标。为我们的系统创建一个功能视图可以让我们清楚地了解所有涉及其中的业务流程和相关事项。

部署视图

部署的要求非常简单。根据需求,我们需要随时添加新的服务来支持各种业务能力。比如,现在支付方式是PayPal,但将来可能需要支持一些本地支付选项,比如银行钱包。那时,我们应该能够轻松地添加新的微服务,而不会破坏整个生态系统。以下图表显示了部署视图。现在有两个节点(一个主节点和一个从节点),但根据需求,节点的数量可能会根据业务能力、流量激增和其他要求而增加或减少:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

部署视图

在这一部分,我们简要概述了我们的购物车微服务系统。我们了解了它的功能、业务流程和部署视图。在下一节中,我们将看到购物车微服务的架构设计。

我们系统的架构设计

在这一部分,我们将看一下分布式微服务涉及的架构方面。我们将看一下我们将在整本书中制作的整体架构图,并关注诸如分离关注点、如何应用反应式模式以及微服务效率模型等方面。所以,让我们开始吧。

现在我们知道了我们的业务需求,让我们设计我们的架构。根据我们对微服务和其他概念的了解,我们有最终的整体图表,如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

微服务架构

我们将在后面的章节中更详细地研究 API 网关、服务注册表和发现等组件。在这里,它们只是作为整体视图的一部分提到。

让我们了解前面图表中的关键组件,以更好地了解我们的架构。

不同的微服务

如果我们正确理解了我们的业务需求,我们将得出以下业务能力:

  • 产品目录

  • 价格目录

  • 折扣

  • 发票

  • 支付

  • 库存

根据我们的业务能力和单一责任,我们将我们的微服务简要地划分为各种较小的应用程序。在我们的设计中,我们确保每个业务能力由一个单独的微服务实现,我们不会将一个微服务过载超过一个微服务。我们将整个系统简要地划分为各种微服务,如购物车微服务、产品微服务、支付微服务、消费者微服务、缓存微服务、价格计算和建议微服务等。整体的细粒度流程可以在前面的图表中看到。另一个重要的事情是,每个微服务都有自己的数据存储。不同的业务能力有不同的需求。例如,当一个人结账时,如果交易失败,那么所有的交易,比如将产品添加到客户的购买项目中,从产品库存中扣除数量等,都应该被回滚。在这种情况下,我们需要能够处理事务的关系型数据库,而在产品的情况下,我们的元数据不断变化。一些产品可能比其他产品具有更多的功能。在这种情况下,拥有固定的关系模式是不明智的,我们可以选择 NoSQL 数据存储。

在撰写本书时,MongoDB 4.0 尚未推出。它提供了以下事务加 NoSQL 的优势。

缓存微服务

接下来我们要看的是集中式缓存存储。这个微服务直接与所有微服务进行交互,我们可以使用这个服务在需要时缓存我们的响应。通常情况下,可能会出现一个服务停止运行,但我们仍然可以通过显示缓存数据来保留应用程序(例如产品信息和元数据很少改变;我们可以将它们缓存一段时间,从而避免额外的数据库访问)。拥有缓存可以提高系统的性能和可用性,最终导致成本优化。它提供了极快的用户体验。由于微服务不断移动,通常它们可能无法被访问。在这种情况下,当访问可用性区域失败时,拥有缓存响应总是有利的。

服务注册表和发现

在图表的开始,我们包括了服务注册表。这是一个动态数据库,记录了所有微服务的启动和关闭事件。服务订阅注册表并监听更新,以了解服务是否已经停止。整个过程通过服务注册表和发现完成。当服务停止或启动时,注册器会更新注册表。这个注册表被所有订阅注册表的客户端缓存,所以每当一个服务需要交互时,地址都是从这个注册表中获取的。我们将在《第六章》服务注册表和发现中详细讨论这个过程。

Registrator

接下来我们要看的是与缓存一起提供的Registrator (gliderlabs.github.io/registrator/latest/)。Registrator 是一个第三方服务注册工具,基本上监视微服务的启动和关闭事件,并根据这些事件的输出动态更新集中式服务注册表。不同的服务可以直接与注册表通信,以获取服务的更新位置。Registrator 确保注册和注销代码在系统中不会重复。我们将在《第六章》服务注册表和发现中更详细地讨论这个问题,其中我们将 Registrator 与 consul 集成。

日志记录器

任何应用程序的一个重要方面是日志。当使用适当的日志时,分析任何问题变得非常容易。因此,这里我们有一个基于著名的 Elastic 框架的集中式日志记录器微服务。Logstash 监视日志文件,并在推送到 Elasticsearch 之前将其转换为适当的 JSON 格式。我们可以通过 Kibana 仪表板可视化日志。每个微服务都将有其独特的 UUID 或一些日志模式配置。我们将在《第九章》部署、日志记录和监控中更详细地讨论这个问题。

网关

这是我们微服务的最重要部分和起点。这是我们将处理诸如身份验证、授权、转换等横切关注点的中心点。在不同服务器上创建不同的微服务时,我们通常会将主机和端口的信息从客户端中抽象出来。客户端只需向网关发出请求,网关通过与服务注册表和负载均衡器的交互,并将请求重定向到适当的服务,来处理其余的事情。这是微服务中最重要的部分,应该使其高度可用。

在通过架构图之后,现在让我们了解一些与架构相关的方面,这些方面我们以后会用到。

涉及的设计方面

在实际编码之前,我们需要了解“如何”和“为什么”。比方说,如果我必须砍树(PS:我是一个热爱大自然的人,我不支持这个),我宁愿先磨削斧头,而不是直接砍树。我们将做同样的事情,先磨削我们的斧头。在这一部分,我们将看看设计微服务所涉及的各个方面。我们将看看要经历哪些通信模型,微服务中包括什么,以及为了实现高效的微服务开发而需要注意的哪些方面。

微服务效率模型

根据各种需求和要求,我们已经定义了微服务效率模型。任何微服务的适当实现必须遵守它并提供一套标准的功能,如下所示:

  • 通过 HTTP 和 HTTP 监听器进行通信

  • 消息或套接字监听器

  • 存储能力

  • 适当的业务/技术能力定义

  • 服务端点定义和通信协议

  • 服务联系人

  • 安全服务

  • 通过 Swagger 等工具的服务文档

在下图中,我们总结了我们的微服务效率模型:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

微服务效率模型

现在让我们看看前面图表的四个部分。

核心功能

核心功能是微服务本身的一部分。它们包括以下功能:

  • 技术能力:任何需要的技术功能,如与服务注册表交互,向事件队列发送事件,处理事件等,都涉及在这里。

  • 业务能力:编写微服务以实现业务能力或满足业务需求。

  • HTTP 监听器:技术能力的一部分;在这里,我们为外部消费者定义 API。在启动服务器时,将启动 HTTP 监听器,消除任何其他需求。

  • 消息监听器:事件驱动通信的一部分,发送方不必担心消息监听器是否已实现。

  • API 网关:终端客户端的通信单一点。API 网关是处理任何核心关注点的单一位置。

  • 文档存储或数据存储:我们应用程序的数据层。根据我们的需求,我们可以使用任何可用的数据存储。

支持效率

这些是帮助实现核心微服务的解决方案。它们包括以下组件:

  • 负载均衡器:应用程序负载均衡器,根据服务器拓扑的变化进行重定向。它处理动态服务的上线或下线。

服务注册表:服务的运行时环境,如果服务上线或下线,需要发布到其中。它维护所有服务的活动日志以及可用实例。

中央日志:核心的集中式日志记录解决方案,以观察所有地方的日志,而不是单独打开容器并在那里寻找日志。

安全:通过常用的可用机制(如 OAuth,基于令牌,基于 IP 等)检查真实的客户端请求。

测试:测试微服务和基本功能,如微服务间通信,可伸缩性等。

基础设施角色

以下是实现高效微服务所需的基础设施期望:

  • 服务器层:选择部署我们的微服务的有效机制。众所周知的选项包括亚马逊 EC2 实例,红帽 OpenShift 或无服务器。

  • 容器:将应用程序容器化,以便在任何操作系统上轻松运行,而无需安装太多。

  • CI/CD:维护简单部署周期的过程。

  • 集群:服务器负载均衡器,以处理应用程序中的负载或峰值。

治理

流程和参考信息,以简化我们在应用程序开发中的整体生命周期,包括以下内容:

  • 合同测试:测试微服务的期望和实际输出,以确保频繁的更改不会破坏任何东西

  • 可扩展性:根据需求产生新实例并在需求减少时移除这些实例以处理负载峰值

  • 文档:生成文档以便轻松理解别人实际在做什么

在接下来的部分,我们将为我们的微服务开发制定一个实施计划。

购物车微服务的实施计划

微服务开发中的一个关键挑战是确定微服务的范围:

  • 如果一个微服务太大,你最终会陷入单体地狱,难以添加新功能和实施错误修复

  • 如果一个微服务太小,要么我们会在服务之间出现紧密耦合,要么会出现过多的代码重复和资源消耗

  • 如果微服务的大小合适,但有界上下文并不固定,比如服务共享数据库,会导致更高的耦合和依赖

在这一部分,我们将为我们的购物车微服务制定一个实施计划。我们将制定一个通用的工作流程或计划,并根据计划设计我们的系统。我们还将看看当我们的范围不清晰时该怎么办,以及如何在这种情况下继续,最终达到我们的微服务目标。我们将看看如何潜在地避免上述的漏洞。

当范围不清晰时该怎么办

到目前为止,我们已经设计了基于微服务范围的架构计划,但那是在我们的需求非常明确的情况下。我们清楚地知道我们需要做什么。但在大多数情况下,你可能不会有类似的情景。你要么是从单体系统迁移到微服务,要么是被不断变化的需求或业务能力所困扰,或者可能是技术能力的复杂性在初期无法估计,使得确定微服务的范围变得困难。接下来的部分是针对这种情况的,你可以执行以下步骤:

  1. 梦想大,从大开始:决定微服务的范围总是一个巨大的任务,因为它定义了整体的有界上下文。如果这不明确,我们最终会陷入单体地狱。然而,如果范围过于狭窄,也有其缺点。你将遇到困难,因为你最终会在两个微服务之间出现数据重复,责任不清晰,以及难以独立部署服务。从现有微服务中划分出微服务要比管理范围过窄的微服务容易得多。

  2. 从现有微服务中分离出微服务:一旦你觉得一个微服务太大,你需要开始分离服务。首先,需要根据业务和技术能力为现有和新的微服务确定范围。任何与新微服务有关的内容都放入自己的模块。然后,现有模块之间的任何通信都移动到公共接口,比如 HTTP API/基于事件的通信等等。微服务也可以计划以后开发;如果有疑问,总是创建一个单独的模块,这样我们可以轻松地将其移出去。

  3. 确定技术能力:技术能力是支持其他微服务的任何东西,比如监听事件队列发出的事件,注册到服务注册表等等。将技术能力保留在同一个微服务中可能是一个巨大的风险,因为它很快会导致紧密耦合,同样的技术能力可能也会被许多其他服务实现。

  4. 基于业务和技术能力的微服务遵循标准:微服务遵循固定的标准——自给自足、弹性、透明、自动化和分布。每个点都可以简要陈述为:

  • 微服务提供单一的业务能力(模块化是关键)。

  • 微服务可以很容易地单独部署。每个服务都将有自己的构建脚本和 CI/CD 流水线。共同点将是 API 网关和服务注册表。

  • 你可以很容易地找出微服务的所有者。它们将是分布式的,每个团队可以拥有一个微服务。

  • 微服务可以很容易地被替换。我们将通过服务注册表和发现有共同的注册选项。我们的每个服务都可以通过 HTTP 访问。

通过遵循这些步骤,最终将达到微服务级别,其中每个服务将提供单一的业务能力。

模式设计和数据库选择

任何应用程序的主要部分是其数据库选择。在本节中,我们将看看如何为微服务设计我们的数据库,是将其保持独立,还是共享,以及选择哪种数据库——SQL 还是 NoSQL?我们将根据数据类型和业务能力来分类数据存储。有很多选择。微服务支持多语言持久性。根据业务能力和需求选择特定数据存储的方法称为多语言持久性。以下几点讨论了基于用例选择哪种数据库:

  • 我们可以利用 Apache Cassandra 来支持表格数据,例如库存数据。它具有分布式一致性和轻量级事务的选项,以支持 ACID 事务。

  • 我们可以利用 Redis 来支持缓存数据,其中数据模型只是一个键值对。Redis 中的读操作非常快。

  • 我们可以利用 MongoDB 来支持以非结构化形式存储的产品数据,并具有在任何特定字段上建立索引的能力。像 MongoDB 这样的面向文档的数据库具有强大的选项,比如在特定属性上建立索引以实现更快的搜索。

  • 我们可以利用 GraphQL 来支持复杂的关系。GraphQL 对于多对多关系非常有用,例如我们的购物车推荐系统。Facebook 使用 GraphQL。

  • 我们可以使用关系数据库来支持传统系统或需要维护结构化关系数据的系统。我们在数据不经常更改的地方使用关系数据。

在本节中,我们将详细了解这些要点,并了解微服务中数据层应该如何。然后,我们将了解数据库类型及其优缺点和用例。所以,让我们开始吧。

如何在微服务之间划分数据

微服务最困难的是我们的数据。每个微服务都应该通过拥有自己的数据库来维护数据。数据不应通过数据库共享。这条规则有助于消除导致不同微服务之间紧密耦合的常见情况。如果两个微服务共享相同的数据库层,并且第二个服务不知道第一个服务更改了数据库模式,它将失败。由于这个原因,服务所有者需要保持不断联系,这与我们的微服务路径不同。

我们可能会想到一些问题,比如数据库在微服务世界中如何保持?服务是否会共享数据库?如果是的话,共享数据会有什么后果?让我们回答这些问题。我们都知道这句话,“拥有就意味着责任”。同样,如果一个服务拥有数据存储,那么它就是唯一负责保持其最新的。此外,为了实现最佳性能,微服务需要的数据应该是附近或本地的,最好是在微服务容器内部,因为微服务需要经常与其进行交互。到目前为止,我们已经了解了如何划分数据的两个原则:

  • 数据应该被划分,以便每个微服务(满足某种业务能力)可以轻松确保数据库是最新的,并且不允许任何其他服务直接访问。

  • 与该微服务相关的数据应该在附近。将其放得太远会增加数据库成本和网络成本。

对数据进行分离的一般过程之一是建立一个包含实体、对象和聚合的领域模型。假设我们有以下用例——允许客户搜索产品,允许客户购买特定类型的产品,以及允许客户购买产品。我们有三个功能——搜索、购买和库存。每个功能都有自己的需求,因此产品数据库存储在产品目录服务中,库存以不同的方式存储,搜索服务查询产品目录服务,这些结果被缓存。

在本节中,我们将通过一个例子详细讨论这些规则,这将帮助我们决定在哪里保留数据层以及如何划分数据层以获得最大优势。

假设 1 - 数据所有权应通过业务能力进行规范

在决定数据在微服务系统中属于哪里的一个主要想法是基于业务能力进行决定。微服务只是满足业务能力的服务,而没有数据存储是不可能的。业务能力定义了微服务的包含区域。属于处理该能力的一切东西都应该驻留在微服务内部。例如,只有一个微服务应该拥有客户的个人详细信息,包括送货地址和电子邮件地址。另一个微服务可以拥有客户的购买历史,第三个微服务可以拥有客户的偏好。负责业务能力的微服务负责存储数据并保持其最新状态。

假设 2 - 为了速度和鲁棒性而复制数据库

在选择在微服务系统中存储数据的位置时,第二个因素是基于范围或局部性来决定。即使我们谈论的是相同的数据,如果数据存储在微服务附近或远离微服务,都会有很大的变化。微服务可以查询自己的数据库获取数据,或者微服务可以查询另一个微服务获取相同的数据。后者当然会带来缺点和紧密的依赖关系。在本地邻域查找比在不同城市查找要快得多。一旦你决定了数据的范围,你会意识到微服务需要经常彼此交流。

这种微服务通常会创建非常紧密的依赖关系,这意味着我们被困在同样的旧式单片系统中。为了解除这种依赖,耦合缓存数据库或者维护缓存存储通常会很有用。你可以将响应缓存下来,或者你可以添加一个读取模型来在一定时间间隔后使缓存失效。拥有本地数据的微服务应该处于最佳位置,根据业务能力来决定何时特定的代码变得无效。应该使用 HTTP 缓存头来控制缓存。管理缓存就是简单地控制缓存控制头。例如,cache-control: private, max-age:3600这一行将响应缓存 3,600 秒。

在下一节中,我们将根据以下标准来选择最佳数据库:

  • 我的数据是什么?是一堆表、一个文档、一个键值对还是一个图?

  • 我的数据写入和读取频率有多高?我的写入请求是随机的还是在时间上均匀分布的?是否存在一次性读取所有数据的情况?

  • 写操作多还是读操作多?

如何为你的微服务选择数据存储

在设计微服务时最基本的问题之一是如何选择正确的数据存储?我们将在第七章的服务状态和服务间通信部分中更详细地讨论这个问题,但在这里,让我们先搞清楚基本原理。

选择任何理想数据存储的首要步骤是找出我们微服务数据的性质。根据数据的性质,我们可以简要定义以下类别:

  • 短暂或短暂的数据:缓存服务器是短暂数据的经典示例。它是一个临时存储,其目标是通过实时提供信息来增强用户体验,从而避免频繁的数据库调用。这在大部分操作都是读取密集的情况下尤为重要。此外,此存储没有额外的耐久性或安全性问题,因为它没有数据的主要副本。然而,这不应被轻视,因为它必须具有高可用性。故障可能导致用户体验不佳,并随后使主数据库崩溃,因为它无法处理如此频繁的调用。此类数据存储的示例包括 Redis、Elasticsearch 等。

  • 瞬态或瞬时数据:例如日志和消息等数据通常以大量和频率出现。摄取服务在将信息传递到适当的目的地之前处理这些信息。这种数据存储需要高频率的写入。时间序列数据或 JSON 格式等功能是额外的优势。瞬态数据的支持要求更高,因为它主要用于基于事件的通信。

  • 运营或功能性数据:运营数据侧重于从用户会话中收集的任何信息,例如用户配置文件、用户购物车、愿望清单等。作为主要数据存储,这种微服务提供了更好的用户体验和实时反馈。为了业务连续性,这种数据必须被保留。这种数据的耐久性、一致性和可用性要求非常高。根据我们的需求,我们可以根据需要提供以下任何一种结构的数据存储:JSON、图形、键值、关系等。

  • 事务性数据:从一系列流程或交易中收集的数据,例如支付处理、订单管理,必须存储在支持 ACID 控制以避免灾难的数据库中(我们将主要使用关系数据库来处理事务性数据)。在撰写本书时,仍然没有支持事务性数据的 MongoDB 4.0。一旦普遍可用,NoSQL 数据存储甚至可以用于事务管理。

产品微服务的设计

根据我们的需求,我们可以将数据分类为以下各种部分:

微服务数据存储类型
缓存短暂(例如:ELK)
用户评论、评分、反馈和畅销产品瞬态
产品目录运营
产品搜索引擎运营
订单处理事务性
订单履行事务性

对于我们的产品目录数据库,我们将按照以下设计进行。

在当前章节中,我们将使用产品目录服务,这要求我们使用运营数据存储。我们将使用 MongoDB。产品至少包括以下项目:变体、价格、层次结构、供应商、反馈电子邮件、配置、描述等。我们将使用以下模式设计,而不是在单个文档中获取所有内容:

{"desc":[{"lang":"en","val":"TypescriptMicroservicesByParthGhiya."}],"name":"TypescriptMicroservices","category":"Microservices","brand":"PACKT","shipping":{"dimensions":{"height":"13.0","length":"1.8","width":"26.8"},"weight":"1.75"},"attrs":[{"name":"microservices","value":"exampleorientedbook"},{"name":"Author","value":"ParthGhiya"},{"name":"language","value":"Node.js"},{"name":"month","value":"April"}],"feedbackEmail":"ghiya.parth@gmail.com","ownerId":"parthghiya","description":"thisistestdescription"}

这种模式设计的一些优点包括:

  • 可以进行快速毫秒级的分面搜索

  • 每个索引都将以_id结尾,使其对分页非常有用

  • 可以对各种属性进行高效的排序

微服务预开发方面

在本节中,我们将了解一些通常的开发方面,这些方面将贯穿整本书。我们将了解一些常见的方面,例如使用哪种 HTTP 消息代码,如何设置日志记录,保留哪些类型的日志记录,如何使用 PM2 选项,以及如何跟踪请求或附加唯一标识符到微服务。让我们开始吧。

HTTP 代码

HTTP 代码主导着标准 API 通信,并且是任何通用 API 的通用标准之一。它解决了向服务器发出的每个请求的常见问题,无论请求是否成功,是否产生服务器错误等等。HTTP 使用代码范围来指示代码的性质。HTTP 代码是基于各种代码和响应行为采取相应措施的标准(www.w3.org/Protocols/rfc2616/rfc2616-sec10.html),因此在这里基本上适用于不重复造轮子的概念。在本节中,我们将看一些标准代码范围以及它们的含义。

1xx – 信息

1xx 代码提供原始功能,例如后台操作、切换协议或初始请求的状态。例如,100 Continue表示服务器已收到请求头,并正在等待请求体,101 Switching Protocols表示客户端已请求从服务器更改协议,并且请求已获批准,102表示操作正在后台进行,需要一些时间来完成。

2xx – 成功

这是为了指示在 HTTP 请求中使用了一定程度的成功信息成功代码。它将多个响应打包成特定代码。例如,200 Ok表示一切正常,GET 或 POST 请求成功。201 Created表示 GET 或 POST 请求已完成,并为客户端创建了一个新资源。202 Accepted表示请求已被接受并正在处理。204 No Content表示服务器没有返回内容(与200非常相似)。206 Partial Content通常用于分页响应,表示还有更多数据要返回。

3xx – 重定向

3xx 范围涉及资源或端点的状态。它指示必须采取哪些额外操作才能完成该请求,因为服务器仍然接受通信,但所联系的端点不是系统中的正确入口点。最常用的代码包括301 Moved Permanently,表示未来的请求必须由不同的 URI 处理,302 Found,表示出于某种原因需要临时重定向,303 See other,告诉浏览器查看另一个页面,以及308 Permanent Redirect,表示该资源的永久重定向(与301相同,但不允许 HTTP 方法更改)。

4xx – 客户端错误

这一范围的代码是最为人熟知的,因为传统的404 Not found错误是一个众所周知的占位符,用于表示 URL 格式不正确。这一范围的代码表示请求存在问题。其他众所周知的代码包括400 Bad Request(语法错误的请求),401 Unauthorized(客户端缺乏身份验证),以及403 Forbidden(用户没有权限)。另一个常见的代码是429 Too Many Requests,用于限制请求速率,表示特定客户端的流量被拒绝。

5xx – 服务器错误

这些代码范围表示服务器上发生了处理错误或服务器出现了问题。每当发出5xx代码时,它表示服务器出现了某种问题,客户端无法解决,必须相应地处理。一些广泛使用的代码包括500 Internal Server Error(表示服务器软件发生错误,未披露任何信息),501 Not Implemented(表示尚未实现的端点,但仍在请求),以及503 Service Unavailable(表示服务器由于某种原因宕机,无法处理更多请求)。收到503时,必须采取适当措施重新启动服务器。

为什么 HTTP 代码在微服务中至关重要?

微服务是完全分布式且不断移动的。因此,如果没有标准的通信手段,我们将无法触发相应的故障转移措施。例如,如果我们实现了断路器模式,断路器应该知道每当它收到5xx系列代码时,它应该保持断路器打开,因为服务器不可用。同样,如果它收到429,那么它应该阻止来自该特定客户端的请求。完整的微服务生态系统包括代理、缓存、RPC 和其他服务,其中 HTTP 是共同的语言。根据上述代码,它们可以相应地采取适当的行动。

在下一节中,我们将学习有关日志记录方面以及如何处理微服务中的日志记录。

通过日志审计

到目前为止,我们听说微服务是分布式的,服务不断变化。我们需要跟踪所有服务和它们产生的输出。使用console.log()是一个非常糟糕的做法,因为我们无法跟踪所有服务,因为console.log()没有固定的格式。此外,每当出现错误时,我们需要堆栈跟踪来调试可能的问题。为了进行分布式日志记录,我们将使用winston模块(github.com/winstonjs/winston)。它具有各种选项,如日志级别、日志格式等。对于每个微服务,我们将传递一个唯一的微服务 ID,这将在我们聚合日志时对其进行标识。对于聚合,我们将使用著名的 ELK Stack,详见第九章,部署、日志记录和监控。以下是按优先级排序的各种日志类型,通常使用:

  • 严重/紧急(0):这是最灾难性的级别,当系统无法恢复或正常运行时使用。这会强制执行关机或其他严重错误。

  • 警报(1):收到这个严重的日志后,必须立即采取行动以防止系统关闭。这里的关键区别在于系统仍然可用。

  • 关键(2):在这里,不需要立即采取行动。此级别包括诸如无法连接到套接字、无法获取最新聊天消息等情况。

  • 错误(3):这是一个应该调查的问题。系统管理员必须被通知,但我们不需要把他从床上拽起来,因为这不是紧急情况。通常用于跟踪整体质量。

  • 警告(4):当可能存在错误或可能不存在错误时使用此级别。警告条件接近错误,但它们不是错误。它们指示可能有害的情况或事件,可能会导致错误。

  • 通知(5):这个级别是一个正常的日志,但具有一些重要的条件。例如,您可能会收到诸如在…中捕获到 SIGBUS 尝试转储核心之类的消息。

  • Info(6): 这个级别用于不可察觉的信息,比如服务器已经运行了 x 小时和有趣的运行时事件。这些日志立即在控制台上可见,因为这些日志的目的是保守。这些日志应该保持最少。

  • Debug(7): 这用于详细了解系统的流程。它包括用于调试的消息,例如,像“打开文件…”或“获取产品的产品 ID 47”。

需要启用日志。如果启用了致命日志,那么所有日志都将被看到。如果启用了信息日志,那么只有信息和调试日志会被看到。所有级别的日志都有自己的 Winston 自定义方法,我们可以添加我们自己的格式。

PM2 进程管理器

Node.js 是单线程的,这意味着任何对 JavaScript throw 语句的使用都会引发一个必须使用 try...catch 语句处理的异常。否则,Node.js 进程将立即退出,导致无法处理任何进一步的请求。由于 Node.js 运行在单进程未捕获异常上,需要小心处理。如果不处理,它将崩溃并导致整个应用程序崩溃。因此,在 Node.js 中的黄金法则是 如果任何异常未经处理冒泡到顶部,我们的应用程序就会死掉

PM2 是一个旨在永远保持我们服务运行的进程管理器。它是一个带有内置负载均衡器的生产进程管理器,是微服务的完美候选者。PM2 非常方便,因为它允许我们使用简单的 JSON 格式声明每个微服务的行为。PM2 是一个带有内置监控和零停机工具的高级任务运行器。扩展 PM2 命令只是简单地输入我们想要生成或减少的实例数量。使用 PM2 启动一个新进程将启动一个进程的分叉模式,并让负载均衡器处理其余部分。PM2 在主进程和进程工作线程之间进行轮询,以便我们可以同时处理额外的负载。PM2 提供的一些标准部署功能如下:

pm2 start <process_name>以分叉模式启动进程,并在服务器宕机时自动重启
pm2 stop <process_name>停止 PM2 进程
pm2 restart <process_name>重新启动一个带有更新代码的进程
pm2 reload <process_name>重新加载 PM2 进程,零停机时间
pm2 start <process_name> -i max以最大分叉模式启动一个 PM2 进程;也就是说,它将根据可用的 CPU 数量生成最大数量的实例
pm2 monit监控一个 PM2 进程
pm2 start ecosystem.config.js --env staging启动一个进程,使用 ecosystem.config.js 中的配置

PM2 也可以用作部署工具或高级的 CI/CD 手段。你只需要在 ecosystem.config.js 文件中定义你的部署脚本,如下所示:

"deploy": {
    "production": {
        "key": "/path/to/key.pem", // path to the private key to authenticate
        "user": "<server-user>", // user used to authenticate, if its AWS than ec2-user
        "host": "<server-ip>", // where to connect
        "ref": "origin/master",
        "repo": "<git-repo-link>",
        "path": "<place-where-to-check-out>",
        "post-deploy": "pm2 startOrRestart ecosystem.config.js --env production"
    },
}

然后,我们只需要输入以下命令:

pm2 deploy ecosystem.config.js production

这个命令作为一个本地部署工具。添加路径、PEM 文件密钥等步骤是我们可以连接到服务器的步骤。一旦使用指定用户连接到服务器,PM2 进程就会启动,我们可以运行我们的应用程序。最新的 Git 存储库将被克隆,然后 PM2 将在 forever 选项中启动 dist/Index.js 文件。

追踪请求

追踪请求的来源非常重要,因为有时我们需要重构客户在我们系统中的整个旅程。它提供了有关系统的有用信息,例如延迟的来源。它还使开发人员能够观察如何通过搜索所有聚合日志来处理单个请求,使用一些唯一的微服务 ID,或者通过传递时间范围来找出用户的整个旅程。以下是通过 Winston 生成的示例日志:

{ level: 'info', serviceId: 'hello world microservice' , 
  message: 'What time is the testing at?', 
  label: 'right meow!', timestamp: '2017-09-30T03:57:26.875Z' }

所有重要数据都可以从日志中看到。我们将使用 ELK Stack 进行日志记录。ELK 具有巨大的优势,因为它结合了以下三个工具的功能——Logstash(配置为从各种来源读取日志或注册事件并将日志事件发送到多个来源)、Kibana(可配置的 Web 仪表板,用于查询 Elasticsearch 的日志信息并呈现给用户)和Elasticsearch(基于 Lucene 的搜索服务器,用于收集日志、解析日志并将其存储以供以后使用,提供 RESTful 服务和无模式的 JSON 文档)。它具有以下优势:

  • 每个Winston实例都配置了 ELK。因此,我们的日志服务是外部化的,日志的存储是集中的。因此,有一个单一的数据源可以追踪请求。

  • 由于 Winston 的自动模式定义和正确格式,我们拥有日志结构化数据。例如,如果我想查询从4:404.43的所有日志,我只需通过 Elasticsearch 查询,因为我知道我的所有日志在 JSON 中的固定级别上都有时间组件。

  • Winston 日志格式负责创建和传递跨所有请求的相关标识符。因此,如果需要,可以通过查询特定参数轻松追踪特定服务器的日志。

  • 我们可以通过 Elasticsearch 搜索我们的日志。Elasticsearch 提供 Kibana 以及 REST API,可以随时调用以查看数据源中的所有数据。基于 Lucene 的实现有助于更快地获取结果。

  • Winston 中的日志级别可以在运行时更改。我们可以有各种日志级别,并根据日志的优先级,可能会或可能不会看到较低级别的日志。这在解决生产级别的问题时非常有帮助。

在本节中,我们看了日志记录以及它如何解决了解客户行为(客户在页面上花费多少时间,每个页面上的操作花费多少时间,可能存在的一些问题等)等问题。在下一节中,我们将开始开发购物车微服务。

为购物车开发一些微服务

在本节中,我们将为购物车开发一些微服务,这些微服务以其业务能力而独特标识。因此,在动手之前,让我们快速概述一下我们当前的问题。购物车单体应用程序进展顺利,但随着数字化的出现,交易量大幅增加——比原始估计增加了 300-500 倍。端到端架构经过审查,发现了以下限制,基于这些限制引入了微服务架构:

  • 坚固性和稳固性:由于错误和线程阻塞,系统的坚固性受到了很大的影响,这迫使 Node.js 应用服务器不接受任何新的事务并进行强制重启。内存分配问题和数据库锁线程是主要问题。某些资源密集型操作影响整个应用程序,资源分配池总是被消耗。

  • 部署中断:由于添加了越来越多的功能,服务器中断窗口大大增加,因为服务器启动时间增加。由于node_modules的大小,导致了这个问题。由于整个应用程序被打包为单体应用,整个应用程序需要一遍又一遍地安装node模块,然后启动我们的 node-HTTP 服务器。

  • 锐度:随着时间的推移,代码的复杂性呈指数增长,工作的分布也是如此。团队之间形成了紧密的耦合依赖关系。因此,实施和部署变得更加困难。影响分析变得过于复杂。结果就是,修复一个 bug,就会出现 13 个其他 bug。这样的复杂性导致node_modules的大小超过 1GB。这样的复杂性最终停止了持续集成CI)和单元测试。最终,产品的质量下降了。

这样的情况和问题需要一种进化的方法。这样的情况需要一种微服务开发方法。在这一部分,我们将看到微服务设置方法,这将给我们带来各种优势,比如选择性服务扩展、技术独立性(易于迁移到新技术)、容错等等。

行程

让我们快速浏览一下我们将在本次练习中执行的行程:

  • 开发设置和先决模块:在这一部分,我们将总结项目中将使用的开发工具和npm模块。我们将关注应用程序属性、自定义中间件、依赖注入等先决条件。

  • 应用程序目录配置:我们将分析我们将在其他微服务中使用的结构,并了解我们将需要的所有文件以及在哪里编写逻辑。

  • 配置文件:我们将查看所有配置文件,通过这些文件我们可以指定各种设置,比如数据库主机名、端口 URL 等等。

  • 处理数据:我们将简要总结代码模式以及它们如何支持最佳开发者产出,并使开发者的生活更轻松。

  • 准备服务:我们将分析package.json和 Docker 文件,并看看如何使用这两个文件使我们的微服务准备好为任何服务请求提供服务。

所以,让我们开始我们的行程。

开发设置和先决模块

在这一部分,我们将看到在开发和创建我们的开发沙盒时需要注意的几个方面。我们将概述将使用的所有 node 模块以及每个node模块将满足的核心方面。所以,现在是动手的时候了。

注意:我们在第二章中看到了如何为任何不是用 ES6 编写的 node 模块编写自定义类型,为任何在DefinitelyTyped存储库中没有可用类型的模块利用这一点。

存储库模式

在这一部分,我们将了解存储库模式,它赋予我们将代码放在一个地方的能力。TypeScript 引入了泛型(就像 Java 中的特性),我们将充分利用这一点在我们的微服务中。存储库模式是创建企业级应用程序最广泛使用的模式之一。它使我们能够通过为数据库操作和业务逻辑创建一个新层直接在应用程序中处理数据。

结合泛型和存储库模式,可以带来无数的优势。在处理 JavaScript 应用程序时,我们需要解决诸如应用程序之间的代码共享和模块化等问题。泛型存储库模式通过在具有泛型的抽象类(或根据业务能力的多个抽象类)中给我们写入数据的抽象来解决这个问题,并且可以独立于数据模型重用实现层,只需将类型传递给某些类。当我们谈论存储库模式时,它是一个存储库,我们可以将数据库的所有操作(CRUD)集中在一个地方,适用于任何通用业务实体。当您需要在数据库中执行操作时,您的应用程序调用存储库方法,从而使调用者能够透明地进行调用。将这与泛型结合使用会导致一个抽象,一个具有所有常用方法的基类。我们的EntityRepository只扩展了具有所有数据库操作实现的基类。

此模式遵循开闭原则,其中基类对扩展开放但对修改关闭。

它有各种优势,如下:

  • 它可以用作可扩展性措施,您只需为所有常见操作编写一个类,例如 CRUD,当所有其他实体应具有类似操作时

  • 业务逻辑可以在不触及数据访问逻辑的情况下进行单元测试

  • 数据库层可以被重用

  • 数据库访问代码是集中管理的,以实施任何数据库访问策略,就像缓存一样简单

配置应用程序属性

根据十二要素标准(回想一下,微服务的十二要素应用程序,第一章中的揭秘微服务),一个代码库应该适用于多个环境,如 QA、开发、生产等。确保我们在应用程序中有应用程序属性文件,在其中可以指定环境名称和与环境相关的内容。Config(www.npmjs.com/package/config)就是这样一个模块,它可以帮助您组织所有配置。此模块只需读取./config目录中的配置文件(它应该与package.json处于同一级别)。

配置的显着特点如下:

  • 它可以支持 YAML、YML、JSON、CSV、XML 等格式。

  • 它可以创建一个与package.json并行的 config 目录,并在其中创建一个文件default.ext(这里,.ext可以是前述格式之一)。

  • 要从配置文件中读取,只需使用以下代码行:

import * as config from 'config';
const port = config.get('express.port');
  • 它支持各种配置文件,维护层次结构以支持各种环境。

  • 它甚至支持多个节点实例;非常适合微服务。

自定义健康模块

有时,向应用程序添加新模块会导致应用程序失序。我们需要自定义健康模块来实际监视服务并警告我们服务失序(服务发现正是这样做的,我们将在第六章中看到,服务注册表和发现)。我们将使用express-pingwww.npmjs.com/package/express-ping)来查看我们节点的健康状况。通过在我们的中间件中引入此模块,我们可以公开一个简单的 API,告诉操作员和其他应用程序有关其内部健康状况的信息。

express-ping的显着特点如下:

  • 这是一个零配置模块,只需将其注入中间件即可公开一个健康端点。

  • 要使用此模块,只需使用以下代码行:

import * as health from 'express-ping';
this.app.use(health.ping());
  • 仅添加先前的 LOCs 将公开一个<url>/health端点,我们可以用于健康检查目的。我们可以添加授权访问,甚至使用中间件来使用我们公开的/ping API,这只是普通的 express:
app.get('/ping', basicAuth('username', 'password'));
app.use(health.ping('/ping'));
  • 此端点可用于任何地方,只需检查应用程序的健康状况。

依赖注入和控制反转

在本节中,我们将看到如何使用基本原则,如依赖注入和控制反转。来自 Java 背景,我倾向于在任何应用程序中使用这些原则,以使我的开发过程更加顺畅。幸运的是,我们有与我们的要求完全匹配的模块。我们将使用inversifywww.npmjs.com/package/inversify)作为控制反转容器,typediwww.npmjs.com/package/typedi)用于依赖注入。

Inversify

控制反转IOC)是关于获得自由、更灵活,减少对他人的依赖。比如你正在使用一台台式电脑,你是被奴役的(或者说受控制)。你必须坐在屏幕前,使用键盘输入和鼠标导航。糟糕的软件也会让你类似地被奴役。如果你用笔记本电脑替换台式电脑,那么你就实现了控制反转。你可以轻松携带它并四处移动。所以,现在你可以控制你的电脑在哪里,而不是电脑控制它。软件中的 IOC 非常类似。传统上来说,IOC 是一个设计原则,根据这个原则,计算机程序的自定义部分从一个通用框架中接收控制流。我们有inversifyJS作为npm模块可用。根据官方文档:

InversifyJS 是一种轻量级的 TypeScript 和 JavaScript 应用程序的控制反转容器。IOC 容器将使用类构造函数来识别和注入其依赖项。它具有友好的 API,并鼓励使用最佳的面向对象编程和 IoC 实践,遵循 SOLID 原则。

Typedi

依赖注入是一种类、组件和服务指定其依赖库的方式。通过简单地将依赖项注入到微服务中,服务就能够直接引用依赖项,而不是在服务注册表中查找它们或使用服务定位器。封装任何服务、发现它并分发负载的能力对于微服务来说是一个非常有价值的补充。Typedi是 JavaScript 和 TypeScript 的依赖注入工具。使用 Typedi 非常容易。你所要做的就是创建一个容器,并开始在该容器上使用依赖注入原则。Typedi 提供各种注解,如@Service@Inject等。你甚至可以创建自己的自定义装饰器。

TypeORM

受 hibernate 和 doctrine 等框架的启发,Entity Framework TypeORMwww.npmjs.com/package/typeorm)是一个支持活动记录和数据映射器模式的 ORM 框架,不同于所有其他 JavaScript ORM。这使我们能够以最高效的方式编写高质量、松散耦合、可扩展和可维护的应用程序。它具有以下优势:

  • 使用多个数据库连接

  • 适用于多种数据库类型

  • 查询缓存

  • 钩子,如订阅者和监听器

  • 用 TypeScript 编写

  • 支持数据映射器和活动记录模式

  • 复制

  • 连接池

  • 流式原始结果(响应式编程)

  • 急切和懒惰的关系

  • 支持 SQL 和 NoSQL 数据库

应用程序目录配置

该应用程序的目录结构侧重于基于关注点分离的架构方法。每个文件夹结构将具有专门与文件夹名称相关的文件。在下面的截图中,您可以看到整体结构和详细结构:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

配置结构

在前面的屏幕截图中,您可以看到两个文件夹结构。第一个是高级和整体的文件夹结构,突出显示重要的文件夹,而第二个是src文件夹的详细扩展视图。文件夹结构遵循关注点分离的方法,以消除代码重复并在控制器之间共享单例服务。

在计算机科学中,关注点分离SoC)是将计算机程序分成不同的部分或功能的设计原则,以便每个部分都处理一个单独的关注点,并且独立于其他部分。关注点是影响任何应用程序代码的一组信息。

让我们了解我们的文件夹结构及其包含的文件,以及该文件夹实际上解决的问题。

src/data-layer

这个文件夹负责数据的整体组织、存储和访问方法。模型定义和 iridium 文件可以在这里找到。它包括以下文件夹:

  • 适配器:这实现了连接到 MongoDB 数据库的连接方法,并在连接、错误、打开、断开连接、重新连接和强制退出方法上添加事件

  • 数据抽象:这里有表示每个 MongoDB 集合结构的模式和表示集合中每组数据的文档

  • 数据代理:这里有针对每个 MongoDB 集合的数据存储的查询事务

  • 模型:这里有一个由 MongoDB 文档描述的数据的 TypeScript 类表示

src/business-layer

这个文件夹包含了服务层或中间件层所需的业务逻辑和其他资源的实现,如下所示:

  • 安全:如果我们想在特定的微服务级别上添加一些安全性或令牌,这就是我们将添加我们的身份验证逻辑的地方(通常,我们不会在单个服务级别编写身份验证层)。相反,我们会在 API 网关级别编写它,我们将在第五章中看到,理解 API 网关。在这里,我们将编写用于服务注册/注销、验证、内部安全、微服务与服务注册表、API 网关等通信的代码。

  • 验证器:这里将包含用于验证 API 请求发送的数据的模式和处理逻辑。我们将在这里编写我们的 class-validator (www.npmjs.com/package/class-validator) 模式,以及一些自定义验证函数。

src/service-layer

这个文件夹包括建立 API 端点的过程,以路由的形式处理所有数据请求的响应。它包括以下文件夹:

  • 控制器:这用作处理与路由相关的任何数据请求的基础。自定义控制器npm模块routing-controllerswww.npmjs.com/package/routing-controllers)提供,使用内置装饰器,如@Get@Put@Delete@Param等。这些函数实现了基本的 GET、POST、DELETE 和 PUT 方法,用于通过 RESTful API 与数据库进行交互。我们甚至可以有套接字初始化代码等。我们将使用依赖注入来注入一些服务,这些服务将在这里使用。

  • 请求:这里有 TypeScript 接口定义和展示控制器中每种不同请求类型的属性。

  • 响应:这里有 TypeScript 接口定义和展示控制器中每种不同响应类型的属性。

src/middleware

这包含了任何服务器配置的资源,以及一个可以在整个应用程序中共享的某些实用程序过程的存储位置。我们可以有集中的配置,比如loggercacheelk等等:

  • common:这里有一个日志记录模块的实例化,可以在整个应用程序中共享。这个模块基于winston (www.npmjs.com/package/winston)。

  • config:这里有特定于供应商的实现。我们将在这里定义 express 配置和 express 中间件,以及组织 REST API 端点的所有重要配置。

  • custom-middleware:这个文件夹将包含我们所有自定义的中间件,我们可以在任何控制器类或任何特定方法中使用它们。

在下一节中,我们将查看一些配置文件,这些文件配置和定义了应用程序,并确定它将如何运行。例如,它将运行在哪个端口,数据库连接到哪个端口,安装了哪些模块,编译配置等等。

配置文件

让我们看一些我们将在整个项目中使用的配置文件,并使用它们来管理不同环境下的项目或根据用例:

  • default.json:Node.js 有一个很棒的模块,node-config。你可以在package.json旁边的config文件夹中找到config文件。在这里,你可以有多个配置文件,可以根据环境进行选择。例如,首先加载default.json,然后是{deployment}.json,依此类推。以下是一个示例文件:
{
    "express": {
        "port": 8081,
        "debug": 5858,
        "host": "products-service"
    }
 }
}
  • src/Index.ts:这将通过创建一个在middleware/config/application中定义的应用程序的新对象来初始化我们的应用程序。它导入了反射元数据,初始化了我们的依赖注入容器。

  • package.json:这在所有 Node.js 应用程序中作为清单文件。它将外部库分为两个部分,dependenciesdevDependencies。这提供了一个scripts标签,其中包含用于构建、运行和打包模块的外部命令。

  • tsconfig.json:这为 TypeScript 提供了选项,当它执行转换为 JavaScript 的任务时。例如,如果我们有sourceMaps:true,我们将能够通过生成的 sourcemaps 调试 TypeScript 代码。

  • src/data-layer/adapters/MongoAccess.ts:这将连接到 MongoDB 数据库,并附加到 MongoDB 的各种事件的各种事件处理程序,比如openconnectederrordisconnectedreconnected等等:

export class MongooseAccess {
  static mongooseInstance: any;
  static mongooseConnection: Mongoose.Connection;
  constructor() {
    MongooseAccess.connect();
  }
  static connect(): Mongoose.Connection {
    if (this.mongooseInstance) {
      return this.mongooseInstance;
    }
    let connectionString = config.get('mongo.urlClient').toString();
    this.mongooseConnection = Mongoose.connection;
    this.mongooseConnection.once('open', () => {
      logger.info('Connect to an mongodb is opened.');
    });
    //other events
  }
  • src/middleware/config/Express.ts:这是我们的 express 中间件所在的地方。我们将附加标准配置,比如helmetbodyparsercookieparsercors origin等等,并设置我们的controllers文件夹如下:
setUpControllers(){
  const controllersPath = 
       path.resolve('dist', 'service-layer/controllers');
  useContainer(Container);
  useExpressServer(this.app,
    {
      controllers: [controllersPath + "/*.js"],
      cors: true
    }
  );
}

处理数据

与大多数接受并处理来自客户端的请求的 Web 服务器一样,我们在这里有一个非常相似的东西。我们只是在宏观层面上将事物细分。整个流程的概述如下图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传处理数据

通过将任何示例端点通过前面图表中的每个部分来理解该过程。你可以在chapter-4/products-catalog service中找到整个示例:

  1. 向服务器发送一个基于产品属性的特定产品的 API 请求,http://localhost:8081/products/add-update-product
body: {//various product attributes}
  1. 使用/products路径注册的控制器捕获基于URI /products/的请求。如果在Express.ts中注册了中间件,它将首先被触发;否则,将调用控制器方法。注册中间件很简单。创建一个中间件类,其中包含以下代码:
import { ExpressMiddlewareInterface } from "routing-controllers";
export class MyMiddleware implements ExpressMiddlewareInterface {

  use(request: any, response: any, next?: (err?: any) => any): any {
    console.log("custom middleware gets called, here we can do anything.");
    next();
  }
}
  1. 要在任何控制器中使用此中间件,只需在任何方法/控制器的顶部使用@UseBefore@UseAfter装饰器。

  2. 由于我们想执行一些核心逻辑(例如从缓存中选择响应或记录),因此middleware函数首先执行。这位于middleware/custom-middleware/MyMiddleWare.ts中。使用 Node.js 的async功能,该方法将执行必要的操作,然后继续进行下一个请求,使用next()

  3. 在自定义中间件中,我们可以进行各种检查;例如,我们可能只想在存在有效的ownerId时才公开 API。如果请求没有有效的ownerId,则请求将不再通过应用程序的其余部分,并且我们可以抛出一个错误,指示真实性或无效的productId。但是,如果ownerId有效,则请求将继续通过路由进行。这是MyMiddleWare.ts的作用。接下来将介绍控制器的部分。

  4. 接下来是由路由控制器提供的装饰器定义的@JsonControllers。我们定义了我们的路由控制器和用于添加和更新产品的 post API:

@JsonController('/products')
@UseBefore(MyMiddleware)
export class ProductsController {
  constructor() { }

  @Put('/add-update-product')
  async addUpdateProduct( @Body() request: IProductCreateRequest,
    @Req() req: any, @Res() res: any): Promise<any> {
    //API Logic for adding updating product
  }
}

这将为API <host:url>/products/add-update-product创建一个 PUT 请求。@Body注释将请求体的转换转换为IProductCreateRequest (src/service-layer/request/IProductRequest.ts),并将其放入变量请求(如在addIpdateProduct方法的参数中所见),该变量将在整个方法中可用。requestresponses文件夹包含各种requestresponse对象的转换。

  1. 控制器的第一部分是验证请求。验证和安全逻辑将位于src/business-layer文件夹中。在validator文件夹中,我们将有ProductValidationSchema.tsProductValidatorProcessor.ts。在ProductValidationSchema.ts中,使用class-validatorwww.npmjs.com/package/class-validator)内置装饰器(@MinLength, @MaxLength, @IsEmail等)添加验证模式规则(通过这些验证消息,我们希望识别请求是否正确或是否包含垃圾数据):
export class ProductValidationSchema {
  @Length(5, 50)
  name: string;

  @MinLength(2, { message: "Title is too Short" })

  @MaxLength(500, { message: "Title is too long" })
  description: string;

  @Length(2, 15)
  category: string;

  @IsEmail()
  feedbackEmail: string;
  //add other attributes.
}
  1. 接下来,我们将使用这些消息来验证我们的请求对象。在ProductValidationProcessor.ts中,创建一个验证器方法,返回一个合并的消息数组:
async function validateProductRequest(productReqObj: any): Promise<any> {
  let validProductData = new ProductValidationSchema(productReqObj);
  let validationResults = await validate(validProductData);
  let constraints = []
  if (validationResults && validationResults.length > 0) {
    forEach(validationResults,
      (item) => {
        constraints.push(pick(item, 'constraints', 'property'));
      });
  }
  return constraints;
}
  1. ProductsController.ts中,调用该方法。如果请求中存在错误,则请求将在那里停止,并且不会传播到 API 的其余部分。如果响应有效,则它将通过数据代理传递数据到 MongoDB:
let validationErrors: any[] = await validateProductRequest(request);
logger.info("total Validation Errors for product:-", validationErrors.length);
if (validationErrors.length > 0) {
  throw {
    thrown: true,
    status: 401,
    message: 'Incorrect Input',
    data: validationErrors
  }
}
let result = await this.productDataAgent.createNewProduct(request);
  1. 当请求有效时,控制器ProductController.ts调用数据层中的ProductDataAgent.ts方法createNewProduct(..),以将数据放入 MongoDB。此外,基于 Mongoose 模式定义,它将自动维护重复检查条目:
@Put('/add-update-product')
async addUpdateProduct(@Body() request: IProductCreateRequest,
                       @Req() req: any, @Res() res: any): Promise < any > {
  let validationErrors: any[] = await validateProductRequest(request);
  logger.info("total Validation Errors for product:-", validationErrors.length);
  if(validationErrors.length> 0) {
    throw {
      thrown: true,
      status: 401,
      message: 'Incorrect Input',
      data: validationErrors
    }
  }
  let result = await this.productDataAgent.createNewProduct(request);
  if(result.id) {
    let newProduct = new ProductModel(result);
    let newProductResult = Object.assign({ product: newProduct.getClientProductModel() });
    return res.json(<IProductResponse>(newProductResult));
  }else{
    throw result;
  }
}

服务层中的控制器不仅通过用于协商与数据存储的查询的数据代理提供对数据层的访问,而且还提供了访问业务层以处理其他业务规则的入口,例如验证产品输入。ProductDataAgent.ts方法返回 MongoDB 返回的对象。它还有其他方法,例如deleteProductfindAllProductsfindProductByCategory等。

  1. 在与ProductDataAgent.ts中的数据存储完成交易后,以普通对象的形式返回一个承诺给ProductController.ts,指示失败或成功。当成功将产品添加到数据库时,将返回插入的对象以及 MongoDB 的ObjectID()。与产品相关的数据构造为ProductModel,并将解析为IProductResponseProductController.ts
async createNewProduct(product: any): Promise < any > {
  let newProduct = <IProductDocument>(product);
  if(newProduct.id) {
    let productObj = await ProductRepo.findOne({ productId: newProduct.id });
    if (productObj && productObj.ownerId != newProduct.ownerId) {
      return { thrown: true, success: false, status: 403, message: "you are not the owner of Product" }
    }
  }
  let addUpdateProduct = await ProductRepo.create(newProduct);
  console.log(addUpdateProduct);
  if(addUpdateProduct.errors) {
    return { thrown: true, success: false, status: 422, message: "db is currently unable to process request" }
  }
  return addUpdateProduct;
}

如果在ProductDataAgent.ts中处理查询时发生了一些意外,比如与数据存储的连接中断,将返回一个错误消息形式的结果。如果同名对象已经存在,将抛出类似的错误响应。

这完成了数据如何在应用程序中流动的示例。基于许多后端应用程序和交叉因素,这是为了实现流畅的流程并消除冗余代码而设计的。

同样,该项目将具有其他 API,如下所示:

  • 通过 GET 请求获取所有产品

  • 通过 ID 获取产品的 GET 请求

  • 通过 GET 请求按产品类型获取产品

  • 通过 DELETE 请求删除单个产品

准备好提供服务(package.json 和 Docker)

在本节中,我们将看看如何在package.json中编写脚本,然后使用 Docker 自动化整个过程。

package.json

现在我们知道数据如何流动,让我们了解如何使其准备好提供服务。安装 TypeScript 和rimraf作为依赖项,并在scripts标签内添加以下内容:

"scripts": {
  "start": "npm run clean && npm run build && node ./dist/index.js",
  "clean": "node ./node_modules/rimraf/bin.js dist",
  "build": "node ./node_modules/typescript/bin/tsc"
},

要运行整个过程,请执行以下命令:

npm run start

这将首先删除dist文件夹(如果存在),然后基于src文件夹,它将转译文件夹并生成dist文件夹。一旦生成了dist,我们就可以使用node ./dist/Index.jsnpm run start的组合来运行我们的服务器。

在后面的章节中,我们将在这里做更多的事情,包括测试覆盖和生成 swagger 文档。我们的构建脚本应该涵盖以下内容:

  • 通过swagger-gen生成文档

  • 调用Express.ts,其中将配置所有路由以及中间件和依赖注入

  • tsc命令将使用tsconfig.json中的“outputDirectory”:“./dist”属性将 TypeScript 文件转译为 JavaScript 文件,以确定 JavaScript 文件应放置的位置

  • SwaggerUI 将生成文档,可在网络上使用

现在,要测试 API,请创建以下顺序的产品 JSON,并使用以下有效负载进行 POST 请求:

{"desc":[{"lang":"en","val":"TypescriptMicroservicesByParthGhiya."}],"name":"TypescriptMicroservices","category":"Microservices","brand":"PACKT","shipping":{"dimensions":{"height":"13.0","length":"1.8","width":"26.8"},"weight":"1.75"},"attrs":[{"name":"microservices","value":"exampleorientedbook"},{"name":"Author","value":"ParthGhiya"},{"name":"language","value":"Node.js"},{"name":"month","value":"April"}],"feedbackEmail":"ghiya.parth@gmail.com","ownerId":"parthghiya","description":"thisistestdescription"}

您将看到一个成功的响应,响应代码为 200,以及 MongoDB 的 ObjectId。它看起来像这样:“id”:“5acac73b8bd4f146bcff9667”。

这是我们将如何编写我们的微服务的一般方法。它向我们展示了控制分离的更多行为,以及如何使用 TypeScript 和一些企业设计模式来实现它,薄控制器位于服务层,依赖于业务层和数据层的引用,以实现可以消除冗余代码并使控制器之间共享服务的过程。同样,您可以基于相同的方法编写无数的服务。假设您想编写一个支付微服务,您可以使用typeorm模块进行 SQL 操作,并具有相同的代码结构。

Docker

现在我们的应用程序已经启动运行,让我们将其容器化,这样我们就可以将我们的镜像推送给任何人。诸如 Docker 之类的容器帮助我们打包整个应用程序,包括库、依赖项、环境以及应用程序运行所需的任何其他内容。容器很有用,因为它们将应用程序与基础架构隔离开来,这样我们就可以轻松地在不同平台上运行它,而不必担心我们正在运行的系统。

我们的目标如下:

  1. 通过运行docker-compose up来启动我们的产品目录微服务的工作版本,Mongo 微服务

  2. Docker 工作流应该是我们使用包括转译和服务dist文件夹的 Node.js 工作流

  3. 使用数据容器初始化 MongoDB

所以,让我们开始吧。我们将创建我们的container文件,并通过执行以下步骤在其中编写启动脚本。您可以在Chapter 4/products-catalog -with-docker文件夹中找到源代码:

  1. 首先,创建.dockerignore文件以忽略我们不希望出现在构建容器中的内容:
Dockerfile
Dockerfile.dev
./node_modules
./dist
  1. 现在,我们将编写我们的Dockerfile。镜像由我们在Dockerfile中定义的一组层和指令组成。我们将在这里初始化我们的 Node.js 应用程序代码。
#LATEST NODE Version -which node version u will use.
FROM node:9.2.0
# Create app directory
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app
#install dependencies
COPY package.json /usr/src/app
RUN npm install
#bundle app src
COPY . /usr/src/app
#3000 is the port which we want to expose for outside container world.
EXPOSE 3000 
CMD [ "npm" , "start" ]
  1. 我们已经完成了 Node.js 部分。现在,我们需要配置我们的 MongoDB。我们将使用docker compose,这是一个用于运行多个容器应用程序的工具,它将启动并运行我们的应用程序。让我们添加一个docker-compose.yml文件来添加我们的 MongoDB:
version: "2"
services:
  app:
    container_name: app
    build: ./ 
  restart: always
    ports:
      - "3000:8081"
    links:
      - mongo
  mongo:
    container_name: mongo
    image: mongo
    volumes:
      - ./data:/data/db
    ports:
      - "27017:27017"

在单个容器内运行多个容器是不可能的。我们将利用 Docker Compose up 工具(docs.docker.com/compose/overview/),可以通过运行sudo curl -L https://github.com/docker/compose/releases/download/1.21.0/docker-compose-$(uname -s)-$(uname -m) -o/usr/local/bin/docker-compose来下载。我们将在第九章中查看docker compose部署、日志和监控

分解此文件后,我们看到以下内容:

  • 我们有一个名为app的服务,为产品目录服务添加了一个容器。

  • 我们指示 Docker 在容器自动失败时重新启动容器。

  • 构建应用程序服务(我们的 TypeScript Node.js 应用程序),我们需要告诉Dockerfile的位置,它可以找到构建说明。build ./命令告诉 Docker,Dockerfiledocker-compose.yml在同一级别。

  • 我们映射主机和容器端口(这里我们保持两者相同)。

  • 我们已经添加了另一个服务 Mongo,它从 Docker Hub 注册表中拉取标准的 Mongo 镜像。

  • 接下来,我们通过挂载/data/db和本地数据目录/data来定义数据目录。

  • 这将具有类似于启动新容器时的优势。Docker compose 将使用先前容器的卷,从而确保没有数据丢失。

  • 最后,我们将应用程序容器链接到 Mongo 容器。

  • 端口3000:8081基本上告诉我们,Node.js 服务暴露给外部容器世界可以在端口3000访问,而在内部应用程序在端口8081上运行。

  1. 现在,只需在父级别打开终端并输入以下命令:
docker-compose up

这将启动两个容器并聚合两个容器的日志。我们现在已经成功地将我们的应用程序 Docker 化。

  1. 运行docker-compose up将给出一个错误,无法连接到 MongoDB。我们可能做错了什么?我们通过docker-compose选项运行多个容器。Mongo 在其自己的容器内运行;因此,它无法通过localhost:27017访问。我们需要更改我们的连接 URL,将其指向 Docker 服务而不是 localhost。在default.json中更改以下行:
"mongo":{"urlClient": "mongodb://127.0.0.1:27017/products"}, to 
"mongo":{"urlClient": "mongodb://mongo:27017/products"}
  1. 现在,运行docker-compose up,您将能够成功地启动和运行服务。

通过将我们的微服务 Docker 化,我们已经完成了开发和构建周期。在下一节中,我们将快速回顾我们到目前为止所做的工作,然后转向下一个主题,微服务最佳实践

概要

在本节中,我们将快速查看我们使用的一些模块,并描述它们的目的:

routing-controllers具有各种选项,基于 ES6。它有许多装饰器,如@GET@POST@PUT,可以帮助我们设计无需配置的服务。
config从中我们可以根据不同环境编写各种文件的配置模块,从而帮助我们遵守十二要素应用程序。
typedi用作依赖注入容器。然后我们可以使用它将服务(@Service)注入到任何控制器中。
winston用于日志记录模块。
typeORM用 TypeScript 编写的用于处理关系数据库的模块。
mongoose用于处理 MongoDB 的流行 Mongoose ORM 模块。
cors为我们的微服务启用 CORS 支持。
class-validator用于根据我们配置的规则验证任何输入请求。

同样,基于这个文件夹结构和模块,我们可以创建支持任何数据库的任意数量的微服务。现在我们已经清楚了如何设计微服务,在下一节中我们将看一些微服务设计最佳实践。

微服务设计最佳实践

现在我们已经开发了一些微服务,是时候了解一些围绕它们的模式和设计决策。为了获得更广泛的视角,我们将看看微服务应该处理什么,以及不应该处理什么。在设计微服务时需要考虑许多因素,要牢记最佳实践。微服务完全是基于单一责任原则设计的。我们需要定义边界并包含我们的微服务。以下部分涵盖了需要考虑的所有因素和设计原则,以便有效地开发微服务。

建立适当的微服务范围

设计微服务的一个最重要的决定是微服务的大小。大小和范围对微服务设计有很大影响。与传统方法相比,我们可以说每个容器或执行单一责任的任何组件应该有一个 REST 端点。我们的微服务应该是面向领域的,其中每个服务都与该领域中的特定上下文绑定,并将处理特定的业务能力。业务能力可以定义为为实现业务目标而做出的贡献。在我们的购物车微服务系统中,付款、加入购物车、推荐产品和发货是不同的业务能力。每个不同的业务能力应该由一个单独的微服务实现。如果我们采用这种模式,我们将在我们的微服务列表中得到产品目录服务、价格目录服务、发票服务、付款服务等。如果有的话,每个技术能力应该作为单独的微服务捆绑在一起。技术能力不直接为实现业务目标做出贡献,而是作为支持其他服务的简化。一个例子包括集成服务。我们应该遵守的主要要点可以总结为:

  • 微服务应该负责单一的能力(无论是技术还是业务)

  • 微服务应该可以单独部署和扩展

  • 微服务应该由一个小团队轻松维护,并且可以随时替换

自我管理的功能

在确定微服务范围时的另一个重要因素是决定何时提取功能。如果功能是自给自足的,即它对外部功能的依赖很少,它处理给定的输出并产生一些输出。那么它可以被视为微服务边界,并作为单独的微服务保留。常见的例子包括缓存、加密、授权、认证等。我们的购物车有许多这样的例子。例如,它可以是一个中央日志服务,或者是一个价格计算微服务,接受各种输入,如产品名称、客户折扣等,然后根据促销折扣计算产品的价格。

多语言架构

支持多语言架构是产生微服务的一个关键需求。不同的业务能力需要不同的处理方式。"一刀切"的原则不再适用。需要不同的技术、架构和方法来处理所有的业务和技术能力。当我们规划微服务时,这是另一个需要注意的关键因素。例如,在我们的购物微服务系统中,产品搜索微服务不需要关系数据库,但是添加到购物车和支付服务需要 ACID 兼容性,因为在那里处理交易是一个非常独特的需求。

独立可部署组件的大小

分布式微服务生态系统将充分利用当前不断增长的 CI/CD 流程进行自动化。自动化各种步骤,如集成、交付、部署、单元测试、扩展和代码覆盖,然后创建可部署单元,可以让生活更轻松。如果我们在一个单一的微服务容器中包含太多东西,那将带来巨大的挑战,因为涉及到很多过程,比如安装依赖项、自动文件复制或从 Git 下载源代码、构建、部署,然后启动。随着微服务的复杂性增加,微服务的大小将增加,这很快会增加管理的麻烦。一个设计良好的微服务可以确保部署单元保持可管理性。

根据需要分发和扩展服务

在设计微服务时,根据各种参数对微服务进行分解是很重要的,比如对业务能力的深入分析,基于所有权的服务划分,松散耦合的架构等等。以这种方式设计的微服务在长期内是有效的,因为我们可以根据需求轻松扩展任何服务,并隔离我们的故障点。在我们的产品微服务中,大约 60%的请求将基于搜索。在这种情况下,我们的搜索微服务容器必须单独运行,以便在需要时单独扩展。Elasticsearch 或 Redis 可以在这个微服务之上引入,这将提供更好的响应时间。这将带来各种优势,比如成本降低,资源的有效利用,业务利益,成本优化等等。

敏捷

随着需求的动态变化,敏捷开发方法已经被广泛采用。在规划微服务时,一个重要的考虑因素是以每个团队可以开发饼图的不同部分的方式进行开发。每个团队构建不同的微服务,然后我们构建完整的饼图。例如,在我们的购物车微服务中,我们可以有一个推荐服务,专门针对用户的偏好和历史来定位受众。这可以通过考虑用户的跟踪历史、浏览器历史等来开发,这可能导致复杂的算法。这就是为什么它将作为一个单独的微服务开发,可以由不同的团队处理。

单一业务能力处理程序

与传统的单一责任原则有些偏离,一个微服务应该处理一个业务能力或技术能力。一个微服务不应该承担多个责任。根据设计模式,一个业务能力可以被划分为多个微服务。例如,在我们的购物车微服务中的库存管理中,我们可以引入 CQRS 模式来实现一些质量属性,其中我们的读写将分布在不同的服务容器中。当每个服务映射到一个有界上下文,处理一个业务能力时,更容易管理它们。每个服务可以作为单独的产品存在,针对特定的社区。它们应该是可重用的,易于部署等等。

适应不断变化的需求

微服务应该被设计成可以很容易地从系统中分离出来,而不需要进行大量的重写。这使我们可以很容易地添加实验性功能。例如,在我们的购物车微服务中,我们可以根据收到的反馈添加一个产品排名服务。如果服务不起作用或者业务能力没有达到预期,这个服务可以被抛弃或者很容易地替换为另一个服务。在这里,微服务的范围起着重要的作用,因为可以制作一个最小可行产品,然后根据需求添加或删除功能。

处理依赖和耦合

在确定服务范围的另一个重要因素是服务的依赖和引入的耦合。必须评估微服务中的依赖关系,以确保系统中没有引入紧密耦合。为了避免高度耦合的系统,将系统分解为业务/技术/功能能力,并创建功能依赖树。过多的请求-响应调用、循环依赖等因素可能会破坏微服务。设计健壮的微服务的另一个重要方面是具有事件驱动架构;也就是说,微服务应该在接收到事件后立即做出反应,而不是等待响应。

决定微服务中端点的数量

虽然这可能看起来是设计微服务时需要考虑的重要点,但实际上并不是设计考虑的一部分。微服务容器可以承载一个或多个端点。更重要的考虑是微服务的边界。根据业务或技术能力,可能只有一个端点,而在许多情况下,一个微服务可能有多个端点。例如,回到我们的购物车服务和库存管理中引入了 CQRS 模式,我们有单独的读和写服务,每个包含单个端点。另一个例子可以是多语言体系结构,我们可以有多个端点以便在各种微服务之间进行通信。我们通常根据部署和扩展的需求将服务分解为容器。对于我们的结账服务,所有服务都连接并使用相同的关系数据库。在这种情况下,没有必要将它们分解为不同的微服务。

微服务之间的通信风格

在设计微服务时需要考虑的另一个重要因素是微服务之间的通信方式。可以有同步模式(发送请求,接收响应)或异步通信模式(发送并忘记)。这两种模式都有各自的优缺点,以及它们可以使用的特定用例。为了拥有可扩展的微服务,需要结合这两种方法。除此之外,现在“实时性”是新的趋势。基于套接字的通信促进了实时通信。另一种划分通信风格的方式是基于接收者的数量。对于单个接收者,我们有基于命令的模式(如前几章中所见的 CQRS)。对于多个接收者,我们有基于事件驱动架构,它基于发布和订阅模式的原则,其中使用服务总线。

指定和测试微服务契约

合同可以被定义为消费者和提供者之间的一组协议、请求体、地址等协议,这有助于平滑地进行它们之间的交互。微服务应该被设计成可以独立部署,而不依赖于彼此。为了实现这种完全的独立性,每个微服务都应该有良好编写、版本化和定义的合同,所有它的客户(其他微服务)都必须遵守。在任何时候引入破坏性变化可能会成为一个问题,因为客户可能需要合同的先前版本。只有在适当的沟通之后,才能停用或关闭合同。一些最佳实践包括并行部署新版本,并在 API 中包含版本信息。例如,/product-service/v1,然后/product-service/v2。使用消费者驱动的合同CDCs)是测试微服务的现代方式之一,与集成测试相比。此外,在本书中,我们将使用 Pact JS 来测试我们的合同(第八章,测试、调试和文档)。

容器中微服务的数量

将您的微服务容器化是部署微服务的最推荐方式之一。容器为您的系统提供了灵活性,并简化了开发和测试体验。容器在任何基础设施上都是可移植的,并且也可以轻松地部署在 AWS 上。决定容器中可以包含多少微服务是至关重要的,并且取决于各种因素,例如容器容量、内存、选择性扩展、资源需求、每个服务的流量量等。基于这些事实,我们可以决定是否可以将部署合并在一起。即使服务被合并在一起,也必须确保这些服务是独立运行的,它们不共享任何东西。选择性扩展也是决定容器中微服务数量的关键因素之一。它应该是这样的,即部署是自管理的,例如 AWS Lambda。以下是可用的模式和每种模式的限制:

  • 每个虚拟机一个服务实例:在这里,您将每个服务打包为虚拟机镜像(传统方法),例如 Amazon EC2 EMI。在这里,每个服务都是一个单独的 VM,它在单独的 VM 镜像中启动:

  • 限制

  • 资源利用效率低

  • 您需要为整个 VM 付费;因此,如果您没有利用整个 VM,您将为无用的费用付费

  • 在服务上部署新版本非常慢

  • 管理多个 VM 很快就会成为一个巨大的痛苦和耗时的活动

  • 每个容器一个服务实例:在这里,每个服务都在自己的容器上运行。容器是一种便携式的虚拟化技术。它们有自己的根文件系统和便携式命名空间。您可以限制它们的 CPU 资源和内存。

  • 限制

  • 容器不像 VM 那样成熟

  • 处理负载的激增是一个额外的任务

  • 监控 VM 基础设施和容器基础设施又是一个额外的任务

  • 无服务器:最新的“无忧趋势”之一是无服务器架构,您可以将微服务打包为 ZIP 文件,并部署到无服务器平台,如 AWS Lambda。您只需根据所用时间和内存消耗对每个请求进行计费。例如,Lambda 函数是无状态的。

  • 限制

  • 这种方法不适用于长期运行的服务。一个例子是一个服务依赖于另一个服务或第三方代理。

  • 请求必须在 300 秒内完成。

  • 服务必须是无状态的,因为每个单独的实例都是为每个请求运行的。

  • 服务必须快速启动,否则它们将超时。

  • 服务必须在支持的语言中运行。例如,AWS Lambda 支持 Java、Node.js 和 Python。

微服务中的数据源和规则引擎

另一个重要因素是应用规则引擎,并在我们的分布式系统中决定数据源。规则是任何系统的重要部分,因为它们帮助我们管理整个系统。许多组织使用集中式规则引擎或遵循 BPMN 标准示例的工作流程,比如 Drools。嵌入式规则引擎可以放置在服务内部,也可以根据使用情况放置在服务外部。如果存在复杂规则,具有嵌入式引擎的中央编写存储库将是最佳选择。由于它是集中分布的,可能存在技术依赖性,规则在某些应用服务器边界内运行规则等。

业务流程建模符号(BPMN)是标准化符号,其目标是创建任何业务或组织流程的可视模型。在业务能力中,我们经常需要一个明确定义的工作流程,可以根据需求进行更改。我们从不硬编码任何流程或编写自己的引擎,而是利用 BPMN 工具进行操作。

就像规则引擎一样,在微服务中决定数据存储也是至关重要的。事务边界应该在我们定义的业务能力内设置。例如,在我们的购物车微服务中,在结账时,我们需要维护事务,并且我们可以选择关系型数据库作为数据源,以确保完整性并遵循 ACID 原则。然而,产品目录数据库没有任何事务,我们可以为其使用 NoSQL 数据库。

总结

在本章中,我们开始为购物车服务设计我们的微服务。我们根据技术、功能和业务能力分析了我们的需求,这些是微服务范围的主要驱动因素。我们设计了我们的模式,分析了我们的微服务结构,并在 Docker 上运行了它。最后,我们研究了一些微服务设计的最佳实践,并学习了如何根据我们的业务能力来确定微服务的范围。

在下一章中,我们将学习如何向我们的微服务引入网关,并了解网关解决的问题。我们将看到 API 网关如何解决分布式系统中的集中式问题。我们将熟悉一些 API 网关设计模式,并为购物车微服务设计我们的网关。

第五章:理解 API 网关

设计了一些微服务后,我们将在这里讨论微服务网关。与单片应用程序相比,微服务不通过内存调用进行通信,而是使用网络调用。因此,网络设计和实现在分布式系统的稳定性中起着重要作用。我们将揭示 API 网关,并了解它如何处理基于微服务的架构中的重要关注点。

本章将从理解 API 网关及其必要性开始。然后将讨论 API 网关处理的所有集中关注点,以及引入网关的好处和缺点。我们将为购物车微服务设计我们的网关,并查看网关的所有可用选项,并熟悉 API 网关中涉及的设计模式和方面。本章将讨论以下主题:

  • 揭示 API 网关

  • API 网关处理的关注点

  • API 网关设计模式

  • 断路器及其作用

  • 我们的购物车微服务中需要网关

  • 可用的网关选项

  • 为购物车微服务设计我们的网关

揭示 API 网关

随着我们深入微服务开发,我们看到前方有各种陷阱。现在我们的微服务已经准备就绪,当我们考虑客户端利用这些微服务时,我们将遇到以下问题:

  • 消费者或 Web 客户端在浏览器上运行。前端没有任何发现客户端,负责识别容器/VM 服务的位置,也不负责负载平衡。我们需要一个额外的拼图,它连接后端不同容器中的微服务,并将该实现从客户端抽象出来。

  • 到目前为止,我们还没有讨论像认证服务、版本化服务、过滤或转换任何请求/响应等集中关注点。经过反思,我们意识到它们需要一个中央控制点,可以在整个系统中应用,而无需在每个地方重新实现相同的逻辑。

  • 此外,不同的客户端可能有不同的合同要求。一个客户端可能期望 XML 响应,而另一个需要 JSON 响应。我们需要一个中心组件来处理路由请求,根据协议需求翻译响应,并根据需要组合各种响应。

  • 如果我们想独立按需扩展任何微服务,需要根据需要添加新实例,其位置应该对客户端进行抽象。因此,我们需要一个不断与所有微服务通信并维护注册表的中央客户端。此外,如果服务宕机,它应该通知客户端并在那里断开连接,从而防止故障传播。此外,它可以作为中央缓存管理的地方。

API 网关是一种解决所有上述问题的服务类型。它是我们微服务世界的入口点,并为客户端提供与内部服务通信的共享层。它可以执行路由请求、转换协议、认证、对服务进行速率限制等任务。它是治理的中心点,有助于实现以下各种事项:

  • 监控整个分布式移动系统,并相应地采取行动

  • 通过抽象实例和网络位置,以及通过 API 网关路由每个请求,将消费者与微服务解耦

  • 通过将可重用代码保存在一个地方,避免代码重复

  • 根据需要实现按需扩展,并从一个地方对故障服务采取行动

  • 定义 API 标准,例如 Swagger,Thrift IDL 等

  • 设计合同

  • 跟踪 API 的生命周期,包括版本化、利用率、监控和警报、限流等

  • 避免客户端和微服务之间的啰嗦通信

作为进入完全移动的分布式系统的单一入口点,很容易强制执行任何新的治理标准(例如,每个消费者都应该有 JWT 令牌),进行实时监控,审计,API 消费政策等。

JWT 令牌模式利用加密算法:令牌验证方法。在任何成功的身份验证之后,我们的系统生成一个具有 userID 和时间戳值的唯一令牌。将此令牌返回给客户端,需要在进一步的请求中发送。在接收任何服务请求时,服务器会读取并解密令牌。此令牌通常被称为JSON Web TokenJWT。为了防止跨站点请求伪造CSRF)等攻击,我们使用这种技术。

网关提供了灵活性,可以自由操纵微服务实例,因为客户端完全与此逻辑抽象。这是处理基于客户端设备的转换需求的最佳位置。网关充当缓冲区,防止任何形式的攻击。服务被污染,不会危及整个系统。网关通过满足所有这些标准来处理安全性,保密性,完整性和可用性。随着利益的增加,如果网关没有得到适当处理,也会有很多缺点。网关可能会引入指数级的复杂性,随着动态系统的增加,响应时间会增加。

在下图中,详细解释了 API 网关:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

现在我们知道网关的作用,让我们现在了解网关的基本知识以及它总体上处理的事情。

API 网关处理的问题

API 网关成为微服务架构中最重要的组件之一,因为它是处理核心问题的唯一位置。因此,在所有微服务实现中看到的常见实现是引入提供关键功能的 API 网关。此外,API 网关是连接到服务发现的部分,动态维护所有新添加服务的路由。在本节中,我们将研究网关功能,并了解我们中央操作层的整体架构的角色和影响。

安全性

随着分布的增加,自由度相当高。有很多移动服务,可能随时上升或下降。从安全性的角度考虑,当有很多移动部分时,事情可能出现相当大的问题。因此,需要一定的规则来管理安全性。因此,我们需要保护所有面向公众的 API 端点的远程服务调用。我们需要处理各种事情,如身份验证,威胁漏洞,授权,消息保护和安全通信。我们将添加 SSL/TLS 兼容的端点,以防范各种攻击,如中间人攻击,双向加密防止篡改。此外,为了处理 DDoS 攻击,API 网关将处理各种因素,如限制请求速率,按需连接数量等。网关将关闭慢速连接,黑名单或白名单 IP 地址,限制与其他后端微服务的连接,维护数据库连接数量等。API 网关将处理身份验证和授权等事项。我们可以在这里引入联邦身份,如OpenIDSAMLOAuth。此外,该层将生成 JWT 并验证所有请求。

微服务开发中的一个棘手部分是身份和访问管理。在大型企业中,通常通过 LDAP 等常见系统处理这一问题。联邦身份有点像授权服务器(它们在各种应用程序中使用:例如,您可以考虑将单个 Google 帐户链接到 Google 文档、Google Drive 等各种服务,然后授权用户并提供 ID 令牌和访问令牌)。著名的联邦身份提供者包括 OAuth 和安全断言标记语言SAML)。

愚蠢的网关

网关的最基本原则之一是网关始终是愚蠢的。在设计网关时,需要注意的一个重要方面是,网关不应过于雄心勃勃;也就是说,它不应包含非通用逻辑或任何业务需求。使其过于雄心勃勃会违背网关的目的,并且可能使其成为单点故障,并且也可能使其难以测试和部署。智能网关无法轻松进行版本控制或集成到大型管道中。此外,它引入了紧密耦合,因为当您升级网关时,通常必须处理升级其依赖项和与之相关的核心逻辑。

简而言之,API 网关应包含任何我们可以在其中验证或维护的内容,而无需其他服务或共享状态的帮助。除此之外的任何内容都应移出 API 网关。以下几点简要总结了 API 网关的愚蠢和其功能:

  • 像 JWT 令牌验证这样的验证(我们不请求任何外部服务)

  • 提高服务质量(例如缩小响应、HTTP 头缓存、发送缓存数据等)

  • 请求和响应操作(处理多种内容类型并相应操作请求和响应)

  • 与服务发现的交互(与服务注册表进行非阻塞交互以获取服务请求详细信息)

  • 速率限制和节流(隔离的功能)

  • 断路器(检测故障并相应处理)

转换和编排

我们已经很好地将我们的微服务划分为单一责任原则;然而,为了实现业务能力,我们经常需要结合多个微服务。例如,购买产品的人是支付微服务、库存微服务、运输微服务和结账微服务的混合体。就像 Linux 管道结合各种命令一样,我们需要类似的编排解决方案。这对消费者来说是至关重要的,因为逐个调用每个细粒度服务绝对是一场噩梦。以我们的购物车微服务为例。我们有以下两个微服务:

  • 受众定位:这些微服务接收用户信息并返回所有推荐的列表(它返回产品 ID 的列表)

  • 产品详情:这些微服务接收产品 ID 并通过提供产品元数据和详细信息来做出响应

假设我们正在为 20 个项目设计一个推荐页面。如果我们保持原样,那么消费者将不得不进行总共 21 次 HTTP 调用(1 次获取产品 ID 列表的调用,20 次获取产品详细信息的调用),这是一场噩梦。为了避免这种情况,我们需要编排器(可以组合所有这些 21 次调用的东西)。此外,微服务必须处理需要不同响应的不同客户端。API 网关是一个转换的地方,可以处理通信协议、响应格式、协议转换等所有事情。我们可以在 API 网关中放置诸如 JSON 到 XML 转换、HTTP 到 gRPC 或 GraphQL 协议转换等内容。

监控、警报和高可用性

微服务架构中有很多移动部件。因此,系统范围的监控和避免级联故障变得至关重要。API 网关为这个问题提供了一站式解决方案。我们可以监控和捕获所有数据流的信息,可以用于安全目的。我们可以监控健康、流量和数据。API 网关可以监控各种事物,如网络连接、日志维护、备份和恢复、安全性以及系统状态和健康状况。此外,API 网关还可以监控一些基本事物,如 API 的请求数、维护远程主机、浏览器、操作系统、性能统计、消息的堆栈跟踪、违反网关策略的违规行为等。API 网关可以集成警报工具,如 consul alerts (github.com/AcalephStorage/consul-alerts),并相应地采取适当的行动以实现高可用性。我们必须在负载均衡器后部署多个 API 网关实例,以有效地在多个 API 网关实例之间平衡流量。我们必须计划高容量和负载。如果部署在云中,我们可以启用自动扩展,如果没有,则必须确保它有足够的数据资源来处理未来的负载。

缓存和错误处理

为了实现最大的优化和性能,缓存经常被引入到分布式系统中。Redis 因为它轻量级并且可以很好地满足缓存的目的,因此得到了巨大的增长。此外,在某些业务能力中,可以容忍陈旧的数据,这是离线优先时代。API 网关可以处理这一部分,如果微服务宕机或者防止过多的数据库调用,提供缓存响应。设计缓存机制的黄金法则可以是那些实际上永远不需要进行的服务调用应该是最快的调用。例如,考虑 IMDB 中《复仇者联盟 4》页面的更新。它每秒都在获得超过 20,000 次的点击。

数据库受到这些访问的冲击,因为它还必须获取其他东西(如评论、评论等)。这就是缓存变得有用的地方。很少改变的东西,如演员描述、电影描述等,来自缓存层。返回的响应非常快,它节省了网络跳跃,也不会增加 CPU 性能。通过实施缓存层,API 网关确保用户体验不受影响。在分布式系统中,由于通信频繁,错误很可能发生,因此错误应该通过超时和断路器等模式得到适当处理,这些模式应该提供缓存响应。

我们可以在以下两个级别进行缓存管理:

  • 在 API 网关级别进行缓存: 选择这个选项,我们可以在网关或中央级别缓存服务响应。这样可以节省服务调用的优势,因为我们可以直接在网关级别返回数据。此外,在服务不可用或无响应的情况下,API 网关可以从缓存中返回数据。

  • 在服务级别进行缓存: 选择这个选项,每个服务都可以管理自己的缓存数据。API 网关不知道内部缓存或内部任何精确的东西。服务可以根据需要轻松地使缓存失效。然而,在实施这个选项时,我们应该在中央缓存级别准备好默认响应。

Netflix Hystrix 是一个非常有用的库,具有强大的选项,如超时调用超过特定阈值,不必要等待,定义回退操作,如返回默认值或从缓存返回值。它也有一个 Node.js 客户端 (www.npmjs.com/package/hystrixjs)。

服务注册和发现

微服务的一个关键优势是易于扩展。在任何时候,新的微服务都可以根据流量进行调整,可以进行扩展,并且现有的单片式可以分解为多个微服务。所有这些服务实例都具有动态分配的网络位置。API 网关可以维护与服务注册表的连接,该注册表可以跟踪所有这些服务实例。API 网关与包含所有实例的网络位置的数据库进行通信。每个服务实例在启动和关闭时都会告诉注册表其位置。与 API 网关连接的另一个组件是服务发现。消费各种微服务的客户端需要具有简单的发现模式,以防止应用程序变得过于啰嗦。

Consul 是最广泛使用的服务注册和发现工具之一。它知道特定服务有多少活动容器失败,如果该数字为零,它会将该服务标记为损坏。

有以下两种类型的方法:

  • 推送:微服务本身负责向 API 网关确认其入口

  • 拉取:API 网关负责检查所有微服务

断路器

API 网关处理的另一个重要问题是当服务宕机时断开连接。比如说一个微服务宕机并开始抛出大量错误。排队进一步请求该微服务是不明智的,因为它很快就会有很高的资源利用率。在这里引入的 API 网关可以实现诸如断开连接或者简单地说当某个阈值被超过时,网关将停止向该失败组件发送数据,直到组件被解决,分析日志,实施修复,推送更新,从而防止整个系统中的故障级联。因此,扩展底层和流行的微服务变得非常容易。网关因此可以水平和垂直扩展。API 网关通过以滚动方式部署配置来实现零停机时间,也就是说,在新部署时,电路被触发,新请求不会被服务,旧请求在单个集群中被接受,同时另一个集群接受新请求。我们将在第七章中看到断路器的实时示例,服务状态和服务间通信

版本控制和依赖解析

当微服务非常细粒度并且基于单一职责原则设计时,它们只处理特定的问题,因此它们变得啰嗦(太多的网络调用):也就是说,为了执行一组常规任务,需要向不同的服务发送许多请求。网关可以提供虚拟端点或外观,可以在内部路由到许多不同的微服务。API 网关可以解析所有依赖关系,并将所有响应分离成一个单一的响应,从而使客户端易于消费。此外,随着不断变化的业务需求和能力,我们需要保持版本控制,因此在任何时候,我们都可以回到旧服务。

API 版本控制有两种方式进行管理——一种是通过在 URI 中发送(URI 不要与 URL 混淆,它是包含信息的统一资源标识符,例如http://example.com/users/v4/1234/),另一种是通过在标头中发送。API 网关可以通过以下两种方式处理这个问题:

  • 微服务发现:这是最广泛使用的模式,其中微服务和客户端应用程序之间的耦合完全消除,因为微服务是动态注册的(我们将在下一章中更详细地看到这一点)。这个组件直接与 API 网关联系,并向其提供有关服务位置的信息,从而防止传统的 SOA 单片式方法。

  • 微服务描述:另一方面,这种方法更注重通过合同进行通信。它以非常详细的描述性合同表达微服务的特性,这些合同可以被其他客户端应用程序理解。合同还包含元数据信息,如 API 版本、要求等。

在这一部分,我们看了 API 网关副处理的所有关注点。对于 API 网关,应特别注意以下几个方面:

  • 它不应该是单点故障

  • 它不应该是集中化的或具有同步协调

  • 它不应该依赖于任何状态

  • 它应该只是另一个微服务

  • 业务逻辑不应该封装在内部

API 网关设计模式和方面

现在我们知道 API 网关处理什么,让我们现在看看 API 网关涉及的常见设计方面。在这一部分,我们将看看在设计 API 网关时需要考虑的所有设计方面。我们将了解设计 API 网关的模式,这将帮助我们设计一个具有高可用性的可扩展系统。

作为处理集中关注点并且是微服务的起点的核心部分,API 网关应该被设计成:

  • 它支持并发性:由于基于单一责任的设计而具有高度分布性,需要服务器端并发性,这可以减少网络通信。Node.js 是非阻塞和异步的,每个请求都与其他请求并行执行,因此单个重型客户端请求并不比许多轻量级非并发请求好多少。虽然业务用例可能需要对后端系统进行阻塞调用,但 API 网关应该通过响应式框架以高效的方式组合这些调用,这不会增加资源池的利用率。

  • 它应该是反应式的:反应式编程提供了各种操作符,能够过滤、选择、转换、组合和组合可观察对象,从而在 API 网关层实现高效的执行和组合。它提倡随时间填充的变量的概念。它提倡非阻塞架构,因为在可观察模式中,生产者只是在值可用时向消费者推送值,而不是在那段时间内阻塞线程。值可以在任何时间点异步或同步到达。它还有额外的优势,比如生产者可以向消费者发出结束信号,告诉消费者没有更多的数据,或者发生了错误。

  • 服务层遵循可观察模式:当 API 网关中的所有方法都返回Observable<T>时,默认启用并发性。服务层然后遵循诸如根据条件返回缓存响应以及如果资源不可用或服务不可用,则阻止请求等操作。这可以在不改变客户端端的情况下发生。

  • 它处理后端服务和依赖关系:由于网关在虚拟外观层后面抽象了所有后端服务和依赖关系,因此任何入站请求都可以访问业务能力而不是整个系统。这将允许我们在对依赖它的代码影响有限的情况下更改底层实现。因此,服务层确保所有模型和紧密耦合保持内部,并且被抽象化并且不允许泄漏到端点中。

  • 它们应该是无状态的:API 网关应该是无状态的,这意味着不创建任何会话数据。这将使我们能够扩展网关,因为在灾难情况下不需要在以后复制会话。但是,API 网关可以维护缓存数据,可以使用点对点关系复制这些数据,或者引入缓存库(如 Redis)而不是进行内存调用。以下是一些常见陷阱的一般指导方针:

  • 为了实现最佳的可用性,API 网关应该在主动-主动模式下使用。这意味着系统应该始终保持完全运作,并能够维持当前的系统状态。

  • 适当的分析和监控工具以防止消息洪泛。在这种情况下,对该服务的流量应该受到限制。

  • 使用工具不断监视系统,可以通过一些可用的工具、系统日志或网络管理协议。

主动-主动模式是一种处理故障转移、负载平衡和保持系统高度可用性的方法。这里使用两个或更多服务器,它们聚合网络流量负载,并一起工作作为一个团队将其分配给可用的网络服务器。负载均衡器还会持久保存信息请求,并将此信息保存在缓存中。如果它们返回寻找相同的信息,用户将直接锁定到之前提供其请求的服务器上。这个过程大大减少了网络流量负载。

断路器及其作用

在实际世界中,错误确实会发生。服务可能会超时,变得无法访问,或者需要更长时间才能完成。作为一个分布式系统,整个系统不应该崩溃。断路器是解决这个问题的方法,它是 API 网关中非常重要的组件。

该模式基本上分为两种状态。如果电路关闭,一切正常,请求被分派到目的地,接收到响应。但如果有错误或超时,电路就会打开,这意味着该路由目前不可用,我们需要采用不同的路线或方式来实现服务请求。为了实现这个功能,Netflix 开源了他们的项目——Hystrix。然而,这是同样的 Node.js 版本:www.npmjs.com/package/hystrixjs(这不是 Netflix 官方的,而是一个开源项目)。它甚至有用于监控目的的 Hystrix 仪表板。根据 Hystrix 的库,它具有以下功能:

  • 保护系统免受因网络问题或任何第三方客户端或库而发生的任何故障

  • 停止传播失败,避免错误的扩散

  • 快速失败,经常失败,更好地失败,向前失败,并迅速恢复与对策

  • 使用回退机制来降级失败,比如从缓存中返回响应

  • 提供监控目的的仪表板

看一下下面的图表:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

断路器遵循与原始Hystrix模块相同的一套规则。为了计算命令的健康状况,执行以下步骤:

  1. 在整个电路中保持对音量的监控如下:
  • 如果电路中的网络音量没有超过预定义值,那么 Hystrix 可以简单地执行运行函数,而根本不需要比较任何东西。度量可以记录所有这些情况以供将来参考。

  • 如果电路中的网络音量超过配置的边界值,Hystrix 可以首先检查健康状况以采取预防措施。

  • 在检查健康状况时,如果错误百分比超过预定义的阈值,电路的转换会从关闭到打开,所有后续的请求都将被拒绝,以防止进一步的请求。

  1. 经过一段时间的组织,Hystrix 可以允许一个请求通过以检查服务是否已恢复。如果它通过了期望的测试,电路再次转换为关闭状态,并且所有计数器被重置。要在应用程序中使用它,只需创建服务命令并添加值:
var serviceCommand = CommandsFactory.getOrCreate("Service on port :"+ service.port +":"+ port)
 .circuitBreakerErrorThresholdPercentage(service.errorThreshold)
 .timeout(service.timeout)
 .run(makeRequest)
 .circuitBreakerRequestVolumeThreshold(service.concurrency)
 .circuitBreakerSleepWindowInMilliseconds(service.timeout)
 .statisticalWindowLength(10000)
 .statisticalWindowNumberOfBuckets(10)
 .errorHandler(isErrorHandler)
 .build();
 serviceCommand.service = service;
 commands.push(serviceCommand);
  1. 要执行这些命令,只需使用 execute 方法。在hystrix文件夹中的源代码中可以找到完整的要点。

我们购物车微服务中网关的需求

在详细解释网关之后,让我们回到我们的购物车微服务系统。我们将看看我们系统中网关的需求以及它将处理的内容,然后继续设计网关。在本节中,我们将看看在设计网关时需要考虑的各种设计方面。

处理性能和可伸缩性

作为系统性能、可伸缩性和 API 网关高可用性的入口点,非常关键。因为它将处理所有请求,使其成为异步非阻塞 I/O 似乎非常合乎逻辑,这正是 Node.js 的特点。来自我们的购物车微服务的所有请求都需要经过身份验证、缓存、监控,并不断发送健康检查。考虑一个场景,我们的产品服务有大量流量。API 网关应该自动产生服务器的新实例并维护新实例的地址。然后新实例需要不断向网关发送健康检查,以便知道哪些实例是活着的。考虑之前我们看到的同样的例子,我们有产品微服务,我们需要向客户显示 20 个项目的详细列表。现在客户不会发出 21 个 HTTP 请求,而是我们需要一个核心组合组件,它将从各种请求中组合响应。

响应式编程提高胜算

为了确保我们不必频繁更改客户端代码,API 网关简单地将客户端请求路由到微服务。它可能通过进行多个后端服务调用来发出其他请求,然后聚合所有结果。为了确保最小的响应时间,API 网关应该同时进行独立调用,这就是响应式编程模型发挥作用的地方。在各种情况下都需要 API 组合,比如获取用户的过去订单,我们首先需要获取用户详情,然后获取他们的过去订单。使用传统的异步回调编写组合逻辑很快就会导致回调地狱的问题,这将产生耦合、混乱、难以理解和容易出错的代码,这就是响应式编程非常有帮助的地方。

调用服务

微服务确实需要根据业务能力同步或异步地相互通信。必须有进程间通信机制。我们的购物车微服务可以有两种通信模式。一种涉及消息代理,它排队消息并在可用时将它们发送到服务。另一种涉及无代理通信,服务直接与另一个服务通信,这可能会导致数据丢失。有许多事件驱动的消息代理,如 AMQP、RabbitMQ 等,还有一些无代理的,如 Zeromq。

一些业务能力需要异步通信模式,比如在产品结账时,我们需要调用支付服务。只有成功支付,产品才能被购买。API 网关需要支持基于业务能力的各种机制。我们将在第七章中看到一个实时例子,服务状态和服务间通信,在NetFlix 案例研究部分。

发现服务

随着不断动态和发展的服务,我们的网关需要知道系统中每个微服务的位置(IP 地址、服务端口)。现在,这可以在系统中进行热插拔,但由于它们不断发展,我们需要更动态的方法,因为服务不断自动扩展和升级。例如,在我们的购物车微服务中,我们可能会根据用例不断添加新服务。现在 API 网关需要知道这些服务的位置,以便随时查询任何服务以返回响应给客户端。API 网关必须与服务注册表保持通信,服务注册表只是所有微服务位置及其实例的数据库。

处理部分服务故障

另一个需要解决的问题是处理部分故障。当一个服务调用另一个服务时,可能根本不会收到响应,或者可能会收到延迟的响应。随着服务数量的增加,任何服务都可能在任何时间点宕机。API 网关应能够通过实施以下一些/全部策略来处理部分故障:

  • 默认使用异步通信模式。仅在需要时使用同步模式。

  • 应处理多次重试,采用指数退避,即 1、2、4、16 等。

  • 定义良好的网络超时,以防止资源阻塞。

  • 断路器模式用于在服务宕机或过载时中断请求。

  • 回退或返回缓存值。例如,产品的图像不会经常更改,可以进行缓存。

  • 监控排队请求的数量。如果数量超过限制,那么发送进一步的请求就没有意义。

设计考虑

一个良好的 API 网关应遵循以下设计考虑,以便拥有坚固的微服务设计:

  • **依赖性:**不应依赖任何其他微服务。API 网关只是另一个微服务。如果任何服务 ID 在预先配置的时间内不可用或不遵循 SLA,则 API 网关不应等待该服务。它应该使用断路器或其他回退策略快速失败,如返回缓存响应。

  • **数据库和业务逻辑:**API 网关不应具有数据库连接。网关是愚蠢的,即它们没有任何状态。如果需要数据库,我们需要创建一个单独的微服务。同样,业务逻辑应该驻留在服务本身。网关只是将任何服务请求路由到适当的目的地。

  • **编排和处理多种内容类型:**服务编排(微服务相互通信的模式)应该在 API 网关而不是编排中完成。网关应连接到服务注册表,这样我们就可以得到动态移动服务的位置。

  • **版本控制:**网关应具有适当的版本控制策略。就像我们需要将一块巨大的岩石移到山上,但由于它太大,我们将岩石分成较小的块并分发给每个人。现在每个人都会按自己的步伐前进,但这并不意味着他必须满足其他人的期望,因为最终重要的是整块岩石而不是较小的块。同样,服务的任何特定版本都不应该破坏暴露的合同。新合同应根据需要进行更新,以便其他客户端了解新的期望,直到需要向后兼容性。

  • **高可用性:**它应该是高可用和可扩展的。应该为高容量和高负载进行规划。如果部署在云中,我们可以选择:AWS 自动扩展。

在下一节中,我们将深入研究可用的网关选项并进行详细讨论。我们还将查看一些云提供商选项,并了解每个选项的优缺点。

可用的 API 网关选项

现在让我们看一些可用的 API 网关的实际实现。在本节中,我们将看到 Express 网关、Netflix OSS、消息代理、NGINX 作为反向代理以及用于设计网关的工具等选项。

HTTP 代理和 Express 网关

HTTP 代理是用于代理的 HTTP 可编程库。这对于应用反向代理或负载平衡非常有帮助。npm 中可用的http-proxy每天的下载量超过 10 万次。为了实现请求分发,我们可以使用http-proxy。这很容易实现,可以像这样实现:

const express = require('express')
const httpProxy = require('express-http-proxy')
const app = express();
const productServiceProxy= httpProxy('https://10.0.0.1/') 
//10.0.0.1 is product container location
// Authentication
app.use((req, res, next) => {
    // TODO: Central Authentication logic for all
    next()
    })
// Proxy request
app.get('/products/:productId', (req, res, next) => {
    productServiceProxy(req, res, next)

Express 网关是基于 Express.js 和 Node.js 构建的网关之一,是最简单易用的,具有诸如面向微服务用例的语言不可知性和可移植性等广泛选项,因此可以在 Docker 中的任何地方(公共或私有云)运行。它可以与任何 DevOps 工具一起使用,并且配备了预打包的经过验证和流行的模块。我们可以使用任何 express 中间件来扩展它,这完全基于配置,配置会自动检测并进行热重载。以下是 Express 网关中的核心组件:

端点(API 和服务)它们只是 URL。Express 网关以两种形式维护它们。API 端点和服务端点。API 端点是公开的,并将 API 请求代理到服务中请求的微服务。
策略一组条件、操作或合同,用于评估并对通过网关的任何请求采取行动。中间件被利用。
管道一组与微服务相关联的策略,按顺序执行。对于策略执行,API 请求通过管道,最终遇到一个代理策略,该策略指导请求到服务端点。
消费者消费微服务的任何人。为了处理不同的消费者,Express 网关配备了一个消费者管理模块。其中的黄金法则是应用程序必须属于一个用户。
凭证认证和授权的类型。消费者或用户可能有一个或多个凭证。凭证与范围相关联。Express 网关配备了凭证管理模块。
范围用于分配授权的标签。保护端点的授权策略查看凭证,以确保系统的完整性,并且消费者具有相应的范围。

现在让我们看一个 Express 网关的示例。以下是使用 Express 网关生成的gateway.config.yml文件的示例(www.express-gateway.io/)。

http:
  port: 8990
  serviceEndpoints:
    example: # will be referenced in proxy policy
    url: 'http://example.com'
  apiEndpoints:
  api:
    path: '/*'
  pipelines:
  example-pipeline:
    apiEndpoints: # process all request matching "api" apiEndpoint
    - api
  policies:
  - jwt:
  - action:
  secretOrPublicKeyFile: '/app/key.pem'
  - proxy:
  - action:
  serviceEndpoint: example # reference to serviceEndpoints Section

上述配置是网关和代理路由中 JWT 的最简单示例,并且是不言自明的。

Zuul 和 Eureka

接下来我们要看的选项是 Netflix 提供的 Zuul 代理服务器。Zuul 是一个边缘服务,其目标是代理请求到各种后端服务。因此,它充当了消费服务的“统一前门”。Zuul 可以与 Netflix 提供的其他开源工具集成,例如 Hystrix 用于容错、Eureka 用于服务发现、路由引擎、负载平衡等。Zuul 是用 Java 编写的,但可以用于任何语言编写的微服务。Zuul 在以下方面提供了便利:

  • 验证每个资源的合同要求。如果合同未得到满足,则拒绝那些不符合要求的请求。

  • 通过跟踪有意义的数据和统计信息,为我们提供准确的生产视图。

  • 连接到服务注册表,并根据需要动态路由到不同的后端集群。

  • 为了逐渐增加集群中的流量来衡量性能。

  • 丢弃超出限制的请求,从而通过为每种类型的请求分配容量来实现负载分担。

  • 处理静态或缓存响应,从而防止内部容器的频繁访问。

Zuul 2.1 正在积极开发,旨在在网关级别实现异步操作。然而,Zuul 2 是非阻塞的,并且完全依赖于 RxJava 和响应式编程。要将 Zuul 作为 API 网关运行,请执行以下步骤:

  1. Zuul 需要 Java 环境。克隆以下 Spring boot 项目:github.com/kissaten/heroku-zuul-server-demo

  2. 使用mvn spring-boot:run启动项目。

  3. 在项目的src/main/resources/application.yml文件中,我们将编写我们的 Zuul 过滤器逻辑。

  4. 我们将在那里添加故障转移逻辑。例如考虑以下示例配置:

zuul:
  routes:
    httpbin:
      path: /**
      serviceId: httpbin
    httpbin:
    ribbon:
      listOfServers: httpbin.org,eu.httpbin.org
    ribbon:
eureka:
  client:
    serviceUrl:
    defaultZone:  
    ${EUREKA_URL:http://user:password@localhost:5000}/eureka/

此配置告诉zuul将所有请求发送到httpbin服务。如果我们想在这里定义多个路由,我们可以。然后,httpbin服务定义了可用服务器的数量。如果第一个主机出现故障,那么代理将故障转移到第二个主机。

下一章通过另一个 Netflix 库 Eureka 实现了服务发现。

API 网关与反向代理 NGINX

在本节中,我们将查看服务器级别可用的可能选项。反向代理(NGINX 或 Apache httpd)可以执行诸如验证请求、处理传输安全性和负载平衡等任务。NGINX 是在微服务网关级别使用反向代理的广泛工具之一。以下代码示例描述了使用反向代理、SSL 证书和负载平衡的配置:

#user gateway;
 worker_processes 1;
 events {worker_connections 1024;}
 http {
     include mime.types;
     default_type application/json;
     keepalive_timeout 65;
     server {
         listen 443 ssl;
         server_name yourdomain.com;
         ssl_certificate cert.pem;
         ssl_certificate_key cert.key;
         ssl_session_cache shared:SSL:1m;
         ssl_session_timeout 5m;
         ssl_ciphers HIGH:!aNULL:!MD5;
         ssl_prefer_server_ciphers on;
         location public1.yourdomain.com {proxy_pass  
         http://localhost:9000;}
         location public2.yourdomain.com {proxy_pass 
         http://localhost:9001;}
         location public3.yourdomain.com {proxy_pass   
         http://localhost:9002;}
     }
 }

上述配置在中心级别添加了 SSL 证书,并在三个域中添加了代理,并在它们之间平衡所有请求。

RabbitMQ

RabbitMQ 是最广泛部署的消息代理之一,它使用 AMQP 协议。Node.js 的amqplib客户端被广泛采用,每天的下载量超过 16,000 次。在本节中,我们将查看amqp的示例实现,并了解它提供的选项。RabbitMQ 更多地遵循基于事件的方法,其中每个服务都监听 RabbitMQ 的“tasks”队列,当监听到事件时,服务完成其任务,然后将其发送到completed_tasks队列。API 网关监听completed_tasks队列,当收到消息时,将响应发送回客户端。因此,让我们通过执行以下步骤设计我们的 RabbitMQ 类:

  1. 我们将定义我们的构造函数如下:
constructor(host, user, password, queues, prefetch) {
    super();
    this._url = `amqp://${user}:${password}@${host}`;
    this._queues = queues || [ 'default' ];
    this._prefetch = prefetch;
}
  1. 接下来,我们将定义我们的连接方法如下:
connect() {
    return amqp.connect(this._url)
    .then(connection => (this._connection =  
    connection).createChannel())
    .then(channel => {
        this._channel = channel;
        channel.prefetch(this._prefetch);
        var promises = [];
        for (var queue of this._queues) {
            promises.push(
            channel.assertQueue(queue, { durable: true })
            .then(result => channel.consume(result.queue, 
            (message) => {
                if (message != null) {
                    this.emit('messageReceived',  
                    JSON.parse(message.content), 
                    result.queue, message);
                }
            }, { noAck: false }))
            );
        }
        return Promise.all(promises);
    });
}
  1. 接下来,我们将有一个send方法,该方法将消息发送到 RabbitMQ 通道,如下所示:
send(queue, message) {
    var messageBuff = new Buffer(JSON.stringify(message));
    return this._channel.assertQueue(queue, { durable: true })
    .then(result => this._channel.sendToQueue(result.queue, 
    messageBuff, { persistent: true }));
}

您可以在此处查看完整文件,其中将找到所有可用选项。与之前的用例类似,您还可以在 definitely typed 存储库中找到amqp模块的类型gist.github.com/insanityrules/4d120b3d9c20053a7c6e280a6d5c5bfb

  1. 接下来,我们只需使用该类。例如,看一下以下代码:
...
constructor(){ this._conn = new RabbitMqConnection(host, user, password, queues, 1);}
...
addTask(queue, task) {
    return this._conn.send(queue, task);
}
...

作为此项目的先决条件,RabbitMQ 必须安装在系统上,这需要安装 Erlang。一旦 RabbitMQ 启动运行,您可以通过键入rabbitmqctl status来检查 RabbitMQ 服务是否正在运行。

设计我们的购物车微服务网关

在看到各种选项后,现在让我们动手开始实现购物车微服务的微服务网关。在本节中,我们将从头开始实现网关,该网关将具有从公共端点到内部端点的请求分派功能,从多个服务聚合响应,并处理传输安全性和依赖关系解析。在继续编码之前,让我们先看一下我们将在此模块中使用的所有概念。

我们将使用什么?

在本节中,我们将查看所有以下节点模块和概念,以便有效地构建我们的网关:

  • ES6 代理:一般来说,代理服务器是指作为客户端请求的中间服务器。ES6 中最强大和有趣的功能之一就是代理。ES6 代理在 API 消费者和服务对象之间充当中间人。当我们希望在访问基础目标对象的属性时获得自己想要的行为时,通常会创建代理。为了配置代理的陷阱,控制基础目标对象,我们使用处理程序函数。

  • NPM 模块 dockerode:它是用于 Docker 远程 API 的 Node.js 响应式模块。它具有一些不错的功能,如用于响应式编程的流、支持附加的多路复用和承诺以及基于回调的接口,便于编程。

  • 依赖注入:这是最重要的设计模式之一(最初在 Java 中开始,现在到处都有),其中一个或多个服务的依赖项被注入或通过引用传递给依赖对象。

请查看第五章的源代码,其中包括服务发现的自定义实现。在完成第六章后,您可以重新访问这个练习。

总结

在本章中,我们揭示了 API 网关。我们了解了引入 API 网关的利弊,以及 API 网关可以集中处理哪些问题。我们研究了 API 网关的设计方面,并了解了在我们的系统中需要 API 网关的原因。我们看了一下断路器以及为什么拥有它是至关重要的。我们研究了可用的网关选项,如 Zuul、Express Gateway、反向代理,并为购物车微服务设计了我们自己的网关。

在下一章中,我们将学习服务注册表和服务发现。我们将看到网关如何连接到服务发现,自动了解移动服务的位置。我们将看到服务可以注册的方式,并了解每种方法的利弊。我们将看到一些选项,比如 consul,并在我们的购物车微服务中实现它们。

第六章:服务注册表和发现

通过网关处理我们分布式系统的核心问题后,我们现在将在本章中讨论服务注册表和发现。我们拥有的服务越多,仅使用预定义端口来处理它们就变得越复杂。在上一章中,我们看到网关与服务注册表进行交互,后者在数据库中维护服务位置。客户端请求根据数据库中的信息分派到服务。在本章中,我们将看到服务注册表是如何填充的,以及服务、客户端和网关如何与之交互。

本章将从理解服务发现开始,了解动态维护服务注册表的方式,注册服务到注册表的不同方式以及每种方式的利弊。我们将了解维护服务注册表的端到端流程,以及根据注册表发现服务的方式。我们将看到设计服务注册表的可用选项,熟悉每个步骤,然后使用可用的最佳实践设计我们的动态服务注册表。在本章中,我们将研究以下主题:

  • 服务注册表的介绍

  • 服务注册表和发现的什么、为什么和如何

  • 服务发现模式

  • 服务注册表模式

  • 服务注册表和发现选项

  • 如何选择服务注册表和发现

服务注册表的介绍

在本节中,我们将看到服务发现的需求以及服务注册表的需求,并尝试理解服务注册表和发现之间的区别。我们已经设置了一些购物车微服务,但是核心依赖于静态的网络位置。我们的代码从配置文件中读取一个值,并在服务位置发生任何变化时,在我们的配置中进行更新。在实际世界中,很难维护这一点,因为服务实例是动态分配位置的。此外,服务实例根据自动扩展、故障处理和更新过程的需要动态变化,这些过程是在微服务世界中从消费者客户端中抽象出来的。因此,客户端需要使用更加强大的服务发现机制。

服务发现可以定义为:

在一个中心位置(API 网关或数据库)注册服务的完整端到端流程,并通过在服务注册表中查找来到达目标服务的消费。

在微服务世界中,不同的微服务通常分布在平台即服务(PaaS)环境中。基础设施通常是不可变的,因为我们通常有容器或不可变的虚拟机镜像。服务通常可以根据流量和预设的指标进行扩展或缩减。由于一切都是不断变化的,直到服务准备好被使用和部署之前,服务的确切地址可能是未知的。这种动态性是微服务世界中需要处理的最重要的方面之一。一个逻辑和显而易见的解决方案是将这些端点持久化在某个地方,这本身就是服务注册表的基础。在这种方法中,每个微服务都向一个中央代理(我们在第五章中看到的组件,理解 API 网关)注册,并提供有关该微服务的所有详细信息,如端点地址、合同细节、通信协议等。消费服务通常会查询代理以查找该服务的可用位置,然后根据检索到的位置调用它。这方面一些常见的选项包括 Zookeeper、Consul、Netflix Eureka 和 Kubernetes,我们很快将更详细地了解它们。

服务注册表和发现的什么、为什么和如何

在简要了解了服务注册表之后,我们将在本节中了解服务注册和发现的原因、目的和方法。从理解服务发现的需求开始,然后了解涉及该过程的过程和组件。

服务注册和发现的原因

无论我们选择哪种容器技术,在生产环境中,我们总是会有三四个主机和每个主机内的多个容器。一般来说,我们在所有可用主机上分发我们的服务的方式是完全动态的,取决于业务能力,并且可以随时更改,因为主机只是服务器,它们不会永远持续下去。这就是服务发现和注册的作用。我们需要一个外部系统来解决常见 Web 服务器的限制,始终关注所有服务,并维护 IP 和端口的组合,以便客户端可以无缝地路由到这些服务提供者。

为了理解服务注册和发现的需求,我们将举一个经典的例子。假设我们有 10 个产品目录微服务的实例在任意数量的节点上运行。现在,为了拥有一个弹性系统,有人需要跟踪这 10 个节点,因为每当需要消费产品目录服务时,至少应该有一个正确的 IP 地址或主机名可用,否则消费者必须查询一个中央位置,找到产品目录服务的位置。这种方法非常类似于 DNS,不同之处在于这只是用于内部服务之间的通信。大多数基于微服务的架构都是动态变化的。服务根据开发、折旧和流量进行扩展和缩减。每当服务端点发生变化时,注册表都需要知道这个变化。服务注册表就是为了维护关于如何到达每个服务的所有信息。

市场上有很多可用的工具来解决这个问题,作为架构师,我们需要根据我们的需求来决定合适的工具。我们需要考虑诸如可以做多少自动化以及我们对工具有多少控制等因素。从低级工具如 Consul 到高级工具如 Kubernetes 或 Docker swarm,都可以满足高级需求,比如负载均衡容器和容器调度能力。

服务注册和发现是如何工作的?

今天,服务注册和发现有三种基本方法:

  • 首先,最基本和初步的方法是使用现有的 DNS 基础设施。一个良好部署的 DNS 将是高度可用和分布式的。这种方法的例子包括httpdconfdsystemd等。在这种方法中,标准的 DNS 库被用作注册客户端。每个微服务条目在 DNS 区域文件中接收一个条目,并进行 DNS 查找以连接或定位微服务。另一种方法是使用诸如 NGINX 之类的代理,它们定期轮询 DNS 以进行服务发现。这种方法的优点包括语言不可知性:它可以与任何语言一起工作,几乎不需要或零改变。然而,它也有一些缺点,比如 DNS 不能提供实时视图,管理服务注册和注销时的新区域文件,以及维护此组件的高可用性以实现弹性。

  • 第二种方法更加动态,更适合使用一致性键值数据存储的微服务,比如 Hashicorp 的 Consul、Apache Zookeeper、etcd 等。这些工具是高度分布式系统。通过键值存储和边车模式,解决了在使用 DNS 时遇到的所有问题。这种方法旨在对任何编写代码的开发人员完全透明。开发人员可以使用任何编程语言编写代码,而不必考虑微服务如何与其他服务交互。它也有一些限制,比如边车仅限于主机的服务发现,而不是更精细的路由。它还通过引入额外的跳跃为每个微服务增加了额外的延迟。

  • 服务发现的最终方法是采用诸如 Netflix Eureka 之类的现成框架,专门为服务发现而设计和优化。这种模型直接向最终开发人员公开功能。

无论选择哪种工具,每个微服务都需要一个中央客户端进行服务发现通信,其主要功能是允许服务注册和解析。每当一个服务启动时,服务发现就会使用注册过程向其他服务表明其可用性。一旦可用,其他服务就会使用服务解析来定位网络上的服务。涉及的两个过程如下。

服务注册

在启动和关闭时,服务会自行注册,或者通过第三方注册,服务注册客户端还会发送持续的心跳,以便客户端知道服务是活动的。心跳是定期发送给其他服务的消息,表明服务正在运行并且活动。它们应该是异步发送或者作为基于事件的实现,以避免性能问题。其他方法包括不断轮询服务。服务注册阶段还负责设置服务的契约,即服务名称、协议、版本等。

服务解析

这是返回微服务的网络地址的过程。理想的服务发现客户端具有缓存、故障转移和负载均衡等关键功能。为了避免网络延迟,缓存服务地址至关重要。缓存层订阅来自服务发现的更新,以确保它始终是最新的。典型的微服务实现层部署在各个位置以实现高可用性;服务解析客户端必须知道如何根据负载可用性和其他因素返回服务实例的地址。

服务注册和发现的内容

在这一部分,我们将看一下服务注册和发现的内容。我们将看到服务注册的所有方面,并查看关于维护它的所有可能选项。

维护服务注册表

在这一部分,我们将看到消费者最终如何找到服务提供者。我们将看到所有可用的方法,并查看每个选项的利弊:

  • 通过套接字进行更新:定期轮询很快就会成为一个问题,因为消费者最不关心向发现服务注册自己,对于发现服务来说,维护消费者列表也变得困难。更好的解决方案是客户端与发现服务建立套接字连接,并持续获取所有服务更改的最新列表。

  • 服务发现作为代理:这更多是一个服务器端的实现,路由逻辑存在于发现服务中,使得客户端不需要维护任何列表。他们只需向发现服务发出出站请求,发现服务将请求转发给适当的服务提供者,并将结果返回给提供者。

及时的健康检查

有两种方法可以进行及时的发现健康检查。一种方法是服务应该向集中式发现服务发送消息,而另一种方法是发现服务向服务提供者发送请求:

  • **服务轮询注册器:**在这种方法中,服务提供者会定期向注册发现服务发送消息。发现服务会跟踪上次接收请求的时间,并且如果超过一定时间阈值,就会认为服务提供者已经失效。

  • **注册器轮询服务:**这是一种中央发现服务向服务提供者发送请求的方法。然而,这种方法的一个缺点是,集中式发现服务可能会因为执行太多的出站请求而耗尽资源。此外,如果服务提供者消失,那么注册器就必须进行大量的失败健康查找,这将是网络资源的浪费。

服务发现模式

发现是客户端视角下的服务注册的对应物。每当客户端想要访问一个服务时,它必须找到关于服务的详细信息,它的位置以及其他合同信息。这通常使用两种方法来完成,即客户端发现和服务器端发现。服务发现可以简要总结如下:

  • 微服务或消费者对其他服务的物理位置没有任何先验知识。他们不知道服务何时下线或另一个服务节点何时上线。

  • 服务广播它们的存在和消失。

  • 服务能够基于其他广播的元数据找到其他服务实例。

  • 实例故障会被检测到,并且任何对于失败节点的请求都会被阻止并作废。

  • 服务发现不是单点故障。

在本节中,我们将研究服务发现的模式,并了解每种模式的优缺点。

客户端发现模式

在使用客户端模式时,客户端或网关的职责是确定可用服务实例的位置,并在它们之间进行负载均衡。客户端查询服务注册表,这只是一组可用的服务实例,存储其响应,然后根据响应中的位置地址路由请求。客户端使用一些著名的负载平衡算法来选择一个服务实例,并向该实例发出请求。每当服务启动时,该服务实例的物理网络位置会在注册表中注册,并在服务关闭时注销。服务实例的注册会使用心跳机制、轮询或通过实时更新的套接字来刷新。

优势:

  • 该模式除了服务注册表之外相当静态,因此更容易维护

  • 由于客户端知道服务实例,客户端可以做出智能的、特定于应用程序的、情境依赖的负载均衡决策,比如不断使用哈希

痛点:

  • 客户端与服务注册表紧密耦合

  • 需要在每种服务客户端使用的编程语言和框架中实现客户端服务发现

一个著名的客户端注册工具是 Netflix Eureka。它提供了一个用于管理服务实例注册和查询可用实例的 REST API。可以在github.com/Netflix/eureka/wiki/Eureka-REST-operations找到完整的 API 列表和可用选项,其中包含所有可用的操作。

服务器端发现模式

对此的反对意见是为注册表单独设置一个组件,这就是服务器端发现模式。在这种方法中,客户端通过负载均衡器向服务发出请求。负载均衡器然后查询服务注册表,并将每个请求路由到可用的服务实例,以向消费者提供服务响应。这种方法的一个典型例子是内置的 AWS 负载均衡器。Amazon 弹性负载均衡器ELB)通常用于处理来自互联网的大量外部流量,并在传入流量中进行负载均衡,但 ELB 的用途远不止于此。ELB 也可以用于将流量负载均衡到虚拟机的内部流量。当客户端通过其 DNS 向 ELB 发出请求时,ELB 会将流量在一组注册的 EC2 实例或容器之间进行负载均衡。

维护服务器端发现的方法之一是在每个主机上使用代理。这个代理扮演着服务器端发现负载均衡器的角色。代理透明地将请求转发到服务器上任何地方运行的可用服务实例。Kubernetes 采用了类似的方法。一些可用的工具是 NGINX 和 Consul 模板。这些工具配置了反向代理并重新加载 NGINX 或 HAProxy 服务器。

服务器端发现模式的优势:

  • 在服务器端的客户端发现中,代码更简单,因为我们不必为每个服务编写发现代码,而且它完全与客户端抽象无关

  • 通过这种方法来处理负载均衡等功能

服务器端发现模式的缺点:

  • 路由器是另一个需要在服务器上维护的组件。如果环境是集群的,那么它需要在每个地方进行复制。

  • 除非路由器是 TCP 路由器,否则路由器应该支持诸如 HTTP、RPC 等协议。

  • 与客户端发现相比,它需要更多的网络跳数。

让我们在这个图表中看看这两种方法:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

客户端与服务器端服务发现

服务注册表模式

在分布式系统中发现服务的一个关键方面是服务注册表。服务注册表只是一个包含所有服务实例的网络位置的数据库。由于它包含关键信息,因此必须在高效的系统上保持高可用性并保持最新。根据系统客户端(在我们的情况下是 API 网关),我们甚至可以缓存从服务注册表获取的网络位置。然而,它必须每天更新,否则客户端将无法发现服务实例并按服务进行通信。为了保持高可用性,服务注册表由集群组成,其中使用复制协议来保持一致性。服务注册表保存微服务实例的元数据,其中包括实际位置、主机端口、通信协议等。微服务的启动和关闭过程会不断受到监控。在本节中,我们将看看服务注册表和常见的服务注册选项。我们将分析每种方法的优缺点。

所有服务实例必须在中央注册表中注册和注销,以建立一个容错系统。有各种方法来处理这个注册和注销的过程。一种选择是服务注册表提供端点,服务实例自行注册,即自注册。另一种选择是使用其他系统组件来管理服务实例的注册。让我们深入了解这两种模式的细节。

自注册模式

在使用自注册过程时,服务实例本身负责在服务注册表中注册和注销。此外,服务实例必须不断发送心跳请求,以便让注册表知道服务的状态。如果注册表没有收到心跳,它可以假定服务不再存在,并注销或停止监听该服务。自注册模式迫使微服务自己与服务注册表通信。每当服务启动或关闭时,它都必须与注册表通信,告知其状态。微服务处理单一关注点,因此在任何地方引入另一个关注点可能是额外的负担,可能看起来是一种反模式;但是,它的优势在于服务维护自己的状态模型,知道当前状态,即STARTINGAVAILABLESHUTDOWN,而不依赖于任何其他第三方服务。

自注册过程的一个著名例子是 Netflix OSS Eureka 客户端。Eureka 客户端处理客户端注册和注销的所有方面。我们将在后面的章节中看到 Eureka 的详细实现。

自注册模式的缺点:

  • 该服务与服务注册表耦合。它必须不断与服务器通信,告诉它有关服务状态的信息。

  • 服务注册逻辑不是集中的,必须在我们生态系统中的每种语言中实现。

第三方注册模式

在使用第三方注册过程和服务实例时,微服务遵循单一责任原则,不再负责向服务注册表注册自己。相反,我们在系统中引入一个新组件,服务注册器,它负责维护服务注册表。为了维护注册表,服务注册器通过轮询环境或订阅启动和关闭事件来跟踪实例。每当它注意到一个新的可用服务时,它就会将该实例注册到注册表中。同样,如果它无法收到健康检查,那么它就会从注册表中注销该服务。与自注册模式不同,微服务代码要简单得多,因为它不负责注册自己,但它也有缺点。如果注册器没有经过精心选择,它就会成为必须安装、配置、维护和高可用的另一个组件,因为它是系统的关键组件。在工业界,第三方注册通常是首选,因为它可以自动管理注册表。注册表所需的额外数据可以以策略或合同的形式提供,并可以在数据库中更新。Apache Zookeeper 或 Netflix Eureka 等工具通常与其他工具结合使用。

第三方注册有各种优势。比如,如果一个服务宕机,第三方注册器可以采取适当的措施,如提供安全回退,触发自我修复机制等。如果某个服务的流量很大,注册过程可以通过请求新的微服务实例自动添加新的端点。对服务执行的这些健康检查可以帮助自动注销,以阻止故障级联到整个系统。一个著名的例子是 Registrator,我们将在本章后面看到。

一些著名的第三方注册模式的例子包括:

  • Netflix Prana:由 Netflix 外包,Netflix OSS Prana 专门针对非 JVM 语言。它是边车模式的实现,与服务实例并行运行,并通过 HTTP 公开它们。Prana 使用 HTTP 与 Netflix Eureka 注册和注销服务实例。

  • **内置组件,如 ELB:**大多数部署环境都有内置组件。通过自动扩展创建的 EC2 实例会自动注册到 ELB。同样,Kubernetes 服务会自动注册并可供发现(我们将在第十章的扩展部分中更详细地了解这一点,加固您的应用)。

第三方注册模式的优势如下:

  • 代码较少复杂,因为每个服务不必为自己的注册和注销编写代码

  • 中央注册器还包含执行健康检查的代码,这不需要在所有地方复制。

第三方注册模式的缺点如下:

  • 除非由服务发现工具提供,否则它是另一个需要维护并保持高可用性的组件

服务注册和发现选项

在本节中,我们将研究市场上一些常见的服务发现和注册选项。选项范围从提供高度控制的低级解决方案(如 CoreOS 的 etcd 和 HashiCorp 的 Consul)到提供容器调度解决方案的高端解决方案(如 Google 的 Kubernetes、Docker swarm 等)。在本节中,我们将了解各种选项,并查看每种选项的优缺点。

Eureka

Eureka 是由 Netflix 外包的服务注册和发现框架,需要主要用于定位服务以进行负载平衡和故障转移。在本节中,我们将使用 Eureka 进行服务发现和注册。

整体 Eureka 架构由两个组件组成:Eureka 服务器和客户端。Eureka 服务器是一个独立的服务器应用程序,负责:

  • 管理服务实例的注册表

  • 提供注册任何服务、注销任何微服务和查询实例作为服务发现的一部分的手段

  • 将实例的注册传播到其他 Eureka 服务器和客户端,提供类似心跳的机制来不断监视服务

Eureka 客户端是生态系统的一部分,具有以下责任:

  • 在启动、关闭等过程中向 Eureka 服务器注册和注销绑定的微服务

  • 通过不断发送心跳来保持与 Eureka 服务器的连接

  • 检索其他服务实例信息,缓存并每天更新

我们将经常在 Eureka 中使用以下术语:

Eureka 服务器它是发现服务器。它通过注册和注销任何服务以及发现任何服务的 API 来拥有所有服务的注册表及其当前状态。
Eureka 服务Eureka 服务注册表中发现的任何内容,以及为其他服务注册并且意图被发现的任何内容。每个服务都有一个逻辑标识符,可以引用该应用程序的实例 ID,称为 VIP 或服务 ID。
Eureka 实例注册到 Eureka 服务器的任何应用程序,以便其他服务可以发现它。
Eureka 客户端可以注册和发现任何微服务的任何微服务应用程序。

在本节中,我们将设置 Eureka 服务器注册一个示例微服务,并在其他微服务中找到该微服务的位置。所以,让我们开始吧。

设置 Eureka 服务器

Eureka 服务器是 Netflix OSS 产品,是一种服务发现模式的实现,其中每个微服务都注册,客户端在服务器上查找依赖的微服务。Eureka 服务器在 JVM 平台上运行,因此我们将直接使用可用模板。

要运行 Eureka 服务器,您需要安装 Java 8 和 Maven。

让我们来看看设置 Eureka 服务器的步骤:

  1. 转到本章节的提取源代码中的eureka文件夹。您将找到一个名为euraka-server的 Eureka 服务器的现成 Java 项目。

  2. 在根目录中,打开终端并运行以下命令:

mvn clean install
  1. 您应该看到依赖项正在安装,最后,您将收到一条确认成功构建的消息,并生成target文件夹。

  2. 打开target文件夹,您将能够看到 Eureka 服务器的.jar文件(demo-service-discovery-0.0.1-SNAPSHOT.jar)。

  3. 打开终端并输入以下命令。您应该会看到服务器启动:

java -jar demo-service-discovery-0.0.1-SNAPSHOT.jar

以下是上述命令的输出截图:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

启动 Spring Eureka 服务器

  1. 访问http://localhost:9091/,您应该能够看到 Eureka 服务器已启动。您应该会看到类似于这样的内容:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Spring Eureka 服务器

现在我们已经启动了 Eureka 服务器,我们将向其注册我们的服务。在注册到 Eureka 服务器后,我们将能够在 Eureka 当前注册的实例下看到我们的服务。

向 Eureka 服务器注册

现在我们的 Eureka 服务器已经启动并准备好接受微服务的注册,我们将注册一个演示微服务并在 Eureka 仪表板上看到它。您可以跟随源文件中附带的源代码(first-microservice-register)。让我们开始吧:

  1. 从第二章中提取我们的第一个微服务代码,为旅程做准备。我们将在项目中使用eureka-js-client(www.npmjs.com/package/eureka-js-client)模块,这是 Netflix OSS Eureka 的 JavaScript 实现。

  2. 打开终端并安装eureka-js-client

npm i eureka-js-client --save
  1. 接下来,我们将安装eureka-js-client的类型,以在我们的 TypeScript 项目中使用。在撰写本文时,DefinitelyTyped存储库中的类型尚未更新。因此,我们现在将编写我们自定义的类型。

  2. 创建一个名为custom_types的文件夹,并在其中添加eureka-js-client.d.ts。可以从附加的源代码或我的 gist(gist.github.com/insanityrules/7461385aa561db5835c5c35279eb12bf)中复制内容

  3. 接下来,我们将使用 Eureka 注册我们的 Express 应用程序。打开Application.ts并在其中编写以下代码:

let client = new Eureka(
  {
    instance: {
      app: 'hello-world-chapter-6',
      hostName: 'localhost',
      ipAddr: '127.0.0.1',
      statusPageUrl: `http://localhost:${port}`,
      healthCheckUrl: `http://localhost:${port}/health`,
      port: {
        '$': port,
        '@enabled': true
      },
      vipAddress: 'myvip',
      dataCenterInfo: {
        '@class': 'com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo',
        'name': 'MyOwn',
      },
    }, eureka: {
      host: 'localhost',
      port: 9091,
      servicePath: '/eureka/apps/'
    }
  })

我们刚刚做了什么?请查看以下要点,以便更好地理解:

    • 我们使用名称为hello-world-chapter-6的应用实例注册了一个名为myvip的键和数据中心myOwn到 Eureka
  • 我们提供了statusPageURLIpAddress

  • 我们添加了 Eureka 信息,包括主机、端口和服务路径

  • 可以在此处找到完整的配置列表(www.npmjs.com/package/eureka-js-client)

  1. 接下来,我们将从客户端开始;只需添加以下内容:
client.start()
  1. 我们的注册已经准备就绪;现在我们可以使用npm start启动我们的服务。现在,转到localhost:9091检查服务器实例:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在 Eureka 服务器中注册的服务

  1. 我们的服务将不断获取服务注册表并发送心跳以告知服务正在运行。当我们的微服务被终止时,让我们停止并注销服务。只需将以下代码添加到Application.ts中:
process.on('SIGINT', function() {client.stop(); });

现在我们的服务已与 Eureka 同步,在下一节中我们将看到如何发现服务。

使用 Eureka 服务器进行发现

在本节中,我们将在另一个微服务中发现我们注册的服务。我们将从该服务获取响应,而不知道服务地址或在任何地方硬编码位置。从第二章中复制first-microservice的结构,为旅程做准备。由于我们将在各处需要 Eureka 客户端,我们将创建EurekaService.ts。您可以在项目的eureka/eureka-service-discovery/src文件夹中找到完整的源代码。

让我们来看看发现我们注册服务的步骤:

  1. 创建一个名为EurekaService.ts的文件,并创建用于初始化客户端的静态方法:
static getClient(): Eureka{
  if (!this._client) {
    this._client = new Eureka({
      instance: {}, //set instance specific parameters,
      Eureka: {} //set Eureka parameters
    })
  }
  1. Application.ts中,启动您的客户端并添加停止进程如下:
EurekaService.getClient().start();
…
process.on('SIGINT', () => {
  /*stop client*/
 EurekaService.getClient().stop();
  this.server.close()
 });
  1. 在我们的HelloWorld.ts中,我们将调用first-microservice-register中的服务并获取其响应。我们不会硬编码位置。在HelloWorld.ts中添加以下 LOCs:
let instances: any =
  EurekaService.getClient().getInstancesByAppId("HELLO-WORLD-CHAPTER-6");
let instance = null;
let msg = "404 Not Available";
if (instances != null && instances.length > 0) {
  instance = instances[0];
  let protocol = instances[0].securePort["@enabled"] == "true" ? "https" : "http";
  let url = protocol + "://" + instance.ipAddr + ":" + 
                       instances[0].port.$ + "/";
  const { res, payload } = await Wreck.get(url);
  msg = payload.toString();
} 

正如您所看到的,我们从我们的服务注册表中选择了协议、端口和 IP 地址。

  1. 运行您的应用程序,您将能够看到来自first-microservice-register的响应。

Eureka 的关键点

在进行 Eureka 服务注册和发现的练习之后,让我们来看看 Eureka 的一些要点:

  • Eureka 包括服务器组件和客户端组件。服务器组件是所有微服务通信的组件。它们通过不断发送心跳来注册其可用性。消费服务还使用服务器组件来发现服务。

  • 当使用我们的 Eureka 服务引导微服务时,它会联系 Eureka 服务器并广播其存在以及合同细节。注册后,服务端点每 30 秒发送心跳请求以更新其租约期。如果服务端点未能这样做一定次数,它将从服务注册表中移除。

  • 您可以通过设置以下选项之一来启用调试日志:

  • NODE_DEBUG=request

  • client.logger.level('debug');

  • 客户端不断地在每个预定义的点获取注册表并对其进行缓存。因此,当它想要发现另一个服务时,就可以防止额外的网络跳跃。

  • Eureka 客户端提供了可用服务的列表,并提供了按主机名或实例名提供它们的选项。

  • Eureka 服务器具有区域感知功能。在相同区域注册服务时可以提供区域信息。为了进一步引入负载均衡器,我们可以使用一个等同于 Netflix Ribbon 的弹性客户端(www.npmjs.com/package/resilient)。

  • 它具有健康检查、状态页面、注册、注销、最大重试次数等选项。

  • Eureka 是服务器端客户端注册和自注册选项的典型示例。

Consul

我们用于服务注册和发现的另一个选项是 HashiCorp Consul (www.consul.io/)。Consul 是分布式键值存储和其他服务发现和注册功能的开源实现。它可以作为主节点或代理运行。主节点编排整个网络并维护注册表。Consul 代理充当主节点的代理,并将所有请求转发到主节点。在本节中,我们将了解使用 Consul 进行服务发现和注册。

在这个练习中,我们将使用 Consul 进行服务注册和发现。我们将看看使用 Consul 进行自注册/注销的方法。让我们开始吧;在这个练习中,我们将使用 Linux 操作系统。

设置 Consul 服务器

让我们来看看设置 Consul 服务器的步骤:

  1. 设置 Consul 服务器非常简单。只需从www.consul.io/downloads.html下载可执行文件,并将其解压缩到您选择的位置。解压缩后,输入以下命令使其可用于二进制执行:
cp consul /usr/local/bin/
  1. 通过打开终端并输入consul -v来测试您的 Consul 安装;您应该能够看到版本 1.0.7。

  2. 现在,我们将打开 Consul UI 终端。Consul 默认带有一个 UI 仪表板;要启动带有 UI 仪表板的 Consul 终端,请输入以下命令:

consul agent -server -bootstrap-expect=1 -data-dir=consul-data -ui -bind=<Your_IPV4_Address>
  1. 打开localhost:8500;您应该能够看到类似于这样的东西:

Consul 服务器

我们已经成功启动了 Consul 服务器;接下来我们将在 Consul 中注册一些服务。

与 Consul 服务器交谈

与 Eureka 一样,Consul 也暴露了一些 REST 端点,可以用来与 Consul 服务器交互。在本节中,我们将看到如何:

  • 注册服务实例

  • 发送心跳并进行健康检查

  • 注销服务实例

  • 订阅更新

注册服务实例

让我们从第二章的第一个微服务开始克隆,为旅程做准备。您可以在chapter-6/consul/consul-producer文件夹中找到整个源代码:

  1. 打开终端并输入以下命令:
npm install consul  @types/consul --save
  1. 现在在Application.ts中,我们将初始化我们的 Consul 客户端。写下这段代码:
import * as Consul from 'consul';
import { ConsulOptions } from 'consul';let consulOptions: ConsulOptions =
  { host: '127.0.0.1', port: '8500', secure: false, promisify: false }.
let details =
  {
    name: 'typescript-microservices-consul-producer',
    address: HOST,
    check: { ttl: '10s', deregister_critical_service_after: '1m' },
    port: appPort, id: CONSUL_ID
  };
let consul = new Consul(consulOptions);
  1. 接下来,我们将在 Consul 中注册我们的服务:
consul.agent.service.register(
  details, err => {
    if (err) {
      throw new Error(err.toString());
    }
    console.log('registered with Consul');
  }
  1. 运行程序,您应该能够看到成功的日志。您将能够看到类似以下的输出:

与 Consul 和 Consul 仪表板的服务注册

发送心跳并进行健康检查

现在,我们将添加一个调度程序,不断发送心跳以告诉我们的 Consul 服务器它是活动的。在与上一个练习相同的代码中,只需添加以下代码行:

  setInterval(() => {
   consul.agent.check.pass({id:`service:${CONSUL_ID}`}, 
   (err:any) => {
        if (err) throw new Error(err); 
        console.log('Send out heartbeat to consul');
        });
   }, 5 * 1000);

我们做了什么?

  • 每五秒钟,我们向 Consul 发送心跳,以确保我们生成的CONSUL_ID的服务是活动的。

  • 定期心跳被发送出去,以确保 Consul 知道我们的服务是活动的,并且不会断开我们的服务。早些时候,我们在设置中保留了 TTL 值为 10 秒,这意味着如果 Consul 服务器在 10 秒后没有收到心跳,它将假定服务已经停止。

  • 较高的 TTL 值意味着 Consul 将在应用程序死亡或无法提供请求时知道得很晚。另一方面,较短的 TTL 值意味着我们在网络上传输了太多数据,这可能会淹没 Consul,因此这个值应该谨慎选择。

  • 您总是需要传递一个唯一的 ID,所以在这个练习中,我们生成了 UUID,并将主机和端口混合在一起。

  • 健康检查 API 可通过 HTTP 获得。我们所要做的就是输入以下内容:

GET /agent/check/pass/service:<service_id>

注销应用程序

在本节中,我们将在服务器终止或有人杀死服务器时注销我们的应用程序。这确保 Consul 不必等到 TTL 期限才真正知道服务已经停止。只需在Application.ts中添加以下代码行:

   process.on('SIGINT', () => {
  console.log('Process Terminating. De-Registering...');
  let details = { id: CONSUL_ID };
  consul.agent.service.deregister(details,
    (err) => {
      console.log('de-registered.', err);
      process.exit();
    });

现在,当您在优雅地终止应用程序时检查 Consul 服务器时,您将无法看到我们的 Consul 生产者已注册。

订阅更新

就像 Eureka 一样,我们将不断获取 Consul 注册表,所以每当我们需要与另一个注册表通信时,我们就不需要进行另一个注册表调用,因为注册表已经在我们这边缓存了。Consul 通过提供一个名为watch的功能来处理这个问题。对服务的响应将有一个索引号,可以用于将来的请求进行比较。它们只是一个用来跟踪我们离开的位置的光标。让我们向我们的应用程序添加观察者:

  1. 通过添加以下代码来创建一个新的观察者。在这里,我们在 Consul 中的名为data的服务上创建了一个观察者:
let watcher = consul.watch({
  method: consul.health.service,
  options: {
    service: 'data',
    passing: true
  }
});
  1. 接下来,我们将在我们的观察者上添加一个更改事件,所以每当它接收到新的更新时,我们将只是缓存我们的服务数据的注册表。创建一个数组,并在观察时持久化它接收到的条目:
let known_data_instances: string[];
..
watcher.on('change', (data, res) => {
  console.log('received discovery update:', data.length);
  known_data_instances = [];
  data.forEach((entry: any) => {
    known_data_instances.push(`http://${entry.Service.Address}:
    ${entry.Service.Port}/`);
  });
  console.log(known_data_instances);
});
  1. 添加一个错误处理程序:
watcher.on('error', err => {
  console.error('watch error', err);
});
  1. 就是这样。现在,使用npm start运行程序,并使用名称data注册另一个服务(注册新服务的步骤与注册新服务相同)。然后,您应该能够看到以下输出:
received discovery update: 1
 [ 'http://parth-VirtualBox:8081/' ]

就是这样。我们刚刚进行了服务注册,并与 Eureka 服务器进行了交互。每当数据服务关闭时,此值也将动态更新。现在我们有了动态地址和端口,我们随时可以使用它来发现服务的位置。

Consul 的关键点

完成了对 Consul 的练习后,现在让我们总结一下 Consul 的关键点:

  1. Consul 使用 gossip 协议(告诉每个活着并与其他人保持不断联系的人)来形成动态集群。

  2. 它具有内置的键值存储,不仅存储数据,还用于注册观察,可用于许多任务,如通知其他人有关数据更改、运行不同的健康检查以及根据用例运行一些自定义命令。

  3. 服务发现是内置的,因此我们不需要任何第三方工具。它具有内置功能,如健康检查、观察等。

它具有对多个数据中心的开箱即用支持,gossip 协议也适用于所有数据中心。它还可以用于发现有关其他部署服务和它们所在节点的信息。它具有内置的健康检查、TTL 和自定义命令支持,我们可以在其中添加自己的中间件函数。

Registrator

虽然 Consul 似乎是服务发现和注册的一个很好的选择,但存在一个相当大的缺点,即每个服务都需要维护它们的启动和关闭代码,这似乎在各处都有相当多的重复代码。我们需要一个工具,根据监听它们的启动和关闭事件,自动将服务注册到 Consul 服务器。Registrator 正是这样的工具。它是一个用于 Docker 的服务注册桥接器,具有根据需要插入适配器的选项。当服务上线或下线时,Registrator 会自动注册和注销服务。它具有可插拔的服务注册选项,这意味着它可以与各种其他服务注册客户端一起使用,如 Consul、etcd 等。

让我们开始使用 Registrator。在这个练习中,我们将使用 Consul 的服务注册表,将其插入 Registrator,然后启动一个服务,让 Registrator 在 Consul 服务器中自动注册它:

  1. 首先,使用以下命令启动 Consul 服务器:
consul agent -server -bootstrap-expect=1 -data-dir=consul-data -ui -bind=<Your_IPV4_Address>
  1. 现在,我们将拉取 Registrator 的 Docker 镜像,并指定将其插入到 Consul 注册表中,这样当 Registrator 发现任何服务时,它们将自动添加到 Consul 服务器。打开终端并输入以下命令:
sudo docker run -d 
 --name=registrator
 --net=host 
 --volume=/var/run/docker.sock:/tmp/docker.sock 
 gliderlabs/registrator:latest 
 consul://localhost:8500

我们以分离模式运行容器并对其命名。我们以主机网络模式运行,以确保 Registrator 具有实际主机的主机名和 IP 地址。最后一行是我们的注册 URI。Registrator 需要在每个主机上运行;对于我们的练习,我们选择了单个主机。要启动 Registrator,我们需要提供的基本配置是如何连接到注册表,在这种情况下是 Consul。

  1. 为了确保 Registrator 已成功启动,请输入以下命令,您应该能够看到日志流和消息Listening for Docker events ...
sudo  docker logs registrator
  1. 现在,我们只需使用 Docker 启动任何服务,我们的服务将自动注册到 Consul。打开终端,只需使用以下命令在 Docker 中启动我们的服务:
sudo docker run -p 8080:3000 -d firsttypescriptms:latest

或者您可以只是启动任何服务,比如redis,只需输入以下命令:

sudo docker run -d -P --name=redis redis 
  1. 打开 Consul 用户界面,您将能够在那里看到我们的服务已注册。

在这里,我们使用 Registrator 和 Consul 有效地实现了自动发现。它可以作为自动发现。

Registrator 的关键点

让我们讨论 Registrator 的关键要点:

  1. Registrator 充当自动发现代理,它监听 Docker 的启动和关闭事件。

  2. Registrator 具有以下内置选项,取自他们的 GitHubReadme文件:

Usage of /bin/registrator:
   /bin/registrator [options] <registry URI>
   -cleanup=false: Remove dangling services
   -deregister="always": Deregister exited services "always" or "on- 
    success"
   -internal=false: Use internal ports instead of published ones
   -ip="": IP for ports mapped to the host
   -resync=0: Frequency with which services are resynchronized
   -retry-attempts=0: Max retry attempts to establish a connection  
    with the backend. Use -1 for infinite retries
   -retry-interval=2000: Interval (in millisecond) between retry-
    attempts.
   -tags="": Append tags for all registered services
   -ttl=0: TTL for services (default is no expiry)
   -ttl-refresh=0: Frequency with which service TTLs are refreshed
  1. 使用 Consul 与 Registrator 可以为我们的服务发现和注册提供非常可行的解决方案,而无需在各处重复编写代码。

这些是目前广泛使用的一些解决方案。除此之外,还有其他解决方案,比如 ELB、Kubernetes 等。

在本节中,我们看到了使用 Eureka、Consul 和 Registrator 进行服务注册表和发现,并根据我们的服务发现和注册表模式看到了一些其他选项。在下一节中,我们将了解如何选择正确的服务注册表和发现解决方案。

如何选择服务注册表和发现

之前,我们根据服务注册表和发现模式看到了各种服务注册表和发现选项。因此,接下来显而易见的问题是,选择哪种解决方案?这个问题非常广泛,实际上取决于需求。您的需求很可能与大多数其他公司的需求不同,因此与其选择最常见的解决方案,不如根据您的需求进行评估,并基于此制定自己的策略。为了制定策略,应该适当评估以下问题:

  • 系统是否只使用一种语言编码,还是使用多语言环境?在不同语言中编写相同的代码非常麻烦。在这种情况下,Registrator 非常有帮助。

  • 是否涉及旧系统?这两个系统是否会运行一段时间?在这种情况下,自注册解决方案可能非常有帮助。

  • 服务发现过程有多简化?是否会有网关?是否会有负载均衡器在中间?

  • 是否需要为服务发现提供 API?个别微服务是否需要与其他微服务通信?在这种情况下,基于 HTTP 或 DNS 的解决方案非常有帮助。

  • 服务发现解决方案是否嵌入到每个微服务中,还是需要将逻辑集中嵌入?

  • 我们是否需要单独的应用程序配置,还是可以将其存储在诸如 Redis 或 MongoDB 之类的键值存储中?

  • 部署策略是什么?是否需要像蓝绿策略这样的部署策略?应根据适当的服务发现选择解决方案。

蓝绿是一种部署策略,通过运行两个名为蓝色和绿色的相同生产环境来减少停机时间。

  • 系统将如何运行?是否会有多个数据中心?如果是这样,那么运行 Eureka 是最合适的。

  • 如何维护您的确认?访问控制列表如何维护?如果是这样,那么 Consul 有内置解决方案。

  • 有多少支持?它是否开源并且有广泛的支持?有太多问题吗?

  • 如何决定自动扩展解决方案?

根据这些问题,并经过适当评估后,我们可以决定适当的解决方案。在仔细评估这些之后,我们可以选择任何解决方案。以下是在选择任何解决方案时需要注意的一些要点。

在选择 Consul 或 Eureka 时,请注意这些要点。

如果选择 Consul

虽然 Consul 有很多好处,但在选择 Consul 时需要注意以下几点:

  • 客户端需要编写自己的负载均衡、超时和重试逻辑。为了避免编写完整的逻辑,我们可以利用以下node模块:www.npmjs.com/package/resilient

  • 客户端需要单独实现获取逻辑、缓存和 Consul 故障处理,除非我们使用了 Registrator。这些需要分别为生态系统中的每种语言编写。

  • 无法为服务器设置优先级;需要编写自定义逻辑。

如果选择 Eureka

虽然 Eureka 有许多附加优势,但在选择 Eureka 时需要注意以下几点:

  • 客户端必须添加自己的负载均衡、超时和重试逻辑,因此我们需要将其与 Netflix Ribbon 等外部工具集成。

  • 文档非常贫乏。如果您使用非 JVM 环境,将无法使用 Eureka。Eureka 服务器需要在 JVM 平台上运行。对于非 JVM 客户端,文档非常模糊。

  • Web UI 非常单调且缺乏信息。

在本节中,我们了解了在选择 Eureka 或 Consul 时的主要要点。我们总结了一些重要观点,以帮助我们实际决定服务注册和发现解决方案。

摘要

在本章中,我们了解了服务注册和发现。我们深入了解了服务发现的时间、内容和原因,并了解了服务注册和发现模式。我们看到了每种模式的优缺点以及它们的可用选项。然后,我们使用 Eureka、Consul 和服务注册器实现了服务发现和注册。最后,我们看到了如何选择服务发现和注册解决方案,以及在选择 Eureka 或 Consul 时的关键要点。

在下一章中,我们将看到服务状态以及微服务之间的通信。我们将学习更多的设计模式,如基于事件的通信和发布-订阅模式,看到服务总线的运作,共享数据库依赖等等。我们将通过一些实际示例了解有状态和无状态的服务。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值