原文:
zh.annas-archive.org/md5/042BAEB717E2AD21939B4257A0F75F63
译者:飞龙
前言
在过去的几年中,微服务已经成为摇滚明星,并且现在是企业中最切实可行的解决方案之一,用于快速、有效和可扩展的应用程序。微服务包括一种架构风格和模式,其中一个庞大的系统被分解为更小的服务,这些服务是自给自足的、自治的、自包含的,并且可以单独部署。
TypeScript 的明显崛起和从 ES5 到 ES6 的长期演变已经看到许多大公司转向 ES6 堆栈。由于其支持类和模块、静态类型检查和与 JavaScript 类似的语法等巨大优势,TypeScript 已成为许多企业的事实解决方案。由于其异步、非阻塞、轻量级的特性,Node.js 已被许多公司广泛任命。用 TypeScript 编写的 Node.js 为各种机会打开了大门。
然而,微服务也有自己的困难需要解决,比如监控、扩展、分发和服务发现。最大的挑战是规模化部署,因为我们不希望最终出现系统故障。在不了解或解决这些问题的情况下采用微服务将导致重大问题。本书最重要的部分涉及采用实用的技术独立方法来处理微服务,以便充分利用一切。
这本书分为三个部分,解释了这些服务是如何工作的,以及构建任何应用程序的微服务方式的过程。您将遇到基于设计的架构方法,以及实施各种微服务元素的指导。您将获得一组配方和实践,以满足实际、组织和文化挑战。这本书的目标是让用户了解一种实用的、逐步的方法,以实现规模化的反应式微服务。本书将带领读者深入了解 Node.js、TypeScript、Docker、Netflix OSS 等内容。本书的读者将了解如何使用 Node.js 和 TypeScript 部署可以独立运行的服务。用户将了解无服务器计算的不断发展趋势,以及不同的 Node.js 功能,实现 Docker 进行容器化的用途。用户将学习如何使用 Kubernetes 和 AWS 对系统进行自动扩展。
我相信读者会喜欢本书的每一部分。此外,我相信这本书不仅对 Node.js 开发人员有价值,对于其他想要尝试微服务并成功在业务中实施它们的人也有帮助。在整本书中,我采取了实用的方法,提供了许多例子,包括来自电子商务领域的案例研究。在本书结束时,您将学会如何使用 Node.js、TypeScript 框架和其他实用工具实现微服务架构。这些是经过实战考验的、强大的工具,用于开发任何微服务,并按照 Node.js 的最新规范编写。
这本书适合谁?
这本书适合寻求利用他们的 Node.js 和 TypeScript 技能构建微服务并摆脱单片式架构风格的 JavaScript 开发人员。假定读者具有 TypeScript 和 Node.js 的先验知识。本书将帮助回答一些关于微服务解决了什么问题以及组织如何结构化采用微服务的重要问题。
本书涵盖的内容
第一章《揭秘微服务》为您介绍了微服务的世界。它从单块到微服务架构的转变开始。本章使您熟悉了微服务世界的演变;回答了关于微服务经常被问到的问题,并让您熟悉了各种微服务设计方面、微服务的十二要素应用;以及微服务实现的各种设计模式,以及它们的优缺点,何时使用以及何时不使用它们。
第二章《为旅程做准备》介绍了 Node.js 和 TypeScript 中必要的概念。它从准备我们的开发环境开始。然后讨论了基本的 TypeScript 要点,比如类型、tsconfig.json、为任何节点模块编写自定义类型以及 Definitely Typed 存储库。然后,我们转向 Node.js,在那里我们用 TypeScript 编写我们的应用程序。我们将学习一些基本要点,比如 Node 集群、事件循环、多线程和 async/await。然后,我们转向编写我们的第一个 hello world TypeScript 微服务。
第三章《探索响应式编程》进入了响应式编程的世界。它探讨了响应式编程的概念以及其方法和优势。它解释了响应式编程与传统方法的不同之处,然后转向使用 Highland.js、Bacon.js 和 RxJS 的实际示例。最后,它比较了这三个库以及它们的优缺点。
第四章《开始您的微服务之旅》开始了我们的微服务案例研究——购物车微服务。它从系统的功能需求开始,然后是整体业务能力。我们从架构方面、设计方面和生态系统中的整体组件开始这一章,然后转向微服务的数据层,我们将深入讨论数据类型以及数据库层应该是什么样的。然后,我们以关注点分离的方式开发微服务,并学习一些微服务的最佳实践。
第五章《理解 API 网关》探讨了 API 网关涉及的设计。它解释了为什么需要 API 网关以及它的功能是什么。我们将探讨各种 API 网关设计以及其中的各种选项。我们将看看断路器以及它为何在客户端弹性模式中扮演重要角色。
第六章《服务注册表和发现》讨论了在您的微服务生态系统中引入服务注册表。服务的数量和/或位置可能会根据负载和流量而变化。固定的位置会扰乱微服务的原则。本章涉及解决方案,并深入讨论了服务发现和注册表模式。本章进一步解释了各种可用选项,并详细讨论了 Consul 和 Eureka。
第七章《服务状态和服务间通信》着重于服务间通信。微服务需要相互合作以实现业务能力。本章探讨了各种通信模式。然后讨论了包括 RPC 和 HTTP 2.0 协议在内的下一代通信样式。我们了解了服务状态以及状态可以持久化的位置。我们将介绍各种数据库系统以及它们的用例。本章探讨了缓存世界、在依赖项之间共享代码以及版本控制策略,并详细介绍了如何使用客户端弹性模式来处理故障。
第八章,《测试、调试和文档编制》,概述了开发之后的生活。我们学习如何编写测试用例,并通过一些著名的工具集深入了解 BDD 和 TDD 的世界。我们看到了微服务的合同测试——一种确保没有重大变化的方法。然后我们看到了调试以及如何对我们的服务进行性能分析以及我们可以使用的所有选项。我们继续进行文档编制,并了解文档编制的需求以及围绕 Swagger 的所有工具集,这是我们将使用的工具。
第九章,《部署、日志记录和监控》,涵盖了部署和其中涉及的各种选项。我们看到了一个构建流水线,并熟悉了持续集成和持续交付。我们详细了解了 Docker,并通过在微服务前面添加 Nginx 来将我们的应用程序 docker 化。我们继续进行日志记录,并了解了定制的、集中的日志记录解决方案的需求。最后,我们转向监控,并了解了各种可用的选项,如 keymetrics、Prometheus 和 Grafana。
第十章,《加固您的应用》,着眼于加固应用,解决安全性和可扩展性问题。它讨论了应该在部署时采取的安全最佳实践,以避免任何意外事件。它提供了一个详细的安全检查表,可在部署时使用。然后我们将学习如何使用 AWS 和 Kubernetes 实现可扩展性。我们将看到如何通过自动添加或删除 EC2 实例来解决 AWS 的扩展问题。本章以 Kubernetes 及其示例结束。
附录,《Node.js 10.x 和 NPM 6.x 有什么新内容?》,涵盖了 Node.js v10.x 和 NPM v6.x 的新更新。附录不在书中,但可以通过以下链接下载:www.packtpub.com/sites/default/files/downloads/Whats_New_in_Node.js_10.x_and_NPM_6.x.pdf
我写这本书的目的是让这本书的主题与职业和业务用例相关、有用,并且最重要的是,专注于实用示例。我希望您阅读这本书时能像我写这本书时一样享受,这肯定会让您乐在其中!
为了充分利用本书
这本书需要在 Unix 机器上安装先决条件。在继续阅读本书之前,需要对 Node.js 和 TypeScript 有基本的了解。大部分代码在 Windows 上也可以运行,但安装方式不同;因此,建议使用 Linux(带有 Ubuntu 的 Oracle VM Box 也是一个完美的选择)。为了充分利用本书,尽快尝试练习示例并将概念应用到自己的示例中。这些程序或案例研究中的大多数都使用可以轻松安装或设置的开源软件。然而,有些情况确实需要设置一些付费设置。
在第九章,《部署、日志记录和监控》,我们需要在 logz.io 上拥有一个账户,以便准备完整的 ELK 设置,而不是单独管理它。试用版可用 30 天,但您可以延长一些计划。需要一个关键指标的账户来发掘其全部潜力。
在第十章,《加固您的应用》,您需要获取 AWS 账户以进行扩展和部署。您还需要获取 Google Cloud Platform,以独立测试 Kubernetes,而不是通过手动设置过程。这两个账户都有免费套餐,但您需要使用信用/借记卡进行注册。
下载示例代码文件
您可以从您在www.packtpub.com的账户中下载本书的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,文件将直接发送到您的邮箱。
您可以按照以下步骤下载代码文件:
-
登录或注册www.packtpub.com。
-
选择“支持”选项卡。
-
点击“代码下载和勘误”。
-
在搜索框中输入书名并按照屏幕上的说明操作。
下载文件后,请确保使用以下最新版本的软件解压缩文件夹:
-
WinRAR/7-Zip for Windows
-
Zipeg/iZip/UnRarX for Mac
-
7-Zip/PeaZip for Linux
本书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/TypeScript-Microservices
。如果代码有更新,将在现有的 GitHub 存储库中更新。
我们还有来自我们丰富书籍和视频目录的其他代码包,可在**github.com/PacktPublishing/
**上找到。去看看吧!
下载彩色图片
我们还提供了一个 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图片。您可以在这里下载:www.packtpub.com/sites/default/files/downloads/TypeScriptMicroservices_ColorImages.pdf
。
使用的约定
本书中使用了许多文本约定。
CodeInText
:表示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名。以下是一个例子:“在前面的表中可以找到所有运算符的示例,位于rx_combinations.ts
、rx_error_handing.ts
和rx_filtering.ts
。”
代码块设置如下:
let asyncReq1=await axios.get('https://jsonplaceholder.typicode.com/posts/1');
console.log(asyncReq1.data);
let asyncReq2=await axios.get('https://jsonplaceholder.typicode.com/posts/1');
console.log(asyncReq2.data);
任何命令行输入或输出都将按照以下方式书写:
sudo dpkg -i <file>.deb
sudo apt-get install -f # Install dependencies
粗体:表示新术语、重要词或屏幕上看到的词。例如,菜单或对话框中的单词会在文本中以这种方式出现。以下是一个例子:“打开您的实例,然后转到负载均衡|负载均衡器选项卡。”
警告或重要提示会以这种方式出现。
提示和技巧会以这种方式出现。
第一章:揭秘微服务
“如果我问人们他们想要什么,他们会说更快的马。”
- 亨利·福特
无论您是技术负责人、开发人员还是渴望适应新的现代网络标准的技术专家,上述内容都概括了您当前的生活状况。今天成功业务的口号是,快速失败,快速修复和迅速崛起,更快的交付,频繁的变化,适应不断变化的技术和容错系统是一些日常要求。出于同样的原因,最近技术世界已经看到了架构设计的快速变化,这导致行业领袖(如 Netflix、Twitter、亚马逊等)放弃了单片应用程序,转向了微服务。在本章中,我们将揭秘微服务,研究它们的解剖学,并了解它们的概念、特点和优势。我们将了解微服务设计方面,并了解一些微服务设计模式。
在本章中,我们将讨论以下主题:
-
揭秘微服务
-
微服务的关键考虑因素
-
微服务常见问题解答
-
微服务如何满足应用程序的十二因素
-
当前世界的微服务
-
微服务设计方面
-
微服务设计模式
揭秘微服务
微服务开发的核心思想是,如果应用程序被分解为更小的独立单元,每个组都能很好地执行其功能,那么构建和维护应用程序就变得简单。整体应用程序只是各个单元的总和。让我们开始揭秘微服务。
微服务的崛起
今天的世界正在呈指数级增长,并且需要一种能够满足以下问题的架构,这些问题使我们重新思考传统的架构模式,并催生了微服务。
根据需求选择多种语言
技术独立性的需求非常迫切。在任何时候,语言和采用率都会发生变化。像沃尔玛这样的公司已经放弃了 Java 堆栈,转向了 MEAN 堆栈。今天的现代应用程序不仅仅局限于网络界面,还需要移动和智能手表应用程序。因此,用一种语言编写所有内容根本不是可行的选择。我们需要一种架构或生态系统,可以让多种语言共存并相互通信。例如,我们可以在 Go、Node.js 和 Spring Boot 中暴露 REST API,一个网关作为前端的单一联系点。
易于处理所有权
今天的应用程序不仅包括单一的网络界面,还涉及到移动设备、智能手表和虚拟现实(VR)。将逻辑分离成单独的模块有助于控制每个团队拥有一个单独的单元。此外,多个事物应该能够并行运行,从而实现更快的交付。团队之间的依赖关系应该降低到零。追踪正确的人来解决问题并使系统重新运行需要微服务架构。
频繁的部署
应用程序需要不断发展,以适应不断发展的世界。当 Gmail 开始时,它只是一个简单的邮件工具,现在它已经发展成了更多。这些频繁的变化要求频繁的部署,以便最终用户甚至不知道新版本正在发布。通过分成更小的单元,团队可以处理频繁的部署和测试,并迅速将功能交付给客户。应该有优雅的退化,即快速失败并解决问题。
自我维护的开发单元
不同模块之间的紧密依赖很快就会影响整个应用程序。这就需要更小的独立单元,以便如果一个单元不可操作,整个应用程序也不会受到影响。
现在让我们深入了解微服务,它们的特点,优势以及在实施微服务架构时所面临的所有挑战。
什么是微服务?
微服务没有通用的定义。简单地说——微服务可以是任何操作块或单元,它可以非常有效地处理其单一责任。
微服务是构建自主、自我维持、松耦合的业务能力的现代风格,这些能力汇总成一个整个系统。我们将深入了解微服务的原则和特征,微服务提供的好处,以及需要注意的潜在风险。
原则和特征
有一些原则和特征定义了微服务。任何微服务模式都可以通过这些要点进一步区分和解释。
没有单片模块
微服务只是满足单个操作业务需求的另一个新项目。微服务与业务单元的变化相关联,因此必须松耦合。微服务应该能够持续满足不断变化的业务需求,而不受其他业务单元的影响。对于其他服务来说,只是一种消费方式,消费方式不应改变。实现可以在后台更改。
愚蠢的通信管道
微服务促进基本、经过时间考验的微服务之间的异步通信机制。根据这一原则,业务逻辑应保留在端点内,而不应与通信渠道混合在一起。通信渠道应该是愚蠢的,并且只在决定的通信协议中进行通信。HTTP 是一种受欢迎的通信协议,但更具反应性的方法——队列如今更为普遍。Apache Kafka和RabbitMQ是一些普遍的愚蠢通信管道提供者。
去中心化或自我治理
在使用微服务时,经常会出现故障。一个应急计划最终可以阻止故障传播到整个系统。此外,每个微服务可能都有自己的数据存储需求。去中心化管理了这一需求。例如,在我们的购物模块中,我们可以将客户及其交易相关信息存储在 SQL 数据库中,但由于产品数据高度非结构化,我们将其存储在 NoSQL 相关数据库中。每个服务都应该能够在故障情况下做出决策。
服务合同和无状态性
微服务应通过服务合同进行明确定义。服务合同基本上提供了有关如何使用服务以及需要传递给该服务的所有参数的信息。Swagger和AutoRest是一些广泛采用的用于创建服务合同的框架。另一个显著的特征是微服务不存储任何东西,也不维护任何状态。如果需要持久化某些东西,那么它将被持久化在缓存数据库或某些数据存储中。
轻量级
微服务作为轻量级,有助于在任何托管环境中轻松复制设置。容器比虚拟化更受青睐。轻量级应用容器帮助我们保持较低的占用空间,从而将微服务绑定到某个上下文。设计良好的微服务应该只执行一个功能,并且执行得足够好。容器化的微服务易于移植,从而实现轻松的自动扩展。
多语种
在微服务架构中,服务 API 后面的一切都是抽象和未知的。在前面的购物车微服务示例中,我们可以将我们的支付网关完全作为云中部署的服务(无服务器架构),而其余服务可以使用 Node.js。内部实现完全隐藏在微服务后面,唯一需要关注的是通信协议在整个过程中应该是相同的。
现在,让我们看看微服务架构为我们提供了哪些优势。
微服务的优点
采用微服务有许多优势和好处。我们将看看在使用微服务时获得的好处和更高的商业价值。
自主团队
微服务架构使我们能够独立扩展任何操作,按需提供可用性,并在零到非常少的配置下非常快速地引入新服务。技术依赖也大大减少。例如,在我们的购物微服务架构中,库存和购物模块可以独立部署和处理。库存服务只会假设产品存在并相应地工作。只要库存和产品服务之间的通信协议得到满足,库存服务可以用任何语言编码。
服务的优雅降级
任何系统的故障都是自然的,优雅降级是微服务的一个关键优势。故障不会级联到整个系统。微服务设计成遵守约定的服务水平协议;如果服务水平协议未能达到,则服务将被丢弃。例如,回到我们的购物微服务示例,如果我们的支付网关宕机,那么对该服务的进一步请求将停止,直到服务恢复运行。
支持多语言体系结构和 DevOps
微服务根据需要利用资源或有效地创建多语言体系结构。例如,在购物微服务中,您可以将产品和客户数据存储在关系数据库中,但任何审计或日志相关数据都可以存储在 Elasticsearch 或 MongoDB 中。由于每个微服务都在其有界上下文中运行,这可以促进实验和创新。变更影响的成本将会非常低。微服务使得 DevOps 达到了全面水平。成功的微服务架构需要许多 DevOps 工具和技术。小型微服务易于自动化,易于测试,如果需要,易于污染故障,并且易于扩展。Docker 是容器化微服务的主要工具之一。
事件驱动架构
一个良好设计的微服务将支持异步事件驱动架构。事件驱动架构有助于追踪任何事件,每个动作都是任何事件的结果,我们可以利用任何事件来调试问题。微服务设计采用发布-订阅模式,这意味着添加任何其他服务只需订阅该事件即可。例如,您正在使用一个购物网站,有一个用于添加到购物车的服务。现在,我们想要添加新功能,以便每当产品添加到购物车时,库存应该更新。然后,可以准备一个只需订阅添加到购物车服务的库存服务。
现在,我们将研究微服务架构引入的复杂性。
微服务的不好和具有挑战性的部分
伟大的力量带来了更大的挑战。让我们看看设计微服务的具有挑战性的部分。
组织和编排
这是在适应微服务架构时面临的最大挑战之一。这更多是一个非功能性挑战,新的组织团队需要被组建,并且他们需要在采用微服务、敏捷和 Scrum 方法论方面得到指导。他们需要在这样的环境中进行模拟,以便能够独立工作。他们开发的结果应该以松耦合的方式集成到系统中,并且可以轻松扩展。
平台
创建完美的环境需要一个合适的团队,以及跨所有数据中心的可扩展的故障安全基础设施。选择正确的云服务提供商(AWS、GCP或Azure),添加自动化、可扩展性、高可用性,管理容器和微服务实例是一些关键考虑因素。此外,微服务还需要其他组件需求,如企业服务总线、文档数据库、缓存数据库等。在处理微服务时,维护这些组件成为了一个额外的任务。
测试
完全独立地测试具有依赖关系的服务是极具挑战性的。当微服务引入生态系统时,需要适当的治理和测试,否则它将成为系统的单点故障。任何微服务都需要多个级别的测试。它应该从服务能否访问横切关注点(缓存、安全、数据库、日志)开始。应该测试服务的功能,然后测试它将要进行通信的协议。接下来是与其他服务协同测试微服务。之后是可扩展性测试,然后是故障安全测试。
服务发现
在分布式环境中定位服务可能是一项繁琐的任务。不断变化和交付是当今不断发展的世界的迫切需求。在这种情况下,服务发现可能具有挑战性,因为我们希望团队独立并且团队之间的依赖最小化。服务发现应该是这样的,可以为微服务提供动态位置。服务的位置可能会根据部署、自动扩展或故障而不断变化。服务发现还应该密切关注已经停止或性能不佳的服务。
微服务示例
以下是我们将在整本书中实施的购物微服务的图表。正如我们所看到的,每个服务都是独立维护的,有独立的模块或较小的系统——计费模块、客户模块、产品模块和供应商模块。为了与每个模块协调,我们有API 网关和服务注册表。添加任何额外的服务变得非常容易,因为服务注册表将维护所有动态条目,并相应地进行更新。
采用微服务时的关键考虑因素
微服务架构引入了明确定义的边界,这使得在边界内隔离故障成为可能。但与其他分布式系统一样,应用级别可能存在故障的可能性。为了最小化影响,我们需要设计容错的微服务,对某些类型的故障有预定义的反应。在适应微服务架构时,我们增加了一个网络层来进行通信,而不是内存中的方法调用,这引入了额外的延迟和需要管理的另一个层。以下是一些需要在设计微服务时小心处理的考虑因素,这将对系统产生长期利益。
服务降级
微服务架构允许您隔离故障,从而使您能够隔离故障并获得优雅的降级,因为故障被包含在服务的边界内,不会被级联。例如,在社交网络网站上,消息服务可能会中断,但这不会阻止最终用户使用社交网络。他们仍然可以浏览帖子,分享状态,签到位置等。服务应该被制定以符合某些 SLA。如果微服务停止满足其 SLA,那么该服务应该被恢复备份。Netflix 的 Hystrix就是基于同样的原则。
适当的变更治理
在没有任何治理的情况下引入变化可能会是一个巨大的问题。在分布式系统中,服务相互依赖。因此,当您引入新变化时,应该给予最大的考虑,以确保不会引入任何副作用或不良影响,其影响应该是最小的。应该提供各种变更管理策略和自动部署选项。此外,代码管理中应该有适当的治理。开发应该通过 TDD 或 BDD 进行,只有在达成约定的百分比后才应该进行部署。发布应该逐渐进行。一个有用的策略是蓝绿或红黑部署策略,其中您运行两个生产环境。您只在一个环境中部署变化,并在验证变化后将负载均衡器指向更新的版本。这在维护一个分级环境时更有可能。
健康检查、负载均衡和高效的网关路由
根据业务需求,微服务实例可能会在某些故障、内存不足、自动扩展等情况下启动、重新启动、停止,这可能会使其暂时或永久不可用。因此,架构和框架应相应设计。例如,Node.js 服务器是单线程的,在故障情况下会立即停止,但使用PM2等优雅的工具可以使其一直运行。应该引入一个网关,这将是微服务消费者的唯一联系点。网关可以是一个负载均衡器,应该跳过不健康的微服务实例。负载均衡器应该能够收集健康信息指标并相应地路由流量,它应该能够智能分析任何特定微服务上的流量,并在需要时触发自动扩展。
自愈
自愈设计可以帮助系统从灾难中恢复。微服务实现应该能够自动恢复丢失的服务和功能。诸如 Docker 之类的工具在服务失败时会重新启动服务。Netflix 提供了广泛的工具作为编排层来实现自愈。Eureka 服务注册表和 Hystrix 断路器是常用的。断路器使您的服务调用更具弹性。它们跟踪每个微服务端点的状态。每当遇到超时时,Hystrix 会断开连接,触发对该微服务的治疗需求,并恢复到一些安全策略。Kubernates是另一个选择。如果一个 pod 或者 pod 内的任何容器宕机,Kubernates 会启动系统并保持副本集完整。
故障转移缓存
故障转移缓存有助于在临时故障或一些故障时提供必要的数据。缓存层应设计得能够智能决定在正常情况下或故障转移情况下缓存可以使用多长时间。可以使用在 HTTP 中设置缓存标准响应头。max-age 头部指定资源被视为新鲜的时间。stale-if-error 头部确定资源应该从缓存中提供的时间。您还可以使用诸如Memcache、Redis等库。
重试直到
由于其自我修复能力,微服务通常可以在很短的时间内启动并运行。微服务架构应该具有重试逻辑直到条件的能力,因为我们可以预期服务将恢复,或者负载均衡器将将服务请求重定向到另一个健康的实例。频繁的重试也可能对系统产生巨大影响。一个常见的想法是在每次失败后增加重试之间的等待时间。微服务应该能够处理幂等性问题;比如说你正在重试购买订单,那么客户不应该出现重复购买。现在,让我们花点时间重新审视微服务的概念,并了解关于微服务架构的最常见问题。
微服务常见问题
在理解任何新术语时,我们经常会遇到一些问题。以下是我们在理解微服务时经常遇到的一些最常见的问题:
- 微服务不就像面向服务的架构(SOA)吗?我不是已经有了吗?我应该何时开始?
如果你在软件行业工作了很长时间,那么看到微服务可能会让你想起 SOA。微服务确实从 SOA 中借鉴了模块化和基于消息的通信的概念,但它们之间有很多不同之处。虽然 SOA 更注重代码重用,微服务遵循“在自己的捆绑上下文中发挥作用”的规则。微服务更像是 SOA 的一个子集。微服务可以根据需求进行扩展。并非所有的微服务实现都相同。在医疗领域使用 Netflix 的实现可能是一个坏主意,因为医疗报告中的任何错误都可能值得一个人的生命。一个有效的微服务的简单答案可能是明确服务的操作目标,如果不能执行操作,则在失败时应该做什么。关于何时以及如何开始使用微服务,有各种不同的答案。Martin Fowler,微服务的先驱之一,建议从单体架构开始,然后逐渐转向微服务。但问题是——*在这个技术创新时代,是否有足够的投资再次进行相同的阶段?*简短的答案是早期使用微服务有巨大的好处,因为它将从一开始就解决所有问题。
- 我们将如何处理所有的部分?谁负责?
微服务引入了本地化和自主规则。本地化意味着之前由中央团队完成的大量工作将不再由中央团队完成。拥抱自主规则意味着信任所有团队让他们自己做决定。这样,软件的更改甚至迁移变得非常容易和快速。话虽如此,并不意味着根本没有中央机构。随着更多的微服务,架构变得更加复杂。然后中央团队应该处理所有集中控制,如安全性、设计模式、框架、企业安全总线等。应该引入某些自我治理流程,如 SLA。每个微服务都应该遵守这些 SLA,系统设计应该聪明地设计,以便如果 SLA 未达到,那么微服务应该被丢弃。
- 我如何引入变化或者如何开始微服务开发?
几乎所有成功的微服务故事都始于一个变得太大而无法管理并被分解的单体架构。突然改变架构的某个部分将产生巨大影响,应该逐渐引入一种“分而治之”的方式。考虑以下问题来决定要在单体架构中分解哪个部分——我的应用是如何构建和打包的?我的应用代码是如何编写的?我可以有不同的数据源,当我引入多个数据源时,我的应用将如何运行?——根据这些部分的答案,重构该部分并测量和观察该应用的性能。确保应用保持在其边界上下文中。另一个可以开始的部分是当前单体架构中性能最差的部分。发现阻碍变化的瓶颈对组织来说是有益的。引入集中化操作最终将允许多个事情并行运行,并使公司受益匪浅。
- 需要什么样的工具和技术?
在设计微服务架构时,应该对任何特定阶段的技术或框架选择进行适当的思考。例如,微服务特性、云基础设施和容器的理想环境。容器提供了异构和易于移植或迁移的系统。使用 Docker 可以在微服务中按需提供弹性和可伸缩性。微服务的任何部分,如 API 网关或服务注册表,都应该是 API 友好的,适应动态变化,而不是单点故障。容器需要在服务器上进行开关,跟踪所有应用程序升级,为此需要适当的框架,如 Swarm 或 Kubernetes 来进行框架部署。最后,一些监控工具可以对所有微服务进行健康检查并采取必要的行动。Prometheus 就是这样一个著名的工具。
- 如何管理微服务系统?
有很多并行服务开发正在进行,有一个集中的管理政策是一个原始的需求。我们不仅需要关注认证和服务器审计,还需要关注集中的问题,如安全性、日志记录、可伸缩性,以及团队所有权、在各种服务之间共享问题、代码检查器、特定于服务的问题等分布式问题。在这种情况下,可以制定一些标准指南,例如每个团队应提供一个 Docker 配置文件,该文件从获取依赖项到构建软件并生成具有服务特定信息的容器。然后可以以任何标准方式运行 Docker 镜像,或者使用诸如 Amazon EC2、GCP 或 Kubernetes 之类的编排工具。
- 所有微服务都应该用相同的语言编码吗?
对这个问题的一般回答是这不是一个先决条件。微服务通过预定义的协议进行相互交互,例如 HTTP、Sockets、Thrift、RPC 等,我们稍后将更详细地看到。这意味着不同的服务可以使用完全不同的技术堆栈编写。微服务的内部语言实现并不重要,重要的是外部结果,即端点和 API。只要保持通信协议,语言实现就不重要,虽然不仅拥有一种语言是一个优势,但添加太多语言也会导致系统开发人员维护语言环境需求的复杂性增加。整个生态系统不应该是一个你可以种植任何东西的野生丛林。
基于云的系统现在有一套标准的指导方针。我们将看一下著名的十二要素应用程序以及微服务如何遵循这些指导方针。
微服务的十二要素应用
“当你没有一个好的流程和平台来帮助你时,好的代码会失败。当你没有一个拥抱 DevOps 和微服务的良好文化时,好的团队也会失败。”
- Tim Spann
十二要素应用程序是一种**软件即服务(SaaS)**或部署在云中的 Web 应用程序或软件的方法论。它告诉我们关于这些应用程序期望的输出特征。它基本上只是概述了制作结构良好且可扩展的云应用程序的必要条件:
-
代码库:我们为每个微服务维护一个单一的代码库,具有特定于它们自己的环境的配置,如开发、QA 和生产。每个微服务都将在版本控制系统(如 Git、mercurial 等)中拥有自己的存储库。
-
依赖关系:所有微服务都将它们的依赖项作为应用程序包的一部分。在 Node.js 中,有一个
package.json
,其中列出了所有的开发依赖和总体依赖。我们甚至可以有一个私有仓库,从中获取依赖项。 -
配置:所有配置应该是外部化的,基于服务器环境。应该将配置与代码分离。您可以在 Node.js 中设置环境变量,或者使用 Docker compose 来定义其他变量。
-
后备服务:任何通过网络消耗的服务,如数据库、I/O 操作、消息查询、SMTP、缓存,都将作为微服务暴露出来,并使用 Docker compose,并独立于应用程序。
-
构建、发布和运行:我们将在分布式系统中使用 Docker 和 Git 等自动化工具。使用 Docker,我们可以使用其推送、拉取和运行命令来隔离所有三个阶段。
-
进程:设计的微服务将是无状态的,并且不共享任何东西,因此实现零容错和轻松扩展。卷将用于持久化数据,从而避免数据丢失。
-
端口绑定:微服务应该是自治的和自包含的。微服务应该将服务监听器嵌入到服务本身中。例如,在 Node.js 应用程序中使用 HTTP 模块,服务网络公开服务以处理所有进程的端口。
-
并发性:微服务将通过复制进行扩展。微服务是通过扩展而不是扩大规模的。微服务可以根据工作负载的流动进行扩展或缩小。并发性将得到动态维护。
-
可处置性:最大限度地提高应用程序的健壮性,实现快速启动和优雅关闭。各种选项包括重启策略,使用 Docker swarm 进行编排,反向代理以及使用服务容器进行负载平衡。
-
开发/生产一致性:保持开发/生产/暂存环境完全相同。使用容器化的微服务通过构建一次,随处运行策略有所帮助。相同的镜像部署在各种 DevOps 阶段。
-
日志:为日志创建单独的微服务,使其集中化,将其视为事件流,并将其发送到诸如弹性堆栈(ELK)之类的框架。
-
管理进程:管理或任何管理任务应该作为其中一个进程打包,这样它们可以轻松执行、监视和管理。这将包括诸如数据库迁移、一次性脚本、修复错误数据等任务。
当前世界中的微服务
现在,让我们来看看当前世界中微服务的先驱实施者,他们获得的优势以及未来的路线图。这些公司采用微服务的共同目标是摆脱单片地狱。微服务甚至在前端看到了它的采用。像Zalando这样的公司也使用微服务原则在 UI 层面进行组合。
Netflix
Netflix是微服务采用的先驱之一。Netflix 每天处理数十亿次观看事件。它需要一个强大和可扩展的架构来管理和处理这些数据。Netflix 使用多语言持久性来获得他们采用的每种技术解决方案的优势。他们使用Cassandra进行高容量和较低延迟的写操作,以及具有调整配置的手工模型进行中等容量的写操作。他们在缓存级别使用Redis进行高容量和较低延迟的读取。Netflix 定制的几个框架现在是开源的,可供使用:
Netflix Zuul | 用于外部世界的边缘服务器或门卫。它不允许未经授权的请求通过。这是外部世界的唯一联系点。 |
---|---|
Netflix Ribbon | 服务消费者用于在运行时查找服务的负载均衡器。如果找到多个微服务实例,ribbon 使用负载平衡来均匀分配负载。 |
Netflix Hystrix | 用于保持系统运行的断路器。Hystrix 会断开那些最终会失败的服务的连接,只有当服务恢复正常时才会重新连接。 |
Netflix Eureka | 用于服务发现和注册。它允许服务在运行时注册自己。 |
Netflix Turbine | 用于检查运行中微服务的健康状况的监控工具。 |
仅仅检查这些存储库上的星星就可以给出使用 Netflix 工具采用微服务的速度的想法。
沃尔玛
沃尔玛是黑色星期五上最受欢迎的公司之一。在黑色星期五期间,每分钟有超过 600 万次页面浏览。沃尔玛采用了微服务架构,以适应 2020 年的世界,以合理的成本实现 100%的可用性。迁移到微服务架构给公司带来了巨大的提升。转化率提高了 20%。他们在黑色星期五没有停机时间。他们节省了 40%的计算能力,整体节省了 20-50%的成本。
Spotify
Spotify每月有 7500 万活跃用户,平均会话长度为 23 分钟。他们采用了微服务架构和多语言环境。Spotify 是一个拥有 90 个团队、600 名开发人员和两个大陆上的五个办公室的公司,所有人都在同一个产品上工作。这在尽可能减少依赖关系方面起到了重要作用。
Zalando
Zalando在前端实施了微服务。他们引入了作为前端的独立服务的片段。片段可以根据提供的模板定义在运行时组合在一起。与 Netflix 类似,他们外包了使用库:
Tailor | 这是一个布局服务,它由各种片段组成页面,因为它进行异步和基于流的获取,所以具有出色的首字节时间(TTFB)。 |
---|---|
Skipper | 用于通信的 HTTP 路由器,更像是 HTTP 拦截器,它具有修改请求和响应的能力。 |
Shaker | 用于在多个团队开发片段时提供一致用户体验的 UI 组件库。 |
Quilt | 带有 REST API 的模板存储和管理器。 |
Innkeeper | 路由的数据存储。 |
Tesselate | 服务器端渲染器和组件树构建器。 |
现在它服务于 1500 多个时尚品牌,创造了超过 34.3 亿美元的收入,开发团队有 700 多人。
在下一节中,我们将从设计的角度来揭示微服务。我们将看到微服务设计中涉及的组件,并了解广泛存在的微服务设计模式。
微服务设计方面
在设计微服务时,需要做出各种重要决策,例如微服务之间如何通信,如何处理安全性,如何进行数据管理等。现在让我们看看微服务设计中涉及的各种方面,并了解其可用的各种选项。
微服务之间的通信
让我们通过一个真实世界的例子来理解这个问题。在购物车应用程序中,我们有产品微服务、库存微服务、结账微服务和用户微服务。现在用户选择购买一个产品;对于用户来说,产品应该被添加到他们的购物车中,支付金额,在成功支付后,结账完成,并更新库存。现在如果支付成功,那么只有结账和库存应该被更新,因此服务需要相互通信。现在让我们看一些微服务可以用来相互通信或与任何外部客户端通信的机制。
远程过程调用(RPI)
简而言之,远程过程调用是一种协议,任何人都可以使用它从网络中远程访问其他提供者的服务,而无需了解网络细节。客户端使用请求和回复协议来请求服务,这是大数据搜索系统中最可行的解决方案之一。它具有序列化时间的主要优势之一。提供 RPI 的一些技术包括Apache Thrift和Google 的 gRPC。gRPC 是一个广泛采用的库,每天从 Node.js 下载量超过 23,000 次。它具有一些很棒的实用程序,如可插拔身份验证、跟踪、负载平衡和健康检查。它被 Netflix、CoreOS、Cisco 等公司使用。
这种通信模式具有以下优势:
-
请求和回复很容易
-
维护简单,因为没有中间代理
-
使用基于 HTTP/2 的双向流传输方法
-
在微服务风格的架构生态系统中高效地连接多语言服务
这种模式的通信对以下挑战和问题需要考虑:
-
调用方需要知道服务实例的位置,即维护客户端注册表和服务器端注册表
-
它只支持请求和回复模式,不支持其他模式,如通知、异步响应、发布/订阅模式、发布异步响应、流等
RPI 使用二进制而不是文本来保持有效负载非常紧凑和高效。这些请求在单个 TCP 连接上进行多路复用,这可以允许多个并发消息在不牺牲网络消耗的情况下进行传输。
消息传递和消息总线
当服务必须处理来自各种客户端接口的请求时,就会使用这种通信模式。服务需要相互协作来处理一些特定的操作,为此它们需要使用进程间通信协议。异步消息传递和消息总线就是其中之一。微服务通过在各种消息通道上交换消息来相互通信。Apache Kafka、RabbitMQ、ActiveMQ、Kestrel是一些广泛可用的消息代理,可用于微服务之间的通信。
消息代理最终执行以下功能集:
-
将来自各种客户端的消息路由到不同的微服务目的地。
-
根据需要将消息更改为所需的转换。
-
能够进行消息聚合,将消息分隔成多个消息,并根据需要发送到目的地并重新组合它们。
-
响应错误或事件。
-
使用发布-订阅模式提供内容和路由。
-
使用消息总线作为微服务之间的通信手段具有以下优势:
-
客户端与服务解耦;它们不需要发现任何服务。整体上松散耦合的架构。
-
消息代理具有高可用性,因为它会持久保存消息,直到消费者能够对其进行操作。
-
它支持各种通信模式,包括广泛使用的请求/回复、通知、异步响应、发布-订阅等。
虽然这种模式提供了几个优点,但增加了添加消息代理的复杂性,该代理应该具有高可用性,因为它可能成为单点故障。这也意味着客户端需要发现消息代理的位置,即联系点。
protobufs
协议缓冲区或protobufs是由谷歌创建的二进制格式。谷歌将 protobufs 定义为一种语言和平台中立的序列化结构化数据的广泛方式,可用作通信协议之一。 Protobufs 还定义了一组定义消息结构的一些语言规则。一些演示有效地表明 protobufs 比 JSON 快六倍。它非常容易实现,包括三个主要阶段,即创建消息描述符、消息实现和解析和序列化。在微服务中使用 protobufs 具有以下优势:
-
protobufs 的格式是自解释的-正式的格式。
-
它具有 RPC 支持;您可以将服务器 RPC 接口声明为协议文件的一部分。
-
它具有结构验证的选项。由于它具有在 protobufs 上序列化的较大数据类型消息,因此可以由负责交换它们的代码自动验证。
虽然 protobuf 模式提供了各种优势,但也有一些缺点,如下所示:
-
这是一种新兴的模式;因此您不会找到许多资源或详细的 protobuf 实现文档。如果您只在 Stack Overflow 上搜索 protobuf 标签,您只会看到大约 1 万个问题。
-
由于它是二进制格式,与 JSON 相比,它是不可读的,而 JSON 在另一方面是简单易读和分析的。下一代 protobuf 和 flatbuffer 现在已经可用。
服务发现
接下来要注意的明显方面是任何客户端接口或任何微服务将发现任何服务实例的网络位置的方法。基于微服务的现代应用程序在虚拟化或容器化环境中运行,其中包括服务实例的数量和位置动态变化。此外,基于自动扩展、升级等,服务实例集会动态变化。我们需要一个详细的服务发现机制。下面讨论的是广泛使用的模式。
服务注册表用于服务-服务通信
不同的微服务和各种客户端接口需要知道服务实例的位置,以便发送请求。通常,虚拟机或容器具有不同或动态的 IP 地址,例如,应用自动扩展的 EC2 组,它根据负载自动调整实例的数量。有多种选项可用于在任何地方维护注册表,例如客户端端或服务器端注册。客户端或微服务查找该注册表以查找其他微服务进行通信。
让我们以 Netflix 的真实例子为例。Netflix Eureka 是一个服务注册提供者。它有各种选项用于注册和查询可用的服务实例。使用公开的POST API
告知服务实例的网络位置。必须每 30 秒使用公开的PUT API
进行不断更新。任何接口都可以使用GET API
获取该实例并根据需求使用。一些广泛可用的选项如下:
-
etcd
:用于共享配置和服务发现的键值存储。诸如 Kubernates 和 Cloud Foundry 之类的项目都基于etcd
,因为它可以是高可用的、基于键值的和一致的。 -
consul
:另一个用于服务发现的工具。它具有广泛的选项,如公开的 API 端点,允许客户端注册和发现服务,并执行健康检查以确定服务的可用性。 -
ZooKeeper
:非常广泛使用,高可用性和高性能的协调服务,用于分布式应用程序。Zookeeper 最初是 Hadoop 的一个子项目,是一个广泛使用的顶级项目,并且预配置了各种框架。
一些系统具有隐式内置的服务注册表,作为其框架的一部分内置。例如,Kubernates、Marathon 和 AWS ELB。
服务器端发现
对任何服务的所有请求都通过已知客户端接口的位置运行的路由器或负载均衡器路由。然后,路由器查询维护的注册表,并根据查询响应转发请求。AWS 弹性负载均衡器是一个经典示例,它具有处理负载平衡、处理内部或外部流量和作为服务注册表的能力。EC2 实例可以通过公开的 API 调用或自动扩展注册到 ELB。其他选项包括 NGINX 和 NGINX Plus。还有可用的 consul 模板,最终从 consul 服务注册表生成nginx.conf
文件,并根据需要配置代理。
使用服务器端发现的一些主要优势如下:
-
客户端不需要知道不同微服务的位置。他们只需要知道路由器的位置,服务发现逻辑完全抽象化,客户端端没有任何逻辑。
-
一些环境免费提供此组件功能。
虽然这些选项有很大的优势,但也有一些需要处理的缺点:
-
它有更多的网络跳数,即来自客户端服务注册表和另一个来自服务注册表微服务。
-
如果负载均衡器不是由环境提供的,那么就必须设置和管理它。如果处理不当,它可能成为单点故障。
-
选定的路由器或负载均衡器必须支持不同的通信协议以进行通信模式。
客户端发现
在这种发现模式下,客户端负责处理可用微服务的网络位置,并在它们之间负载平衡传入请求。客户端需要查询服务注册表(在客户端维护的可用服务的数据库)。然后,客户端根据算法选择服务实例,然后发出请求。Netflix 广泛使用此模式,并已开源其工具 Netflix OSS、Netflix Eureka、Netflix Ribbon 和 Netflix Prana。使用此模式具有以下优势:
-
高性能和可用性,因为转换跳数较少,也就是说,客户端只需调用注册表,注册表将根据其需求重定向到微服务。
-
这种模式相当简单且高度具有弹性,因为除了服务注册表外没有其他移动部分。由于客户端了解可用的微服务,他们可以轻松地做出智能决策,例如何时使用哈希,何时触发自动扩展等。
-
使用此服务发现模式的一个重大缺点是,必须在服务客户端使用的每种编程语言的框架中实现客户端端服务发现逻辑。例如,Java、JavaScript、Scala、Node.js、Ruby 等。
注册模式-自注册
在使用此模式时,任何微服务实例都负责从维护的服务注册表中注册和注销自己。为了维护健康检查,服务实例发送心跳请求以防止其注册表过期。Netflix 使用了类似的方法,并已外包了他们的 Eureka 库,该库处理了服务注册和注销的所有方面。它在 Java 中有自己的客户端,也有 Node.js。Node.js 客户端(eureka-js-client
)每月下载量超过 12,000 次。自注册模式具有重大优势,例如任何微服务实例都将知道自己的状态,因此可以轻松实现或切换到其他模式,例如启动、可用等。
但它也有以下缺点:
-
它将服务紧密耦合到自服务注册表,这迫使我们在框架中使用的每种语言中启用服务注册代码
-
任何运行中但无法处理请求的微服务通常会不知道要追求哪种状态,并且通常最终会忘记从注册表中注销
数据管理
微服务设计方面的另一个重要问题是微服务应用程序中的数据库架构。我们将看到各种选项,例如是否维护私有数据存储,管理事务以及在分布式系统中轻松查询数据存储。最初的想法可能是使用单个数据库,但是如果我们深入思考,很快就会发现这是一个不明智且不合适的解决方案,因为它会导致紧密耦合、不同的需求以及任何服务的运行时阻塞。
每个服务一个数据库
在分布式微服务架构中,不同的服务具有不同的存储需求和使用情况。关系型数据库在维护关系和进行复杂查询时是一个完美的选择。当存在非结构化复杂数据时,NoSQL 数据库如 MongoDB 是最佳选择。有些可能需要图形数据,因此使用 Neo4j 或 GraphQL。解决方案是将每个微服务的数据保持私有,并且只能通过 API 访问。每个微服务都维护其数据存储,并且是该服务实现的私有部分,因此其他服务无法直接访问。
在实施这种数据管理模式时,您可以选择以下一些选项:
-
每个微服务都有一组定义的表或集合,只能由该服务访问
-
每个服务都有一个模式,只能通过其绑定的微服务访问
-
每个微服务维护自己的数据库,根据自己的需求和要求
当考虑到,每个服务维护一个模式似乎是最合乎逻辑的解决方案,因为它将具有较低的开销,并且所有权可以清晰可见。如果一些服务具有高使用率和吞吐量,并且具有不同的使用情况,那么维护单独的数据库是合乎逻辑的选择。一个必要的步骤是添加障碍,以限制任何微服务直接访问数据。添加此障碍的各种选项包括分配具有受限权限的用户 ID 或访问控制机制,例如授予。这种模式具有以下优点:
-
松散耦合的服务可以独立运行;对一个服务的数据存储的更改不会影响任何其他服务。
-
每个服务都有自由选择所需的数据存储。每个微服务都可以根据需要选择关系型或非关系型数据库。例如,任何需要对文本进行密集搜索结果的服务可能会选择 Solr 或 Elasticsearch,而任何需要结构化数据的服务可能会选择任何 SQL 数据库。
这种模式具有以下需要小心处理的缺点和优点:
-
处理涉及跨多个服务的事务的复杂场景。CAP 定理指出,在分布式数据存储中不可能同时满足一致性、可用性和分区中的三个保证中的超过两个,因此通常避免事务。
-
跨多个数据库的查询具有挑战性且消耗资源。
-
管理多个 SQL 和非 SQL 数据存储的复杂性。
为了克服缺点,在维护每个服务的数据库时使用以下模式:
-
Saga:一个 saga 被定义为一批本地事务的序列。批中的每个条目都会更新指定的数据库,并通过发布消息或触发下一个批次中的事件来继续。如果批中的任何条目在本地失败,或者违反了任何业务规则,那么 saga 将执行一系列补偿事务,以补偿或撤消批次更新所做的更改。
-
API 组合:这种模式坚持认为应用程序应该执行连接而不是数据库。举个例子,一个服务专门用于查询组合。因此,如果我们想要获取每月产品分布,那么我们首先从产品服务中检索产品,然后查询分布服务以返回检索到的产品的分布信息。
-
命令查询责任分离(CQRS):这种模式的原则是有一个或多个不断发展的视图,通常这些视图的数据来自各种服务。基本上,它将应用程序分为两部分——命令或操作方和查询或执行方。这更像是一个发布-订阅模式,其中命令方操作创建/更新/删除请求,并在数据发生变化时发出事件。执行方监听这些事件,并通过维护视图来处理这些查询,这些视图根据命令或操作方发出的事件的订阅而保持最新。
共享关注点
分布式微服务架构中的下一个重要问题是如何处理共享关注点。诸如 API 路由、安全性、日志记录和配置等一般事务将如何工作?让我们逐一看看这些要点。
外部化配置
一个应用程序通常会使用一个或多个基础设施的第三方服务,比如服务注册表、消息代理、服务器、云部署平台等等。任何服务都必须能够在多个环境中运行,而不需要进行任何修改。它应该具有获取外部配置的能力。这种模式更多地是一个指导方针,建议我们将所有配置外部化,包括数据库信息、环境信息、网络位置等,创建一个启动服务来读取这些信息并相应地准备应用程序。有各种可用的选项。Node.js 提供设置环境变量;如果您使用 Docker,那么它有docker-compose.yml
文件。
可观测性
重新审视应用程序所需的十二要素,我们可以观察到,即使是分布式的,任何应用程序都需要一些集中的功能。这些集中的功能帮助我们在出现问题时进行适当的监控和调试。让我们看一些常见的可观测性参数。
日志聚合
每个服务实例都会以标准化格式生成有关其正在执行的操作的信息,其中包含各种级别的日志,如错误、警告、信息、调试、跟踪、致命等。解决方案是使用集中式日志服务,从每个服务实例收集日志并将其存储在用户可以搜索和分析日志的某个常见位置。这使我们能够为某些类型的日志配置警报。此外,集中式服务还将有助于进行审计日志记录、异常跟踪和 API 指标。可用且广泛使用的框架包括 Elastic Stack(Elasticsearch、Logstash、Kibana)、AWS CloudTrail 和 AWS CloudWatch。
分布式跟踪
下一个重大问题是理解行为和应用程序,以便在需要时解决问题。这种模式更像是一个设计指南,指出要维护一个由微服务维护的唯一外部请求 ID。这个外部请求 ID 需要传递给处理该请求的所有服务以及所有日志消息。另一个指南是在微服务执行操作时包括请求和操作的开始时间和结束时间。
基于前述的设计方面,我们将看到常见的微服务设计模式,并深入了解每种模式。我们将看到何时使用特定模式,它解决了什么问题,以及在使用该设计模式时要避免的陷阱。
微服务设计模式
随着微服务的发展,其设计原则也在不断发展。以下是一些常见的设计模式,可以帮助设计高效和可扩展的系统。Facebook、Netflix、Twitter、LinkedIn 等公司遵循了一些模式,提供了一些最可扩展的架构。
异步消息传递微服务设计模式
在分布式系统中需要考虑的最重要的事情之一是状态。尽管 REST API 功能强大,但它有一个非常原始的缺陷,即同步和阻塞。这种模式是关于实现非阻塞状态和异步性,以可靠地在整个应用程序中保持相同的状态,避免数据损坏,并允许应用程序快速变化的速度:
-
问题:在特定上下文中,如果我们遵循单一责任原则,应用程序中的模型或实体可能对不同的微服务意味着不同的东西。因此,每当发生任何更改时,我们需要确保不同的模型与这些更改同步。这种模式通过异步消息传递来解决这个问题。为了确保整个过程中的数据完整性,需要在微服务或数据存储之间复制关键业务数据和业务事件的状态。
-
解决方案:由于这是异步通信,客户端或调用者假设消息不会立即收到,继续并将回调附加到服务。回调是为了在接收到响应时进行进一步操作。最好使用轻量级消息代理(不要与 SOA 中使用的编排器混淆)。消息代理是愚蠢的,也就是说,它们对应用程序状态一无所知。它们与处理事件的服务通信,但它们从不处理事件。一些广泛采用的示例包括 RabbitMQ、Azure 总线等。Instagram 的动态由这个简单的 RabbitMQ 提供动力。根据项目的复杂性,您可以引入单个接收器或多个接收器。单个接收器虽然不错,但很快就可能成为单点故障。更好的方法是采用响应式并引入发布-订阅通信模式。这样,发送方的通信将一次性提供给订阅微服务。实际上,当我们考虑常规情况时,对模型的任何更新都将触发所有订阅者的事件,这可能进一步触发它们自己模型的更改。为了避免这种情况,事件总线通常在这种类型的模式中引入,可以充当微服务之间的通信角色并充当消息代理。一些常见的可用库包括AMQP、RabbitMQ、NserviceBus、MassTransit等,用于可扩展的架构。
以下是使用 AMQP 的示例:gist.github.com/parthghiya/114a6e19208d0adca7bda6744c6de23e
。
-
**注意:**要成功实现这个设计,应考虑以下几个方面:
-
当您需要高可伸缩性,或者您当前的领域已经是基于消息的领域时,应优先考虑基于消息的命令而不是 HTTP。
-
在微服务之间发布事件,以及在原始微服务中更改状态。
-
确保事件是跨越通信的;模仿事件将是一个非常糟糕的设计模式。
-
保持订阅者的消费者位置以提高性能。
-
何时进行 REST 调用,何时使用消息调用。由于 HTTP 是同步调用,只有在需要时才应使用。
-
**何时使用:**这是最常用的模式之一。根据以下用例,您可以根据自己的需求使用这种模式或其变体:
-
当您想要使用实时流时,使用Event Firehouse模式,其中KAFKA是其关键组件之一。
-
当您的复杂系统是由各种服务编排时,该系统的变体之一,RabbitMQ,非常有帮助。
-
通常,直接订阅数据存储而不是服务订阅是有利的。在这种情况下,使用GemFire或Apache GeoCode遵循这种模式是有帮助的。
-
**不适用于:**在以下情况下,不推荐使用这种模式:
-
当事件传输期间有大量数据库操作时,因为数据库调用是同步的。
-
当您的服务是耦合的
-
当您没有定义处理数据冲突情况的标准方式时
前端后端
当前的世界要求在任何地方都采用移动优先的方法。服务可能会对移动设备和网页作出不同的响应,在移动设备上,它可能只显示少量内容,因为内容很少。在网页上,它可能要显示大量内容,因为有很多空间。根据设备,情景可能会有很大的不同。例如,在移动应用中,我们可能允许条形码扫描,但在桌面上,这不是一个明智的选择。这种模式解决了这些问题,并有助于有效地设计跨多个接口的微服务:
-
问题:随着支持多个接口的服务的发展,管理一个服务中的所有内容变得非常痛苦。任何单个接口的不断变化都可能很快成为一个瓶颈和难以维护的问题。
-
解决方案:与维护通用 API 不同,为每个用户体验或接口设计一个后端,更好地称为前端的后端(BFFs)。BFF 与单个接口或特定用户体验紧密相关,并由其特定团队维护,以便轻松适应新变化。在实施这种模式时,经常出现的一个问题是维护 BFF 的数量。更通用的解决方案是分离关注点,并让每个 BFF 处理自己的责任。
-
注意:在实施这种设计模式时,应注意以下几点,因为它们是最常见的陷阱:
-
要考虑要维护的 BFF 数量。只有在可以将通用服务的关注点分离出特定接口时,才应创建新的 BFF。
-
BFF 应该只包含客户端/接口特定的代码,以避免代码重复。
-
在团队之间分配 BFF 的维护责任。
-
这不应该与Shim混淆,它是一个转换器,用于将转换为特定接口格式所需的类型接口。
-
何时使用:在以下情况下,这种模式非常有用:
-
通用后端服务在多个接口之间存在差异,并且在单个接口中可能会有多个更新。
-
您希望优化单个接口,而不会干扰其他接口的效用。
-
有各种团队,并且要为特定接口实现另一种语言,并且希望将其单独维护。
-
不适用于以下情况:虽然这种模式解决了许多问题,但在以下情况下不建议使用这种模式:
-
不要使用此模式来处理通用参数问题,如身份验证、安全性或授权。这只会增加延迟。
-
如果部署额外服务的成本太高。
-
当接口发出相同的请求并且它们之间没有太大的区别时。
-
当只有一个接口,不支持多个接口时,BFF 就没有太多意义。
网关聚合和卸载
将专门的、常见的服务和功能转储或移动到网关。通过将共享功能移入单一部分,这种模式可以引入简单性。共享功能可以包括 SSL 证书的使用、身份验证和授权。网关还可以用于将多个请求合并为单个请求。这种模式简化了客户端必须对不同的微服务进行多次调用的需求:
-
问题:通常,为了执行简单的任务,客户端可能需要向各种不同的微服务发出多个 HTTP 调用。向服务器发出太多调用会增加资源、内存和线程,从而对性能和可伸缩性产生不利影响。许多功能通常在多个服务中共同使用;身份验证服务和产品结账服务都会以相同的方式使用登录。这种服务需要配置和维护。此类服务还需要额外的关注,因为它们是必不可少的。例如,令牌验证、HTTPS 证书、加密、授权和身份验证。随着每次部署,跨整个系统进行管理变得困难。
-
解决方案:这种设计模式中的两个主要组件是网关和网关聚合器。网关聚合器应始终放置在网关后面。因此,实现了单一责任,每个组件都执行其预定的操作。
-
**网关:**它将一些常见操作,如证书管理,认证,SSL 终止,缓存,协议转换等,转移到一个地方。它简化了开发,并将所有这些逻辑抽象到一个地方,加快了在一个大型组织中的开发,不是每个人都能访问网关,只有专门的团队才能使用它。它在整个应用程序中保持一致性。网关可以确保最少量的日志记录,从而帮助找到有问题的微服务。这很像面向对象编程中的外观模式。它的作用如下:
-
过滤器
-
暴露各种微服务的单一入口点
-
解决常见操作,比如授权,认证,中央配置等,将这些逻辑抽象成一个地方
-
路由器用于流量管理和监控
Netflix 使用了类似的方法,他们能够处理超过每小时 50,000 个请求,并且他们开源了 ZuuL:
- **网关聚合器:**它接收客户端请求,然后决定要将客户端请求分派给哪些不同的系统,获取结果,然后将它们聚合并发送回客户端。对于客户端来说,这只是一个请求。客户端和服务器之间的总往返次数减少了。
这里有一个聚合器的例子:gist.github.com/parthghiya/3f1c3428b1cf3cc6d76ddd18b4521e03.js
-
注意:为了成功实现微服务中的这种设计模式,应该正确处理以下陷阱:
-
不要引入服务耦合,也就是说,网关可以独立存在,没有其他服务消费者或服务实现者。
-
在这里,每个微服务都将依赖于网关。因此,网络延迟应该尽可能低。
-
确保网关有多个实例,因为只有一个网关实例可能会引入单点故障。
-
每个请求都经过网关。因此,应该确保网关具有高效的内存和足够的性能,并且可以轻松扩展以处理负载。进行一轮负载测试,确保它能够处理大量负载。
-
引入其他设计模式,如舱壁、重试、节流和超时,以实现高效的设计。
-
网关应该处理逻辑,比如重试次数,等待服务直到。
-
应该处理缓存层,这可以提高性能。
-
网关聚合器应该在网关后面,因为请求聚合器将有另一个。将它们合并在一个网关中可能会影响网关及其功能。
-
在使用异步方法时,你会发现自己被太多的回调地狱所困扰。采用响应式方法,更具有声明性风格。响应式编程在从 Java 到 Node.js 到 Android 都很普遍。你可以查看这个链接,了解不同链接上的响应式扩展:
github.com/reactivex
。 -
业务逻辑不应该在网关中。
-
何时使用:在以下情况下应该使用这种模式:
-
有多个微服务,客户端需要与多个微服务通信。
-
当客户端在较小范围的网络或蜂窝网络中时,想要减少频繁的网络调用。将其分解为一个请求是有效的,因为这样前端或网关只需要缓存一个请求。
-
当你想要封装内部结构或向你组织中存在的大团队引入一个抽象层时。
-
不适用于:以下情况是这种模式不适合的情况:
-
当你只想减少网络调用时。你不能为了满足这个需求引入整个层级的复杂性。
-
网关的延迟太大。
-
您在网关中没有异步选项。您的系统对网关中的操作进行了太多同步调用。这将导致阻塞系统。
-
您的应用程序无法摆脱耦合的服务。
代理路由和节流
当您有多个微服务想要跨单个端点公开,并且该单个端点根据需要路由到服务时。当您需要处理即将发生的瞬态故障并在操作失败时进行重试循环时,这种应用是有帮助的,从而提高了应用的稳定性。当您想要处理微服务使用的资源消耗时,这种模式也是有帮助的。
这种模式用于满足约定的 SLA,并在需求增加时处理资源负载和资源分配消耗:
-
问题:当客户端必须消耗大量微服务时,很快就会出现挑战,例如客户端管理每个端点并设置单独的端点。如果您重构任何服务中的任何部分,则客户端也必须更新,因为客户端直接与端点联系。此外,由于这些服务在云中,它们必须具有容错能力。故障包括临时失去连接或服务不可用。这些故障应该是自我纠正的。例如,正在处理大量并发请求的数据库服务应该在内存负载和资源利用率减少之前限制进一步的请求。在重试请求时,操作完成。任何应用程序的负载在时间段上都会有很大的变化。例如,社交媒体聊天平台在高峰办公时间负载很小,而购物门户在节日季销售期间负载很大。为了使系统高效运行,必须满足约定的 LSA,一旦超过,需要停止后续请求,直到负载消耗减少。
-
解决方案:将网关层放置在微服务前面。该层包括节流组件以及一旦失败的重试组件。通过添加这一层,客户端只需与该网关交互,而不是与每个不同的微服务交互。它允许您将后端调用从客户端抽象出来,从而使客户端端简单,因为客户端只需与网关交互。任意数量的服务可以添加,而无需在任何时间点更改客户端。这种模式还可以用于有效处理版本控制。可以并行部署微服务的新版本,并且网关可以根据传递的输入参数进行路由。只需在网关级别进行配置更改即可轻松维护新更改。这种模式可以用作自动扩展的替代策略。该层应该仅允许网络请求达到一定限制,然后节流请求并在资源释放后进行重试。这将有助于系统维护 SLA。在实施节流组件时应考虑以下几点:
-
节流的考虑参数之一是用户请求或租户请求。假设特定租户或用户触发了节流,那么可以安全地假设调用者存在某些问题。
-
节流并不一定意味着停止请求。如果有低质量的资源可用,可以提供,例如,移动友好的网站,低质量的视频等。谷歌也是这样做的。
-
优先考虑微服务。根据优先级,它们可以放置在重试队列中。作为理想的解决方案,可以维护三个队列——取消、重试和稍后重试。
-
注意:在成功实施这种模式时,以下是一些常见的陷阱:
-
网关可能是单点故障。在开发过程中必须采取适当的步骤,确保它具有容错能力。此外,应该运行多个实例。
-
网关应该有适当的内存和资源分配,否则会引入瓶颈。应该进行适当的负载测试,以确保故障不会级联。
-
可以根据 IP、标头、端口、URL、请求参数等进行路由。
-
重试策略应该根据业务需求非常小心地制定。在某些地方,可以选择“请重试”而不是等待一段时间和重试。重试策略也可能影响应用程序的响应性。
-
为了有效应用,这种模式应该与断路器应用程序相结合。
-
如果服务是幂等的,那么只有在这种情况下才应该重试。在其他服务上尝试重试可能会产生不良后果。例如,如果有一个支付服务等待其他支付网关的响应,重试组件可能会认为它失败,然后发送另一个请求,导致客户被收取两次费用。
-
根据异常情况,应该相应地处理重试逻辑。
-
重试逻辑不应干扰事务管理。应根据重试策略使用。
-
所有触发重试的失败都应该被记录并妥善处理,以备将来的情况。
-
需要考虑的一个重要点是,这并不是异常处理的替代品。始终应该优先考虑异常,因为它们不会引入额外的层并增加延迟。
-
限流应该尽早添加到系统中,因为一旦系统实施,就很难添加;它应该被精心设计。
-
限流应该快速执行。它应该足够智能,能够检测活动增加并采取适当措施做出相应反应。
-
根据业务需求决定限流和自动扩展之间的考虑。
-
应该根据优先级有效地将被限流的请求放入队列中。
-
**何时使用:**这种模式在以下情况下非常有用:
-
确保维护约定的 LSA。
-
避免单个微服务消耗大部分资源池,并避免单个微服务耗尽资源。
-
处理微服务消耗突然增加的情况。
-
处理瞬态和短暂的故障。
-
**何时不使用:**在以下情况下,不应该使用这种模式:
-
限流不应该被用作处理异常的手段。
-
当故障持续时间很长时。如果在这种情况下应用这种模式,它将严重影响应用程序的性能和响应性。
大使和边车模式
当我们想要分离常见的连接功能,如监视、日志记录、路由、安全性、身份验证、授权等时,就会使用这种模式。它创建了充当大使和边车的辅助服务,以实现代表服务发送请求的目标。它只是位于进程外部的另一个代理。专门的团队可以在此上工作,让其他人不必担心它,以提供封装和隔离。它还允许应用程序由多个框架和技术组成。
这种模式中的边车组件就像连接到摩托车上的边车一样。它与父微服务具有相同的生命周期,与父微服务一样退役,并且执行基本的外围任务:
-
解决方案:找到一组在不同微服务中通用的操作,并将它们放在它们自己的容器或进程中,从而为整个系统中的所有框架和平台服务提供相同的接口。添加一个充当应用程序和微服务之间代理的大使层。这个大使可以监视性能指标,比如延迟量、资源使用等。大使内的任何内容都可以独立于应用程序进行维护。大使可以部署为容器、常见进程、守护进程或 Windows 服务。大使和侧车不是微服务的一部分,而是连接到微服务的一部分。这种模式的一些常见优势如下:
-
与语言无关的开发侧车和大使,也就是说,你不必为架构中的每种语言构建侧车和大使。
-
只是主机的一部分,因此它可以访问与任何其他微服务相同的资源。
-
由于与微服务的连接,几乎没有延迟
Netflix 使用了类似的方法,并且他们已经开源了他们的工具Prana (github.com/Netflix/Prana
)。看一下下面的图表:
-
注意事项:应该注意以下几点,因为它们是最常见的陷阱:
-
大使可能会引入一些延迟。应该深思熟虑是否使用代理或将通用功能暴露为库。
-
在大使和侧车中添加通用功能是有益的,但对于所有情况都是必需的吗?例如,考虑向服务重试的次数,这可能并不适用于所有用例。
-
大使和侧车将构建、管理和部署的语言或框架的策略。根据需要创建单个实例或多个实例的决定。
-
灵活性,可以从服务传递一些参数到大使和代理,反之亦然。
-
部署模式:当大使和侧车部署在容器中时,这是非常合适的。
-
微服务之间的通信模式应该是框架无关或语言无关的。这在长期来看是有益的。
-
何时使用:这种模式在以下情况下非常有帮助:
-
当涉及多个框架和语言,并且您需要一组通用功能,例如客户端连接、日志记录等,贯穿整个应用程序。大使和侧车可以被应用程序中的任何服务使用。
-
服务由不同的团队或不同的组织拥有。
-
您需要独立的服务来处理这些横切功能,并且它们可以独立维护。
-
当您的团队庞大,您希望专门的团队来处理、管理和维护核心横切功能时。
-
您需要支持遗留应用程序或难以更改的应用程序中的最新连接选项。
-
您希望监视整个应用程序的资源消耗,并在其资源消耗巨大时切断微服务。
-
不适用于:虽然这种模式解决了许多问题,但在以下情况下不建议使用这种模式:
-
当网络延迟至关重要时。引入代理层会带来一些开销,这将导致轻微延迟,这对实时情况可能不利。
-
当连接功能无法通用化,并且需要与另一个服务进行另一级别的集成和依赖时。
-
当创建客户端库并将其作为软件包分发给微服务开发团队时。
-
对于引入额外层实际上是一种负担的小型应用程序。
-
当一些服务需要独立扩展时;如果是这样,更好的选择是将其单独部署和独立运行。
反腐微服务设计模式
通常,我们需要在传统和现代应用程序之间进行互操作或共存。通过在现代和传统应用程序之间引入一个外观,这种设计为此提供了一个简单的解决方案。这种设计模式确保了应用程序的设计不会受到传统系统依赖的阻碍或阻挠:
-
**问题:**新系统或正在迁移过程中的系统通常需要与传统系统进行通信。新系统的模型和技术可能会有所不同,考虑到旧系统通常比较薄弱,但仍然可能需要传统资源进行某些操作。通常,这些传统系统的设计和模式设计都很差。为了实现互操作性,我们可能仍然需要支持旧系统。这种模式是为了解决这种腐败,并且仍然拥有一个更清洁、更整洁、更易于维护的微服务生态系统。
-
**解决方案:**为了避免使用传统代码或传统系统,设计一个层,完成以下任务:作为与传统代码通信的唯一层,防止直接访问传统代码,不同的人可能以不同的方式处理它们。核心概念是通过放置一个 ACL 来分离传统或腐败应用程序,从而避免改变传统层,并且避免妥协其方法或主要技术变更。
-
反腐层(ACL)应该包含根据新需求从旧模型进行翻译的所有逻辑。这一层可以作为一个独立的服务或翻译器组件引入到需要的任何地方。组织 ACL 设计的一般方法是将外观、适配器、翻译器和通信器结合起来,以与系统进行通信。ACL 用于防止外部系统的意外行为泄漏到现有上下文中:
-
**注意:**在有效实施这种模式时,应考虑以下几点,因为它们是一些主要的陷阱:
-
ACL 应该被适当地扩展,并提供更好的资源池,因为它会增加两个系统之间的通话延迟。
-
确保您引入的腐败层实际上是一种改进,而不是引入另一层腐败。
-
ACL 添加了额外的服务;因此必须进行有效的管理、维护和扩展。
-
有效地确定 ACL 的数量。引入 ACL 可能有很多原因——将对象的不良格式转换为所需格式的手段,在不同语言之间进行通信等等。
-
有效措施确保在两个系统之间保持事务和数据一致性,并且可以进行监控。
-
ACL 的持续时间,它会是永久的吗,通信将如何处理。
-
虽然 ACL 应该成功处理来自腐败层的异常,但不应完全处理,否则将非常难以保留有关原始错误的任何信息。
-
**何时使用:**在以下情况下,强烈推荐使用反腐模式,并且极其有用:
-
有一个大型系统需要从单片到微服务进行重构,计划进行分阶段迁移,而不是一次性迁移,其中传统系统和新系统需要共存并相互通信。
-
如果您正在处理任何数据源的系统,其模型是不良的或与所需模型不同步,可以引入这种模式,并且它将完成从不良格式到所需格式的翻译任务。
-
每当需要链接两个有界上下文时,也就是说,一个系统是由完全不同的其他人开发的,对它的理解非常有限,这种模式可以作为系统之间的链接引入。
-
**不适用于:**在以下情况下,强烈不建议使用这种模式:
-
新系统和传统系统之间没有主要区别。新系统可以在没有传统系统的情况下共存。
-
您有大量的事务操作,并且在 ACL 和损坏层之间维护数据一致性会增加太多的延迟。在这种情况下,可以将此模式与其他模式合并。
-
您的组织没有额外的团队来在需要时维护和扩展 ACL。
分隔设计模式
将微服务应用程序中的不同服务分开到各种池中,以便如果其中一个服务失败,其他服务将继续运行而不受失败的影响。为每个微服务创建一个不同的池以最小化影响:
-
问题:这种模式受到船体的分隔部分的启发。如果一艘船的船体受损,那么只有受损的部分会进水,这将防止船沉没。假设您正在连接各种使用共同线程池的微服务。如果其中一个服务开始显示延迟,那么所有池成员都会过度等待响应。逐渐地,来自一个服务的大量请求会耗尽可用资源。这就是这种模式建议为每个单独的服务提供专用池的地方。
-
解决方案:根据负载和网络使用情况将单独的服务实例分成不同的组。这样可以隔离系统故障并防止连接池资源耗尽。这个系统的主要优势是防止故障传播和能够配置资源池的容量。对于优先级较高的服务,可以分配更高的池。
例如,给出了一个示例文件,我们可以看到服务购物管理的池分配:gist.github.com/parthghiya/be80246cc5792f796760a0d43af935db
。
-
注意:确保注意以下几点,以确保正确实施分隔设计:
-
根据业务和技术要求在应用程序中定义适当的独立分区。
-
分隔可以以线程池和进程的形式引入。决定哪种适合您的应用程序。
-
在微服务的部署中进行隔离。
-
何时使用:在以下情况下,分隔模式具有优势:
-
应用程序庞大,您希望保护它免受级联或传播故障的影响
-
您可以将关键服务与标准服务隔离,并为它们分配单独的池
-
何时不使用:不建议在以下情况下使用这种模式:
-
当你没有足够的预算来维护成本和管理方面的独立开销时
-
维护单独的池的额外复杂性是不必要的
-
您的资源使用是意外的,您无法隔离您的租户并对其进行限制,因为当您将多个租户放在一个分区中时是不可接受的
断路器
有时服务需要相互协作处理请求。在这种情况下,另一个服务不可用、显示高延迟或不可用的可能性非常高。这种模式通过引入断路器来解决这个问题,停止整个架构中的传播:
-
问题:在微服务架构中,当服务之间进行通信时,需要调用远程调用而不是内存调用。可能会出现远程调用失败或达到超时限制而挂起没有任何响应的情况。在这种情况下,如果有很多调用者,那么所有这些被锁定的线程可能会耗尽资源,整个系统将变得无响应。
-
解决方案:解决这个问题的一个非常原始的想法是引入一个保护函数调用的包装器,它监视失败。现在,这个包装器可以通过任何触发,比如失败的特定阈值、数据库连接失败等。所有进一步的调用都将返回错误并停止灾难性的传播。这将打开断路器,并且在断路器打开时,它将避免进行受保护的调用。实现分为以下三个阶段,就像电路一样。它有三个阶段:关闭状态、打开状态和半开状态,如下图所示:
以下是 Node.js 中实现的示例:Netflix 开源了 Hystrix gist.github.com/parthghiya/777c2b423c9c8faf0d427fd7a3eeb95b
-
注意事项:当您想应用断路器模式时,需要注意以下事项:
-
由于您正在调用远程调用,并且可能有许多远程调用异步和反应性原则,因此必须使用未来、承诺、异步和等待。
-
维护一个请求队列;当您的队列过度拥挤时,您可以轻松地触发断路器。始终监视断路器,因为您经常需要再次激活它以获得高效的系统。因此,准备好重置和故障处理程序的机制。
-
您有一个持久存储和网络缓存,比如Memcache或Redis来记录可用性。
-
记录、异常处理和转发失败的请求。
-
何时使用:在以下用例中,您可以使用断路器模式:
-
当您不希望耗尽资源时,也就是说,一个注定会失败的操作在修复之前不应该尝试。您可以使用它来检查外部服务的可用性。
-
当您可以在性能上做出一些妥协,但希望获得系统的高可用性并且不耗尽资源。
-
不适用的情况:在以下情景中,不建议使用断路器模式:
-
您没有一个有效的缓存层,用于在集群节点之间维护服务状态的给定时间内的请求。
-
用于处理内存结构或作为业务逻辑中异常处理的替代方案。这会增加性能开销。
窒息器模式
今天的世界是一个技术不断发展的世界。今天写的东西,明天就成了遗留代码。这种模式在迁移过程中非常有帮助。这种模式是关于通过逐步用新的微服务应用程序和服务替换特定功能的遗留系统。最终引入一个代理,将流量重定向到遗留系统或新的微服务,直到迁移完成,最后可以关闭窒息器或代理:
-
问题:随着老化系统、新兴的开发工具和托管选项,云和无服务器平台的发展,维护当前系统变得非常痛苦,因为需要增加新功能和功能。完全替换一个系统可能是一项艰巨的任务,需要逐步迁移,以便仍然处理尚未迁移的部分。这种模式解决了这个问题。
-
解决方案:窒息器解决方案类似于一根藤蔓,它缠绕在树上窒息。随着时间的推移,迁移的应用程序窒息原始应用程序,直到您可以关闭单片应用程序。因此,整个过程如下:
-
重构:构建一个新的应用程序或站点(基于现代原则的无服务器或 AWS 云)。以敏捷的方式逐步重构功能。
-
共存:保留旧应用程序不变。引入一个最终充当代理并根据当前迁移状态决定路由请求的外观。这个外观可以根据 IP 地址、用户代理或 cookie 等各种参数在 Web 服务器级别或编程级别引入。
-
终止:将所有内容重定向到现代迁移的应用程序,并解除与旧应用程序的所有联系。
可以在此链接找到充当外观的.htaccess
的示例要点:gist.github.com/parthghiya/a6935f65a262b1d4d0c8ac24149ce61d
。
解决方案指示我们创建一个具有拦截请求能力的外观或代理,该请求将发送到后端旧系统。然后,外观或代理决定是将其路由到旧应用程序还是新的微服务。这是一个渐进的过程,两个系统可以共存。最终用户甚至不会知道迁移过程何时完成。它的附加优势是,如果采用的微服务方法不起作用,有一种非常简单的方法可以更改它。
-
注意:有效应用窒息模式需要注意以下要点:
-
外观或代理需要随着迁移而更新。
-
外观或代理不应该是单点故障或瓶颈。
-
迁移完成后,外观将作为适配器适用于旧应用程序。
-
新编写的代码应该易于拦截,这样将来我们可以在迁移中替换它。
-
何时使用:当要用微服务替换旧的单片应用程序时,窒息应用程序非常有用。该模式在以下情况下使用:
-
当您想要遵循测试驱动或行为驱动开发,并快速运行全面测试,以便访问代码覆盖率并适应 CI/CD 时。
-
您的应用程序可以在区域内应用有界上下文。例如,在购物车应用程序中,产品模块将是一个上下文。
-
不适用于:在以下情况下,此模式可能不适用:
-
当您无法拦截用户代理请求,或者无法在架构中引入外观时。
-
当您考虑一次一页页迁移或一次全部迁移时。
-
当您的应用程序更多地受前端驱动时;这就是您必须完全改变并重新设计基于前端与服务交互的框架的地方,因为您不希望暴露用户代理与服务交互的各种方式。
总结
在本章中,我们揭示了微服务,以了解微服务的演变、微服务的特点和优势。我们讨论了微服务的各种设计原则,从单片应用程序到微服务的重构过程,以及各种微服务设计模式。
在下一章中,我们将开始我们的微服务之旅。我们将了解微服务之旅所需的所有设置。我们将学习与本书始终相关的 Node.js 和 TypeScript 相关的概念。我们还将创建我们的第一个微服务Hello World
。
第二章:为旅程做好准备
在学习了关于微服务的理论之后,我们现在将转向实际实现。本章将为前进的旅程奠定基础,并重新审视对本书至关重要的 Node.js 和 TypeScript 概念。它将告诉您关于这两种语言的趋势和采纳率。我们将通过所有必需的安装,并准备好我们的开发环境。我们将通过实现传统的Hello World
微服务来测试开发环境。在本章中,我们将重点关注以下主题:
-
设置主要开发环境:我们将设置一个带有所有必需先决条件的主要环境。我们将了解微服务开发所需的所有方面。
-
TypeScript 入门:在本节中,我们将介绍一些我们将在整本书中使用的主要 TypeScript 主题。我们将证明在 Node.js 中使用 TypeScript 作为我们的语言,并了解如何使用 TypeScript 和 Node.js 编写应用程序。
-
Node.js 入门:在本节中,我们将介绍一些高级的 Node.js 主题,如 Node.js 中的集群、最近引入的 async/await 等。我们将了解事件循环,并简要介绍 Node 流和 Node.js 的最新趋势。
-
微服务实现:我们将编写一个
Hello World
微服务,该微服务将使用我们的开发环境。
设置主要环境
在这一部分,我们将设置我们前进旅程所需的环境。您已经全局安装了 Node.js 和 TypeScript。在撰写本文时,Node.js 的可用版本是9.2.0,TypeScript 的版本是2.6.2。
Visual Studio Code(VS Code)
VS Code是目前最好的 TypeScript 编辑器之一。默认情况下,VS Code TypeScript 会显示有关不正确代码的警告,这有助于我们编写更好的代码。VS Code 提供了 Linter、调试、构建问题、错误等功能。它支持 JSDoc、sourcemaps、为生成的文件设置不同的输出文件、隐藏派生的 JavaScript 文件等。它支持自动导入,直接生成方法骨架,就像 Java 开发人员的 Eclipse 一样。它还提供了版本控制系统的选项。因此,它将是我们作为 IDE 的首选。您可以从code.visualstudio.com/download
下载它。
在 Windows 上安装它是最简单的,因为它是一个.exe
文件,您只需选择一个路径并按照步骤操作即可。在 Unix/Ubuntu 机器上安装它涉及下载deb
文件,然后执行以下命令行:
sudo dpkg -i <file>.deb
sudo apt-get install -f # Install dependencies
一旦 VS Code 可用,打开扩展并下载marketplace.visualstudio.com/items?itemName=pmneo.tsimporter
和marketplace.visualstudio.com/items?itemName=steoates.autoimport
。我们将使用这些扩展的优势,这将有助于轻松管理代码、预构建骨架等。
PM2
它是 Node.js 的高级处理器管理器。Node.js 是单线程的,需要一些附加工具来进行服务器管理,如重新启动服务器、内存管理、多进程管理等。它有一个内置的负载均衡器,并允许您使应用程序永久运行。它具有零停机时间和其他简化生活的系统管理选项。它还作为一个模块暴露出来,因此我们可以在 Node.js 应用程序的任何阶段运行时触发各种选项。要安装 PM2,请打开终端并输入以下命令:
npm install pm2 -g
更详细的选项和 API 可以在pm2.keymetrics.io/docs/usage/pm2-api/
找到。
NGINX
NGINX是最受欢迎的 Web 服务器之一。它可以用作负载均衡器、HTTP 缓存、反向代理和减震器。它具有处理超过 10,000 个同时连接的能力,占用空间非常小(大约每 10,000 个非活动连接占用 2.5 MB en.wikipedia.org/wiki/HTTP_persistent_connection
)。它专门设计用来克服 Apache。它大约可以处理比 Apache 多四倍的每秒请求。NGINX 可以以各种方式使用,例如以下方式:
-
独立部署
-
作为 Apache 的前端代理,充当网络卸载设备
-
充当减震器,防止服务器突然出现的流量激增或慢的互联网连接
它是我们微服务应用程序的完美选择,因为容器化的微服务应用程序需要一个前端,能够隐藏和处理其后运行的应用程序的复杂和不断变化的特性。它执行一些重要的功能,如将 HTTP 请求转发到不同的应用程序,减震保护,路由,日志记录,Gzip 压缩,零停机时间,缓存,可伸缩性和容错性。因此,它是我们理想的应用交付平台。让我们开始 NGINX 101。
从这个网站nginx.org/en/download.html
下载最新版本,根据你的操作系统。在撰写本文时,主线版本是1.13.7。
解压后,您可以按照以下方式简单地启动 NGINX:
start nginx
要检查 NGINX 是否启动,可以在 Windows 中输入以下命令:
tasklist /fi "imagename eq nginx.exe"
在 Linux 的情况下,您可以使用以下命令行:
ps waux | grep nginx
以下是其他有用的 NGINX 命令:
nginx -s stop | 快速关闭 |
---|---|
nginx -s quit | 优雅关闭 |
nginx -s reload | 更改配置,使用新配置启动新的工作进程,并优雅地关闭旧的工作进程 |
nginx -s reopen | 重新打开日志文件 |
Docker
Docker是一个开源平台,用于开发、发布和运行应用程序,其主要优势是将应用程序与基础架构分离,因此您可以轻松快速地适应重大变化。Docker 提倡容器的理念。容器是任何配置图像的可运行实例。容器与其他容器和主机完全隔离。这非常类似于我们的微服务理念。当我们进行部署时,我们将更详细地了解 Docker。让我们在系统上安装 Docker。
Windows 的 Docker 需要 Windows 10 专业版和 Hyper-V。因此,作为一个普遍可用的替代方案,我们将选择 Linux。Windows 用户可以下载 Oracle VM VirtualBox,下载任何 Linux 镜像,然后按照相同的过程进行。请按照这里给出的步骤进行操作:docs.docker.com/engine/installation/linux/docker-ce/ubuntu/
。
要检查安装情况,请输入以下命令:
sudo docker run hello-world
您应该看到如下输出:
Docker 安装
TypeScript 入门
TypeScript起源于 JavaScript 开发中的缺陷,随着将 JavaScript 用于大规模应用的出现。TypeScript 引入了一个 JavaScript 编译器,具有语法语言扩展的预设、基于类的编程和将扩展转换为常规 JavaScript 的方法。TypeScript 因引入了 JavaScript 的类型安全而变得极为流行,而 JavaScript 恰好是最灵活的语言之一。这使 JavaScript 成为了一种更面向对象和编译安全的语言。TypeScript 更像是 ES 标准的超集,它使开发人员能够编写更清晰、易于重构和可升级的代码。在本节中,我们将介绍 TypeScript 的各种主要主题,这对我们未来的旅程至关重要。TypeScript 是带有类型注释的 JavaScript。TypeScript 具有一个转译器和类型检查器,如果类型不匹配,则会抛出错误,并将 TypeScript 代码转换为 JavaScript 代码。我们将简要介绍以下主题,这些主题基本上将帮助我们编写 TypeScript 中的 Node.js:
-
理解
tsconfig.json
-
理解类型
-
在 Node.js 中调试 TypeScript
理解 tsconfig.json
添加tsconfig.json
文件表示有一个 TypeScript 项目的目录,并且需要一个配置文件来将 TypeScript 编译成 JavaScript。您可以使用tsc
命令将 TypeScript 编译成 JavaScript。调用它,编译器会搜索在tsconfig.json
中加载的配置。您可以指定对整个项目(从当前目录到父目录)的编译,或者您可以为特定项目指定tsc
。您可以使用以下命令找到所有可能的选项:
tsc --help
让我们看看这个命令做了什么:
tsc 帮助命令
截至撰写时,TypeScript 的版本为2.6.2,所有上下文都将基于相同的版本进行。如果您没有更新的版本,请运行以下命令:
npm uninstall typescript -g
npm install typescript@latest -g
现在让我们看一下示例tsconfig.json
文件和所有可用的选项:
{
"compilerOptions":{
"target":"es6",
"moduleResolution":"node",
"module":"commonjs",
"declaration":false,
"noLib":false,
"emitDecoratorMetadata":true,
"experimentalDecorators":true,
"sourceMap":true,
"pretty":true,
"allowUnreachableCode":true,
"allowUnusedLabels":true,
"noImplicitAny":true,
"noImplicitReturns":false,
"noImplicitUseStrict":false,
"outDir":"dist/",
"baseUrl":"src/",
"listFiles":false,
"noEmitHelpers":true
},
"include":[
"src/**/*"
],
"exclude":[
"node_modules"
],
"compileOnSave":false
}
现在让我们解析这个文件,并了解最常用的选项。
compilerOptions
这里提到了编译项目所需的所有设置。可以在此网站找到所有编译器选项的详细列表,以及默认值:www.typescriptlang.org/docs/handbook/compiler-options.html
。如果我们不指定此选项,那么将选择默认值。这是我们指示 TypeScript 如何处理各种事物的文件,例如各种装饰器、支持 JSX 文件和转译纯 JavaScript 文件。以下是一些最常用的选项,我们可以根据前面的示例代码了解:
noImplicitAny | 这告诉tsc 编译器,如果发现变量声明具有接受任何类型的声明,但缺少任何类型的显式类型定义,则会发出警告。 |
---|---|
experimentalDecorators | 此选项启用在 TypeScript 项目中使用装饰器。ES 尚未引入装饰器,因此默认情况下它们是禁用的。装饰器是可以附加到类声明、方法、访问器、属性或参数的任何声明。使用装饰器简化了编程。 |
emitDecoratorMetaData | TypeScript 支持为具有装饰器的任何声明发出某些类型的元数据。要启用此选项,必须在tsconfig.json 中将其设置为 true。 |
watch | 此选项更像是livereload ;每当源文件中的任何文件更改时,编译过程将重新触发,以再次生成转译文件。 |
reflect-metadata | 它保留对象元数据中的类型信息。 |
module | 这是输出模块类型。Node.js 使用 CommonJS,所以模块中有 CommonJS。 |
target | 我们正在针对的输出预设;Node.js 使用 ES6,所以我们使用 ES6。 |
moduleResolution | 此选项将告诉 TypeScript 使用哪种解析策略。Node.js 用户需要一个模块策略,因此 TypeScript 使用此行为来解析这些依赖项。 |
sourceMap | 这告诉 TypeScript 生成源映射,可以像调试 JavaScript 一样轻松地用于调试 TypeScript。 |
outDir | 应该保存转换后文件的位置。 |
baseUrl 和 paths | 指导 TypeScript 在哪里可以找到类型文件。我们基本上告诉 TypeScript 对于每个(* )在 .ts 文件中找到的内容,它需要在文件位置 <base_url> + src/types/* 中查找。 |
包括和排除
在这里,我们定义了项目的上下文。它基本上采用了一个需要包含在编译路径中的全局模式数组。您可以包含或排除一组全局模式,以添加或删除文件到转换过程中。请注意,这不是最终值;还有属性文件,它们接受文件名数组,并覆盖包含和排除。
extends
如果我们想要扩展任何基本配置,那么我们可以使用此选项并指定它必须扩展的文件路径。您可以在 json.schemastore.org/tsconfig
找到 tsconfig.json
的完整模式。
理解类型
如果我们想要有效地和全局地使用 TypeScript,TypeScript 需要跨越其他 JavaScript 库。TypeScript 使用 .d.ts
文件来提供未在 ES6 或 TypeScript 中编写的 JavaScript 库的类型。一旦定义了 .d.ts
文件,就可以很容易地看到返回类型并提供简单的类型检查。TypeScript 社区非常活跃,并为大多数文件提供类型:github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types
。
重新审视我们的 tsconfig.json
文件,我们已经指定了选项 noImplicitAny: true
,并且我们需要为我们使用的任何库都有一个强制的 *.d.ts
文件。如果将该选项设置为 false,tsc
将不会给出任何错误,但这绝对不是推荐的做法。为我们使用的每个库都有一个 index.d.ts
文件是标准做法之一。我们将看看各种主题,比如如何安装类型,如果类型不可用怎么办,如何生成类型,以及类型的一般流程是什么。
从 DefinitelyTyped 安装类型
任何库的类型都将是一个 dev
依赖项,您只需从 npm
安装它。以下命令安装 express 类型:
npm install --save-dev @types/express
此命令将下载 express 类型到 @types
文件夹,并且 TypeScript 会在 @types
文件夹中查找以解析该类型的映射。由于我们只在开发时需要它,所以我们添加了 --save-dev
选项。
编写自己的类型
许多时候,我们可能需要编写自己的 .d.ts
文件,以便有效地使用 TypeScript。我们将看看如何生成我们自己的类型,并指导 TypeScript 从哪里找到这些类型。我们将使用自动化工具,并学习如何手动编写我们自己的 .d.ts
文件,然后告诉 TypeScript 在哪里找到自定义类型的位置。
使用 dts-gen 工具
这是微软提供的一个开源实用工具。我们将使用它为任何项目生成我们的类型。作为管理员启动终端,或者使用 sudo su -
并输入以下内容:
npm install -g dts-gen
对于所有全局模块,我们将在 Windows 上使用命令提示符作为管理员,而在 Linux/Mac 上,我们将使用 root 用户或 sudo su -
。
我们将使用一个全局可用的模块并生成其类型。安装 lusca
并使用以下命令生成其类型:
dts-gen -m lusca
你应该看到输出,比如Wrote 83 Lines to lusca.d.ts
,当你检查时,你可以看到所有的方法声明,就像一个接口一样。
编写你自己的*.d.ts 文件
当你编写自己的*.d.ts
文件时,风险非常高。让我们为任何模块创建我们自己的*.d.ts
文件。比如我们想为my-custom-library
编写一个模块:
- 创建一个名为
my-custom-library.d.ts
的空文件,并在其中写入以下内容:
declare module my-library
这将使编译器静音,不会抛出任何错误。
- 接下来,你需要在那里定义所有的方法以及每个方法期望的返回类型。你可以在这里找到几个模板:
www.typescriptlang.org/docs/handbook/declaration-files/templates.html
。在这里,我们需要定义可用的方法以及它们的返回值。例如,看一下以下代码片段:
declare function myFunction1(a: string): string;
declare function myFunction2(a: number): number;
调试
下一个重要的问题是如何调试返回 TypeScript 的 Node.js 应用程序。调试 JavaScript 很容易,为了提供相同的体验,TypeScript 有一个名为sourcemaps的功能。当 TypeScript 中启用 sourcemaps 时,它允许我们在 TypeScript 代码中设置断点,当命中等效的 JavaScript 行时会暂停。sourcemaps 的唯一目的是将生成的源映射到生成它的原始源。我们将简要看一下在我们的编辑器 VS Code 中调试 Node.js 和 TypeScript 应用程序。
首先,我们需要启用 sourcemaps。首先,我们需要确保 TypeScript 已启用 sourcemaps 生成。打开你的tsconfig.json
文件,并写入以下内容:
"compilerOptions":{
"sourceMap": true
}
现在当你转译你的项目时,你会在生成的每个 JavaScript 文件旁边看到一个.js.map
文件。
接下来要做的是配置 VS Code 进行调试。创建一个名为.vscode
的文件夹,并添加一个名为launch.json
的文件。这与使用node-inspector
非常相似。我们将调试node-clusters
项目,你可以在源代码中找到。在 VS Code 中打开该项目;如果没有dist
文件夹,则在主级别执行tsc
命令生成一个分发,这将创建dist
文件夹。
接下来,创建一个名为.vscode
的文件夹,在其中创建一个名为launch.json
的文件,并进行以下配置:
VS Code 调试
当你点击开始调试时,会出现以下屏幕。看一下屏幕,其中有关调试点的详细描述:
VS 调试器
Node.js 入门
Node.js 经过多年的发展,现在已成为任何想要拥抱微服务的人的首选技术。Node.js 是为了解决大规模 I/O 扩展问题而创建的,当应用于我们的微服务设计时,将会产生一种天作之合。Node.js 的包管理器比 Maven、RubyGems 和 NuGet 拥有更多的模块,可以直接使用并节省大量的生产时间。异步性质、事件驱动 I/O 和非阻塞模式等特性使其成为创建高端、高效性能、实时应用程序的最佳解决方案之一。当应用于微服务时,它将能够处理极大量的负载,响应时间低,基础设施低。让我们来看一下 Node.js 和微服务的成功案例之一。
PayPal看到 Node.js 的趋势,决定在他们的账户概览页面使用 Node.js。他们对以下结果感到困惑:
-
Node.js 应用程序开发的速度是 Java 开发的两倍,而且人手更少
-
代码的行数(LOC)减少了 33%,文件减少了 40%
-
单核 Node.js 应用程序处理的请求每秒是五核 Java 应用程序设置的两倍
Netflix、GoDaddy、Walmart 等许多公司都有类似的故事。
让我们看一些对 Node.js 开发至关重要的主要和有用的概念,这些概念将贯穿我们的旅程。我们将涉及各种主题,如事件循环、如何实现集群、异步基础知识等。
事件循环
由于 Node.js 的单线程设计,它被认为是最复杂的架构之一。作为完全事件驱动的,理解事件循环对于掌握 Node.js 至关重要。Node.js 被设计为一个基于事件的平台,这意味着在 Node.js 中发生的任何事情都只是对事件的反应。在 Node.js 中进行的任何操作都会经过一系列的回调。完整的逻辑被开发人员抽象出来,并由一个名为libuv
的库处理。在本节中,我们将对事件循环有一个全面的了解,包括它的工作原理、常见误解、各种阶段等。
以下是关于事件循环的一些常见谬误以及实际工作的简要介绍:
-
谬误#1—事件循环在与用户代码不同的线程中工作:有两个线程,一个是用户相关代码或用户相关操作运行的父线程,另一个是事件循环代码运行的线程。任何时候执行操作,父线程将工作传递给子线程,一旦子线程操作完成,就会通知主线程执行回调:
-
事实:Node.js 是单线程的,一切都在单个线程内运行。事件循环维护回调的执行。
-
谬误#2—线程池处理异步事件:所有异步操作,如回调到数据库返回的数据,读取文件流数据和 WebSockets 流,都会从
libuv
维护的线程池中卸载: -
事实:
libuv
库确实创建了一个包含四个线程的线程池来传递异步工作,但今天的操作系统已经提供了这样的接口。因此,作为一个黄金法则,libuv
将使用这些异步接口而不是线程池。线程池只会被用作最后的选择。 -
谬误#3—事件循环像 CPU 一样维护操作的堆栈或队列:事件循环按照FIFO 规则维护一系列异步任务的队列,并执行队列中维护的定义的回调:
-
事实:虽然
libuv
中涉及类似队列的结构,但回调并不是通过堆栈处理的。事件循环更像是一个阶段执行器,任务以循环方式处理。
理解事件循环
现在我们已经排除了关于 Node.js 中事件循环的基本误解,让我们详细了解事件循环的工作原理以及事件循环阶段执行周期中的所有阶段。Node.js 在以下阶段处理环境中发生的所有事情:
-
定时器:这是所有
setTimeout()
和setInterval()
回调被执行的阶段。这个阶段会尽早运行,因为它必须在调用函数中指定的时间间隔内执行。当定时器被安排时,只要定时器是活动的,Node.js 事件循环将继续运行。 -
I/O 回调:除了定时器、关闭连接事件、
setImmediate()
之外,大多数常见的回调都在这里执行。I/O 请求既可以是阻塞的,也可以是非阻塞的。它执行更多的事情,比如连接错误,无法连接到数据库等。 -
轮询:当阈值已经过去时,这个阶段执行定时器的脚本。它处理轮询队列中维护的事件。如果轮询队列不为空,事件循环将同步迭代整个队列,直到队列为空或系统达到硬峰值大小。如果轮询队列为空,事件循环将继续下一个阶段——检查并执行那些定时器。如果没有定时器,轮询队列是空闲的,它将等待下一个回调并立即执行它。
-
检查:当轮询阶段处于空闲状态时,将执行检查阶段。现在将执行使用
setImmediate()
排队的脚本。setImmediate()
是一个特殊的计时器,它使用libuv
API,并且安排在轮询阶段之后执行回调。它被设计成在轮询阶段之后执行。 -
关闭回调:当任何句柄、套接字或连接突然关闭时,将在此阶段发出关闭事件,例如
socket.destroy()
,连接close()
,也就是说,所有(close
)事件回调都在此处处理。虽然不是事件循环的技术部分,但另外两个主要阶段是nextTickQueue
和其他微任务队列。nextTickQueue
在当前操作完成后处理,不管事件循环的阶段如何。它会立即触发,在调用它的同一阶段,并且独立于所有阶段。nextTick
函数可以包含任何任务,它们只是按照以下方式调用:
process.nextTick(() => {
console.log('next Tick')
})
接下来重要的部分是微任务和宏任务。NextTickQueue
的优先级高于微任务和宏任务。任何在nextTickQueue
中的任务都将首先执行。微任务包括已解决的 promise 回调等函数。一些微任务的例子可以是promise.resolve
,Object.resolve
。这里有一个有趣的地方要注意,原生 promise 只属于微任务。如果我们使用q
或bluebird
等库,我们会看到它们首先被解决。
Node.js 集群和多线程
任何 Node.js 实例都在单个线程中运行。如果发生任何错误,线程会中断,服务器会停止,你需要重新启动服务器。为了利用系统中所有可用的核心,Node.js 提供了启动一组 Node.js 进程的选项,以便负载均衡。有许多可用的工具可以做同样的事情。我们将看一个基本的例子,然后学习关于PM2这样的自动化工具。让我们开始吧:
- 第一步是创建一个 express 服务器。我们需要
express
,debug
,body-parser
和cookie-parser
。打开终端并输入以下命令:
npm install body-parser cookie-parser debug express typescript --save
- 接下来,我们下载这些模块的类型:
npm install @types/debug @types/node @types/body-parser @types/express
- 然后,我们创建我们的
app.ts
和www.ts
文件。构建你的app.ts
文件如下:
表达 TypeScript 的方式
- 对于
www.ts
,我们将使用cluster
模块,并创建可用作核心数量的工作进程。我们的逻辑将分为以下几部分:
import * as cluster from "cluster";
import { cpus } from "os";
if (cluster.isMaster) {
/* create multiple workers here cpus().length will give me number of cores available
*/
cluster.on("online", (worker) => { /*logic when worker becomes online*/ });
cluster.on("exit", (worker) => { /*logic when worker becomes online*/ });
} else {
//our app intialization logic
}
- 现在当我们转译源代码并运行
www.js
时,我们会看到多个工作进程在线。
完整的文件可以在node-clusters/src/bin/www.ts
中找到。去运行应用程序。现在你应该看到多个工作进程在线了。
另一种方法是使用PM2 (www.npmjs.com/package/pm2
)。PM2 有各种选项,如livereload
,零停机重新加载和集群启动模式。PM2 中可用的一些示例命令如下:
pm2 start www.js -i 4 | 在集群模式下启动应用程序的四个实例。它将平衡负载到每个节点。 |
---|---|
pm2 reload www.js | 重新加载www.js 并进行零停机时间。 |
pm2 scale www.js 10 | 将集群应用程序扩展到 10 个进程。 |
异步/等待
由于 JavaScript 是异步的,一旦一个进程完成,就很难维护任务的执行。曾经以回调开始,很快就转向了 promises、async 模块、生成器和 yield,以及 async 和 await。让我们从 async/await 101 开始:
-
异步/等待是编写异步代码的现代方式之一
-
建立在 promise 之上,不能与普通回调或 Node promises 一起使用
-
异步/等待是非阻塞代码,尽管它看起来是同步的,这是它的主要优势
-
基于
node-fibers
,它是轻量级的,并且对 TypeScript 友好,因为类型已嵌入其中
现在让我们看一下 async/await 的实际实现。曾经作为巨大的回调地狱和嵌套的.then()
链的东西现在可以简化为以下内容:
let asyncReq1=await axios.get('https://jsonplaceholder.typicode.com/posts/1');
console.log(asyncReq1.data);
let asyncReq2=await axios.get('https://jsonplaceholder.typicode.com/posts/1');
console.log(asyncReq2.data);
现在我们将研究两种常见的 async/await 设计模式。
重试失败的请求
通常,我们在系统中添加安全或重试请求,以确保如果服务返回错误,我们可以在服务暂时关闭时重试服务。在此示例中,我们有效地使用了异步/等待模式作为指数重试参数,即 1、2、4、8 和 16 秒后重试。可以在源代码中的retry_failed_req.ts
中找到一个工作示例:
wait(timeout: number){
return new Promise((resolve) => {
setTimeout(() => {
resolve()
}, timeout)
})
}
async requestWithRetry(url: string){
const MAX_RETRIES = 10;
for (let i = 0; i <= MAX_RETRIES; i++) {
try { return await axios.get(url); }
catch (err) {
const timeout = Math.pow(2, i);
console.log('Waiting', timeout, 'ms');
await this.wait(timeout);
console.log('Retrying', err.message, i);
}
}
}
您将看到以下输出:
指数重试请求
并行多个请求
使用 async/await 执行多个并行请求变得非常简单。在这里,我们可以同时执行多个异步任务,并在不同的地方使用它们的值。完整的源代码可以在src
中的multiple_parallel.ts
中找到:
async function executeParallelAsyncTasks() {
const [valueA, valueB, valueC] = await
Promise.all([
await axios.get('https://jsonplaceholder.typicode.com/posts/1')
await axios.get('https://jsonplaceholder.typicode.com/posts/2'),
await axios.get('https://jsonplaceholder.typicode.com/posts/3')])
console.log("first response is ", valueA.data);
console.log(" second response is ", valueB.data);
console.log("third response is ", valueC.data);
}
流
简而言之,流是 Node.js 中用于连续流式传输数据的抽象接口。流可以是从源头不断传输数据到目的地的数据序列。源头可以是任何东西——5000 万条记录的数据库,大小为 4.5GB 的文件,一些 HTTP 调用等等。流不是一次性全部可用的;它们不适合内存,它们只是一次传输一些数据块。流不仅用于处理大文件或大量数据,而且它们通过管道和链接提供了一个很好的组合选项。流是响应式编程的一种方式,我们将在下一章中更详细地讨论。Node.js 中有四种流可用:
-
可读流:只能从中读取数据的流;也就是说,这里只能消耗数据。可读流的示例可以是客户端上的 HTTP 响应、
zlib
流和fs
读取流。该流中的任何阶段的数据都处于流动状态或暂停状态。在任何可读流上,可以附加各种事件,如数据、错误、结束和可读。 -
可写流:可以写入数据的流。例如,
fs.createWriteStream()
。 -
双工流:可读可写的流。例如,
net.socket
或 TCP 套接字。 -
转换流:基本上是一个双工流,可以在写入或读取数据时用于转换数据。例如,
zlib.createGzip
是用于使用 gzip 压缩大量数据的流之一。
现在,让我们通过一个示例来了解流的工作原理。我们将创建一个自定义的Transform
流,并扩展Transform
类,从而一次看到读取、写入和转换操作。在这里,转换流的输出将从其输入中计算出来:
-
问题:我们有用户的信息,我们想隐藏敏感部分,如电子邮件地址、电话号码等。
-
解决方案:我们将创建一个转换流。转换流将读取数据并通过删除敏感信息来进行转换。所以,让我们开始编码。创建一个空项目,使用
npm init
,添加一个文件夹src
和之前部分的tsconfig.json
文件。现在,我们将从DefinitelyTyped
中添加 Node.js 类型。打开终端并输入以下内容:
npm install @types/node --only=dev
现在,我们将编写我们自定义的过滤器转换流。创建一个filter_stream.ts
文件,在其中编写转换逻辑:
import { Transform } from "stream";
export class FilterTransform extends Transform {
private filterProps: Array<String>;
constructor(filterprops: Array<String>, options?: any) {
if (!options) options = {};
options.objectMode = true;
super(options);
this.filterProps = filterprops;
}
_transform(chunk: any, encoding?: string, callback?: Function) {
let filteredKeys = Object.keys(chunk).filter((key) => {
return this.filterProps.indexOf(key) == -1;
});
let filteredObj = filteredKeys.reduce((accum: any, key: any) => {
accum[key] = chunk[key];
return accum;
}, {})
this.push(filteredObj);
callback();
}
_flush(cb: Function) {
console.log("this method is called at the end of all transformations");
}
}
我们刚刚做了什么?
-
我们创建了一个自定义的转换并导出它,这样它可以在其他文件中的任何地方使用。
-
如果构造函数中的选项不是必需的,我们可以创建默认选项。
-
默认情况下,流期望缓冲区/字符串值。有一个
objectMode
标志,我们必须在流中设置它,以便它可以接受任何 JavaScript 对象,这是我们在构造函数中做的。 -
我们重写了 transform 方法以适应我们的需求。在 transform 方法中,我们删除了在过滤选项中传递的那些键,并创建了一个经过过滤的对象。
接下来,我们将创建一个过滤器流的对象,以测试我们的结果。并行创建一个名为stream_test.ts
的文件,添加以下内容:
import { FilterTransform } from "./filter_stream";
//we create object of our custom transformation & pass phone and email as sensitive properties
let filter = new FilterTransform(['phone', 'email']);
//create a readable stream that reads the transformed objects.
filter.on('readable', function () { console.log("Transformation:-", filter.read()); });
//create a writable stream that writes data to get it transformed
filter.write({ name: 'Parth', phone: 'xxxxx-xxxxx', email: 'ghiya.parth@gmail.com', id: 1 });
filter.write({ name: 'Dhruvil', phone: 'xxxxx-xxxxx', email: 'dhruvil.thaker@gmail.com', id: 2 });
filter.write({ name: 'Dhaval', phone: 'xxxxx-xxxxx', email: 'dhaval.marthak@gmail.com', id: 3 });
filter.write({ name: 'Shruti', phone: 'xxxxx-xxxxx', email: 'shruti.patel@gmail.com', id: 4 });
filter.end();
打开您的package.json
文件,并在scripts
标签中添加"start":"tsc && node .\\dist\\stream_test.js"
。现在当您运行npm start
时,您将能够看到转换后的输出。
请注意,如果您使用的是 Linux/macOS,请用//
替换\\
。
编写您的第一个 Hello World 微服务
让我们从编写我们的第一个微服务开始。基于之前的主题,我们将使用最佳实践和广泛使用的node_modules
构建我们的第一个微服务。我们将使用:
CORS (www.npmjs.com/package/cors ) | 添加 CORS 标头,以便跨应用程序可以访问它。 |
---|---|
Routing Controllers (www.npmjs.com/package/routing-controllers ) | 此模块提供了美丽的装饰器,帮助我们轻松编写 API 和路由。 |
Winston (www.npmjs.com/package/winston ) | 具有许多高级功能的完美日志记录模块。 |
因此,打开终端并创建一个带有默认package.json
的 Node 项目。按照以下步骤进行。可在提取源中的first-microservice
文件夹中找到用于参考的完整源代码:
- 首先,我们将下载前面的依赖项和基本的 express 依赖项。输入以下命令行:
npm install body-parser config cookie-parser cors debug express reflect-metadata rimraf routing-controllers typescript winston --save
- 按照以下方式下载必要模块的类型:
npm install @types/cors @types/config @types/debug @types/node @types/body-parser @types/express @types/winston --only=dev
- 现在,我们将创建我们的应用程序结构,如下截图所示:
文件夹结构
- 因此,让我们创建我们的 express 文件,并使用
routing_controllers
模块进行配置。创建一个 express 配置类,并指示它使用我们的目录控制器作为可以找到路由的源:
export class ExpressConfig {
app: express.Express;
constructor() {
this.app = express();
this.app.use(cors());
this.app.use(bodyParser.json());
this.app.use(bodyParser.urlencoded({ extended: false }));
this.setUpControllers();
}
setUpControllers() {
const controllersPath = path.resolve('dist', 'controllers');
/*useExpressServer has lots of options, can be viewed at node_modules\routing-controllers\RoutingControllersOptions.d.ts*/
useExpressServer(this.app, {
controllers: [controllersPath + "/*.js"]
}
);
}
}
- 现在,让我们在
application.ts
中编写我们的应用程序启动逻辑:
export class Application {
server: any; express: ExpressConfig;
constructor() {
this.express = new ExpressConfig();
const port = 3000; this.server =
this.express.app.listen(port, () => {
logger.info(`Server Started! Express: http://localhost:${port}`);
});
}
}
- 下一步是编写我们的控制器并返回 JSON:
@Controller('/hello-world')
export class HelloWorld {
constructor() { }
@Get('/')
async get(): Promise<any> {
return { "msg": "This is first Typescript Microservice" }
}
}
- 下一步是在
index.ts
中创建我们的Application
文件的新对象:
'use strict';
/* reflect-metadata shim is required, requirement of routing-controllers module.*/
import 'reflect-metadata';
import { Application } from './config/Application';
export default new Application();
-
您已经完成了;编译您的 TypeScript 并启动
index.ts
的转译版本。当您访问localhost:3000/hello-world
时,您将看到 JSON 输出—{"msg":"This is first Typescript Microservice"}
。 -
为了在启动服务器时自动执行所有任务,我们在
package.json
中定义脚本。第一个脚本是始终在转译之前进行清理:
"clean":"node ./node_modules/rimraf/bin.js dist",
下一个脚本是使用node
模块中可用的typescript
版本构建 TypeScript:
"build":"node ./node_modules/typescript/bin/tsc"
最后一个基本上指示它清理、构建并通过执行index.js
启动服务器:
"start": "npm run clean && npm run build && node ./dist/index.js".
- 下一步是创建 Docker 构建。创建一个
Docker
文件,让我们编写 Docker 镜像脚本:
#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 depedencies
COPY package.json /usr/src/app
RUN npm install
#bundle app src
COPY . /usr/src/app
CMD [ "npm" , "start" ]
- 我们将在以后的章节中更详细地学习 Docker。现在,继续并输入以下命令:
sudo docker build -t firstypescriptms .
在构建镜像时,不要忘记命令的末尾加上点。点表示我们在本地目录中使用 Dockerfile。
您的 Docker 镜像将被构建。您将看到以下类似的输出:
Docker 创建镜像
- 您可以使用
sudo docker images
命令来检查镜像,稍后您可以在任何地方使用它。要运行镜像,只需使用以下命令行:
sudo docker run -p 8080:3000 -d firstypescriptms:latest
- 之后,您可以访问
localhost:8080/hello-world
来检查输出。
虽然我们只是暴露了 REST API,对外部世界来说,它只是 8080 端口上的另一个服务;内部实现对消费者来说是抽象的。这是 REST API 和微服务之间的主要区别之一。容器内的任何内容都可以随时更改。
总结
在本章中,我们首先介绍了一些 Node.js 和 TypeScript 的最基本概念,这些概念对于开发适合企业需求的可扩展应用程序至关重要。我们搭建了我们的主要环境,并学习了 Docker、PM2 和 NGINX。最后,我们用 Node.js 的 TypeScript 方式创建了我们传统的Hello World
微服务。
在下一章中,我们将学习响应式编程的基本原理、响应式编程的优势,以及如何在 Node.js 中进行响应式编程。我们将了解响应式编程中提供的各种操作符,这些操作符可以简化我们日常开发工作。我们将结合传统的基于 SOA 的编排和响应式流程,通过各种情况来看看哪种方法适用于哪里。
第三章:探索响应式编程
到目前为止,我们描述了我们的应用程序是非常著名的行业术语的混合体,如异步、实时、松散耦合、可扩展、分布式、消息驱动、并发、非阻塞、容错、低延迟和高吞吐量。在本章中,我们将进一步了解响应式编程,它将所有这些特征汇集在一起。我们将看到并了解响应式宣言-一组原则,当集体应用时,将带来所有前述的优势。我们将了解响应式微服务的一些关键方面,它应该是什么,响应式编程的主要优势是什么。我们将看看响应式编程解决了什么问题,响应式编程的不同风格,等等。
在本章中,我们将重点关注以下内容:
-
响应式编程介绍
-
响应式宣言
-
响应式微服务-主要构建模块和关注点
-
何时反应,何时不反应(编排)-混合方法的介绍
-
在 Node.js 中成为响应式
响应式编程介绍
如果我们想从 5 万英尺的高空看响应式编程的视图,它可以简要地被称为:
当任何函数中的输入 x 发生变化时,相应的输出 y 会在对应的响应中自动更新,而无需手动调用。简而言之,唯一的目的是在输出世界提示时不断响应外部输入。
响应式编程是通过 map、filter、reduce、subscribe、unsubscribe、streams 等实用程序实现的。响应式编程更注重事件和消息驱动模式,而不是手动处理庞大的实现细节。
让我们以一个实际的日常例子来理解响应式编程。我们从 IT 生涯的开始就使用 Excel。现在,假设你根据一个单元格的值编写一个公式。现在,每当单元格的值发生变化时,基于该值的所有相应结果都会自动反映出变化。这就是所谓的响应式。
简要了解响应式编程,当与处理各种数据流相结合时,响应式编程可以是具有处理以下内容的高级数据流的能力:
-
事件流,我们可以接入和订阅的流,然后使用订阅输出作为数据源。
-
拥有流使我们能够操作流,从原始流创建新流,并根据需要应用转换。
-
转换应该在分布式系统中独立工作。特定的转换可以是从各个地方接收到的多个流的合并。
我们将使用函数式响应式编程。简而言之,我们的函数式响应式微服务应具有以下两个基本属性:
-
指示性或表示性:每个函数、服务或类型都是精确、简单、单一、负责和实现无关的。
-
连续时间:编程应该考虑到时间变化的值。函数式响应式编程中的变量值持续时间很短。它应该为我们提供转换灵活性、效率、模块化、单一责任。
函数式响应式编程的特点如下:
-
动态:知道如何对时间做出反应或处理各种输入变化
-
处理时间变化:当反应值不断变化时,处理适当的变化
-
高效:当输入值发生变化时,只在需要时进行最少量的处理
-
了解历史转换:在本地维护状态变化,而不是全局
既然我们简要了解了响应式编程,让我们看看在采用响应式编程时我们能得到什么优势。下一节将讨论并给出非常强烈的理由,说明为什么你应该放下一切开始响应式编程。
为什么我应该考虑采用反应式编程?
现在我们已经揭开了反应式编程的神秘面纱,下一个重要问题是为什么我们应该关注反应式编程以及在进行反应式编程时可以获得什么优势。在本节中,我们将看到反应式编程的主要优势以及如何轻松地管理代码,以在任何时候引入重大新功能:
-
与回调或中间件相比,更容易解释或利用任何功能。
-
轻松处理错误和内存管理,无需任何集中配置。单个订阅可以有一个错误函数,您可以轻松地处理资源。
-
高效处理与时间相关的复杂性。有时,我们受到调用一些外部 API 的速率限制约束,例如 Google Cloud Vision API。在这种情况下,反应式编程具有巨大的用例。
-
走向市场率更快。当正确实施时,反应式编程大大减少了老式代码到很少的代码行。
-
易于处理可节流的输入流,即,我的输入流是动态的。它可以根据需求增加或减少。
现在我们已经了解了反应式编程的一些主要优势,在下一节中我们将讨论反应式编程的结果,即反应式系统。我们将看到在反应式宣言中定义的一组标准。
反应式宣言
反应式系统旨在更松散耦合,灵活,易于迁移,并且可以根据需求轻松扩展。这些特质使其易于开发,优雅地处理故障,并对错误做出反应。错误会得到优雅的处理,而不是引起恐慌性灾难。反应式系统是有效的,并立即做出反应,为用户提供有效和互动的反馈。为了总结反应式系统的所有特征,引入了反应式宣言。在本节中,我们将看一下反应式宣言和所有所需的标准。现在,让我们看看反应式宣言陈述了什么。
响应式系统
作为响应标准的一部分,反应式系统始终需要响应。它们需要及时地向用户提供和响应。这提高了用户体验,我们可以更好地处理错误。服务的任何故障都不应传播到系统,因为这可能会导致一系列错误。响应是一个重要的事情。即使服务降级,也应该提供响应。
对错误具有弹性
系统应该对所有错误具有弹性。弹性应该是这样的,错误应该得到优雅处理,而不是导致整个系统崩溃。可以通过以下方式实现弹性架构:
-
复制以确保在主节点出现故障时有一个副本。这避免了单点故障。为了确保组件或服务应该以这样一种方式委托服务,以便单一责任得到处理。
-
确保系统中的组件被包含在其边界内,以防止级联错误。组件的客户端不需要处理自己的故障。
弹性可扩展
这通常用于指代系统处理不同负载的能力,通过增加或减少在某个时间内利用的资源数量。反应式系统应该能够对某一时刻的负载做出反应,并相应地采取行动来提供成本效益的解决方案,即在资源不需要时缩减规模,在需要时仅扩展到所需资源的百分比,以保持基础设施成本在预设值以下。系统应该能够分片或复制组件,并在它们之间分发输入。系统应该能够根据需要为客户服务请求生成下游和上游服务的新实例。应该有一个高效的服务发现过程来帮助弹性扩展。
消息驱动
异步消息传递是反应式系统的基础。这有助于我们在组件之间建立边界,并同时确保松耦合、隔离和位置透明性。如果某个特定组件现在不可用,系统应该将失败委托为消息。这种模式帮助我们通过控制系统中的消息队列来实现负载管理、弹性和流量控制,并在需要时应用反压。非阻塞通信会减少系统开销。有许多可用于消息传递的工具,如 Apache Kafka、Rabbit MQ、Amazon Simple Queue Service、ActiveMQ、Akka 等。代码的不同模块通过消息传递相互交互。深入思考反应式宣言,微服务似乎只是反应式宣言的延伸。
主要构建模块和关注点
继续我们的反应式之旅,我们现在将讨论反应式编程(确切地说是函数式反应式编程)的主要构建模块以及反应式微服务实际应该处理的关注点。以下是反应式编程的主要构建模块及其处理的内容。反应式微服务应该基于类似的原则进行设计。这些构建模块将确保微服务是隔离的,具有单一职责,可以异步传递消息,并且是可移动的。
可观察流
可观察流实际上就是随时间构建的数组。项目不是存储在内存中,而是随时间异步到达。可观察流可以被订阅,并且可以监听并对其发出的事件做出反应。每个反应式微服务都应该能够处理本机可观察事件流。可观察对象允许您通过调用系列中的next()
函数向订阅者发出值。
-
热和冷可观察流:可观察流根据订阅者的生产者进一步分类为热和冷。如果需要创建多次,则称为热可观察流,而如果只需要创建一次,则称为冷可观察流。简单来说,热可观察流通常是多播,而冷可观察流通常是单播。举个例子,当你在 YouTube 上打开任何视频时,每个订阅者都会看到相同的序列,从开始到结束,这基本上是一个冷可观察流。然而,当你打开一个直播流时,你只能看到最近的视图并进一步查看。这是一个热可观察流,其中只有对生产者/订阅者的引用,生产者并不是从每次订阅的开始创建的。
-
主题:主题只是一个可以自行调用
next()
方法的可观察对象,以便根据需要发出新值。主题允许您从一个公共点广播值,同时限制订阅只发生一次。创建一个共享订阅。主题可以被称为观察者和可观察对象。它可以充当一组订阅者的代理。主题用于实现通用工具的可观察对象,如缓存、缓冲、日志等。
订阅
虽然可观察对象是随时间填充的数组,但订阅是一个随时间迭代该数组的for
循环。订阅提供了易于使用和易于处理的方法,因此没有内存加载问题。在取消订阅时,可观察对象将停止监听特定的订阅。
发射和映射
当一个可观察对象抛出一个值时,有一个订阅者监听可观察对象抛出的值。发射和映射允许您监听这个值并根据您的需求对其进行操作。例如,它可以用于将 HTTP 可观察对象的响应转换为 JSON。为了进一步扩展链,提供了flatMap
操作符,它从函数的返回值创建一个新的流。
操作符
当一个可观察对象发出值时,它们并不总是以我们期望的形式。操作符很有用,因为它们帮助我们改变可观察对象发出值的方式。操作符可以在以下阶段使用:
-
在创建可观察序列时
-
将事件或一些异步模式转换为可观察序列
-
处理多个可观察序列,将它们合并为单个可观察对象
-
共享可观察对象的副作用
-
对可观察序列进行一些数学转换
-
基于时间的操作,如节流
-
处理异常
-
过滤可观察序列发出的值
-
分组和窗口化发出的值
反压策略
到目前为止,我们已经玩过可观察对象和观察者。我们使用数据流(可观察对象)模拟了我们的问题,将其转换为我们期望的输出(使用操作符),并丢弃了一些值或一些副作用(观察者)。现在,也可能出现这样一种情况,即可观察对象的数据抛出速度比观察者处理速度快。这最终导致数据丢失,这就是反压问题。为了处理反压,我们需要接受数据丢失,或者我们需要缓冲可观察流并在不允许数据丢失时以块的方式处理它。在这两种选择中都有不同的策略:
当输掉是一个选择 | 当输掉不是一个选择 |
---|---|
去抖动:只有在经过一段时间后才发出数据。 | 缓冲:设置一定时间或最大事件数来缓冲。 |
暂停:暂停源流一段时间。 | 缓冲暂停:缓冲源流发出的任何内容。 |
受控流:这是生产者推送事件,消费者只拉取它能够处理的事件的推送-拉取策略。 |
柯里化函数
柯里化是一个逐个评估函数参数的过程,在每次评估结束时产生一个少一个参数的新函数。当函数的参数需要在不同的地方进行评估时,柯里化是有用的。使用柯里化过程,一个参数可以在某个组件中进行评估,然后可以传递到任何其他地方,然后结果可以传递到另一个组件,直到所有参数都被评估。这似乎与我们的微服务类比非常相似。当我们有服务依赖关系时,我们将在以后使用柯里化。
何时做出反应,何时不做出反应(协调)
现在,我们已经熟悉了微服务的核心概念。我们经常接触的下一个问题是关于微服务的实现,以及它们如何相互交互。最常见的问题是何时使用编排,何时使用反应,以及是否可能使用混合方法。在本节中,我们将了解每种方法,其优缺点,并查看每种方法的实际示例。让我们从编排开始。
编排
编排更多地是一种**面向服务的架构(SOA)**方法,在 SOA 中我们处理各种服务之间的交互。当我们说编排时,我们维护一个控制器,即编排者或所有服务交互的主协调者。这通常遵循更多的请求/响应类型模式,其中通信模式可以是任何东西。例如,在我们的购物微服务中可以有一个编排者,它同步执行以下任务——首先接受客户订单,然后检查产品,准备账单,成功付款后更新产品库存。
优势
它提供了一种系统化的处理编程流程的方式,您可以实际控制请求的发出方式。例如,您可以确保只有在请求 A 完成后才能成功调用请求 B。
缺点
虽然编排模式看起来有利,但这种模式涉及到一些权衡,比如:
-
对系统有严格的依赖。比如如果最初的某个服务宕机,那么链中的下一个服务将永远不会被调用。系统很快就会成为一个瓶颈,因为会有几个单点故障。
-
系统中将引入同步行为。总的端到端时间将是处理所有单个服务所需时间的总和。
反应式方法
微服务是为了能够独立存在的。它们不应该相互依赖。反应式方法倾向于解决编排方法的一些挑战。与控制逻辑在何时发生哪些步骤的编排器不同,反应式模式促进了服务知道逻辑要提前构建和执行。服务知道要对什么做出反应以及如何提前处理。服务之间的通信模式是愚蠢的管道,它们内部没有任何逻辑。由于其异步性质,它消除了编排过程中的等待部分。服务可以产生事件并继续处理。生产和消费服务是解耦的,因此生产者不需要知道消费者是否在线。在这种方法中可以有多种模式,其中生产者可能希望从消费者那里收到确认。集中的事件流在反应式方法中处理所有这些事情。
优势
反应式方法有很多优势,它克服了许多传统问题:
-
并行或异步执行可以更快地完成端到端处理。异步处理基本上不会在提供请求时阻止资源。
-
拥有集中的事件流或愚蠢的通信管道作为通信模式具有在任何时间点轻松添加或删除任何服务的优势。
-
系统的控制是分布式的。系统中不再有单一故障点作为编排者。
-
当这种方法与其他几种方法结合时,就可以实现各种好处。
-
当这种方法与事件溯源结合时,所有事件都被存储,并且它可以进行事件重放。因此,即使某个服务宕机,事件存储仍然可以在服务再次在线时重放该事件,并且服务可以检查更新。
-
另一个优点是命令查询责任分离(CQRS)。如第一章所示,揭秘微服务,我们可以将这种模式应用于分离读取和写入活动。因此,任何服务都可以独立扩展。这在应用程序是读取或写入密集型的情况下非常有帮助。
缺点
虽然这种方法解决了大部分复杂性,但也引入了一些权衡:
-
异步编程有时可能很难处理。仅通过查看代码无法弄清楚。必须深入了解事件循环,如第二章所示,为旅程做准备,才能理解异步编码的实际工作流程。
-
复杂性和集中的代码现在转移到了各个服务中。流程控制现在被分解并分布到所有服务中。这可能会在系统中引入冗余代码。
像所有事物一样,一刀切的方法在这里行不通。出现了几种混合方法,它们充分利用了两种过程。现在让我们来看看一些混合方法。混合方法可以增加很多价值。
外部反应,内部编排
第一个混合模式促进了不同微服务之间的反应式模式和服务内的编排。让我们举个例子来理解这一点。考虑我们的购物微服务示例。每当有人购买产品时,我们将检查库存,计算价格,处理付款,结账付款,添加推荐产品等。每个微服务都是不同的。在这里,我们可以在产品库存服务、付款服务和推荐产品之间采用反应式方法,在结账服务、处理付款和发货产品之间采用编排方法。一个集体服务根据这三个服务的结果产生一个事件,然后可以产生。有几个优点和附加值,例如:
-
大多数服务是解耦的。只有在需要时才会出现编排。应用程序的整体流程是分布式的。
-
具有异步事件和基于事件的方法可确保没有单点故障。如果服务错过了事件,那么可以在服务再次上线时重放事件。
虽然有几个优点,但也引入了一些权衡:
-
如果服务耦合在一起,它们很快就会成为单点故障。它们无法独立扩展。
-
同步处理可能会导致系统阻塞,资源会被占用,直到请求完全完成。
驱动流程的反应式协调器
第二种方法引入了更像是反应式协调器的东西,用于驱动各种服务之间的流程。它更多地使用基于命令和基于事件的方法来控制整个生态系统的整体流程。命令指示需要完成的任务,事件是完成命令的结果。反应式协调器接收请求并生成命令,然后将其推送到事件流中。已经为命令设置的各种微服务消耗这些命令,进行一些处理,然后在成功执行命令时抛出一个事件。反应式协调器消耗这些事件,并根据需要编程和反应事件。这种方法有几个附加值,例如:
-
服务是解耦的;即使协调器和服务之间似乎存在耦合,但反应式方法和集中的事件流解决了大部分以前的缺点。
-
事件流或集中的事件总线确保微服务之间的异步编程。事件可以按需重放。没有单点故障。
-
整体流程可以在反应式协调器中集中在一个地方。所有这样的集中逻辑都可以在那里保留,而且任何地方都不会有重复的代码。
虽然有很多好处,但这种方法引入了以下权衡——协调器需要被照顾。如果协调器出现问题,整个系统可能会受到影响。协调器需要知道需要哪些命令以便做出反应或执行一组预设动作。
概要
在经历了纯反应式、纯编排和两种不同的混合方法之后,我们现在将介绍可以应用前面四种方法的各种用例。我们将学习哪种方法适用于哪种情况。
当纯反应式方法是一个完美的选择时
在以下情况下,纯粹的反应式方法是一个完美的选择:
-
当应用程序中的大部分处理可以异步完成时。当应用程序可以进行并行处理时,反应式架构模式非常适合处理应用程序需求。
-
在每个服务中分散应用程序流程是可以管理的,而且不会成为一个痛点。对于监控和审计,可以使用相关 ID(UUID,GUID,CUID)生成集中视图。
-
当应用程序需要快速部署,市场速度是最重要的目标。当微服务与反应式方法结合时,它有助于增加解耦,最小化依赖关系,处理临时关闭的情况,从而有助于更快地将产品推向市场。
当纯编排是一个完美的选择
在以下情况下,纯编排方法是一个完美的选择:
-
当应用程序的需求无法通过并行处理满足时。所有步骤必须按顺序进行处理,没有机会进行并行处理。
-
如果应用程序需要集中的流程控制。各个领域,如银行和ERP,都有需要在一个地方查看端到端流程的需求。如果有 100 个服务,每个服务都有自己的控制流程,那么维护集中的流程可能很快成为分发的瓶颈。
在外部反应,内部编排是一个完美的选择
在以下情况下,混合方法,更具体地说是在外部反应,在内部编排,是一个完美的选择:
-
大部分处理可以异步完成。您的服务可以通过事件流相互通信,并且您可以在系统中进行并行处理,也就是说,您可以通过事件流或基于系统的命令传递数据。例如,每当付款成功入账时,一个微服务显示相关产品,另一个微服务将订单发送给卖家。
-
在每个微服务中分散流程很容易管理,而且不会在各处重复代码。
-
市场速度是主要优先事项。
-
顺序步骤不适用于系统,但适用于服务。只要顺序步骤不适用于整个系统。
引入反应式协调器是完美的选择时
在以下情况下,引入一个反应式协调器是完美的解决方案:
-
根据正在处理的数据,应用程序的流程可能会发生变化。流程可能包括数百个微服务,应用程序需要临时关闭,一旦应用程序恢复在线,事件就可以被重放。
-
系统中有几个需要同步处理的异步处理块。
-
它允许轻松的服务发现。服务可以随时轻松扩展。整个服务可以轻松地移动。
根据您的整体需求,您可以在微服务架构中选择任何一种策略。
在 Node.js 中成为反应式
现在我们已经了解了响应式编程的概念和在微服务中的优势,让我们现在看看在 Node.js 中响应式编程的一些实际实现。在本节中,我们将通过在 Node.js 中实现响应式编程来了解响应式编程的构建模块。
Rx.js
这是最流行的库之一,它得到了积极的维护。该库以不同形式提供给大多数编程语言,如RxJava、RxJS、Rx.Net、RxScala、RxClojure等。在撰写本文时,上个月的下载量超过 40 万次。除此之外,该库还有大量的文档和在线支持可用。我们将大部分时间使用这个库,除非需要其他库。您可以在以下网址查看:reactivex.io/
。在撰写本文时,Rx.js 的稳定版本是5.5.6。Rx.js 有很多操作符。我们可以使用 Rx.js 操作符进行各种操作,如组合各种内容,根据需要应用条件,从承诺或事件创建新的 observables,错误处理,过滤数据,具有发布者-订阅者模式,转换数据,请求-响应工具等。让我们快速动手试试。为了安装 RxJS,我们需要安装 Rx 包和 Node-Rx 绑定。打开终端并输入npm install rx node-rx --save
。由于此库需要支持我们的 Node.js 作为构建系统,因此我们还需要安装一个模块。在终端中输入以下命令:npm install @reactivex/rxjs --save
。在本章中,我们将使用我们在第二章中创建的Hello World
微服务骨架,并继续进行。以下是我们将在演示中看到的各种选项:
forkjoin | 当我们有一组 observable 并且只想要最后一个值时使用。如果其中一个 observable 永远不完成,则无法使用此操作符。 |
---|---|
combineAll | 通过等待外部 observable 完成,然后自动应用combineLatest 来简化/组合 observable 的 observable。 |
race | 首先发出值的 observable 将被使用。 |
retry | 如果发生错误,重试特定次数的 observable 序列。 |
debounce | 忽略少于指定时间的发出值。例如,如果我们将防抖设置为一秒,那么在一秒之前发出的任何值都将被忽略。 |
throttle | 仅在由提供的函数确定的持续时间后发出值。 |
以下示例将值节流到两秒钟:
const source = Rx.Observable.interval(1000);
const example2 = source.throttle(val => Rx.Observable.interval(2000));
const subscribe2 = example2.subscribe(val => console.log(val));
以下示例将在 observables 上触发竞争条件:
let example3=Rx.Observable.race(
Rx.Observable.interval(2000)
.mapTo("i am first obs"),
Rx.Observable.of(1000)
.mapTo("i am second"),
Rx.Observable.interval(1500)
.mapTo("i am third")
)
let subscribe3=example3.
subscribe(val=>console.log(val));
您可以在源文件夹中的using_rxjs
中跟随源代码。在前面的表中,可以在rx_combinations.ts
、rx_error_handing.ts
和rx_filtering.ts
中找到所有操作符的示例。可以在reactivex.io/rxjs/
找到完整的 API 列表。
Bacon.js
Bacon.js是一个小巧的函数式响应式编程库。与 Node.js 集成后,您可以轻松将混乱的代码转换为清晰的声明式代码。它每月的下载量超过 29,000 次。在撰写本文时,可用的版本是1.0.0。让我们快速动手试试。为了安装 Bacon.js,我们需要安装 Bacon.js 及其类型。打开终端并输入npm install baconjs --save
和npm install @types/baconjs --only=dev
。现在,让我们看一个基本示例,看看代码有多清晰。我们有一个 JSON 对象,其中一些产品与数字1
(手机)、2
(电视)等相对应。我们创建一个服务来返回产品名称,如果产品不存在,则应返回Not found
。以下是服务代码:
baconService(productId: number){
return Bacon.constant(this.productMap[productId])
}
以下是控制器代码:
@Get('/:productId')
async get(@Req() req: Request,@Res() res: Response,@Param("productId") productId: number) {
let resp: any;
this.baconService.baconService(productId)
.flatMap((x) => {
return x == null || undefined ? "No Product Found" : x;
})
.onValue((o: string) => {
resp = o;
})
return resp;
}
您可以在源文件夹中的using_baconjs
中查看源代码。完整的 API 列表可以在baconjs.github.io/api.html
找到。
HighLand.js
这更像是一个通用的函数库,它是建立在 Node.js 流之上的,因此允许它处理异步和同步代码。HighLand.js最好的特点之一是它处理背压的方式。它具有用于暂停和缓冲的内置功能,也就是说,当客户端无法处理更多数据时,流将被暂停,直到准备好,如果源无法暂停,那么它将保持一个临时缓冲区,直到可以恢复正常操作。是时候用一个实际的例子来动手了。让我们偏离 express 主题,专注于文件读取主题。我们将看到 Node.js I/O 操作与并行执行的强大功能。打开终端并输入npm install highland --save
。
根据我们之前的骨架,创建index.ts
,其中包含以下代码,基本上读取三个文件并打印它们的内容:
import * as highland from "highland";
import { Stream } from "stream";
import * as fs from "fs";
var readFile = highland.wrapCallback(fs.readFile);
console.log("started at", new Date());
var filenames = highland(['file1.txt', 'file2.txt', 'file3.txt']);
filenames
.map(readFile)
.parallel(10) //reads up to 10 times at a time
.errors((err: any, rethrow: any) => {
console.log(err);
rethrow();
})
.each((x: Stream) => {
console.log("---");
console.log(x.toString());
console.log("---");
});
console.log("finished at", new Date());
转换文件,保持三个.txt
文件与package.json
平行,并运行node
文件。内容将被读取。您可以在源代码的src
文件夹中的using_highlandjs
项目中跟踪。完整的 API 列表可以在highlandjs.org/
找到。
主要观点
现在我们已经看到了这三个库,我们将总结以下关键点和显著特点:
Rx.js | Bacon.js | Highland.js | |
---|---|---|---|
文档 | 文档完善,API 非常成熟,有很多选项,在其他语言中也有扩展。 | Node.js 示例较少,API 文档很好,对 Node.js 有原生支持。 | 文档很少,辅助方法较少,发展中的足迹。 |
背压 | 已实现。 | 不支持。 | 最佳实现。 |
社区 | 被 Netflix 和微软等大公司使用。在所有其他语言中都有类似的概念,更像是 Java,学习曲线陡峭。 | 比 Rx.js 小,学习曲线降低。 | 社区活动最少,必须直接深入代码库。 |
许可证 | Apache 2.0 | MIT | Apache 2.0 |
摘要
在本章中,我们了解了响应式宣言。我们将响应式原则应用于微服务。我们学习了如何在 Node.js 中应用响应式编程。我们了解了设计微服务架构的可能方法,看到了它的优点和缺点,并看到了一些实际场景,以找出在哪些情况下可以应用这些模式。我们看到了编排过程、反应过程和两种混合方法的特殊情况。
在下一章中,我们将开始开发我们的购物车微服务。我们将设计我们的微服务架构,编写一些微服务,并部署它们。我们将看到如何将我们的代码组织成适当的结构。