Java-云原生应用-全-

Java 云原生应用(全)

原文:zh.annas-archive.org/md5/3AA62EAF8E1B76B168545ED8887A16CF

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

在部署图中,使用云的形状来描述防火墙外的互联网。直到我读了尼古拉斯·G·卡尔的《大转变》之后,我才意识到云的全部潜力和即将到来的事情。快进 10 年,现在我们在绘制云形状围绕整个系统时不再犹豫,以描述云无处不在。云原生对初创公司来说是很自然的,但对许多企业来说,这仍然是一片未知的领域。只是进行搬迁并不是云的正确使用方式,尽管这可能是大多数大型公司为了减轻其数据中心的负担或避免租约延期而做的第一件事。云的力量在于我们能够构建基于云原生架构的业务关键应用程序,可以推动变革性价值。因此,我一直鼓励我的团队学习如何在云上设计和构建更智能的应用程序。

Munish、Ajay 和 Shyam 是核心团队的一部分,他们一直在研究和应用新兴技术,利用它们来解决业务问题。他们是企业数字化转型的领先专家和顾问,专注于使用微服务和新兴技术(如响应式框架、开源和容器技术(Docker 和 Kubernetes)等)的分布式系统。因此,我鼓励他们撰写这本书,以便让下一代开发人员能够快速启动他们的云原生应用程序之旅。

这本书采用了一种循序渐进的方法来理解、设计和编写云应用程序。作者带领你进行一次学习之旅,从概念开始,然后构建一个小型的 REST 服务,然后逐步增强服务以实现云原生。他们涵盖了云特定细微差别的各个方面,比如如何在分布式架构中发现服务以及服务发现工具所起的作用。您还将学习如何将应用程序迁移到公共云提供商——AWS 和 Azure。该书涵盖了 AWS Lamda 和 Cloud Functions 等无服务器计算模型。

我鼓励您充分利用这本书,引领您在云上应用开发之旅。

Hari Kishan Burle

副总裁兼全球架构服务负责人

Wipro 有限公司

第一章:贡献者

本书涵盖了什么

第一章,云原生简介,介绍了云原生应用程序的什么和为什么:是什么驱使应用程序转移到云端?为什么云开发和部署与常规应用程序不同?什么是 12 要素应用程序?

第二章,编写您的第一个云原生应用程序,介绍了使用微服务方法进行应用程序设计的核心概念。然后展示了一个样本的product服务,随着书中讨论的进行,它将被增强。您将学习如何使用 Spring Boot 进行微服务应用程序开发,并了解用于构建云原生应用程序的微服务原则。

第三章,设计您的云原生应用程序,涵盖了设计云原生应用程序时的一些高级架构考虑因素。它包括事件驱动架构、使用编排进行解耦,以及使用领域驱动设计DDD)概念,如有界上下文。您将了解在云上开发和使用面向消费者友好的 API 来前端化应用程序的架构模式和考虑因素,而不是以系统为中心的服务定义。

第四章,扩展您的云原生应用程序,深入探讨了使用各种堆栈、原则和支持组件创建应用程序。它涵盖了在实现服务时的模式。本章重点介绍了差异化方面,如错误处理和命令查询响应分离CQRS)和缓存等模式,这些模式对云开发有重大影响。

第五章,测试云原生应用程序,深入探讨了如何测试您的微服务以及如何使用行为驱动开发编写测试。

第六章,云原生应用部署,深入探讨了微服务的部署模型,包括如何将应用程序打包到 Docker 容器中,并设置 CI/CD 流水线。

第七章,云原生应用程序运行时,涵盖了服务的运行时方面。我们将介绍如何将配置外部化到配置服务器,并由 Zuul(Edge)前端化。我们将研究 Pivotal Cloud Foundry 并在 PCF Dev 上部署我们的服务。我们还将涵盖容器编排。

第八章,平台部署 - AWS,描述了 AWS 环境,并讨论了使用早期章节中讨论的概念(注册表、配置、日志聚合和异步消息传递)进行云开发的 AWS 特定工具。

第九章,平台部署 - Azure,描述了 Azure 环境,并讨论了用于进行云开发的 Azure 特定工具(包括 Service Fabric 和 Cloud Functions)。

第十章,作为服务集成,讨论了各种类型的 XaaS,包括 IaaS、PaaS、iPaaS 和 DBaaS,以及如何将基础设施元素暴露为服务。在云原生模式下,您的应用程序可能正在集成社交媒体 API 或 PaaS API,或者您可以托管其他应用程序将使用的服务。本章涵盖了如何连接/使用其他外部服务或提供此类服务。

第十一章,API 设计最佳实践,讨论了如何设计以消费者为中心的细粒度和功能导向的 API。它还讨论了 API 设计中的各种最佳实践,比如在 API 层级还是在服务中进行编排,如何创建 API 的免费版本,如何在 API 层面解决特定渠道的问题,以使服务保持渠道无关,以及 API 设计中的安全性方面。

第十二章,数字化转型,涵盖了云开发对企业现有格局的影响,以及如何实现转型迈向数字化企业。

关于作者

Ajay Mahajan是 Wipro Technologies 的杰出技术人员(DMTS),目前担任零售垂直领域的首席技术专家。在他目前的角色中,他帮助客户采用云原生和数字架构来开发下一代零售应用程序。他曾与欧洲和美国的零售和银行客户合作开发大规模的关键任务系统。在过去 19 年的 Java 平台工作中,他见证了企业 Java 从 Netscape 应用服务器到 servlets/JSP,JEE,Spring,以及现在的云和微服务的演变。

这本书中的许多想法、最佳实践和模式都源自我们在新兴技术领域的工作,该工作由 Aravind Ajad Yarra 领导。我特别感谢共同作者 Shyam,他是我遇到的最有才华的技术人员。特别感谢 Munish 对本书结构和内容的头脑风暴。我要感谢 Hari Burle,他的鼓励和指导帮助我专注于这本书。

Munish Kumar Gupta是 Visa 的首席系统架构师。他的日常工作涉及具有严格非功能性要求的应用程序解决方案架构、应用程序性能工程、应用程序基础设施管理,以及探索尖端开源技术在企业中的可采用性。他是Akka Essentials的作者。他对软件编程和工艺非常热衷。他在技术趋势、应用程序性能工程和 Akka 方面撰写博客。

我必须首先感谢我的妻子 Kompal。她督促我继续写作,现在我有了第二本书。感谢 Packt 团队的每个人对我的帮助。特别感谢 Zeeyan、Nitin 和 Romy。

Shyam Sundar是 Wipro Technologies 位于班加罗尔的高级架构师。他是 Wipro 新兴技术架构组的一员。他负责帮助团队在项目中采用新兴技术。他主要关注客户端和云技术。他是一个终身学习者,非常关心软件工艺。他不断尝试新的工具和技术,以改善开发体验。

我首先要感谢我的共同作者 Ajay 和 Munish,让我与他们一起踏上这不可思议的旅程。作为一个更习惯于用代码而不是文字表达自己的人,Ajay 和 Munish 给了我很多关于如何构建内容和简化概念的深思熟虑的建议。我还必须感谢我的老板 Aravind Ajad Yarra,他一直支持和鼓励我。

关于审阅者

Andreas Olsson 是 Java 和 Spring 培训师,专门从事云原生解决方案。他自 2001 年以来一直是 Java 开发人员,并于 2004 年开始使用 Spring。在设计应用程序架构时,他通常会在 Spring 生态系统中找到解决方案。2011 年,云原生平台开始出现时,他成立了自己的公司,自那时起一直是云原生的爱好者。Andreas 居住在瑞典,目前在国际上担任培训师。他是一名经过认证的 Java 和 Spring 专业人员,非常喜欢每天学习新东西。

Packt 正在寻找像您这样的作者

如果您有兴趣成为 Packt 的作者,请访问authors.packtpub.com并立即申请。我们已经与成千上万的开发人员和技术专业人士合作,就像您一样,帮助他们与全球技术社区分享见解。您可以进行一般申请,申请我们正在招聘作者的特定热门主题,或者提交您自己的想法。

目录

  1. 标题页

  2. 版权和鸣谢

  3. Java 中的云原生应用程序

  4. 致谢

  5. Packt 升级

  6. 为什么订阅?

  7. PacktPub.com

  8. 前言

  9. 贡献者

  10. 关于作者

  11. 关于审阅者

  12. Packt 正在寻找像您这样的作者

  13. 前言

  14. 这本书是为谁准备的

  15. 本书涵盖了什么

  16. 充分利用本书

  17. 下载示例代码文件

  18. 下载彩色图像

  19. 使用的约定

  20. 联系我们

  21. 评论

  22. 云原生简介

  23. 为什么选择云原生?

  24. 什么是云原生?

  25. 提升和转移

  26. 本地化

  27. 无服务器化

  28. 云原生和微服务

  29. 12 要素应用程序

  30. 微服务启用服务生态系统

  31. 微服务采用

  32. 单体转换

  33. 总结

  34. 编写您的第一个云原生应用程序

  35. 设置您的开发人员工具箱

  36. 获取 IDE

  37. 设置互联网连接

  38. 了解开发生命周期

  39. 要求/用户故事

  40. 架构

  41. 设计

  42. 测试和开发

  43. 构建和部署

  44. 选择框架

  45. Dropwizard

  46. Vert.x

  47. Spring Boot

  48. 编写产品服务

  49. 创建 Maven 项目

  50. 编写 Spring Boot 应用程序类

  51. 编写服务和域对象

  52. 运行服务

  53. 在浏览器上测试服务

  54. 创建可部署的

  55. 启用云原生行为

  56. 外部配置

  57. 计量您的服务

  58. 服务注册和发现

  59. 运行服务注册表

  60. 注册产品服务

  61. 创建产品客户端

  62. 看查找操作

  63. 总结

  64. 设计您的云原生应用程序

  65. 三重奏 - REST、HTTP 和 JSON

  66. API 的崛起和流行

  67. API 网关的作用

  68. API 网关的好处

  69. 应用程序解耦

  70. 有界上下文/领域驱动设计

  71. 分类为上游/下游服务

  72. 业务事件

  73. 微服务识别

  74. 微服务和面向服务的架构(SOA)之间的区别

  75. 服务粒度

  76. 微服务设计准则

  77. 设计和部署模式

  78. 设计模式

  79. 内容聚合模式

  80. 客户端聚合

  81. API 聚合

  82. 微服务聚合

  83. 数据库聚合

  84. 协调模式

  85. 业务流程管理(BPM)

  86. 复合服务

  87. 为什么要使用复合服务?

  88. 微服务协调的能力

  89. 协调模型

  90. 异步并行

  91. 异步顺序

  92. 使用请求/响应进行编排

  93. 折叠微服务

  94. 部署模式

  95. WAR 文件中的多个服务

  96. 利弊

  97. 适用性

  98. 每个 WAR/EAR 服务

  99. 利弊

  100. 适用性

  101. 每个进程的服务

  102. 利弊

  103. 适用性

  104. 每个 Docker 容器的服务

  105. 利弊

  106. 适用性

  107. 每个 VM 的服务

  108. 利弊

  109. 适用性

  110. 每个主机的服务

  111. 利弊

  112. 适用性

  113. 发布模式

  114. 微服务的数据架构

  115. 命令查询责任分离(CQRS)

  116. 复制数据

  117. 好处

  118. 缺点

  119. 适合目的

  120. 安全的作用

  121. 摘要

  122. 扩展您的云原生应用程序

  123. 实施获取服务

  124. 简单的产品表

  125. 运行服务

  126. 传统数据库的局限性

  127. 缓存

  128. 本地缓存

  129. 在幕后

  130. 本地缓存的局限性

  131. 分布式缓存

  132. 应用 CQRS 以分离数据模型和服务

  133. 关系数据库上的物化视图

  134. Elasticsearch 和文档数据库

  135. 为什么不仅使用文档数据库或 Elasticsearch?

  136. 核心产品服务在文档数据库上

  137. 准备好使用测试数据的 MongoDB

  138. 创建产品服务

  139. 拆分服务

  140. 产品搜索服务

  141. 准备好使用测试数据的 Elasticsearch

  142. 创建产品搜索服务

  143. 数据更新服务

  144. REST 约定

  145. 插入产品

  146. 测试

  147. 更新产品

  148. 测试

  149. 删除产品

  150. 测试

  151. 缓存失效

  152. 验证和错误消息

  153. 格式验证

  154. 数据验证

  155. 业务验证

  156. 异常和错误消息

  157. CQRS 的数据更新

  158. 异步消息传递

  159. 启动 ActiveMQ

  160. 创建主题

  161. Golden source update

  162. 服务方法

  163. 数据更新时触发事件

  164. 使用 Spring JMSTemplate 发送消息

  165. 查询模型更新

  166. 插入、更新和删除方法

  167. 端到端测试 CQRS 更新场景

  168. 摘要

  169. 测试云原生应用

  170. 在开发之前编写测试用例

  171. TDD

  172. BDD

  173. 测试模式

  174. A/B 测试

  175. 测试替身

  176. 测试存根

  177. 模拟对象

  178. 模拟 API

  179. 测试类型

  180. 单元测试

  181. 集成测试

  182. 负载测试

  183. 回归测试

  184. 确保代码审查和覆盖率

  185. 测试产品服务

  186. 通过 Cucumber 进行 BDD

  187. 为什么选择 Cucumber?

  188. Cucumber 是如何工作的?

  189. Spring Boot 测试

  190. 使用 JaCoCo 进行代码覆盖

  191. 集成 JaCoCo

  192. 摘要

  193. 云原生应用部署

  194. 部署模型

  195. 虚拟化

  196. PaaS

  197. 容器

  198. Docker

  199. 构建 Docker 镜像

  200. Eureka 服务器

  201. 产品 API

  202. 连接到外部的 Postgres 容器

  203. 部署模式

  204. 蓝绿部署

  205. 金丝雀部署

  206. 暗部署

  207. 应用 CI/CD 进行自动化

  208. 摘要

  209. 云原生应用运行时

  210. 运行时的需求

  211. 实现运行时参考架构

  212. 服务注册表

  213. 配置服务器

  214. 配置服务器的服务器部分

  215. 配置客户端

  216. 刷新属性

  217. 微服务前端

  218. Netflix Zuul

  219. 幕后发生了什么

  220. 同时运行它们

  221. Kubernetes - 容器编排

  222. Kubernetes 架构和服务

  223. Minikube

  224. 在 Kubernetes 中运行产品服务

  225. 平台即服务(PaaS)

  226. PaaS 的案例

  227. Cloud Foundry

  228. 组织、账户和空间的概念

  229. Cloud Foundry 实现的需求

  230. Pivotal Cloud Foundry (PCF)

  231. PCF 组件

  232. PCF Dev

  233. 安装

  234. 启动 PCF Dev

  235. 在 PCF 上创建 MySQL 服务

  236. 在 PCF Dev 上运行产品服务

  237. 部署到 Cloud Foundry

  238. 摘要

  239. 平台部署 - AWS

  240. AWS 平台

  241. AWS 平台部署选项

  242. 将 Spring Boot API 部署到 Beanstalk

  243. 部署可运行的 JAR

  244. 部署 Docker 容器

  245. 将 Spring Boot 应用程序部署到弹性容器服务

  246. 部署到 AWS Lambda

  247. 摘要

  248. 平台部署 - Azure

  249. Azure 平台

  250. Azure 平台部署选项

  251. 将 Spring Boot API 部署到 Azure App Service

  252. 将 Docker 容器部署到 Azure 容器服务

  253. 将 Spring Boot API 部署到 Azure Service Fabric

  254. 基本环境设置

  255. 打包产品 API 应用程序

  256. 启动 Service Fabric 集群

  257. 将产品 API 应用程序部署到 Service Fabric 集群

  258. 连接到本地集群

  259. 连接到 Service Fabric party 集群

  260. Azure 云函数

  261. 环境设置

  262. 创建新的 Java 函数项目

  263. 构建和运行 Java 函数

  264. 深入代码

  265. 摘要

  266. 作为服务集成

  267. XaaS

  268. 构建 XaaS 时的关键设计问题

  269. 与第三方 API 集成

  270. 摘要

  271. API 设计最佳实践

  272. API 设计关注点

  273. API 资源识别

  274. 系统 API

  275. 过程 API

  276. 通道 API

  277. API 设计指南

  278. 命名和关联

  279. 资源的基本 URL

  280. 处理错误

  281. 版本控制

  282. 分页

  283. 属性

  284. 数据格式

  285. 客户端支持有限的 HTTP 方法

  286. 身份验证和授权

  287. 端点重定向

  288. 内容协商

  289. 安全

  290. API 建模

  291. 开放 API

  292. RESTful API 建模语言(RAML)

  293. API 网关部署模型

  294. 摘要

  295. 数字转型

  296. 应用程序组合理性化

  297. 投资组合分析 - 业务和技术参数

  298. 退休

  299. 保留

  300. 巩固

  301. 转换

  302. 单体应用转换为分布式云原生应用

  303. 将单体应用转换为分布式应用

  304. 客户旅程映射到领域驱动设计

  305. 定义架构跑道

  306. 开发者构建

  307. 打破单体应用

  308. 将所有内容整合在一起

  309. 构建自己的平台服务(控制与委托)

  310. 摘要

  311. 您可能喜欢的其他书籍

  312. 留下评论-让其他读者知道您的想法

第二章:云原生简介

云计算的出现和移动设备的普及导致了消费者面向公司(如亚马逊、Netflix、优步、谷歌和 Airbnb)的崛起,它们重新定义了整个客户体验。这些公司在云上构建了它们的应用程序(包括 Web 和移动界面),利用功能或服务,使它们能够根据需求进行扩展或缩减,随时可用,并准备好处理各个层面的故障。

传统企业正在关注这些面向消费者的公司,并希望采纳它们的一些最佳实践。他们这样做是为了帮助扩展他们快速发展的企业应用程序,使它们能够利用云的弹性和可伸缩性。

在我们深入了解云原生之前,让我们看看这一章节包含什么。本章将涵盖以下主题:

  • 为什么要采用云原生?

  • 什么是云原生?

  • 12 要素应用简介

  • 为什么要从单片应用迁移到基于分布式微服务的应用程序?

  • 构建基于分布式微服务的应用程序的优势

为什么要采用云原生?

让我们看看以下几点,以了解为什么我们需要采用云原生:

  • 云采用的第一波浪潮是关于成本节约和业务敏捷性(特别是基础设施供应和廉价存储)。随着云的不断普及,企业开始发现基础设施即服务(IaaS)和平台即服务(PaaS)服务以及它们在构建应用程序中的利用,这些应用程序利用了云的弹性和可伸缩性,同时接受了云平台固有的故障。

  • 许多企业正在数字化倡议领域采用绿地设计和微服务的开发。在处理物联网(IoT)、移动设备、SaaS 集成和在线业务模式时,企业正在与市场上的利基玩家合作。这些新时代的商业模式被设计和开发为企业端的创新系统。这些模型被迅速迭代,以识别和挖掘客户的需求、他们的偏好、什么有效,什么无效。

  • 企业还在基于其产品线开发数字服务。产品通过物联网得到增强,使其能够发出有关产品性能的数据。这些数据被汇总和分析,以发现预测性维护、使用模式和外部因素等模式。来自客户的数据被汇总和聚合,以构建产品增强和新功能的新模型。许多这些新数字服务使用云原生模型。

  • 这些现代数字解决方案使用来自各种提供商的 API,例如用于位置的谷歌地图,用于身份验证的 Facebook/谷歌,以及用于社交协作的 Facebook/Twitter。将所有这些 API 与企业业务的功能和功能结合起来,使它们能够为客户构建独特的建议。所有这些集成都是在 API 级别进行的。移动应用程序不是为数十亿用户而设计的,而是为数百万用户而设计的。这意味着随着负载的增加,底层应用程序功能应该能够扩展,以为客户提供无缝的体验。

  • 企业扩展资源的一种方式是在负载增加或出现故障时进行服务/环境供应的繁重工作。另一种方式是将底层服务的繁重工作转移到云平台提供商。这是构建云原生应用程序的甜蜜点,利用云提供商的平台服务使企业能够卸载可伸缩性的关键方面,并专注于价值生成部分。

什么是云原生?

当应用程序被设计和架构以利用云计算平台支持的基础 IaaS 和 PaaS 服务时,它们被称为云原生应用。

这意味着构建可靠的系统应用,如五个九(99.999%),在三个九(99.9%)的基础设施和应用组件上运行。我们需要设计我们的应用组件来处理故障。为了处理这样的故障,我们需要一个结构化的可扩展性和可用性方法。为了支持应用程序的整个规模,所有部分都需要自动化。

云采用通常是一系列步骤,企业在开始构建云原生应用之前开始探索服务。采用始于将 Dev/Test 环境迁移到云中,业务和开发人员社区对快速配置是关键需求。一旦企业度过环境配置阶段,下一步/模型是企业应用迁移到云原生模型,将在以下部分讨论。

举起和转移

传统上,企业开始其云计算之旅是通过 IaaS 服务。他们将业务应用工作负载从本地数据中心转移到云计算平台上的相应租用容量。这是云计算平台采用的第一波浪潮,企业从资本支出模式转变为运营支出模式。

IaaS,顾名思义,专注于基础设施——计算节点、网络和存储。在这种模式下,企业可以利用云的弹性,根据需求或负载来增加或减少计算节点。虚拟机(VM)抽象出底层硬件,并提供了通过几次点击来扩展或缩减 VM 数量的能力。

企业通常在第一波浪潮中使用 IaaS,原因如下:

  • 资源的可变性:随意添加/删除资源的能力,从而实现更多的业务敏捷性

  • 实用模型:IaaS 提供按小时租用的基本资源,更具可预测性和运营支出模式

原生应用

一旦企业开始对 IaaS 感到满意,下一波采用的浪潮就是采用 PaaS 作为应用工作负载的一部分。在这个阶段,企业开始发现具有以下好处的服务:

  • 平台服务替换:这涉及识别企业的潜在平台特性,举起和转移工作负载,并用云提供商的等效平台服务替换。例如:

  • 用云提供商提供的排队系统(如 AWS SQS)替换应用消息系统

  • 用等效的托管数据服务(如 AWS RDS)替换数据存储或关系数据库管理系统(RDMBS)

  • 用托管目录或安全服务(如 AWS Directory 和 AWS IAM)替换安全或目录服务

  • 这些服务使企业摆脱所有运营工作,如数据存储备份、可用性、可扩展性和冗余,并用提供所有这些功能的托管服务替换它们

  • 应用服务替换:企业发现可以替换其自有平台或实用服务的新服务。例如:

  • 用云提供商的等效 DevOps 服务(如 AWS CodePipeline、AWS CodeCommit 或 AWS CodeDeploy)替换构建和发布服务或产品

  • 用等效的应用平台服务(如 AWS API Gateway、AWS SWF 和 AWS SES)替换应用服务或产品

  • 用等效的应用分析服务(如 AWS Data Pipeline 和 AWS EMR)替换分析工作负载服务

一旦应用程序开始采用平台服务,应用程序开始抽象出商业现成COTS)产品提供的功能或功能(如消息传递、通知、安全、工作流和 API 网关),并用等效的功能平台服务替换它们。例如,不再托管和运行消息传递 IaaS,转向等效的平台服务意味着转向一种模式,您只支付发送的消息数量,而不会产生任何额外的运营成本。这种模式带来了显著的节省,因为您从租用和运营产品转向了仅在利用时租用产品的模式。

走向无服务器

一旦企业采用 PaaS 构建应用程序,下一步就是将应用程序逻辑抽象为一系列较小的函数并部署它们。这些函数作为对用户或代理的事件的反应而被调用,这导致这些函数计算传入的事件并返回结果。这是最高级别的抽象,应用程序已被划分为一系列函数,这些函数独立部署。这些函数使用异步通信模型相互通信。云计算平台提供了 AWS Lambda 和 Azure Functions 等功能,用于实现无服务器化。

云原生和微服务

为了实现 IaaS 和 PaaS 服务的采用,需要对应用程序的设计和架构进行改变。

在基础平台(即:应用服务器)上设计企业应用程序的模式意味着应用程序的可伸缩性和可用性的重要工作是平台的责任。企业开发人员将专注于使用标准化的 JEE 模式和开发组件(展示、业务、数据和集成)来构建完全功能和事务性的应用程序。应用程序的可伸缩性受到底层平台能力(节点集群和分布式缓存)的限制:

单片应用程序

作为单片应用程序构建的业务应用程序通常具有以下特征:

  • 整个应用程序逻辑被打包成一个单独的 EAR 文件

  • 应用程序的重用是通过共享 JAR 文件实现的

  • 应用程序的更改通常提前数月计划,通常是每个季度进行一次大规模推动

  • 有一个数据库包含了整个应用程序的架构

  • 有成千上万的测试用例表示回归的数量

  • 应用程序的设计、开发和部署需要多个团队之间的协调和重大的管理

随着社交互动和移动用户的出现,应用程序用户和数据的规模开始呈指数级增长。企业很快发现,平台在以下问题方面成为了瓶颈:

  • 业务敏捷性:由于应用程序的单片结构,管理应用程序平台和不断更改功能/功能的运营成本受到了阻碍。即使是一个小的功能更改,整个回归测试和在服务器集群上的部署周期也在影响创新的整体速度。

移动革命意味着问题不仅仅存在于渠道层,而且还渗透到集成和记录系统层。除非企业跨越这些层面解决问题,否则在市场上创新和竞争的能力将受到威胁。

  • 成本:为了满足增加的需求,IT 运营团队不断添加新的服务器实例来处理负载。然而,随着每个新实例的增加,复杂性和许可成本(取决于核心数)也在增加。与世界上的 Facebook 不同,企业每用户成本随着每个用户的获取而增加。

此时,企业开始关注开源产品以及消费者面向公司如何构建现代应用程序,为数百万用户提供服务,处理 PB 级数据,并部署到云端。

面向消费者的公司在其生命周期的早期就遇到了这些障碍。大量的创新导致了新的开源产品的设计和开发,以及云计算的设计模式。

在这种情况下,面向服务的架构(SOA)的整个前提被重新审视,企业调查了应用架构如何采用设计自治服务的原则,这些服务是隔离的、离散的,并且可以与其他服务集成和组合。这导致了微服务模型的兴起,它与云服务模型非常适配和整合,其中一切都作为服务和 HTTP 端点可用。

微服务是用于构建灵活、可独立部署的软件系统的面向服务架构(SOA)的专业化和实现方法

  • 维基百科

微服务是设计和开发的,考虑到一个业务应用可以通过组合这些服务来构建。微服务围绕以下原则设计:

  • 单一责任原则:每个微服务只实现有界域上下文中的一个业务责任。从软件角度来看,系统需要分解为多个组件,其中每个组件都成为一个微服务。微服务必须轻量级,以便实现更小的内存占用和更快的启动时间。

  • 无共享:微服务是自治的、自包含的、无状态的,并通过基于容器的封装模型管理服务状态(内存/存储)。私有数据由一个服务管理,没有其他服务对数据的争用。无状态的微服务比有状态的微服务更容易扩展和启动更快,因为在关闭时没有状态需要备份或在启动时激活。

  • 反应式:这适用于具有并发负载或较长响应时间的微服务。异步通信和回调模型允许资源的最佳利用,从而提高微服务的可用性和吞吐量。

  • 外部化配置:这将配置外部化到配置服务器中,以便可以按环境维护它们的分层结构。

  • 一致性:服务应该按照编码标准和命名约定指南以一致的风格编写。

  • 韧性:服务应该处理由技术原因(连接和运行时)和业务原因(无效输入)引起的异常,并且不会崩溃。诸如断路器和批量标头之类的模式有助于隔离和遏制故障。

  • 良好的公民:微服务应通过 JMX API 或 HTTP API 报告它们的使用统计信息,它们被访问的次数,它们的平均响应时间等。

  • 版本化:微服务可能需要支持不同客户的多个版本,直到所有客户迁移到更高版本。在支持新功能和修复错误方面,应该有明确的版本策略。

  • 独立部署:每个微服务都应该可以独立部署,而不会损害应用程序的完整性:

从单片到基于微服务的应用程序的转变

微服务的设计、开发和部署考虑在后续章节中详细介绍。我们将看到如何为电子商务产品构建服务。我相信每个人都对电子商务非常熟悉,并且会很容易理解产品需求。

12 要素应用

为了构建一个可以在云提供商之间部署的分布式、基于微服务的应用程序,Heroku 的工程师提出了需要由任何现代云原生应用程序实施的 12 个因素:

  • 单一代码库:应用程序必须有一个代码库,每个应用程序(即:微服务)都可以在多个环境(开发、测试、暂存和生产环境)中部署。两个微服务不共享相同的代码库。这种模式允许灵活更改和部署服务,而不会影响应用程序的其他部分。

  • 依赖关系:应用程序必须明确声明其代码依赖关系,并将它们添加到应用程序或微服务中。这些依赖关系被打包为微服务 JAR/WAR 文件的一部分。这有助于隔离微服务之间的依赖关系,并减少同一 JAR 的多个版本带来的任何副作用。

  • 配置:应用程序配置数据被移出应用程序或微服务,并通过配置管理工具进行外部化。应用程序或微服务将根据其运行的环境选择配置,从而允许相同的部署单元在各个环境中传播。

  • 后备服务:所有外部资源访问都应该是可寻址的 URL。例如,SMTP URL、数据库 URL、服务 HTTP URL、队列 URL 和 TCP URL。这允许 URL 被外部化到配置中,并为每个环境进行管理。

  • 构建、发布和运行:整个构建、发布和运行过程被视为三个独立的步骤。这意味着作为构建的一部分,应用程序被构建为一个不可变的实体。这个不可变的实体将根据环境(开发、测试、暂存或生产)选择相关的配置来运行进程。

  • 进程:微服务建立在并遵循共享无状态模型。这意味着服务是无状态的,状态被外部化到缓存或数据存储中。这允许无缝扩展,并允许负载均衡或代理将请求发送到服务的任何实例。

  • 端口绑定:微服务是在容器内构建的。服务将通过端口(包括 HTTP)导出和绑定所有其接口。

  • 并发性:微服务进程是按比例扩展的,这意味着为了处理增加的流量,会向环境中添加更多的微服务进程。在微服务进程内部,可以利用反应式模型来优化资源利用率。

  • 可处置性:构建微服务的想法是将其作为不可变的,具有单一职责,以最大程度地提高鲁棒性和更快的启动时间。不可变性也有助于服务的可处置性。

  • 开发/生产一致性:应用程序生命周期中的环境(DEV、TEST、STAGING 和 PROD)尽量保持相似,以避免后续出现任何意外。

  • 日志:在不可变的微服务实例中,作为服务处理的一部分生成的日志是状态的候选者。这些日志应被视为事件流,并推送到日志聚合基础设施。

  • 管理进程:微服务实例是长时间运行的进程,除非它们被终止或替换为更新版本。所有其他管理和管理任务都被视为一次性进程:

12 要素应用

遵循 12 要素的应用程序不对外部环境做任何假设,这使它们可以部署在任何云提供商平台上。这允许在各种环境中运行相同的工具/流程/脚本,并以一致的方式部署分布式微服务应用程序。

微服务启用的服务生态系统

为了成功运行微服务,需要一些必要的启用组件/服务。这些启用服务可以被标记为 PaaS,用于支持微服务的构建、发布、部署和运行。

在云原生模型的情况下,这些服务可以作为云提供商自身的 PaaS 服务提供:

  • 服务发现:当应用程序被分解为微服务模型时,一个典型的应用程序可能由数百个微服务组成。每个微服务运行多个实例,很快就会有成千上万个微服务实例在运行。为了发现服务端点,有必要有一个可以查询的服务注册表,以发现所有微服务实例。此外,服务注册表跟踪每个服务实例的心跳,以确保所有服务都正常运行。

此外,服务注册表有助于在服务实例之间实现负载均衡请求。我们可以有两种负载均衡模型:

  • 客户端负载均衡:

  • 服务消费者向注册表请求服务实例

  • 服务注册表返回服务运行的服务列表

  • 服务器端负载均衡:

  • 服务端点被 Nginx、API 网关或其他反向代理隐藏

这个领域的典型产品有 Consul 和 Zookeeper:

服务注册表

  • 配置服务器:微服务需要用多个参数初始化(例如,数据库 URL、队列 URL、功能参数和依赖标志)。在文件或环境变量中管理超过一定数量的属性可能变得难以控制。为了跨环境管理这些属性,所有这些配置都在配置服务器上进行外部管理。在启动时,微服务将通过调用配置服务器上的 API 加载属性。

微服务还使用监听器来监听配置服务器上属性的任何更改。微服务可以立即捕获属性的运行时更改。这些属性通常被分类为多个级别:

  • 特定于服务的属性:保存与微服务相关的所有属性

  • 共享属性:保存可能在服务之间共享的属性

  • 公共属性:保存在服务之间共同的属性

配置服务器可以将这些属性备份到源代码控制系统中。这个领域的典型产品有 Consul、Netflix Archaius 和 Spring Cloud Config 服务器:

配置服务器

  • 服务管理/监控:一个普通的业务应用程序通常会被分解成大约 400 个微服务。即使我们运行了两到三个实例的这些微服务,我们也将需要管理超过 1,000 个微服务实例。如果没有自动化模型,管理/监控这些服务将成为一个运营挑战。以下是需要被管理和监控的关键指标:

  • 服务健康:每个服务都需要发布其健康状态。这些需要被管理/跟踪以识别慢或死亡的服务。

  • 服务指标:每个服务还发布吞吐量指标数据,如 HTTP 请求/响应的数量、请求/响应大小和响应延迟。

  • 进程信息:每个服务将发布 JVM 指标数据(如堆利用率、线程数和进程状态),通常作为 Java VisualVM 的一部分。

  • 作为流记录事件:每个服务也可以将日志事件发布为一组流事件。

所有这些信息都是从服务中提取出来的,并结合在一起来管理和监控应用服务的景观。需要进行两种类型的分析——事件相关性和纠正决策。警报和执行服务是作为服务监控系统的一部分构建的。例如,如果需要维护一定数量的服务实例,而数量减少(由于健康检查导致服务不可用),那么执行服务可以将该事件视为添加另一个相同服务实例的指示器。

此外,为了跟踪服务调用流程通过微服务模型,有第三方软件可用于帮助创建请求标识并跟踪服务调用如何通过微服务流动。这种软件通常会将代理部署到容器上,将它们编织到服务中并跟踪服务指标:

服务指标

  • 容器管理/编排:微服务环境的另一个关键基础设施部分是容器管理和编排。服务通常打包在容器中,并部署在 PaaS 环境中。环境可以基于 OpenShift 模型、Cloud Foundry 模型或纯 VM 模型,具体取决于它们是部署在私有云还是公共云上。为了部署和管理容器之间的依赖关系,需要容器管理和编排软件。通常,它应该能够理解容器之间的相互依赖关系,并将容器部署为一个应用程序。例如,如果应用程序有四个部分——一个用于 UI,两个用于业务服务,一个用于数据存储——那么所有这些容器应该被标记在一起,并作为一个单元部署,注入相互依赖和正确的实例化顺序。

  • 日志聚合:12 个因素之一是将日志视为事件流。容器应该是无状态的。日志语句通常是需要在容器的生命周期之外持久化的有状态事件。因此,来自容器的所有日志都被视为可以推送/拉取到集中日志存储库的事件流。所有日志都被聚合,可以对这些日志运行各种模型以获取各种警报。人们可以通过这些日志跟踪安全和故障事件,这些事件可以反馈到服务管理/监控系统以进行进一步的操作:

日志聚合

  • API 网关/管理:服务应该是简单的,并遵循单一责任模型。问题是:谁来处理其他关注点,比如服务认证、服务计量、服务限流、服务负载平衡和服务免费/付费模型?这就是 API 网关或管理软件出现的地方。API 网关代表微服务处理所有这些关注点。API 网关提供了多种管理服务端点的选项,还可以提供转换、路由和调解能力。与典型的企业服务总线相比,API 网关更轻量级。

API 管理网关

  • DevOps:另一个关键方面是持续集成/部署管道,以及需要设置基于微服务的应用程序的自动化操作。开发人员编写代码时,它经历一系列需要自动化的步骤,并与门控标准进行映射,以允许发布经过回归测试的代码:

开发生命周期

微服务采用

企业内的微服务采用受到数字转型的共同主题的推动,无论他们是要重新架构现有的单片应用程序以增加业务敏捷性和减少技术债务,还是要开发允许他们快速创新和尝试不同业务模式的全新应用程序。

整体转型

企业一直在运行基于 JEE 原则构建的通道应用程序,运行在应用服务器集群上。这些应用程序多年来积累了大量技术债务,并成为一个主要问题——庞大、笨重,难以不断变化。

随着商业环境竞争的加剧和渠道的增加,企业正在寻求更快的创新,并提供无缝的客户体验。另一方面,他们不希望放弃现有应用程序的投资。

在这种情况下,企业正在进行多个项目,将现有应用程序重构和重新架构为现代、分布式、基于微服务的模型,以提供快速迭代的货币化,并具有未来的保障。

企业正在以双管齐下的方式解决这个问题:

  1. 建立提供核心生态系统作为一组服务来部署和运行微服务的基础平台。这些服务包括配置管理、服务发现、弹性计算、容器管理、安全、管理和监控、DevOps 管道等。企业通常在使用公共云和建立私有云之间权衡。云平台的选择取决于所涉及的行业和企业战略的成熟度。

  2. 第二种方法是逐步削减整体应用程序,一次一个功能模块,将核心业务逻辑迁移到微服务模型。GUI 部分则单独迁移到使用 AngularJS 和 ReactJS 等框架的 SPA 模型。例如,许多电子商务企业已将其目录和搜索服务迁移到弹性云提供商。只有当客户点击结账时,他们才将客户带到内部数据中心。

一旦企业建立了关于平台服务的生态系统,增加更多基于微服务的功能变得容易,为业务敏捷性和创新提供所需的推动力。

我们将在第十二章中更详细地介绍数字转型,“数字转型”。

摘要

在本章中,我们介绍了什么是云原生编程以及为什么要选择它。我们看到了企业在云原生应用方面的各种采用模型。我们介绍了分布式应用的 12 个因素,以及微服务设计在云原生启用中的使用。我们介绍了构建基于微服务的应用程序的启用生态系统。

随着我们在本书中的进展,我们将介绍如何设计,构建和运行您的云原生应用程序。我们还将介绍使用两个云提供商平台(AWS 和 Azure)进行云原生应用程序开发。我们将利用它们的平台服务来构建云原生应用程序。

我们还将介绍云原生应用程序的运营方面——DevOps、部署、监控和管理。最后,我们将介绍如何将现有的单片应用程序转变为现代分布式云原生应用程序。在下一章中,我们将直接开始创建我们的第一个云原生应用程序。

第三章:编写您的第一个云原生应用程序

本章将介绍构建第一个云原生应用程序的基本要素。我们将采取最少的步骤,在我们的开发环境中运行一个微服务。

如果您是一名有经验的 Java 开发人员,使用 Eclipse 等 IDE,您会发现自己置身熟悉的领域。尽管大部分内容与构建传统应用程序相似,但也有一些细微差别,我们将在本章中讨论并在最后进行总结。

开始开发的设置步骤将根据开发人员的类型而有所不同:

  • 对于业余爱好者、自由职业者或在家工作的开发人员,可以自由访问互联网,云开发相对简单。

  • 对于在封闭环境中为客户或业务团队开发项目的企业开发人员,并且必须通过代理访问互联网,您需要遵循企业开发指南。您将受到在下载、运行和配置方面的限制。话虽如此,作为这种类型的开发人员的好处是您并不孤单。您有团队和同事的支持,他们可以通过非正式的帮助或维基文档提供正式的帮助。

在本章结束时,您将在自己的机器上运行一个云原生微服务。为了达到这个目标,我们将涵盖以下主题:

  • 开发者的工具箱和生态系统

  • 互联网连接

  • 开发生命周期

  • 框架选择

  • 编写云原生微服务

  • 启用一些云原生行为

  • 审查云开发的关键方面

设置您的开发者工具箱

对于任何职业来说,工具都非常重要,编码也是如此。在编写一行代码之前,我们需要准备好正确的设备。

获取一个 IDE

集成开发环境IDE)不仅仅是一个代码编辑器;它还包括自动完成、语法、格式化等工具,以及搜索和替换等其他杂项功能。IDE 具有高级功能,如重构、构建、测试和在运行时容器的帮助下运行程序。

流行的 IDE 包括 Eclipse、IntelliJ IDEA 和 NetBeans。在这三者中,Eclipse 是最受欢迎的开源 Java IDE。它拥有庞大的社区,并经常更新。它具有工作区和可扩展的插件系统。在各种语言中应用程序的开发潜力是无限的。基于 Eclipse 的其他一些开发 IDE 包括以下内容:

  • 如果您只打算进行 Spring 开发,那么称为Spring Tool SuiteSTS)的 Eclipse 衍生产品是一个不错的选择。

  • 还有一些云 IDE,比如被誉为下一代 Eclipse 的 Eclipse Che。它不需要任何安装。您可以在连接到 Che 服务器的浏览器中进行开发,该服务器在 Docker 容器中远程构建工作区(包含库、运行时和依赖项)。因此,您可以从任何机器进行开发,任何人都可以通过一个 URL 为您的项目做出贡献。如果您认为这很酷,并且需要一个与位置和机器无关的开发环境,请试一试。

为了这本书的目的,让我们坚持使用基本且非常受欢迎的 Eclipse。在撰写本书时,当前版本是 Neon。庞大的社区和可配置的插件支持使其成为云基 Java 开发的首选 IDE。

从以下网址下载最新版本:www.eclipse.org/。假设您已安装了 JDK 8 或更高版本,Eclipse 应该可以正常启动。

配置一个将存储项目文件和设置的工作区:

当您点击确定时,Eclipse IDE 应该会打开。Eclipse Neon 将自动为您获取我们开发所需的两个重要插件:

  • Git 客户端:这将允许我们连接到 Git 源代码控制存储库。本书假设您使用 Git,因为它很受欢迎并且功能强大,但在企业中还有许多旧的选项,如 Subversion 和 Perforce。如果您使用其他选项,请按照您的项目团队或团队 wiki 中给出的开发人员设置说明下载相应的插件。如果这些说明不存在,请要求为新团队成员建立一个。

  • Maven 支持:Maven 和 Gradle 都是很好的项目管理和配置工具。它们有助于诸如获取依赖项、编译、构建等任务。我们选择 Maven 是因为它在企业中的成熟度。

如果你第一次接触这两个工具,请通过阅读它们各自的网站来熟悉它们。

建立互联网连接

如果您在企业中工作并且必须通过代理访问互联网,根据您的企业政策限制您的操作,这可能会很麻烦。

对于我们的开发目的,我们需要以下互联网连接:

  • 下载依赖库,如 Log4j 和 Spring,这些库被配置为 Maven 存储库的一部分。这是一次性活动,因为一旦下载,这些库就成为本地 Maven 存储库的一部分。如果您的组织有一个存储库,您需要进行配置。

  • 随着我们样例应用的发展,从市场中获取 Eclipse 插件。

  • 您的程序调用了公共云中的服务或 API。

对于编写我们的第一个服务,只有第一个点很重要。请获取您的代理详细信息,并在主菜单的 Maven 设置中进行配置,路径为 Windows | Preferences。

settings.xml文件进行更改,添加代理部分:

<proxies> 
   <proxy>
      <id>myproxy</id>
      <active>true</active> 
      <protocol>http</protocol> 
      <host>proxy.yourorg.com</host> 
      <port>8080</port> 
      <username>mahajan</username> 
      <password>****</password> 
      <nonProxyHosts>localhost,127.0.0.1</nonProxyHosts> 
    </proxy> 
    <proxy> 
      <id>myproxy1</id> 
      <active>true</active> 
      <protocol>https</protocol> 
      <host> proxy.yourorg.com</host> 
      <port>8080</port> 
      <username>mahajan</username> 
      <password>****</password> 
      <nonProxyHosts>localhost,127.0.0.1</nonProxyHosts> 
    </proxy> 

保存文件并重新启动 Eclipse。当我们创建一个项目时,我们将知道它是否起作用。

了解开发生命周期

专业软件编写经历各种阶段。在接下来的章节中,我们将讨论在开发应用程序时将遵循的各个阶段。

需求/用户故事

在开始任何编码或设计之前,了解要解决的问题陈述是很重要的。敏捷开发方法建议将整个项目分解为模块和服务,然后逐步实现一些功能作为用户故事。其思想是获得一个最小可行产品(MVP),然后不断添加功能。

我们要解决的问题是电子商务领域。由于在线购物,我们大多数人都熟悉电子商务作为消费者。现在是时候来看看它的内部运作了。

起点是一个product服务,它执行以下操作:

  • 根据产品 ID 返回产品的详细信息

  • 获取给定产品类别的产品 ID 列表

架构

本书的后面有专门的章节来讨论这个问题。简而言之,一旦需求确定,架构就是关于做出关键决策并创建需求实现蓝图的过程,而设计则是关于合同和机制来实现这些决策。对于云原生开发,我们决定实施微服务架构。

微服务架构范式建议使用包含功能单元的较小部署单元。因此,我们的product服务将运行自己的进程并拥有自己的运行时。这使得更容易打包整个运行时,并将其从开发环境带到测试环境,然后再到生产环境,并保持一致的行为。每个product服务将在服务注册表中注册自己,以便其他服务可以发现它。我们将在后面讨论技术选择。

设计

设计深入探讨了服务的接口和实现决策。product服务将具有一个简单的接口,接受产品 ID 并返回一个 Java 对象。如果在存储库中找不到产品,可以决定返回异常或空产品。访问被记录下来,记录了服务被访问的次数和所花费的时间。这些都是设计决策。

我们将在后面的章节中详细讨论特定于云开发的架构和设计原则。

测试和开发

在任何现代企业软件开发中,测试都不是事后或开发后的活动。它是通过诸如测试驱动开发TDD)和行为驱动开发BDD)等概念与开发同时进行或在开发之前进行的。首先编写测试用例,最初失败。然后编写足够的代码来通过测试用例。这个概念对于产品未来迭代中的回归测试非常重要,并与后面讨论的持续集成CI)和持续交付CD)概念完美融合。

构建和部署

构建和部署是从源代码创建部署单元并将其放入目标运行时环境的步骤。开发人员在 IDE 中执行大部分步骤。然而,根据 CI 原则,集成服务器进行编译、自动化测试用例执行、构建部署单元,并将其部署到目标运行时环境。

在云环境中,可部署单元部署在虚拟环境中,如虚拟机VM)或容器中。作为部署的一部分,将必要的运行时和依赖项包含在构建过程中非常重要。这与将.war.ear放入每个环境中运行的应用服务器的传统过程不同。将所有依赖项包含在可部署单元中使其在不同环境中完整和一致。这减少了出现错误的机会,即服务器上的依赖项与开发人员本地机器上的依赖项不匹配。

选择框架

在了解了基础知识之后,让我们编写我们的product服务。在 IDE 设置之后,下一步是选择一个框架来编写服务。微服务架构提出了一些有趣的设计考虑,这将帮助我们选择框架:

  • 轻量级运行时:服务应该体积小,部署快速

  • 高弹性:应该支持诸如断路器和超时等模式

  • 可测量和可监控:应该捕获指标并公开钩子供监控代理使用

  • 高效:应该避免阻塞资源,在负载增加的情况下实现高可伸缩性和弹性

可以在以下网址找到一个很好的比较:cdelmas.github.io/2015/11/01/A-comparison-of-Microservices-Frameworks.html。在 Java 领域,有三个框架正在变得流行,符合前述要求:Dropwizard,Vert.x 和 Spring Boot。

Dropwizard

Dropwizard 是最早推广 fat JAR 概念的框架之一,通过将容器运行时与所有依赖项和库一起放入部署单元,而不是将部署单元放入容器。它整合了 Jetty 用于 HTTP,Jackson 用于 JSON,Jersey 用于 REST 和 Metrics 等库,创建了一个完美的组合来构建 RESTful web 服务。它是早期用于微服务开发的框架之一。

它的选择,如 JDBI,Freemarker 和 Moustache,可能对一些希望在实现选择上灵活的组织来说有所限制。

Vert.x

Vert.x 是一个出色的框架,用于构建不会阻塞资源(线程)的反应式应用程序,因此非常可伸缩和弹性,因此具有弹性。它是一个相对较新的框架(在 3.0 版本中进行了重大升级)。

然而,它的响应式编程模型在行业中并不十分流行,因此它只是在获得采用,特别是对于需要非常高的弹性和可伸缩性的用例。

Spring Boot

Spring Boot 正在迅速成为构建云原生微服务的 Java 框架中最受欢迎的。以下是一些很好的理由:

  • 它建立在 Spring 和 Spring MVC 的基础上,这在企业中已经很受欢迎

  • 与 Dropwizard 一样,它汇集了最合理的默认值,并采用了一种偏向的方法来组装所需的服务依赖项,减少了配置所需的 XML

  • 它可以直接集成 Spring Cloud,提供诸如 Hystrix 和 Ribbon 之类的有用库,用于云部署所需的分布式服务开发

  • 它的学习曲线较低;您可以在几分钟内开始(接下来我们将看到)

  • 它有 40 多个起始 Maven 项目对象模型(POMs)的概念,为选择和开发应用程序提供了很好的灵活性

Spring Boot 适用于适合云原生部署的各种工作负载,因此对于大多数用例来说是一个很好的首选。

现在让我们开始编写一个 Spring Boot 服务。

编写产品服务

为了简单起见,我们的product服务有两个功能:

  • List<int> getProducts(int categoryId)

  • Product getProduct(int prodId)

这两种方法的意图非常明确。第一个返回给定类别 ID 的产品 ID 列表,第二个返回给定产品 ID 的产品详细信息(作为对象)。

服务注册和发现

服务注册和发现为什么重要?到目前为止,我们一直通过其 URL 调用服务,其中包括 IP 地址,例如http://localhost:8080/prod,因此我们期望服务在该地址运行。即使我们可能替换测试和生产 URL,调用特定 IP 地址和端口的服务步骤仍然是静态的。

然而,在云环境中,事情变化很快。如果服务在给定的 IP 上停机,它可以在不同的 IP 地址上启动,因为它在某个容器上启动。虽然我们可以通过虚拟 IP 和反向代理来缓解这一问题,但最好在服务调用时动态查找服务,然后在 IP 地址上调用服务。查找地址可以在客户端中缓存,因此不需要为每个服务调用执行动态查找。

在这种情况下,注册表(称为服务注册表)很有帮助。当服务启动时,它会在注册表中注册自己。注册表和服务之间也有心跳,以确保注册表中只保留活动的服务。如果心跳停止,注册表将注销该服务实例。

对于这个快速入门,我们将使用 Spring Cloud Netflix,它与 Spring Boot 很好地集成。现在我们需要三个组件:

  • 产品服务:我们已经编写了这个

  • 服务注册表:我们将使用 Eureka,它是 Spring Cloud 的一部分

  • 服务客户端:我们将编写一个简单的客户端来调用我们的服务,而不是直接通过浏览器调用

创建一个 Maven 项目

打开您的 IDE(Eclipse Neon 或其他),然后按以下步骤创建一个新的 Maven 项目:

  1. 在 Package Explorer 上右键单击,然后选择 New 和 Project...,如下截图所示:

  1. 选择 Maven 项目:

  1. 在向导的下一个窗口中,选择创建一个简单的项目。

  2. 下一个对话框将要求输入许多参数。其中,Group Id(你的项目名称)和 Artifact Id(应用程序或服务名称)很重要。选择合理的名称,如下面的截图所示:

  1. 选择完成。你应该看到以下结构:

如果 JRE System Library [JavaSE-1.6]不存在,或者你有一个更新的版本,去项目属性中编辑它,选择你的 Eclipse 配置的版本。你可以通过右键单击 JRE System Library [JavaSE-1.6]来改变属性。这是调整 JRE System Library 到 1.8 后的截图。

  1. 现在,你有一个干净的板面。打开 Maven 文件pom.xml,并添加一个依赖项spring-boot-starter-web。这将告诉 Spring Boot 配置这个项目以获取 web 开发的库:
<project xmlns.... 
  <modelVersion>4.0.0</modelVersion> 
  <parent> 
    <groupId>org.springframework.boot</groupId> 
    <artifactId>spring-boot-starter-parent</artifactId> 
    <version>1.4.3.RELEASE</version> 
  </parent> 
  <groupId>com.mycompany.petstore</groupId> 
  <artifactId>product</artifactId> 
  <version>0.0.1-SNAPSHOT</version>    
<dependencies> 
    <dependency> 
        <groupId>org.springframework.boot</groupId> 
        <artifactId>spring-boot-starter-web</artifactId> 
    </dependency> 
</dependencies> 
</project> 

保存这个 POM 文件时,你的 IDE 将构建工作区并下载依赖的库,假设你的互联网连接正常(直接或通过之前配置的代理),你已经准备好开发服务了。

编写一个 Spring Boot 应用程序类

这个类包含了执行开始的主方法。这个主方法将引导 Spring Boot 应用程序,查看配置,并启动相应的捆绑容器,比如 Tomcat,如果执行 web 服务:

package com.mycompany.product;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;

@SpringBootApplication
public class ProductSpringApp {
  publicstaticvoid main(String[] args) throws Exception {
    SpringApplication.run(ProductSpringApp.class, args);
    }
  } 

注意注解@SpringBootApplication

@SpringBootApplication注解等同于使用@Configuration@EnableAutoConfiguration@ComponentScan,它们分别执行以下操作:

  • @Configuration:这是一个核心的 Spring 注解。它告诉 Spring 这个类是Bean定义的来源。

  • @EnableAutoConfiguration:这个注解告诉 Spring Boot 根据你添加的 JAR 依赖来猜测你想要如何配置 Spring。我们添加了 starter web,因此应用程序将被视为 Spring MVC web 应用程序。

  • @ComponentScan:这个注解告诉 Spring 扫描任何组件,例如我们将要编写的RestController。注意扫描发生在当前和子包中。因此,拥有这个组件扫描的类应该在包层次结构的顶部。

编写服务和领域对象

Spring Boot 中的注解使得提取参数和路径变量并执行服务变得容易。现在,让我们模拟响应,而不是从数据库中获取数据。

创建一个简单的 Java 实体,称为Product类。目前,它是一个简单的POJO类,有三个字段:

publicclass Product {
  privateint id = 1 ;
  private String name = "Oranges " ;
  privateint catId = 2 ;

添加获取器和设置器方法以及接受产品 ID 的构造函数:

  public Product(int id) {
    this.id = id;
    }

另外,添加一个空的构造函数,将在后面由服务客户端使用:

  public Product() {
   } 

然后,编写ProductService类如下:

运行服务

有许多方法可以运行服务。

右键单击项目,选择 Run As | Maven build,并配置 Run Configurations 来执行spring-boot:run目标如下:

点击运行,如果互联网连接和配置正常,你将看到以下控制台输出:

[INFO] Building product 0.0.1-SNAPSHOT 
... 
[INFO] Changes detected - recompiling the module! 
[INFO] Compiling 3 source files to C:Appswkneonproducttargetclasses 
... 
 :: Spring Boot ::        (v1.4.3.RELEASE) 

2016-10-28 13:41:16.714  INFO 2532 --- [           main] com.mycompany.product.ProductSpringApp   : Starting ProductSpringApp on L-156025577 with PID 2532 (C:Appswkneonproducttargetclasses started by MAHAJAN in C:Appswkneonproduct) 
... 
2016-10-28 13:41:19.892  INFO 2532 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat initialized with port(s): 8080 (http) 
... 
2016-10-28 13:41:21.201  INFO 2532 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/product/{id}]}" onto com.mycompany.product.Product com.mycompany.product.ProductService.getProduct(int) 
2016-10-28 13:41:21.202  INFO 2532 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/productIds]}" onto java.util.List<java.lang.Integer> com.mycompany.product.ProductService.getProductIds(int) 
... 
... 
2016-10-28 13:41:21.915  INFO 2532 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http) 
2016-10-28 13:41:21.922  INFO 2532 --- [           main] com.mycompany.product.ProductSpringApp   : Started ProductSpringApp in 6.203 seconds (JVM running for 14.199) 

注意 Maven 执行的阶段:

  1. 首先,Maven 任务编译所有的 Java 文件。目前我们有三个简单的 Java 类。

  2. 下一步将其作为一个应用程序运行,其中一个 Tomcat 实例启动。

  3. 注意将 URL /product//productIds映射到Bean方法。

  4. Tomcat 监听端口8080以接收服务请求。

你也可以通过在 Package Explorer 中右键单击具有主方法的类(ProductSpringApp)然后选择 Run As | Java Application 来运行服务。

在浏览器上测试服务

打开浏览器,访问以下 URL:http://localhost:8080/product/1

你应该得到以下响应:

{"id":1,"name":"Oranges ","catId":2}

现在,尝试另一个服务(URL—http://localhost:8080/productIds)。你得到什么响应?一个错误,如下所示:

    There was an unexpected error (type=Bad Request, status=400).
    Required int parameter 'id' is not present

你能猜到为什么吗?这是因为你写的服务定义有一个期望请求参数的方法:

@RequestMapping("/productIds")
List<Integer> getProductIds(@RequestParam("id") int id) {

因此,URL 需要一个id,由于你没有提供它,所以会出错。

给出参数,再次尝试  http://localhost:8080/productIds?id=5

现在你会得到一个正确的响应:

[6,7,8]

创建可部署文件

我们不打算在 Eclipse 上运行我们的服务。我们想要在服务器上部署它。有两种选择:

  • 创建一个 WAR 文件,并将其部署到 Tomcat 或任何其他 Web 容器中。这是传统的方法。

  • 创建一个包含运行时(Tomcat)的 JAR,这样你只需要 Java 来执行服务。

在云应用程序开发中,第二个选项,也称为 fat JAR 或 uber JAR,因以下原因而变得流行:

  • 可部署文件是自包含的,具有其所需的所有依赖项。这减少了环境不匹配的可能性,因为可部署单元被部署到开发、测试、UAT 和生产环境。如果在开发中工作,它很可能会在所有其他环境中工作。

  • 部署服务的主机、服务器或容器不需要预安装应用服务器或 servlet 引擎。只需一个基本的 JRE 就足够了。

让我们看看创建 JAR 文件并运行它的步骤。

包括 POM 文件的以下依赖项:

<build><plugins><plugin> 
            <groupId>org.springframework.boot</groupId> 
            <artifactId>spring-boot-maven-plugin</artifactId> 
</plugin></plugins></build> 

现在,通过在资源管理器中右键单击项目并选择 Run As | Maven Install 来运行它。

你将在项目文件夹结构的目标目录中看到product-0.0.1-SNAPSHOT.jar

导航到product文件夹,以便在命令行中看到目标目录,然后通过 Java 命令运行 JAR,如下面的屏幕截图所示:

你将看到 Tomcat 在启动结束时监听端口。再次通过浏览器测试。里程碑达成。

启用云原生行为

我们刚刚开发了一个基本的服务,有两个 API 响应请求。让我们添加一些功能,使其成为一个良好的云服务。我们将讨论以下内容:

  • 外部化配置

  • 仪器化—健康和指标

  • 服务注册和发现

外部化配置

配置可以是在环境或生产部署之间可能不同的任何属性。典型的例子是队列和主题名称、端口、URL、连接和池属性等。

可部署文件不应该包含配置。配置应该从外部注入。这使得可部署单元在生命周期的各个阶段(如开发、QA 和 UAT)中是不可变的。

假设我们必须在不同的环境中运行我们的product服务,其中 URL 区分环境。因此,我们在请求映射中做的小改变如下:

@RequestMapping("/${env}product/{id}")
Product getProduct(@PathVariable("id") int id) {

我们可以以各种方式注入这个变量。一旦注入,该值在部署的生命周期内不应该改变。最简单的方法是通过命令行参数传递。打开运行配置对话框,在参数中添加命令行参数-env=dev/,如下所示:

现在,运行配置。在启动过程中,你会发现值被替换在日志声明中,如下所示:

... Mapped "{[/dev/product/{id}]}" onto com.mycompany.product.Product com.mycompany.product.ProductService.getProduct(int) 

配置也可以通过配置文件、数据库、操作系统环境属性等提供。

Spring 应用程序通常使用application.properties来存储一些属性,如端口号。最近,YAML,它是 JSON 的超集,由于属性的分层定义,变得更加流行。

在应用程序的/product/src/main/resources文件夹中创建一个application.yml文件,并输入以下内容:

server: 
  port: 8081 

这告诉product服务在端口8081上运行,而不是默认的8080。这个概念进一步扩展到配置文件。因此,可以通过加载特定于配置文件的配置来加载不同的配置文件。

Spring Cloud Config 作为一个项目很好地处理了这个问题。它使用bootstrap.yml文件来启动应用程序,并加载配置的名称和详细信息。因此,bootstrap.yml包含应用程序名称和配置服务器详细信息,然后加载相应的配置文件。

在应用程序的resources文件夹中创建一个bootstrap.yml文件,并输入以下内容:

spring: 
  application: 
    name: product 

当我们讨论服务注册时,我们将回到这些文件。

计量您的服务

仪器化对于云应用程序非常重要。您的服务应该公开健康检查和指标,以便更好地进行监控。Spring Boot 通过actuator模块更容易进行仪器化。

在 POM 中包含以下内容:

    <dependency> 
        <groupId>org.springframework.boot</groupId> 
        <artifactId>spring-boot-starter-actuator</artifactId> 
    </dependency> 

运行服务。在启动过程中,您将看到创建了许多映射。

您可以直接访问这些 URL(例如http://localhost:8080/env)并查看显示的信息:

{ 
  "profiles": [], 
  "server.ports": { 
    "local.server.port": 8082 
  }, 
  "commandLineArgs": { 
    "env": "dev/" 
  }, 
  "servletContextInitParams": {}, 
  "systemProperties": { 
    "java.runtime.name": "Java(TM) SE Runtime Environment", 
    "sun.boot.library.path": "C:\Program Files\Java\jdk1.8.0_73\jrebin", 
    "java.vm.version": "25.73-b02", 
    "java.vm.vendor": "Oracle Corporation", 
    "java.vendor.url": "http://java.oracle.com/", 
    "path.separator": ";", 
    "java.vm.name": "Java HotSpot(TM) 64-Bit Server VM", 
    "file.encoding.pkg": "sun.io", 
    "user.country": "IN", 
    "user.script": "", 
    "sun.java.launcher": "SUN_STANDARD", 
    "sun.os.patch.level": "Service Pack 1", 
    "PID": "9332", 
    "java.vm.specification.name": "Java Virtual Machine Specification", 
    "user.dir": "C:\Apps\wkneon\product", 

指标尤其有趣(http://localhost:8080/metrics):

{ 
  "mem": 353416, 
  "mem.free": 216921, 
  "processors": 4, 
  "instance.uptime": 624968, 
  "uptime": 642521, 
... 
  "gauge.servo.response.dev.product.id": 5, 
... 
   threads.peak": 38, 
  "threads.daemon": 35, 
  "threads.totalStarted": 45, 
  "threads": 37, 
... 

信息包括计数器和量规,用于存储服务被访问的次数和响应时间。

运行服务注册表

Consul 和 Eureka 是两个流行的动态服务注册表。它们在心跳方法和基于代理的操作方面存在微妙的概念差异,但注册表的基本概念是相似的。注册表的选择将受企业的需求和决策的驱动。对于我们的示例,让我们继续使用 Spring Boot 和 Spring Cloud 生态系统,并为此示例使用 Eureka。Spring Cloud 包括 Spring Cloud Netflix,它支持 Eureka 注册表。

执行以下步骤以运行服务注册表:

  1. 创建一个新的 Maven 项目,artifactIdeureka-server

  2. 编辑 POM 文件并添加以下内容:

  • 父级为spring-boot-starter-parent

  • 依赖于eureka-serverspring-cloud-starter-eureka-server

  • dependencyManagementspring-cloud-netflix

  1. 创建一个类似于我们为product项目创建的应用程序类。注意注解。注解@EnableEurekaServer将 Eureka 作为服务启动:

  1. 在应用程序的/product/src/main/resources文件夹中创建一个application.yml文件,并输入以下内容:
server: 
  port: 8761 
  1. 在应用程序的resources文件夹中创建一个bootstrap.yml文件,并输入以下内容:
spring: 
  application: 
    name: eureka 
  1. 构建eureka-server Maven 项目(就像我们为product做的那样),然后运行它。

  2. 除了一些连接错误(稍后会详细介绍),您应该看到以下 Tomcat 启动消息:

启动完成后,访问localhost:8761上的 Eureka 服务器,并检查是否出现以下页面:

查看前面截图中的圈定部分。当前注册到 Eureka 的实例是EUREKA本身。我们可以稍后更正这一点。现在,让我们专注于将我们的product服务注册到这个 Eureka 服务注册表。

注册产品服务

product服务启动并监听端口8081以接收product服务请求。现在,我们将添加必要的指示,以便服务实例将自身注册到 Eureka 注册表中。由于 Spring Boot,我们只需要进行一些配置和注解:

  1. product服务 POM 中添加dependencyManagement部分,依赖于spring-cloud-netflix和现有依赖项部分中的spring-cloud-starter-eureka如下所示:

  1. product服务会在特定间隔内不断更新其租约。通过在application.yml中明确定义一个条目,将其减少到 5 秒:
server: 
  port: 8081 

eureka: 
  instance: 
    leaseRenewalIntervalInSeconds: 5
  1. product项目的启动应用程序类中包含@EnableDiscoveryClient注解,换句话说,ProductSpringApp@EnableDiscoveryClient注解激活 Netflix Eureka DiscoveryClient实现,因为这是我们在 POM 文件中定义的。还有其他实现适用于其他服务注册表,如 HashiCorp Consul 或 Apache Zookeeper:

  1. 现在,像以前一样启动product服务:

product服务初始化结束时,您将看到注册服务到 Eureka 服务器的日志声明。

要检查product服务是否已注册,请刷新您刚访问的 Eureka 服务器页面:

还要留意 Eureka 日志。您会发现product服务的租约续订日志声明。

创建产品客户端

我们已经创建了一个动态产品注册表,甚至注册了我们的服务。现在,让我们使用这个查找来访问product服务。

我们将使用 Netflix Ribbon 项目,该项目提供了负载均衡器以及从服务注册表中查找地址的功能。Spring Cloud 使配置和使用这一切变得更加容易。

现在,让我们在与服务本身相同的项目中运行客户端。客户端将在 Eureka 中查找产品定义后,向服务发出 HTTP 调用。所有这些都将由 Ribbon 库完成,我们只需将其用作端点:

  1. product项目的 Maven POM 中添加依赖如下:

  1. 创建一个ProductClient类,它简单地监听/client,然后在进行查找后将请求转发到实际的product服务:

URL 构造http://PRODUCT/将在运行时由 Ribbon 进行翻译。我们没有提供服务的 IP 地址。

  1. restTemplate通过自动装配在这里注入。但是,在最新的 Spring 版本中需要初始化它。因此,在主应用程序类中声明如下,这也充当配置类:

@LoadBalanced注解告诉 Spring 使用 Ribbon 负载均衡器(因为 Ribbon 在类路径中由 Maven 提供)。

查看查找的实际操作

现在,我们已经准备好运行产品客户端了。简而言之,在这个阶段,我们有一个 Eureka 服务器项目和一个具有以下结构的product项目:

让我们花几分钟时间来回顾一下我们做了什么:

  1. 我们创建了一个 Maven 项目并定义了启动器和依赖项。

  2. 我们为引导和应用程序属性创建了 YML 文件。

  3. 我们创建了包含主方法的ProductSpringApp类,这是应用程序的起点。

  4. 对于product项目,我们有以下类:

  • Product:我们稍后将增强的领域或实体

  • ProductService:负责实现服务和 API 的微服务

  • ProductClient:用于测试服务查找的客户端

现在,让我们看看它的实际操作:

  1. 运行EurekaApplication类(或在eureka-server项目上运行 Maven 构建)。观察日志中的最后几行:

  1. 运行ProductSpringApp类(或在product项目上运行 Maven 构建)。注意日志中的最后几行:

  1. 直接访问product服务:http://localhost:8081/dev/product/4

您将看到以下响应:

{"id":4,"name":"Oranges ","catId":2}
  1. 现在,访问客户端 URL,http://localhost:8081/client/4,它会从服务注册表中查找product服务并将其指向相应的product服务。

您将看到以下响应:

 {"id":4,"name":"Oranges ","catId":2}

您可能会看到内部服务器错误(PRODUCT没有可用实例)。这可能发生在心跳完成并且地址被 Ribbon 负载均衡器重新选择时。等待几秒钟,直到注册表更新,然后再试一次。

在获取此响应的过程中发生了很多事情:

  1. 处理/client/4的 HTTP 请求是由ProductClient类中的getProduct方法处理的。

  2. 它从 Eureka 注册表中查找了该服务。这就是我们找到的日志语句如下:

c.n.l.DynamicServerListLoadBalancer: Using serverListUpdater PollinServerListUpdater
c.netflix.config.ChainedDynamicProperty: Flipping property: PRODUCT.ribbon.ActiveConnectionsLimit to use NEXT property: niws.loadbalancer
c.n.l.DynamicServerListLoadBalancer: DynamicServerListLoadBalancer for client PRODUCT intiated: DynamicServerListLoadBalancer:
  1. 在进行查找后,它通过 Ribbon 负载均衡器库将请求转发到实际的ProductService

这只是一个客户端通过动态查找调用服务的简单机制。在后面的章节中,我们将添加功能,使其在从数据库获取数据方面具有弹性和功能性。

摘要

让我们回顾一下到目前为止我们讨论过的云应用程序的关键概念。通过在 servlet 引擎上运行并在不到 15 秒内启动,我们使我们的应用程序变得轻量级。我们的应用程序是自包含的,因为 fat JAR 包含了运行我们的服务所需的所有库。我们只需要一个 JVM 来运行这个 JAR 文件。它通过从命令行注入环境和从application.ymlbootstrap.yml中注入属性,实现了外部化配置(在某种程度上)。我们将在第七章 Cloud-Native Application Runtime中更深入地研究外部化的下一阶段。Spring 执行器帮助捕获所有指标,并使它们的 URL 可供使用,从而实现了仪表化位置抽象是由 Eureka 实现的。

在接下来的章节中,我们将通过向其添加数据层和弹性,以及添加缓存行为和其他我们在本章中跳过的增强功能,来增强此服务。

第四章:设计您的云原生应用程序

在本章中,我们暂停应用程序开发,退一步看设计云应用的整体情况。正如在第一章中所看到的,云中的应用比我们迄今为止开发的传统企业应用有更多独特的挑战。此外,敏捷的业务需求必须在不牺牲性能、稳定性和弹性的情况下得到满足。因此,看待第一原则变得重要。

在第一章中,我们看到了云环境和传统企业之间的差异,以及 DevOps、12 因素应用程序、微服务和生态系统的概念是如何重要的。在这里,我们将看一下各种原则和技术,使我们能够设计健壮、可扩展和敏捷的应用程序。

我们将涵盖的一些领域包括使用 REST、HTTP 和 JSON 构建 API 的主导地位,API 网关的作用,如何解耦应用程序,如何识别微服务,各种微服务设计指南,数据架构的作用,以及在设计 API 时安全性的作用。

我们将在本章中涵盖以下主题:

  • REST、HTTP 和 JSON 的流行

  • API 的兴起和流行

  • API 网关的角色

  • 解耦-需要更小的应用边界

  • 微服务识别

  • 微服务设计指南

  • 微服务模式

  • 数据架构

  • 安全角色

三者-REST、HTTP 和 JSON

网络使得 HTTP 变得非常流行,并成为访问互联网内容的事实集成机制。有趣的是,这项技术在依赖本地和二进制协议(如 RMI 和 CORBA)进行应用程序访问的应用程序中并不是非常流行。

当社交消费公司(如 Google、Amazon、Facebook 和 Twitter)开始发布 API 以连接/集成其产品时,跨网络的集成的事实标准变成了 HTTP/REST。社交消费公司开始投资于平台,以吸引开发人员开发各种应用程序,从而导致依赖 HTTP 作为协议的应用程序的大量增加。

浏览器端的应用程序是 HTML 和 JavaScript 的混合。从服务器返回的信息或其他应用程序需要以简单和可用的格式。 JavaScript 支持数据操作,最适合的数据格式是JavaScript 对象表示JSON)。

REST 是一种状态表示风格,提供了一种处理 HTTP 交换的方式。 REST 有很多优势:

  • 利用 HTTP 协议标准,为 WWW 上的任何事物提供了巨大的优势

  • 隔离对实体的访问(GET/PUT/POST/DELETE)的机制,同时利用相同的 HTTP 请求模型

  • 支持 JSON 作为数据格式

REST 与 JSON 已经成为主导模型,超过了 SOAP/XML 模型。根据可编程 Web 的统计数据:

73%的可编程 Web API 使用 REST。 SOAP 远远落后,但在 17%的 API 中仍有所体现。

让我们来看一些 REST/JSON 模型受欢迎的高级原因:

  • SOAP 的契约优先方法使得制作 Web 服务变得困难。

  • 与 REST 相比,SOAP 更复杂,学习曲线更陡。

  • 与 SOAP 相比,REST 更轻量级,不会像 SOAP 那样占用带宽。

  • 在 Java 世界之外,对 SOAP 的支持有限,主要将 SOAP 局限于企业世界。

  • 客户端上的 XML 解析需要大量内存和计算资源,这不适合移动世界。

  • XML 模式/标记提供了结构定义和验证模型,但需要额外的解析。 JSON 具有松散的语法,允许对数据模型进行快速迭代。

今天,现实是 REST/JSON 已经成为跨编程语言集成的标准,为通过互联网集成 API 提供了一种简单易行的方式。

API 的兴起和流行

应用程序编程接口API)提供了一个标准的接口或契约,以通过互联网消费其服务。API 定义了输入和输出的结构,并在 API 版本的整个生命周期内保持不变。

API 是客户端层和企业之间的契约。它们是面向消费者的,即由客户端设计,并且将服务实现细节从客户端抽象出来。

回到社交消费者公司的出现,创建新的应用程序并不意味着从头开始。例如,如果我的应用程序需要使用地理地图,我可以利用 Google 地图 API 并在此基础上构建我的应用程序。同样,我可以利用 OAuth 而不是构建自己的身份验证模型,并使用 Google、Facebook 或 Twitter 作为一些 OAuth 提供者。

将一个可重复但通常复杂的功能作为可重用服务提供的整个模型,导致开发人员开始使用这些现有的 API 构建应用程序,从而提高了开发人员的生产力,并推动了现代应用程序或移动应用程序经济的发展。

公司开始寻求是否可以将 API 商品化,这意味着多家公司正在编写/发布提供类似功能的 API。这导致了 API 的民主化,使任何人都可以访问功能/函数。

API 的整个民主化意味着,突然之间,每个流程或功能都可以作为一组 API 来提供,可以编排或编排以构建新的功能。以前需要几个月甚至几年的时间,现在只需要几周甚至几天。所有这些生产力意味着更短的开发周期,允许快速迭代提供新的创新功能。

今天,各种类型的 API 都可以使用:从 Facebook、Google 和 Twitter 等社交公司到 Salesforce、NetSuite 和 PaaS/IaaS 提供商,如 AWS、Azure、Google Cloud EngineGCE)等,它们都提供从提供虚拟机到数据库实例,再到 Watson、AWS AI 和 Azure ML 等 AI 提供商的功能。

API 网关的作用

API 网关是一个单一的接口,它在重定向到内部服务器之前处理所有传入的请求。API 网关通常提供以下功能:

  • 将传入流量路由到提供者的数据中心/云中托管的适当服务。提供反向代理模型,限制提供者数据中心/云中托管的各种 API 和服务的暴露。

  • 过滤来自各种渠道的所有传入流量——Web、移动等。

  • 实施安全机制(如 OAuth)来验证和记录服务的使用情况。

  • 提供了对某些服务的流量控制和限制能力。

  • 在服务消费者和提供者之间转换数据。

  • 提供一个或多个 API,映射到底层的服务提供者。例如,对于不同类型的消费者——移动、Web、付费服务或免费服务,相同的底层服务可以分成多个自定义 API,暴露给不同的消费者,以便消费者只看到它需要的功能:

API 网关的好处

使用 API 网关提供以下好处:

  • 关注点分离:在应用程序端将微服务提供者与服务消费者隔离开来。这允许将应用程序层与服务请求客户端分离。

  • 面向消费者:API 网关为大量的 API 和微服务提供了一个统一的中心。这使得消费者可以专注于 API 的实用性,而不是寻找服务的托管位置,管理服务请求限制,安全性等。

  • 面向 API:根据客户端的类型和所需的协议提供最佳的 API。

  • 编排:提供了将多个服务调用编排成一个 API 调用的能力,从而简化了客户端的逻辑。现在,它可以调用一个 API 而不是调用多个服务。较少的请求意味着较少的调用开销,从而提高了消费者的整体体验。API 网关对移动应用程序至关重要。

  • 监控:API 网关还提供了监控 API 调用的能力,从而使企业能够评估 API 的成功和使用情况。

除了总体利益外,API 网关为整体拼图增加了更多的部分。这意味着需要管理更多的基础设施、更多的配置、更多的故障点和额外的请求跳转。因此,除非利益超过了缺点,否则需要仔细审查 API 网关的使用,以满足业务需求和利益。

接下来,我们将看到将应用程序功能拆分为一组 API 或微服务的过程。

应用程序解耦

传统的应用程序开发模型,将所有功能和功能捆绑在一个称为单体应用程序的大型包中,由于多种原因而变得不太受欢迎。单体应用程序以功能和逻辑的形式承担了太多的责任。正是这一特征使它们具有高耦合和低内聚。单体应用程序中的重用因子往往较低,因为功能的一部分无法与其余的功能和逻辑分离。

当我们开始拆分单体功能或设计新应用程序时,重点需要放在定义服务边界上。定义正确的服务边界及其相关的交互是导致高内聚和低耦合模型的关键。

问题是,应用程序应该根据什么基础被解耦为服务,并定义服务边界?

有界上下文/领域驱动设计

作为应用程序设计的一部分,业务领域需要被拆分为更小的子领域或业务能力。我们需要仔细审查业务实体及其属性,以定义服务边界。例如,在客户 ID 实体的情况下,客户的地址可能是客户的一部分。在应用程序的上下文中,地址维护可能是一个单独的活动,可能需要单独处理。同样,个性化可能需要客户偏好或购物习惯。在这种情况下,个性化引擎更感兴趣这一系列属性。

我们应该组合一个包含所有属性的大型客户服务,还是可以根据业务派生的不同视角进行划分?这些不同的视角导致了领域驱动设计中有界上下文的定义。

有界上下文是一种领域驱动设计范式,有助于添加一个接缝并创建服务组。有界上下文在解决方案空间中工作,表明服务相关并属于一个共同的功能域。它是由一个团队根据反向康威定律与一个业务单元一起构建的。有界上下文可以通过以下方式与其他服务/业务能力进行通信:

  • 暴露内部 API 或服务

  • 在事件总线上发出事件

有界上下文可以拥有自己的数据存储,服务共用,或采用每个服务一个数据存储的范式。

每个有界上下文都有自己的生命周期,并形成一个产品。团队围绕这些有界上下文组织,并全权负责服务的全栈实现。团队是跨职能的,并从开发、测试、用户体验、数据库、部署和项目管理中带来技能。每个产品可能会被拆分成较小的服务集,它们之间异步通信。请记住,重点不是一组功能,而是业务能力。

我们开始围绕业务能力构建我们的服务。服务拥有其业务数据和功能。服务是这些数据的主人,其他服务不能拥有该服务的任何数据。

上游/下游服务的分类

另一种拆分应用系统的方法是通过上游和下游数据流模型对其进行分类。系统中的核心实体包括上游服务。这些上游服务会触发事件,下游服务会订阅这些事件以增强其功能。这旨在解耦系统,并有助于提高整体业务敏捷性。这与反应式架构概念相吻合,也被称为事件驱动架构。

让我们以电子商务应用程序为例,其中核心实体是客户和产品。订单服务依赖于核心实体客户和产品的信息。接下来,我们正在构建为客户提供推荐和个性化服务的服务。推荐和个性化服务依赖于核心实体客户、产品和订单的数据。当核心实体发生变化时,变化会被发布。这些变化会被推荐和个性化服务接收,它们会使用额外的属性来提供相关服务。推荐和个性化服务是这些服务的下游服务。

将业务能力分类为上游和下游的模型有助于定义服务之间的依赖关系,并改变上游服务对下游服务的影响。

业务事件

随着系统的发展,服务将开始聚集成自然的盟友。这意味着找出服务是否依赖于类似的数据元素或提供重叠/配角功能,并且可能成为同一有界上下文的一部分。

在同一领域内工作的有界上下文服务可能需要依赖于主服务以实现准确的功能。这可能意味着一些主服务数据属性需要提供给相关的有界上下文服务。例如,在我们之前的例子中,我们谈到了客户偏好。现在,这些偏好可能需要映射到客户的位置(地址)。在这种情况下,客户偏好是否需要每次调用客户地址服务来构建偏好,还是可以将相关属性复制到自己的领域中?在不重复数据的情况下,这两个服务开始紧密耦合,导致双向通信模型。为了打破这种紧密耦合,我们允许客户偏好服务使用事件来缓存或复制相关的客户属性。这种异步模型打破了服务之间的时间紧密耦合。每当客户地址发生变化时,服务都会发布一个业务事件进行必要的更改。客户偏好服务会订阅这个变化,以更新其偏好模型。

这种异步模型使我们能够确保:

  • 数据所有权仍然清晰。对数据的任何更改都会通知依赖服务。允许依赖服务保存或复制数据,但不更改本地副本,除非更新主副本(黄金源原则)。依赖服务仅存储所需和功能相关的数据子集(需要知道原则)。

  • 异步业务事件导致服务之间的低耦合。核心服务的更改会导致事件。事件向下游传递给感兴趣的依赖服务。唯一的依赖是发布的业务事件的格式。

  • 下游服务遵循最终一致性原则;所有业务事件都以顺序方式存储,以构建/状态一个较晚的时间(事件源/CQRS)。查询模型可以与记录系统不同。

  • 业务事件的异步模型也促进了编排而不是管弦乐,从而导致了松散耦合的系统/服务。

有时,当团队开始一个新产品时,可能无法事先定义界限上下文或服务分解。因此,团队开始构建应用程序作为一个单片应用程序,通过将其功能公开为一组服务。随着团队实施更多的故事,他们可以确定功能的部分,这些功能以快速的速度变化(通常是体验或渠道服务)与变化缓慢的部分(通常是核心服务或实体服务)。

团队可以开始将服务分为两类——体验和系统服务。系统服务可以进一步围绕实体和相互关系进行分组。体验服务映射到客户旅程。团队通常会有冲刺来清理/重构代码,以清除每个周期积累的技术债务。

那么,下一个问题是,什么标识一个服务为微服务?

微服务识别

微服务的名称并不一定意味着服务必须体积小。但它具有以下特点:

  • 单一责任原则:这是微服务的核心设计原则。它们应该完成一个业务任务单元并完全完成它。如果耦合度低,服务将更容易修改和部署,甚至完全替换。

  • 粒度:微服务的粒度包含在单个功能域、单个数据域及其直接依赖、自包含的打包和技术域的交集中。

  • 界限:服务应该可以访问其界限上下文中由同一团队管理的资源。但是,它不应直接访问其他模块的资源,如缓存和数据库。如果服务需要访问其他模块,应通过内部 API 或服务层进行。这有助于减少耦合并促进敏捷性。

  • 独立:每个微服务都是独立开发、测试和部署的,在其自己的源中。它可以使用第三方或共享库。

微服务和服务导向架构(SOA)之间的区别

以下是微服务和服务导向架构(SOA)之间的区别:

  • 服务执行整个业务工作单元。例如,如果一个服务需要客户或产品数据,最好将其存储在服务数据存储中。通常,不需要通过 ESB 获取客户记录。

  • 服务有自己的私有数据库或仅在其界限上下文中共享的数据库,并且可以存储为服务业务工作单元提供所需的信息。

  • 服务是一个智能端点,通常通过 Swagger 或类似的存储库中的合同定义公开 REST 接口。一些被其他部门或客户使用的服务通过 API 平台公开。

服务粒度

以下是服务的类型:

  • 原子或系统服务:这些服务执行单元级别的工作,并且足以通过引用数据库或下游源来服务请求。

  • 复合或过程服务:这些服务依赖于两个或多个原子服务之间的协调。通常情况下,除非业务案例已经涉及使用现有的原子服务,否则不鼓励使用复合微服务。例如,从储蓄账户进行信用卡支付需要调用两个服务,一个是借记储蓄账户,另一个是贷记信用卡账户。复合微服务还引入了固有的复杂性,例如在分布式场景中难以处理的状态管理和事务。

  • 体验服务:这些服务与客户旅程相关,并部署在基础架构的边缘。这些服务处理来自移动和 Web 应用程序的请求。这些服务通过使用诸如 API 网关之类的工具,通过反向代理公开。

微服务设计指南

整个微服务的概念是关于关注点的分离。这需要在具有不同责任的服务之间进行逻辑和架构上的分离。以下是设计微服务的一些建议。

这些指南符合 Heroku 工程师提出的 12 因素应用程序指南。

  • 轻量级:微服务必须轻量级,以便实现更小的内存占用和更快的启动时间。这有助于更快的 MTTR,并允许服务部署在更小的运行时实例上,因此在水平方面更好地扩展。与重型运行时(如应用服务器)相比,更适合的是较小的运行时,如 Tomcat、Netty、Node.js 和 Undertow。此外,服务应该使用轻量级文本格式(如 JSON)或二进制格式(如 Avro、Thrift 或 Protocol Buffers)交换数据。

  • 响应式:这适用于具有高并发负载或稍长的响应时间的服务。典型的服务器实现会阻塞线程以执行命令式编程风格。由于微服务可能依赖于其他微服务或 I/O 资源(如数据库),阻塞线程可能会增加操作系统的开销。响应式风格采用非阻塞 I/O,使用回调处理程序,并对事件做出反应。这不会阻塞线程,因此可以更好地增加微服务的可伸缩性和负载处理特性。例如,数据库驱动程序已开始支持响应式范例,比如 MongoDB 响应式流 Java 驱动程序。

  • 无状态:无状态服务具有更好的扩展性和更快的启动速度,因为在关闭或启动时不需要在磁盘上存储状态。它们也更具弹性,因为终止服务不会导致数据丢失。无状态也是朝着轻量级的一步。如果需要状态,服务可以将状态存储委托给高速持久(键值)存储,或者将其保存在分布式缓存中。

  • 原子性:这是微服务的核心设计原则。如果服务足够小并且执行可以独立完成的最小业务单元,那么它们应该易于更改、测试和部署。如果耦合度低,服务将更容易修改和独立部署。可能需要根据需要使用复合微服务,但设计应该受到限制。

  • 外部化配置:传统上,典型的应用程序属性和配置是作为配置文件进行管理的。鉴于微服务的多个和大规模部署,随着服务规模的增加,这种做法将变得繁琐。因此,最好将配置外部化到配置服务器中,以便可以按环境在分层结构中进行维护。诸如热更改之类的功能也可以更容易地同时反映多个服务。

  • 一致性:服务应该按照编码标准和命名约定指南以一致的风格编写。常见关注点,如序列化、REST、异常处理、日志记录、配置、属性访问、计量、监控、供应、验证和数据访问应该通过可重用资产、注释等一致地完成。另一个团队的开发人员应该更容易理解服务的意图和操作。

  • 具有弹性:服务应该处理由技术原因(连接性、运行时)和业务原因(无效输入)引起的异常,并且不会崩溃。它们应该使用超时和断路器等模式来确保故障得到谨慎处理。

  • 良好的服务对象:通过 JMX API 报告其使用统计数据、访问次数、平均响应时间等,并/或通过库发布到中央监控基础设施、日志审计、错误和业务事件中。通过健康检查接口公开其状态,例如 Spring Actuator 所做的那样。

  • 版本化:微服务可能需要支持不同客户端的多个版本,直到所有客户端迁移到更高版本。因此,部署和 URL 应该支持语义版本控制,即 X.X.X。

此外,微服务还需要利用通常在企业级别构建的额外能力,比如:

  • 动态服务注册表:微服务在启动时会向服务注册表注册自己。

  • 日志聚合:微服务生成的日志可以被聚合起来进行集中分析和故障排除。日志聚合是一个独立的基础设施,通常建立为异步模型。产品如 Splunk 和 ELK Stack 与事件流(如 Kafka)一起被用来构建/部署日志聚合系统。

  • 外部配置:微服务可以从外部配置(如 Consul 和 Zookeeper)中获取参数和属性以初始化和运行。

  • 供应和自动扩展:如果 PaaS 环境检测到需要根据传入负载启动额外实例、某些服务失败或未及时响应,则服务将自动启动。

  • API 网关:微服务接口可以通过 API 网关向客户端或其他部门公开,提供抽象、安全性、限流和服务聚合。

在我们开始构建和部署服务时,我们将在后续章节中涵盖所有服务设计指南。

设计和部署模式

在您开始设计应用程序时,您需要了解各种服务设计和集成模式。

设计模式

微服务设计模式可以根据所解决的问题进行多种分类。最常见的分类和相关模式将在以下部分讨论。

内容聚合模式

随着微服务和有界上下文,内容聚合有了额外的责任。客户端可能需要跨多个领域或业务领域(或在解决方案术语中,有界上下文)获取信息。所需的内容可能无法由一个服务提供。这些模式有助于识别和建模体验服务类别。因此,有各种聚合模式可以应用。

客户端聚合

最后一英里的聚合。这适用于 Web 浏览器或合理的处理能力用户界面,它显示来自各个领域的内容。这种模式通常用于聚合各种主题领域的主页。此外,这是亚马逊广泛使用的模式。

好处

使用客户端模式进行聚合的好处如下:

  • 服务层的解耦方法。更容易实现每个单独服务的灵活性和可维护性。

  • 在 UI 层面,感知性能更快,因为请求可以并行运行,以填充屏幕上的各个区域。当有更高的带宽可用于并行获取数据时,效果更好。

权衡

与客户端模式相关的权衡如下:

  • 需要复杂的用户界面处理能力,如 Ajax 和单页面应用程序。

  • 聚合的知识暴露在 UI 层,因此,如果类似的输出被作为数据集提供给第三方,就需要进行聚合。

API 聚合

在门上进行聚合。这适用于不想了解聚合细节的移动或第三方用例,而是希望在单个请求中期望一个数据结构。API 网关被设计用于进行此聚合,然后向客户端公开统一的服务。如果在内容聚合期间不需要显示任何数据部分,API 网关也可以选择消除这些数据部分:

好处

使用 API 聚合模式的好处如下:

  • API 网关将客户端与个别服务的细节抽象出来。因此,它可以在不影响客户端层的情况下灵活更改服务。

  • 在带宽受限的情况下更好,不适合运行并行 HTTP 请求的情况。

  • 在 UI 处理受限的情况下更好,处理能力可能不足以进行并发页面生成。

权衡

与 API 聚合模式相关的权衡如下:

  • 在有足够带宽的情况下,此选项的延迟高于客户端的聚合。这是因为 API 网关在发送数据给客户端之前需要等待所有内容被聚合。

微服务聚合

业务层的聚合。在这种方法中,一个微服务聚合来自各个组成微服务的响应。如果在聚合数据时需要应用任何实时业务逻辑,这种模式非常有用。例如,显示跨各种业务的客户持有总价值:

好处

使用微服务聚合模式的好处如下:

  • 对聚合的更精细控制。此外,还有可能根据聚合数据应用业务逻辑。因此,提供了更丰富的内容聚合能力。

  • 对 API 网关能力的依赖较低。

权衡

与微服务聚合模式相关的权衡如下:

  • 由于引入了额外的步骤,延迟更低,代码更多。

  • 失败或出错的机会更多。来自微服务的并行聚合将需要诸如响应式或回调机制等复杂的代码。

数据库聚合

数据层的聚合。在这种方法中,数据被预先聚合到一个运营数据存储ODS)中,通常是文档数据库。这种方法对于存在额外业务推断的情况非常有用,这些推断很难通过微服务实时计算,因此可以由分析引擎预先计算:

好处

使用数据库聚合模式的好处如下:

  • 通过分析作业对数据进行额外丰富。例如,在基于 ODS 中聚合的客户投资组合的客户 360°视图中,可以应用额外的分析来实现下一步最佳行动NBA)场景。

  • 与早期方法相比更灵活和功能更强,对数据模型可以进行更精细的控制。

权衡

与数据库聚合模式相关的权衡如下:

  • 更高的复杂性

  • 数据重复和更多的数据存储需求

  • 需要额外的 ETL 或变更数据捕获CDC)工具来将数据从记录系统发送到中央 ODS 存储

协调模式

理想情况下,微服务应该能够执行业务工作单元。然而,在某些业务场景中,微服务必须利用其他服务作为依赖项或组合。例如,考虑首先从储蓄账户借记,然后向信用卡账户贷记的信用卡支付。在这种情况下,两个基础服务,如借记和贷记,可以由各自的储蓄账户和信用卡领域公开,并且它们之间需要协调。

业务流程管理(BPM)

涉及长时间运行过程的复杂协调最好由 BPM 完成。企业可能已经拥有 BPM 产品。然而,对于简单的两步或三步协调,BPM 可能过于复杂。

复合服务

指导方针是对于低复杂度(或简单)但高容量的协调使用复合服务。在讨论的其余部分,这样的协调可以被称为微流程。

为什么使用复合服务?

在微服务架构中,服务定义的实现是通过较小的可部署单元而不是在应用服务器中运行的大型单体应用程序来完成的。这使得服务更容易编写,更快更改和测试,以及更快部署。但这也为跨两个或多个微服务的微流程,甚至跨多个有界上下文的微流程带来了挑战。在单体应用程序中,这样的微流程可以作为单个事务在单个可部署单元中部署的两个模块之间的协调。在微服务架构中,分布式事务是不鼓励的,因此,微流程必须使用组合方法来解决。

微服务协调的能力

本节列出了复合服务所需的能力:

  • 状态管理:通常需要状态管理器组件来管理它协调的服务的输出状态。这种状态将需要保存在对服务器端状态管理SSM)故障免疫的持久存储中。另一个 SSM 实例应该能够检索状态并从上次离开的地方开始。

  • 事务控制:微服务会影响事务边界。现在,对单个事务中的两个方法进行两个独立的函数调用,现在变成了通过复合服务进行两个独立的服务调用。有两种方法来处理这种情况。

  • 分布式事务:这些支持两阶段提交协议。它们不具有可伸缩性,会增加延迟和死锁情况,并且需要昂贵的产品和基础设施来支持它们。它们可能不受选定协议的支持,例如 REST 或消息传递。这种风格的好处是系统始终处于一致的状态。

  • 补偿事务:在这种情况下,事务控制是通过运行功能性反向事务来实现,而不是尝试回滚到较早的事务。这是一种更解耦的,因此可扩展的方法。

由于技术产品要求的简化,我们建议使用补偿事务而不是分布式事务。

  • 邮件服务调度:原子服务调用可能会成功,也就是说,当组成服务成功完成其工作时;或者失败,当协调服务之一未响应或由于技术或功能错误而在处理中失败时。复合服务将需要获取已完成服务的响应,并决定下一步的行动。

  • 超时处理:在启动微流程时启动计时器。如果服务在启动微流程后的特定时间内没有响应,则触发一个事件发送到事件总线。

  • 可配置性:SSM 组件的多个实例将运行以满足各种微流程。在每个微流程中,服务协调、计时器和操作都会有所不同。因此,提供一个可以对计时器、补偿事务和后处理操作进行参数化配置的框架非常重要。

协调模型

我们将讨论复合服务微流程的以下协调样式。

异步并行

复合服务异步地启动对组成原子服务的服务调用,然后监听服务响应。如果任一服务失败,它会向另一个服务发送补偿事务。

这类似于 EIP 的散射-聚集或组合消息处理器模式:

异步顺序

在管道处理中,复合服务按顺序向原子服务发送消息。它在调用下一个服务之前等待前一个服务返回成功。如果任何一个服务失败,那么复合服务将向先前成功的服务发送补偿事务。这类似于 EIP 中的过程管理器模式:

使用请求/响应进行编排

与前面的部分类似,但是以请求/响应和同步方式进行,而不是异步消息传递。

合并微服务

当复合服务与其组成微服务之间存在耦合时,可以将服务合并并作为单个组件运行。例如,可以通过账户服务实现资金转移,额外的transferFunds方法接受fromAcctoAcc和资金金额。然后,它可以作为单个事务的一部分发出debitcredit方法调用。然而,这种方法需要经过充分考虑后才能决定。缺点包括耦合部署信用卡和储蓄领域的借记和贷记服务:

部署模式

微服务试图解决单体应用程序的问题,如依赖关系,并通过具有单独的可部署单元来实现敏捷性。我们可以以各种风格将微服务部署到目标运行时。这些选项按照隔离度(好)和成本(坏)的增加顺序进行描述。

每个 WAR 文件中的多个服务

尽管开发可能是以微服务风格进行的(为服务单独的代码库,不同的团队负责不同的服务),但部署基本上遵循单体应用程序的风格:

利弊

与完全的单体应用程序风格相比,唯一的好处是由于有单独的代码库和较少的依赖关系,对通用代码元素的依赖较低。然而,它并不提供服务行为之间的运行时隔离,因此没有真正的微服务架构模型的好处,如独立发布、扩展单个服务或限制一个服务问题对其他服务的影响。

适用性

这并不是很有用的情况,因为它并不提供运行时隔离。然而,它可能是释放完全分离的中间步骤。

每个 WAR/EAR 的服务

该模型将服务的构建过程分离,以创建每个服务的单独.war/.ear文件。然而,它们最终被部署到同一个 Web 容器或应用服务器中:

利弊

这种风格通过将每个服务的构建过程分开来创建可部署单元,进一步提高了隔离。然而,由于它们部署在同一个 Web 容器或应用服务器上,它们共享相同的进程。因此,服务之间没有运行时隔离。

适用性

一些团队可能会在目标部署上遇到约束,使用与单体风格开发中使用的相同软件或硬件。在这种情况下,这种部署风格是合适的,因为团队仍然可以独立开发,而不会互相干扰,但在部署到传统生产基础设施时,他们将不得不与其他团队协调发布。

每个进程的服务

这种风格使用了之前讨论过的 fat JAR 的概念,将应用服务器或 Web 容器作为部署单元的一部分。因此,目标运行环境只需要一个 JVM 来运行服务。Dropwizard 和 Spring Boot 框架鼓励这种类型的部署构建。我们还在第二章中看到了创建这样一个部署单元的示例,编写您的第一个云原生应用程序

好处和权衡

与每个进程风格相关的服务的好处和权衡如下:

  • 这种方法有助于分离服务运行的运行时进程。因此,它在服务之间创建了隔离,这样一个进程中的内存泄漏或 fat 异常不会在一定程度上影响其他服务。

  • 这允许有选择地扩展服务,允许在现有硬件上部署更多的服务,与其他服务相比。

  • 它还给团队自由,可以根据特定用例或团队需求使用不同的应用服务器/ Web 容器。

  • 然而,它无法阻止任何一个服务占用系统资源(如 CPU、I/O 和内存),从而影响其他服务的性能。

  • 它还减少了运维团队对运行时的控制,因为在这种模型中没有中央 Web 容器或应用服务器。

  • 这种风格需要良好的治理来限制部署环境的变化,并且需要有实质性的用例来支持分歧。

适用性

这种风格为那些受限于使用现有生产基础设施并且尚未拥有 Docker 容器或小型 VM 配置的团队提供了最佳折衷方案。

每个 Docker 容器的服务

在这种风格中,服务以一个带有必要先决条件(如 JVM)的 Docker 容器中的 fat JAR 部署。它比 Linux 容器技术提供的隔离更高一步:

好处和权衡

与每个 Docker 容器风格相关的服务的好处和权衡如下:

  • Linux 容器技术限制了服务的 CPU 和内存消耗,同时提供了网络和文件访问隔离。这种隔离程度对许多服务来说是足够的。

  • 容器从镜像启动速度快。因此,可以非常快速地生成基于应用程序或服务镜像的新容器,以满足应用程序的波动需求。

  • 容器可以通过各种编排机制进行编排,例如 Kubernetes、Swarm 和 DC/OS,以便根据明确定义的应用蓝图自动创建整个应用程序配置。

  • 与之前的风格一样,可以在容器中运行各种服务技术。例如,除了 Java 服务外,还可以运行 Node.js 服务,因为容器镜像将位于操作系统级别,因此可以由编排框架无缝启动。

  • 容器在资源需求方面的开销比虚拟机低得多,因为它们更轻量级。因此,与在自己的虚拟机中运行每个服务相比,它们更便宜。

  • 然而,容器重用主机系统的内核。因此,无法在容器技术上运行需要不同操作系统的工作负载,例如 Windows 或 Solaris。

适用性

这种部署风格在隔离和成本之间取得了很好的平衡。这是推荐的风格,适用于大多数服务部署。

每个虚拟机一个服务

在这种风格中,fat JAR 直接部署在虚拟机上,就像每个进程一个服务部分一样。然而,在这里,每个虚拟机只部署一个服务。这确保了该服务与其他服务完全隔离。

部署是通过诸如 Chef 和 Puppet 等工具自动化的,这些工具可以获取基础镜像(例如已安装 Java)然后运行一系列步骤在虚拟机上安装应用程序 JAR 和其他实用程序:

优点和权衡

与每个虚拟机一个服务风格相关的优点和权衡如下:

  • 如果有任何需要完全 OS 级别隔离的用例,那么这种风格是合适的

  • 这种风格还允许我们在虚拟机上混合完全不同的工作负载,例如 Linux、Windows 和 Solaris

  • 然而,与前一种风格相比,这种风格更加资源密集,启动速度更慢,因为虚拟机包括完整的客户操作系统启动

  • 因此,与之前的选项相比,它的成本效率较低

适用性

这种部署风格倾向于增加成本。这是推荐的风格,适用于云镜像部署,例如创建Amazon Machine ImagesAMI)。

每个主机一个服务

这将隔离从虚拟机的 hypervisor(对于虚拟机)提升到硬件级别,通过在不同的物理主机上部署服务。可以使用微服务器或专门的设备概念来实现这一目的。

优点和权衡

与每个主机一个服务风格相关的优点和权衡如下:

  • 硬件(如处理器、内存和 I/O)可以完全调整到服务的用例。英特尔提供了一系列微服务器,针对特定任务进行了调整,例如图形处理、Web 内容服务等。

  • 这种解决方案可以实现非常高的组件密度。

  • 这种部署风格适用于非常少数需要从硬件级别隔离或专门硬件需求中受益的用例。

  • 这是一种成熟的技术,因此目前还没有很多数据中心云提供商提供。然而,到本书出版时,它将已经成熟。

适用性

这种部署风格非常罕见,因为很少有用例需要这种高级别的隔离或专门的硬件要求。Web 内容或图形处理的设备是一些受益于这种部署风格的专门用例。

发布模式

以下是服务中使用的不同发布模式:

  • Fat JAR:如第二章中所讨论的,编写您的第一个云原生应用程序,fat JAR 有助于将 Web 容器与可部署内容捆绑在一起。这确保了在开发、测试和生产环境中部署版本之间没有不一致。

  • 蓝绿部署:这种模式建议维护两个相同的生产环境。新版本发布到一个未使用的环境,比如绿色环境。从路由器切换流量到绿色部署。如果成功,绿色环境将成为新的生产环境,蓝色环境可以被停用。如果出现问题,回滚更容易。下一个周期将以相反的方式进行,部署到蓝色环境,因此在两个环境之间交替。存在一些挑战,比如数据库升级。对于异步微服务,可以使用这种技术来发布一个微服务或一组具有不同输入队列的微服务。从连接参数加载的配置决定将请求消息放入一个队列还是另一个队列。

  • 语义化版本控制:语义化版本控制是关于使用版本号发布软件,以及它们如何改变底层代码的含义,以及从一个版本到下一个版本进行了什么修改。有关更多详细信息,请参阅semver.org/。在异步微服务中,使用每个微服务一个输入队列的类似策略适用。然而,在这种情况下,两个服务都是活动的,一个用于传统的服务,一个用于新的更改。根据请求,可以使用基于内容的路由模式来切换队列以发送请求。

  • 金丝雀发布:这种模式用于向一小部分用户引入变更,使用选择一组客户的路由逻辑来实现。在异步服务方面,可以通过两组输入队列来处理,重定向逻辑现在决定将请求消息放入哪个队列。

  • 不可变服务器/不可变交付:不可变服务器和不可变交付是相关的。其目的是从配置管理存储库自动构建服务器(虚拟机或容器)及其软件和应用程序。构建后,它不会被改变,即使在从一个环境移动到另一个环境时也不会改变。只有配置参数通过环境、JNDI 或独立的配置服务器注入,比如 Consul 或使用 Git。这确保在生产部署中没有未记录在版本控制系统中的临时更改。

  • 功能切换:这允许在生产中发布的功能从一些配置设置中切换开或关。这个切换通常在前端或 API 网关实现,以便可以对服务/功能的最终用户可见或不可见。这种模式对于暗黑发布能力非常有用,这将在接下来的部分中讨论。

  • 暗黑发布:由 Facebook 推广。暗黑发布意味着在计划发布之前很长时间将服务/能力发布到生产中。这为在生产环境中测试集成点和复杂服务提供了机会。只有前端或 API 的更改使用了之前讨论的金丝雀发布和功能切换。

微服务的数据架构

微服务的一个关键设计理念是有界上下文和管理数据存储的服务。在有界上下文中,多个服务可能访问一个共同的数据存储,或者采用每个服务一个数据存储的范式。

由于可能有多个服务实例在运行,我们如何确保数据读取/更新操作不会导致资源死锁?

命令查询职责分离(CQRS)

CQRS 引入了一个有趣的范例,挑战了使用相同数据存储来创建/更新和查询系统的传统思想。其思想是将改变系统状态的命令与幂等的查询分开。物化视图就是这种模式的一个例子。这种分离还提供了使用不同的数据模型进行更新和查询的灵活性。例如,关系模型可以用于更新,但从更新生成的事件可以用于更新更适合读取的缓存或文档数据库。

用户请求可以广泛分类为两部分,即改变系统状态的命令和为用户获取系统状态的查询。对于命令处理,参与系统收集足够的业务数据,以便可以调用系统记录上的相应服务来执行命令。对于查询,参与系统可以选择要么调用系统记录,要么从专为读取工作负载而设计的本地存储获取信息。这种策略的分离可以产生巨大的好处,例如减少对系统记录的负载和减少延迟:

CQRS 模式有助于利用旧的记录系统以及较新的文档数据库和缓存。我们将在下一章中介绍如何在您的服务中实现 CQRS。

数据重复

在有界上下文内,服务是数据的监护人。但是如果另一个服务需要您数据的子集怎么办?一些可能出现的问题/解决方案如下:

  • 我应该调用服务来获取那些数据吗?

  • 服务之间的通信增加

  • 两个服务之间的紧密耦合

  • 我可以直接从另一个有界上下文中访问数据存储吗?

  • 打破了有界上下文模型

那么,另一个服务(驻留在另一个有界上下文中)如何访问数据的子集?(例如,在个性化服务中需要客户的地址属性(来自客户服务)。)

在这种情况下,最好的方法是从主域中复制数据。所需的更改由主域发布为事件,任何对这些更改感兴趣的域都会订阅这些事件。事件从事件总线中获取,并且使用事件中的数据来更新重复数据存储中的更改:

好处

复制数据的好处如下:

  • 有助于解耦服务边界

  • 包含数据的业务事件是服务之间唯一的关系

  • 有助于避免跨边界的昂贵分布式事务模型

  • 允许我们在不妨碍系统其他部分进展的情况下对服务边界进行更改

  • 我们可以决定希望多快或多慢地看到外部世界的其余部分,并最终变得一致

  • 使用适合我们服务模型的技术在我们自己的数据库中存储数据的能力

  • 灵活性使我们能够对架构/数据库进行更改

  • 使我们变得更具可伸缩性、容错性和灵活性

缺点

复制数据相关的缺点如下:

  • 大量数据更改可能意味着两端需要更强大的基础设施,并且处理丢失事件的能力需要事件的持久性

  • 导致最终一致性模型

  • 复杂的系统,非常难以调试

适用于特定目的

有界上下文模型意味着所包含的数据只能通过定义的服务接口或 API 进行修改。这意味着实际的模式或用于存储数据的存储技术对 API 功能没有影响。这使我们有可能使用适合特定目的的数据存储。如果我们正在构建搜索功能,并且内存数据存储对于给定的业务需求更合适,我们可以继续使用它。

由于数据访问受服务 API 的管理,数据存储的选择和结构对实际服务消费者来说并不重要:

服务 API 模型还提供了灵活性,可以在不影响其他消费服务的情况下从一个数据存储转移到另一个数据存储,只要服务契约得到维护。Martin Fowler 将其称为多语言持久性。

安全性的作用

随着微服务的普及,管理这些服务的安全性的挑战变得更加困难。除了开放式 Web 应用安全项目OWASP)十大网络漏洞之外,还需要回答一些问题,例如:

  • 服务在服务调用之前是否需要客户端进行身份验证(例如 OAuth)?

  • 客户端是否可以调用任何服务,还是只能调用其被授权的服务?

  • 服务是否知道请求的发起客户端的身份,并且是否将其传递给下游服务?下游服务是否有机制来验证其调用的授权?

  • 服务之间的流量调用是否安全(HTTPS)?

  • 我们如何验证来自经过身份验证的用户的请求是否未被篡改?

  • 我们如何检测并拒绝请求的重放?

在分布式微服务模型中,我们需要控制和限制调用方的特权,以及在安全漏洞的情况下每次调用可访问的数据量(最小特权)。大量的微服务和支持数据库意味着存在需要保护的大攻击面。服务之间的服务器加固变成了保护网络的重要和关键活动。监控服务访问并对威胁进行建模非常重要,以分解我们最脆弱的流程并集中精力进行防范。我们将看到 API 网关在解决一些安全问题方面的作用。

总结

这让我们得出了云应用程序的设计原则的结论。在本章中,您了解了 API 受欢迎的原因,如何解耦您的单体应用程序,以及微服务设计的各种模式和数据架构原则。我们还看到了微服务中安全性的作用以及 API 网关的作用。

在下一章中,我们将以第二章中的示例,编写您的第一个云原生应用程序,并开始添加更多内容,使其更适合生产。我们将添加数据访问,缓存选项及其考虑因素,应用 CQRS 和错误处理。

第五章:扩展您的云原生应用

在理解了设计原则之后,让我们拿出在第二章中开发的骨架服务,编写您的第一个云原生应用,并对它们进行一些真正的工作,使它们能够投入生产。

我们定义了两个获取服务;getProduct用于给定产品 ID,getProducts用于给定类别。这两个服务具有高度的非功能性要求。它们必须始终可用,并以尽可能低的延迟提供数据。以下步骤将带领我们实现这一目标:

  1. 访问数据:服务访问跨各种资源的数据

  2. 缓存:进行缓存的选项及其考虑因素

  3. 应用 CQRS:使我们能够拥有不同的数据模型来服务不同的请求

  4. 错误处理:如何恢复,发送什么返回代码,以及实现断路器等模式

我们还将研究添加修改数据的方法,例如insertupdatedelete。在本章中,我们将涵盖:

  • 验证:确保数据在处理之前是干净的

  • 保持两个 CQRS 模型同步:数据一致性

  • 事件驱动和异步更新:它如何扩展架构并同时解耦

实现获取服务

让我们继续开发在第二章中开发的product项目,编写您的第一个云原生应用。我们将在讨论概念的同时逐步增强它。

让我们仔细考虑一下我们两个服务的数据库。getProduct返回产品信息,而getProducts搜索属于该类别的产品列表。首先,对于简单和标准的要求,这两个查询都可以由关系数据库中的单个数据模型回答:

  1. 您将在一个固定数量的列中的产品表中存储产品。

  2. 然后,您将对类别进行索引,以便对其进行的查询可以快速运行。

现在,这个设计对于大多数中等规模公司的要求来说都是可以的。

简单的产品表

让我们在标准关系数据库中使用产品表,并使用 Spring Data 在我们的服务中访问它。Spring Data 提供了优秀的抽象,以使用Java 持久化 APIJPA),并使编写数据访问对象DAO)变得更加容易。Spring Boot 进一步帮助我们开始时编写最少的代码,并在前进时进行扩展。

Spring Boot 可以与嵌入式数据库一起工作,例如 H2、HSQLDB 或外部数据库。进程内嵌入式数据库在我们的 Java 服务中启动一个进程,然后在进程终止时终止。这对于开始是可以的。稍后,依赖项和 URL 可以更改为指向实际数据库。

您可以从第二章中获取项目,编写您的第一个云原生应用,并添加以下步骤,或者只需从 GitHub(github.com/PacktPublishing/Cloud-Native-Applications-in-Java)下载已完成的代码:

  1. Maven POM:包括 POM 依赖项:

这将告诉 Spring Boot 包含 Spring Boot starter JPA 并在嵌入模式下使用 HSQLDB。

  1. 实体:根据 JPA,我们将开始使用实体的概念。我们已经有一个名为Product的领域对象来自我们之前的项目。重构它以放入一个实体包中。然后,添加@Entity@Id@Column的标记,如下所示的Product.java文件:
package com.mycompany.product.entity ; 

import javax.persistence.Column; 
import javax.persistence.Entity; 
import javax.persistence.GeneratedValue; 
import javax.persistence.GenerationType; 
import javax.persistence.Id; 

@Entity 
public class Product { 

   @Id 
   @GeneratedValue(strategy=GenerationType.AUTO) 
   private int id ; 

   @Column(nullable = false) 
   private String name ; 

   @Column(nullable = false) 
   private int catId ; 

其余的代码,如构造函数和 getter/setter,保持不变。

  1. 存储库:Spring Data 提供了一个存储库,类似于 DAO 类,并提供了执行数据的创建读取更新删除CRUD)操作的方法。CrudRepository接口中已经提供了许多标准操作。从现在开始,我们将只使用查询操作。

在我们的情况下,由于我们的领域实体是Product,所以存储库将是ProductRepository,它扩展了 Spring 的CrudRepository,并管理Product实体。在扩展期间,需要使用泛型指定实体和主键的数据类型,如下面的ProductRepository.java文件所示:

package com.mycompany.product.dao; 

import java.util.List; 
import org.springframework.data.repository.CrudRepository; 
import com.mycompany.product.entity.Product; 

public interface ProductRepository extends CrudRepository<Product, Integer> { 

   List<Product> findByCatId(int catId); 
} 

首先要考虑的问题是,这段代码是否足够工作。它只有一个接口定义。如何能足够处理我们的两个方法,即getProduct(根据产品 ID)和getProducts(根据类别)?

Spring Data 中发生的魔术有助于处理样板代码。CrudRepository接口带有一组默认方法来实现最常见的操作。这些包括savedeletefindcountexists操作,这些操作足以满足大部分查询和更新任务。我们将在本章的后半部分讨论update操作,但让我们先专注于查询操作。

根据CrudRepository中的findOne方法,已经存在根据 ID 查找产品的操作。因此,我们不需要显式调用它。

在我们的ProductRepository接口中,根据给定类别查找产品的任务由findByCatId方法完成。Spring Data 存储库基础设施内置的查询构建器机制对于构建存储库实体的查询非常有用。该机制会剥离方法的前缀,如findreadquerycountget,然后根据实体解析剩余部分。该机制非常强大,因为关键字和组合的选择意味着方法名足以执行大部分查询操作,包括操作符(and/or)、distinct 子句等。请参阅 Spring Data 参考文档(docs.spring.io/spring-data/jpa/docs/current/reference/html/)了解详细信息。

这些约定允许 Spring Data 和 Spring Boot 根据解析接口来注入方法的实现。

  1. 更改服务:在第二章中,编写您的第一个云原生应用程序,我们的product服务返回了虚拟的硬编码数据。让我们将其更改为针对数据库的有用内容。我们通过使用之前定义的ProductRepository接口,并通过@Autowiring注入到我们的ProductService类中来实现这一点,如下面的ProductService.java文件所示:
@RestController 
public class ProductService { 

   @Autowired 
   ProductRepository prodRepo ; 

   @RequestMapping("/product/{id}") 
   Product getProduct(@PathVariable("id") int id) { 
         return prodRepo.findOne(id); 
   } 

   @RequestMapping("/products") 
   List<Product> getProductsForCategory(@RequestParam("id") int id) { 
         return prodRepo.findByCatId(id); 
   } 
} 

存储库中的findOne方法根据主键获取对象,我们定义的findByCatId方法有助于根据类别查找产品。

  1. 模式定义:目前,我们将模式创建留给hibernate自动生成脚本。由于我们确实想要看到生成的脚本,让我们在application.properties文件中启用对类的logging,如下所示:
logging.level.org.hibernate.tool.hbm2ddl=DEBUG 
logging.level.org.hibernate.SQL=DEBUG 
  1. 测试数据:由于我们将稍后插入产品,因此需要初始化数据库并添加一些产品。因此,请将以下行添加到import.sql中,并将其放在资源中(与application.properties和引导文件所在的位置):
-- Adding a few initial products
insert into product(id, name, cat_Id) values (1, 'Apples', 1) 
insert into product(id, name, cat_Id) values (2, 'Oranges', 1) 
insert into product(id, name, cat_Id) values (3, 'Bananas', 1) 
insert into product(id, name, cat_Id) values (4, 'Carrot', 2) 
  1. 让 Spring Data 和 Spring Boot 来解决其余问题:但在生产应用程序中,我们希望对连接 URL、用户 ID、密码、连接池属性等进行精细控制。

运行服务

要运行我们的product服务,请执行以下步骤:

  1. 启动 Eureka 服务器(就像我们在第二章中所做的那样,编写您的第一个云原生应用程序),使用EurekaApplication类。我们将始终保持 Eureka 服务运行。

  2. 一旦Eureka项目启动,运行product服务。

注意由hibernate生成的日志。它首先自动使用 HSQLDB 方言,然后创建并运行以下Product表 SQL:

HHH000227: Running hbm2ddl schema export 
drop table product if exists 
create table product (id integer generated by default as identity (start with 1), cat_id integer not null, name varchar(255) not null, primary key (id)) 
HHH000476: Executing import script '/import.sql' 
HHH000230: Schema export complete 

一旦服务开始监听端口,请在浏览器中发出查询:http://localhost:8082/product/1。这将返回以下内容:

{"id":1,"name":"Apples","catId":1} 

当您看到日志时,您会观察到后台运行的 SQL:

select product0_.id as id1_0_0_, product0_.cat_id as cat_id2_0_0_, product0_.name as name3_0_0_ from product product0_ where product0_.id=? 

现在,再次发出一个返回给定类别产品的查询:http://localhost:8082/products?id=1。这将返回以下内容:

[{"id":1,"name":"Apples","catId":1},{"id":2,"name":"Oranges","catId":1},{"id":3,"name":"Bananas","catId":1}] 

为此条件运行的 SQL 如下:

select product0_.id as id1_0_, product0_.cat_id as cat_id2_0_, product0_.name as name3_0_ from product product0_ where product0_.cat_id=? 

尝试使用不同的类别,http://localhost:8082/products?id=2,将返回如下内容:

[{"id":4,"name":"Carrot","catId":2}] 

这完成了一个针对数据源的简单查询服务。

为了生产目的,这将需要增强以将标准数据库作为 Oracle、PostgreSQL 或 MySQL 数据库。您将在类别列上引入索引,以便查询运行更快。

传统数据库的局限性

但是在以下情况下,公司扩大产品和客户会发生什么?

  • 关系数据库的可伸缩性(产品数量和并发请求数量)成为瓶颈。

  • 产品结构根据类别不同,在关系数据库的固定模式中很难建模。

  • 搜索条件开始扩大范围。目前,我们只按类别搜索;以后,我们可能希望按产品描述、过滤字段以及类别描述进行搜索。

单个关系数据库是否足以满足所有需求?

让我们用一些设计技术来解决这些问题。

缓存

随着服务在数据量和并发请求方面的扩展,数据库将开始成为瓶颈。为了扩展,我们可以采用缓存解决方案,通过从缓存中服务请求来减少对数据库的访问次数,如果值在缓存中可用的话。

Spring 提供了通过注解包含缓存的机制,以便 Spring 可以返回缓存值而不是调用实际处理或检索方法。

从概念上讲,缓存分为两种类型,如下节所讨论的。

本地缓存

本地缓存存在于与服务相同的 JVM 中。它的范围是有限的,因为它只能被服务实例访问,并且必须完全由服务实例管理。

让我们首先使我们的产品在本地缓存中可缓存。

Spring 3.1 引入了自己的注释来返回缓存条目、清除或填充条目。但后来,JSR 107 JCache 引入了不同的注释。Spring 4.1 及更高版本也支持这些。

让我们首先使用 Spring 的注释:

  1. 告诉 Spring 应用程序启用缓存并寻找可缓存的实例。这是一次性声明,因此最好在启动类中完成。在主类中添加@``EnableCaching注释:
@SpringBootApplication
@EnableDiscoveryClient 
@EnableCaching 
public class ProductSpringApp { 
  1. 在我们的ProductRepository中启用缓存以通过添加可缓存注释获取产品,我们将提供一个明确的缓存名称,并将用于此方法:
public interface ProductRepository extends CrudRepository<Product, Integer> { 

   @Cacheable("productsByCategoryCache") 
   List<Product> findByCatId(int catId); 
} 

现在,再次运行服务,并观察当您在浏览器中运行以下一组查询时的日志:

  1. http://localhost:8082/products?id=1

  2. http://localhost:8082/products?id=2

  3. http://localhost:8082/products?id=1

  4. http://localhost:8082/products?id=2

您会看到以下 SQL 只被触发了两次:

select product0_.id as id1_0_, product0_.cat_id as cat_id2_0_, product0_.name as name3_0_ from product product0_ where product0_.cat_id=? 

这意味着仓库只有在缓存中找不到类别条目时才执行了findByCatId方法。

底层

尽管 Spring 在幕后处理了许多关注点,如缓存实现,但重要的是要了解正在发生的事情,并意识到其中的局限性。

在内部,缓存是通过内部类(如缓存管理器和缓存解析器)实现的。当没有提供缓存产品或框架时,Spring 默认使用ConcurrentHashMap。Spring 的缓存实现了许多其他本地缓存,如 EHCache、Guava 和 Caffeine。

查看 Spring 文档(docs.spring.io/spring/docs/current/javadoc-api/org/springframework/cache/annotation/Cacheable.html)以获取更多诸如sync=true和条件缓存等复杂性。

本地缓存的局限性

本地缓存在有限的用例中很有用(例如非更改静态数据),因为使用 Spring 注释(如@CachePut@CacheEvict等)在一个服务中进行的更新无法与运行多个服务实例的其他实例上的缓存同步,以实现负载平衡或弹性目的。

分布式缓存

Hazelcast、Gemfire 和/或 Coherence 等分布式缓存是网络感知的,缓存实例作为进程模型(对等模型)运行,其中缓存是服务运行时的一部分,或者作为客户端-服务器模型运行,其中缓存请求从服务到单独的专用缓存实例。

对于此示例,我们选择了 Hazelcast,因为它是一个非常轻量但功能强大的分布式缓存解决方案。它还与 Spring Boot 集成得非常好。以下是如何操作的:

  1. 在 POM(Maven 文件)中,添加对hazelcast-spring的依赖。hazelcast-spring具有一个HazelcastCacheManager,用于配置要使用的 Hazelcast 实例:
<dependency> 
   <groupId>org.springframework.boot</groupId> 
   <artifactId>spring-boot-starter-cache</artifactId> 
</dependency> 
<dependency> 
   <groupId>com.hazelcast</groupId> 
   <artifactId>hazelcast-spring</artifactId>              
</dependency>
  1. 由于 Hazelcast 是一个分布式缓存,它需要元素是可序列化的。因此,我们需要确保我们的Product实体是可序列化的:
public class Product implements Serializable {
  1. 一个简化的 Hazelcast 配置文件,告诉各种 Hazelcast 实例如何发现并与彼此同步:
<hazelcast  
   xsi:schemaLocation="http://www.hazelcast.com/schema/config http://www.hazelcast.com/schema/config/hazelcast-config-3.6.xsd" 
   > 

   <group> 
         <name>ProductCluster</name> 
         <password>letmein</password> 
   </group> 
   <network> 
        <join> 
            <multicast enabled="true"/> 
        </join> 
    </network> 
</hazelcast>

现在,让我们测试这些更改。为此,我们必须运行两个product服务的实例来检查它是否有效。我们可以通过更改端口号来运行两个实例:

  1. 使用端口8082(已配置)运行服务。

  2. application.properties更改为8083

  3. 再次运行服务。

您将在一个服务上看到 Hazelcast 消息,该服务启动如下:

Loading 'hazelcast.xml' from classpath. 
[LOCAL] [ProductCluster] [3.6.5] Picked Address[169.254.104.186]:5701, using socket  
[169.254.104.186]:5701 [ProductCluster] [3.6.5] Hazelcast 3.6.5 (20160823 - e4af3d9) starting 
Members [1] { 
Member [169.254.104.186]:5701 this 
}

但是,一旦第二个服务启动,成员定义就会被2更新:

Members [2] { 
   Member [169.254.104.186]:5701 
   Member [169.254.104.186]:5702 this 
} 

现在,在浏览器上运行以下查询,并观察控制台中的日志:

  1. http://localhost:8082/products?id=1

  2. http://localhost:8082/products?id=2

  3. http://localhost:8082/products?id=1

  4. http://localhost:8082/products?id=2

  5. http://localhost:8083/products?id=1

  6. http://localhost:8083/products?id=2

您会发现在 SQL 中,调试日志只在第一个服务中出现两次。其他四次,缓存条目都是从 Hazelcast 中提取的。与以前的本地缓存不同,缓存条目在两个实例之间是同步的。

将 CQRS 应用于分离数据模型和服务

分布式缓存是解决扩展问题的一种方法。但是,它引入了某些挑战,例如缓存陈旧(使缓存与数据库同步)和额外的内存需求。

此外,缓存是过渡到 CQRS 范例的开始。重新审视我们在第三章设计您的云原生应用程序中讨论的 CQRS 概念。

查询是从缓存中回答的(除了第一次命中),这是查询与从记录系统(即数据库)传递的命令进行分离,并稍后更新查询模型(缓存更新)。

让我们在 CQRS 中迈出下一步,以便清晰地进行这种分离。CQRS 引入的复杂性是:

  • 需要维护两个(或多个)模型,而不是一个

  • 当数据发生变化时更新所有模型的开销

  • 不同模型之间的一致性保证

因此,只有在用例需要高并发、高容量和快速敏捷性需求的情况下才应遵循这种模式。

关系数据库上的物化视图

物化视图是 CQRS 的最简单形式。如果我们假设对产品的更新发生的频率比对产品和类别的读取频率低,那么我们可以有两种不同的模型支持getProduct(根据 ID)和getProducts(根据给定的类别)。

搜索查询getProducts针对此视图,而基于主键的传统getProduct则转到常规表。

如果数据库(例如 Oracle)支持,这应该很容易。如果数据库不支持物化视图,默认情况下可以手动完成,如果有需要,可以通过手动更新统计信息或摘要表来完成,当主产品表使用触发器或更好的事件驱动架构(例如业务事件)更新时。我们将在本章的后半部分看到这一点,当我们为我们的服务集添加addProduct功能时。

Elasticsearch 和文档数据库

为了解决灵活模式、高搜索能力和更高容量处理的限制,我们可以选择 NoSQL 技术:

  • 为了提供不同类型的产品,我们可以选择使用文档数据库及其灵活的模式,例如 MongoDB。

  • 为了处理搜索请求,基于 Lucene 的 Elasticsearch 技术由于其强大的索引能力将是有益的。

为什么不仅使用文档数据库或 Elasticsearch?

也可以考虑以下选项:

  • Elasticsearch 通常是一种补充技术,而不是用作主数据库。因此,产品信息应该在可靠的关系型或 NoSQL 数据库中维护。

  • 像 MongoDB 这样的文档数据库也可以构建索引。但是,性能或索引能力无法与 Elasticsearch 相匹敌。

这是一个典型的适用场景示例。您的选择将取决于您的用例:

  • 无论您是否需要灵活的模式

  • 可扩展和高容量的应用程序

  • 高度灵活的搜索需求

文档数据库上的核心产品服务

保持 REST 接口不变,让我们将内部实现从使用关系数据库(例如我们的 HSQLDB)更改为 MongoDB。我们将 MongoDB 作为服务器单独运行,而不是在进程中运行,例如 HSQLDB。

准备 MongoDB 的测试数据

下载和安装 MongoDB 的步骤如下:

  1. 安装 MongoDB。在 MongoDB 网站上(www.mongodb.com/)可以很容易地按照各种平台的说明进行操作。

  2. 运行mongod.exe启动 MongoDB 的一个实例。

  3. 创建一个包含我们示例数据的测试文件(类似于import.sql)。但是,这次我们将数据保留在 JSON 格式中,而不是 SQL 语句。products.json文件如下:

{"_id":"1","name":"Apples","catId":1} 
{"_id":"2","name":"Oranges","catId":1} 
{"_id":"3","name":"Bananas","catId":1} 
{"_id":"4","name":"Carrot","catId":2} 

请注意_id,这是 MongoDB 的主键表示法。如果您不提供_id,MongoDB 将使用ObjectId定义自动生成该字段。

  1. 将示例数据加载到 MongoDB。我们将创建一个名为masterdb的数据库,并加载到一个名为product的集合中:
mongoimport --db masterdb --collection product --drop --file D:datamongoscriptsproducts.json 
  1. 通过使用use masterdb后,通过命令行检查数据是否加载,使用db.product.find()命令如下:

创建产品服务

创建product服务的步骤如下:

  1. 最好从零开始。从之前的带有 Hazelcast 和 HSQLDB 的示例项目复制或从 GitHub 存储库中拉取(github.com/PacktPublishing/Cloud-Native-Applications-in-Java)。

  2. 调整 Maven POM 文件以具有以下依赖项。删除其他依赖项,因为它们对我们的小例子不是必需的:

<dependencies> 
         <dependency> 
               <groupId>org.springframework.boot</groupId> 
               <artifactId>spring-boot-starter-web</artifactId> 
         </dependency> 
         <dependency> 
               <groupId>org.springframework.boot</groupId> 
               <artifactId>spring-boot-starter-actuator</artifactId> 
         </dependency> 
         <dependency> 
               <groupId>org.springframework.cloud</groupId> 
               <artifactId>spring-cloud-starter-eureka</artifactId> 
         </dependency> 
         <dependency> 
               <groupId>org.springframework.boot</groupId> 
               <artifactId>spring-boot-starter-data- 
                mongodb</artifactId> 
        </dependency> 
</dependencies> 
  1. Product实体应该只有一个@Id字段。在类级别放置@Document注解是可选的。如果不这样做,首次插入性能会受到影响。现在,让我们在Product.java文件中放置注解:
@Document 
public class Product  { 

   @Id 
   private String id ;      
   private String name ;    
   private int catId ; 

   public Product() {} 

   .... (other constructors, getters and setters) 

请注意,这里的idString而不是int。原因是 NoSQL 数据库在生成 ID 时(GUID)比关系系统(如数据库)中的递增整数更好。原因是数据库变得更加分布式,因此相对于生成 GUID,可靠地生成递增数字稍微困难一些。

  1. ProductRepository现在扩展了MongoRepository,其中有从 MongoDB 检索产品的方法,如ProductRepository.java文件中所示:
package com.mycompany.product.dao; 

import java.util.List; 
import org.springframework.data.mongodb.repository.MongoRepository; 
import com.mycompany.product.entity.Product; 

public interface ProductRepository extends MongoRepository<Product, String> { 

   List<Product> findByCatId(int catId); 
}
  1. 我们只需向application.properties添加一个属性,告诉服务从 MongoDB 的masterdb数据库获取数据。此外,最好在不同的端口上运行它,这样我们以后可以并行运行服务,如果我们想这样做的话:
server.port=8085 
eureka.instance.leaseRenewalIntervalInSeconds=5 
spring.data.mongodb.database=masterdb 

由于接口没有更改,因此ProductService类也不会更改。

现在,启动 Eureka 服务器,然后启动服务,并在浏览器中执行以下查询:

  1. http://localhost:8085/products?id=1

  2. http://localhost:8085/products?id=2

  3. http://localhost:8085/product/1

  4. http://localhost:8085/product/2

您将得到与以前相同的 JSON。这是微服务的内部实现更改。

拆分服务

让我们从学习的角度采用所建议的分离的简单实现。由于我们正在分离主模型和搜索模型,将服务拆分是有意义的,因为搜索功能可以被视为产品主模型的下游功能。

对于类别的getProducts功能是搜索功能的一部分,它本身可以成为一个复杂且独立的业务领域。因此,现在是重新考虑是否有意义将它们保留在同一个微服务中,还是将它们拆分为核心产品服务和产品搜索服务的时候了。

产品搜索服务

让我们创建一个专门进行高速、高容量搜索的新微服务。支持搜索微服务的搜索数据存储不需要是产品数据的主数据,而可以作为补充的搜索模型。Elasticsearch 在各种搜索用例中都非常受欢迎,并且符合极端搜索需求的需求。

准备 Elasticsearch 的测试数据

以下是准备 Elasticsearch 的测试数据的步骤:

  1. 安装 Elastic 版本。使用版本 2.4.3,因为最近的 5.1 版本与 Spring Data 不兼容。Spring Data 使用在端口9300上与服务器通信的 Java 驱动程序,因此在客户端和服务器上具有相同的版本非常重要。

  2. 创建一个包含我们的样本数据的测试文件(类似于products.json)。格式与以前的情况略有不同,但是针对 Elasticsearch 而不是 MongoDB。products.json文件如下:

{"index":{"_id":"1"}} 
{"id":"1","name":"Apples","catId":1} 

{"index":{"_id":"2"}} 
{"id":"2","name":"Oranges","catId":1} 

{"index":{"_id":"3"}} 
{"id":"3","name":"Bananas","catId":1} 

{"index":{"_id":"4"}} 
{"id":"4","name":"Carrot","catId":2} 
  1. 使用 Postman 或 cURL 调用 Elasticsearch 上的 REST 服务来加载数据。请参阅以下屏幕截图以查看 Postman 扩展中的输出。在 Elasticsearch 中,数据库的等价物是索引,我们可以命名我们的索引为product。Elasticsearch 还有一个类型的概念,但稍后再说:

  1. 通过在 Postman、浏览器或 cURL 中运行简单的*查询来检查数据是否已加载:
http://localhost:9200/product/_search?q=*&pretty

因此,您应该得到添加的四个产品。

创建产品搜索服务

到目前为止,我们已经完成了两个数据库,现在你一定对这个流程很熟悉了。这与我们为 HSQLDB 和 MongoDB 所做的并没有太大的不同。复制 Mongo 项目以创建productsearch服务,并像以前一样对 Maven POM、实体、存储库类和应用程序属性进行更改:

  1. 在 Maven POM 中,spring-boot-starter-data-elasticsearch取代了之前两个服务示例中的spring-boot-starter-data-mongodbspring-boot-starter-data-jpa

  2. Product实体中,@Document现在表示一个 Elasticsearch 文档。它应该有一个定义相同的indextype,因为我们用来加载测试数据,如Product.java文件所示:

package com.mycompany.product.entity ; 

import org.springframework.data.annotation.Id; 
import org.springframework.data.elasticsearch.annotations.Document; 

@Document(indexName = "product", type = "external" ) 
public class Product  { 

   @Id 
   private String id ;      
   private String name ;    
   private int catId ;           //Remaining class is same as before 
  1. ProductRepository现在扩展了ElasticsearchRepository,如ProductRepository.java文件所示:
package com.mycompany.product.dao; 

import java.util.List; 
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; 
import com.mycompany.product.entity.Product; 

public interface ProductRepository extends ElasticsearchRepository<Product, String> { 

   List<Product> findByCatId(int catId); 
} 
  1. application.properties中进行更改,指示elasticsearch的服务器模型(与嵌入式模型相对,就像我们为 HSQLDB 所做的那样):
server.port=8086 
eureka.instance.leaseRenewalIntervalInSeconds=5 

spring.data.elasticsearch.repositories.enabled=true 
spring.data.elasticsearch.cluster-name=elasticsearch 
spring.data.elasticsearch.cluster-nodes=localhost:9300 

现在,启动 Eureka 服务器,然后启动productsearch服务,并按照以下顺序在浏览器中发出以下查询:

  1. http://localhost:8085/products?id=1

  2. http://localhost:8085/products?id=2

你将得到与之前相同的 JSON。这是微服务的内部实现变化,从第二章中的硬编码实现到 HSQLDB、MongoDB,现在是 Elasticsearch。

由于 Spring Data 框架,访问驱动程序并与其通信的代码已经被大大抽象化,所以我们只需要添加以下内容:

  1. Maven POM 文件中的依赖项。

  2. 在存储库的情况下扩展的基类。

  3. 用于实体的注解。

  4. 在应用程序属性中配置的属性。

数据更新服务

到目前为止,我们已经看过了获取数据。让我们看一些数据修改操作,比如创建、更新和删除(CRUD 操作)。

鉴于 REST 在基于云的 API 操作中的流行度,我们将通过 REST 方法进行数据操作。

让我们选择在本章之前使用 Hazelcast 的 HSQLDB 示例。

REST 惯例

GET 方法是一个不用大脑思考的选择,但是对于创建、插入和删除等操作的方法的选择需要一些考虑。我们将遵循行业指南的惯例:

URL HTTP 操作 服务方法 描述
/product/{id} GET getProduct 获取给定 ID 的产品
/product POST insertProduct 插入产品并返回一个新的 ID
/product/{id} PUT updateProduct 使用请求体中的数据更新给定 ID 的产品
/product/{id} DELETE deleteProduct 删除提供的 ID 的产品

让我们看看ProductService类中的实现。我们已经在本章前面有了getProduct的实现。让我们添加其他方法。

插入产品

暂且不考虑验证(我们一会儿会讨论),插入看起来非常简单,实现 REST 接口。

我们将POST操作映射到insertProduct方法,在实现中,我们只需在已经定义的存储库上调用save

@RequestMapping(value="/product", method = RequestMethod.POST) 
ResponseEntity<Product> insertProduct(@RequestBody Product product) { 

   Product savedProduct = prodRepo.save(product) ; 
   return new ResponseEntity<Product>(savedProduct, HttpStatus.OK);         
}  

注意一下我们之前编码的getProduct方法有一些不同之处:

  • 我们在@RequestMapping中添加了一个POST方法,这样当使用 HTTP POST时,URL 将映射到insertProduct方法。

  • 我们从@RequestBody注解中捕获product的详细信息。这在插入新产品时应该提供。Spring 会为我们将 JSON(或 XML)映射到Product类。

  • 我们返回一个ResponseEntity而不是像getProduct方法中那样只返回一个Product对象。这使我们能够自定义 HTTP 响应和标头,在 REST 架构中这很重要。对于成功的插入,我们返回一个 HTTP OK200)响应,告诉客户端他添加产品的请求成功了。

测试

测试我们的insertProduct方法的步骤如下:

  1. 启动 Eureka 服务器,然后启动product服务(假设它在8082上监听)。

  2. 请注意,现在浏览器不够用了,因为我们想要指示 HTTP 方法并提供响应主体。改用 Postman 或 cURL。

  3. 将内容类型设置为 application/json,因为我们将以 JSON 格式提交新的产品信息。

  4. 以 JSON 格式提供产品信息,例如{"name":"Grapes","catId":1}。注意我们没有提供产品 ID:

  1. 点击发送。你会得到一个包含产品 JSON 的响应。这次,ID 将被填充。这是存储库生成的 ID(它又从底层数据库中获取)。

更新产品

在这里,我们将使用PUT方法,指示 URL 模式中要更新的产品的 ID。与POST方法一样,要更新的产品的详细信息在@RequestBody注解中提供:

@RequestMapping(value="/product/{id}", method = RequestMethod.PUT) 
ResponseEntity<Product> updateProduct(@PathVariable("id") int id, @RequestBody Product product) { 

   // First fetch an existing product and then modify it.  
   Product existingProduct = prodRepo.findOne(id);  

   // Now update it back  
   existingProduct.setCatId(product.getCatId()); 
   existingProduct.setName(product.getName()); 
   Product savedProduct = prodRepo.save(existingProduct) ; 

   // Return the updated product with status ok  
   return new ResponseEntity<Product>(savedProduct, HttpStatus.OK);         
} 

实现包括:

  1. 从存储库中检索现有产品。

  2. 根据业务逻辑对其进行更改。

  3. 将其保存回存储库。

  4. 返回更新后的产品(供客户端验证),状态仍然是OK

如果你没有注意到,最后两个步骤与插入情况完全相同。只是检索和更新产品是新步骤。

测试

测试我们的insertProduct方法的步骤如下:

  1. 与插入产品一样,再次启动 Eureka 和ProductService

  2. 让我们将第一个产品的产品描述更改为Fuji Apples。所以,我们的 JSON 看起来像{"id":1,"name":"Fuji Apples","catId":1}

  3. 准备 Postman 提交PUT请求如下:

  1. 点击发送。你会得到一个包含 JSON {"id":1,"name":"Fuji Apples","catId":1}的响应 200 OK。

  2. 发送一个GET请求http://localhost:8082/product/1来检查变化。你会发现apples变成了Fuji Apples

删除产品

删除产品的映射和实现如下:

@RequestMapping(value="/product/{id}", method = RequestMethod.DELETE) 
ResponseEntity<Product> deleteProduct(@PathVariable("id") int id) {         
   prodRepo.delete(id); 
   return new ResponseEntity<Product>(HttpStatus.OK);           
} 

我们在存储库上调用delete操作,并假设一切正常向客户端返回OK

测试

为了测试,在 Postman 上对产品 ID 1 发送一个DELETE请求:

你会得到一个 200 OK 的响应。要检查它是否真的被删除,尝试对同一产品进行GET请求。你会得到一个空的响应。

缓存失效

如果进行填充缓存的获取操作,那么当进行PUT/POST/DELETE操作更新数据时,缓存要么更新,要么失效。

如果你还记得,我们有一个缓存,保存着与类别 ID 对应的产品。当我们使用为插入、更新和删除创建的 API 添加和移除产品时,缓存需要刷新。我们首选检查是否可以更新缓存条目。然而,拉取与缓存对应的类别的业务逻辑存在于数据库中(通过WHERE子句)。因此,最好在产品更新时使包含关系的缓存失效。

缓存使用情况的一般假设是读取远远高于插入和更新。

为了启用缓存驱逐,我们必须在ProductRepository类中添加方法并提供注释。因此,我们在接口中除了现有的findByCatId方法之外添加了两个新方法,并标记驱逐为 false:

public interface ProductRepository extends CrudRepository<Product, Integer> { 

   @Cacheable("productsByCategoryCache") 
   List<Product> findByCatId(int catId); 

   @CacheEvict(cacheNames="productsByCategoryCache", allEntries=true) 
   Product save(Product product); 

   @CacheEvict(cacheNames="productsByCategoryCache", allEntries=true) 
   void delete(Product product); 
} 

尽管前面的代码是有效的解决方案,但并不高效。它会清除整个缓存。我们的缓存可能有数百个类别,清除与插入、更新或删除产品无关的类别是不正确的。

我们可以更加智能地只清除与正在操作的类别相关的条目:

@CacheEvict(cacheNames="productsByCategoryCache", key = "#result?.catId") 
Product save(Product product); 

@CacheEvict(cacheNames="productsByCategoryCache", key = "#p0.catId") 
void delete(Product product); 

由于Spring 表达式语言SpEL)和CacheEvict的文档,代码有点晦涩:

  1. key表示我们要清除的缓存条目。

  2. #result表示返回结果。我们从中提取catId并用它来清除数据。

  3. #p0表示调用方法中的第一个参数。这是我们想要使用类别并删除对象的product对象。

为了测试缓存清除是否正常工作,启动服务和 Eureka,发送以下请求,并观察结果:

请求 结果
http://localhost:8082/products?id=1 获取与类别1对应的产品并将其缓存。SQL 将显示在输出日志中。
http://localhost:8082/products?id=1 从缓存中获取产品。SQL 中没有更新条目。
POSThttp://localhost:8082/product添加{"name":"Mango","catId":1}作为application/json 将新的芒果产品添加到数据库。
http://localhost:8082/products?id=1 反映了新添加的芒果。SQL 表明数据已刷新。

验证和错误消息

到目前为止,我们一直在非常安全的领域中前行,假设一切都是顺利的。但并不是一切都会一直正确。有许多情景,例如:

  1. GETPUTDELETE请求的产品不存在。

  2. PUTPOST缺少关键信息,例如,没有产品名称或类别。

  3. 业务验证,例如产品,应属于已知类别,名称应超过 10 个字符。

  4. 提交的数据格式不正确,例如类别 ID 的字母数字混合,而预期只有整数。

而且这些还不是详尽无遗的。因此,当出现问题时,进行验证并返回适当的错误代码和消息是非常重要的。

格式验证

如果请求的请求体格式有错误(例如,无效的 JSON),那么 Spring 在到达方法之前就会抛出错误。

例如,对于POST请求到http://localhost:8082/product,如果提交的主体缺少逗号,例如{"id":1 "name":"Fuji Apples" "catId":1},那么返回的错误代码是400。这表示这是一个格式不正确的请求:

{ 
  "timestamp": 1483701698917, 
  "status": 400, 
  "error": "Bad Request", 
  "exception": "org.springframework.http.converter.HttpMessageNotReadableException", 
  "message": "Could not read document: Unexpected character ('"' (code 34)): was expecting comma to separate Object entriesn at ... 

同样,例如在 ID 中使用字母而不是数字,将会很早地被捕获。例如,http://localhost:8082/product/A将导致无法转换值错误:

数据验证

一些错误可以在实体级别捕获,如果它们是不允许的。例如,当我们已经将Product实体注释为以下内容时,没有提供产品描述:

@Column(nullable = false) 
private String name ; 

这将导致错误消息,尝试在请求中保存没有名称的产品,例如{"id":1, "catId":1}

服务器返回500内部服务器错误,并给出详细消息如下:

could not execute statement; SQL [n/a]; constraint [null]; nested exception is org.hibernate.exception.ConstraintViolationException: 

这不是一个很清晰的消息返回给客户。因此,最好在前期捕获验证并向客户返回400错误代码。

业务验证

这通常会在代码中完成,因为它是特定于正在解决的功能或业务用例的。例如,在更新或删除产品之前检查产品。这是一个简单的基于代码的验证,如下所示:

@RequestMapping(value="/product/{id}", method = RequestMethod.DELETE) 
ResponseEntity<Product> deleteProduct(@PathVariable("id") int id) { 

   // First fetch an existing product and then delete it.  
   Product existingProduct = prodRepo.findOne(id);  
   if (existingProduct == null) { 
         return new ResponseEntity<Product>(HttpStatus.NOT_FOUND); 
   } 

   // Return the inserted product with status ok 
   prodRepo.delete(existingProduct); 
   return new ResponseEntity<Product>(HttpStatus.OK);           
} 

异常和错误消息

在出现错误的情况下,最简单的开始是指示一个错误消息,告诉我们出了什么问题,特别是在出现错误的输入请求或业务验证的情况下,因为客户端(或请求者)可能不知道出了什么问题。例如,在前面的情况下,返回NOT_FOUND状态码,但没有提供其他细节。

Spring 提供了有趣的注释,如ExceptionHandlerControllerAdvice来处理这个错误。让我们看看这是如何工作的。

其次,服务方法之前直接通过发送 HTTP 代码来操作ResponseEntity。我们将其恢复为返回业务对象,如Product,而不是ResponseEntity,使其更像 POJO。将之前讨论的deleteProduct代码恢复如下:

@RequestMapping(value="/product/{id}", method = RequestMethod.DELETE) 
Product deleteProduct(@PathVariable("id") int id) { 

   // First fetch an existing product and then delete it.  
   Product existingProduct = prodRepo.findOne(id);  
   if (existingProduct == null) { 
     String errMsg = "Product Not found with code " + id ;            
     throw new BadRequestException(BadRequestException.ID_NOT_FOUND, errMsg); 
   }      
   // Return the deleted product  
   prodRepo.delete(existingProduct); 
   return existingProduct ;             
} 

在上述代码中:

  1. 我们返回Product而不是ResponseEntity,因为处理错误代码和响应将在外部完成。

  2. 抛出异常(运行时异常或其扩展版本),告诉我们请求出了什么问题。

  3. Product方法的范围到此结束。

BadRequestException类是一个简单的类,提供了一个 ID,并继承自RuntimeException类。

public class BadRequestException extends RuntimeException { 

   public static final int ID_NOT_FOUND = 1 ;       
   private static final long serialVersionUID = 1L; 

   int errCode ; 

   public BadRequestException(int errCode, String msg) { 
         super(msg); 
         this.errCode = errCode ; 
   } 
} 

当您现在执行服务时,不仅会得到404 Not Found状态,还会得到一个明确指出出了什么问题的消息。查看发送的请求和收到的异常的截图:

然而,发送500并在日志中得到异常堆栈并不干净。500表明错误处理不够健壮,堆栈跟踪被抛出。

因此,我们应该捕获和处理这个错误。Spring 提供了@ExceptionHandler,可以在服务中使用。在方法上使用这个注解,Spring 就会调用这个方法来处理错误:

@ExceptionHandler(BadRequestException.class) 
void handleBadRequests(BadRequestException bre, HttpServletResponse response) throws IOException { 

   int respCode = (bre.errCode == BadRequestException.ID_NOT_FOUND) ? 
         HttpStatus.NOT_FOUND.value() : HttpStatus.BAD_REQUEST.value() ; 

   response.sendError(respCode, bre.errCode + ":" + bre.getMessage()); 
} 

当我们现在执行服务,并调用一个不可用的产品 ID 的DELETE方法时,错误代码变得更加具体和清晰:

现在,再进一步,如果我们希望所有的服务都遵循这种提出BadRequestException并返回正确的错误代码的模式呢?Spring 提供了一种称为ControllerAdvice的机制,当在一个类中使用时,该类中的异常处理程序可以普遍应用于范围内的所有服务。

创建一个新的类如下,并将其放在异常包中:

@ControllerAdvice 
public class GlobalControllerExceptionHandler { 

   @ExceptionHandler(BadRequestException.class) 
   void handleBadRequests(BadRequestException bre, HttpServletResponse response) throws IOException { 

         ... Same code as earlier ...  
   } 
} 

这允许异常以一致的方式在服务之间处理。

CQRS 的数据更新

如前一章讨论的,并且我们在前一节中看到的,CQRS 模式为处理命令和查询提供了高效和合适的数据模型。回顾一下,我们在 MongoDB 中有一个灵活的文档模型来处理具有事务保证的命令模式。我们在 Elasticsearch 中有一个灵活的查询模型来处理复杂的搜索条件。

尽管这种模式由于合适的查询模型而允许更容易的查询,但挑战在于跨各种模型更新数据。在前一章中,我们讨论了多种机制来保持信息在模型之间的更新,如分布式事务,使用发布-订阅消息的最终一致模型。

在接下来的章节中,我们将看看使用消息传递和异步更新数据的机制。

异步消息

HTTP/REST 提供了请求响应机制来执行服务。客户端等待(或者说是阻塞),直到处理完成并使用服务结束时提供的结果。因此,处理被称为同步的。

在异步处理中,客户端不等待响应。异步处理可以用于两种情况,如发送和忘记请求/响应

在“发送并忘记”中,客户端向下游服务发送命令或请求,然后不需要响应。它通常用于管道处理架构,其中一个服务对请求进行丰富和处理,然后将其发送到另一个服务,后者发送到第三个服务,依此类推。

在异步请求/响应中,客户端向服务发送请求,但与同步处理不同,它不会等待或阻塞响应。当服务完成处理时,必须通知客户端,以便客户端可以使用响应。

在 CQRS 中,我们使用消息传递将更新事件发送到各种服务,以便更新读取或查询模型。

首先,我们将在本章中使用 ActiveMQ 作为可靠的消息传递机制,然后在接下来的章节中查看 Kafka 作为可扩展的分布式消息传递系统。

启动 ActiveMQ

设置 ActiveMQ 的步骤如下:

  1. 从 Apache 网站(activemq.apache.org/)下载 ActiveMQ。

  2. 将其解压到一个文件夹中。

  3. 导航到bin文件夹。

  4. 运行activemq start命令。

打开控制台查看消息并管理 ActiveMQ,网址为http://localhost:8161/admin,使用admin/admin登录。您应该看到以下 UI 界面:

创建一个主题

点击“主题”链接,创建一个名为ProductT的主题。您可以按照您习惯的命名约定进行操作。此主题将获取产品的所有更新。这些更新可以用于各种下游处理目的,例如保持本地数据模型的最新状态。创建主题后,它将出现在管理控制台的主题列表中,如下所示。另外两个主题是 ActiveMQ 自己的主题,我们将不予理睬:

黄金源更新

当 CQRS 中有多个模型时,我们遵循之前讨论的黄金源模式:

  1. 一个模型(命令模型)被认为是黄金源。

  2. 在更新到黄金源之前进行所有验证。

  3. 对黄金源的更新发生在一个事务中,以避免任何不一致的更新和失败状态。因此,更新操作是自动的。

  4. 更新完成后,将在一个主题上放置广播消息。

  5. 如果在将消息放在主题上时出现错误,则事务将被回滚,并向客户端发送错误。

我们使用 MongoDB 和 Elasticsearch 进行了 CQRS 实现。在我们的情况下,MongoDB 是产品数据的黄金源(也是命令模型)。Elasticsearch 是包含从搜索角度组织的数据的查询模型。

首先让我们来看看更新命令模型或黄金源。

服务方法

我们在 HSQLDB 实现中做了三种方法:插入、更新和删除。将相同的方法复制到基于 MongoDB 的项目中,以便该项目中的服务类与 HSQLDB 项目中的完全相同。

此外,复制在 HSQLDB 项目中完成的异常类和ControllerAdvice。您的包结构应该与 HSQLDB 项目完全相同,如下所示:

在这个项目中的不同之处在于 ID 是一个字符串,因为这样可以更好地在 MongoDB 中进行 ID 创建的本地处理。因此,方法签名将是字符串 ID,而不是我们 HSQLDB 项目中的整数。

显示更新 MongoDB 的PUT操作如下:

@RequestMapping(value="/product/{id}", method = RequestMethod.PUT) 
Product updateProduct(@PathVariable("id") String id, @RequestBody Product product) { 

   // First fetch an existing product and then modify it.  
   Product existingProduct = prodRepo.findOne(id);  
   if (existingProduct == null) { 
         String errMsg = "Product Not found with code " + id ; 
         throw new BadRequestException(BadRequestException.ID_NOT_FOUND, errMsg); 
   } 

   // Now update it back  
   existingProduct.setCatId(product.getCatId()); 
   existingProduct.setName(product.getName()); 
   Product savedProduct = prodRepo.save(existingProduct) ; 

   // Return the updated product   
   return savedProduct ;          
} 

测试获取、插入、更新和删除操作是否按预期运行。

在数据更新时引发事件

当插入、删除或更新操作发生时,黄金源系统广播更改是很重要的,这样许多下游操作就可以发生。这包括:

  1. 依赖系统的缓存清除。

  2. 系统中本地数据模型的更新。

  3. 进一步进行业务处理,例如在添加新产品时向感兴趣的客户发送电子邮件。

使用 Spring JMSTemplate 发送消息

使用 JMSTemplate 的步骤如下:

  1. 在我们的 POM 文件中包含 Spring ActiveMQ 的启动器:
        <dependency> 
            <groupId>org.springframework.boot</groupId> 
            <artifactId>spring-boot-starter-activemq</artifactId> 
        </dependency>
  1. 我们必须为我们的 Spring 应用程序启用 JMS 支持。因此,请在ProductSpringApp.java文件中包括注解,并提供消息转换器。消息转换器将帮助将对象转换为 JSON,反之亦然:
@SpringBootApplication 
@EnableDiscoveryClient 
@EnableJms 
public class ProductSpringApp {
  1. 创建一个封装Product和操作的实体,这样无论谁收到产品消息,都会知道执行的操作是删除还是插入/更新,通过在ProductUpdMsg.java文件中添加实体,如下所示:
public class ProductUpdMsg { 

   Product product ; 
   boolean isDelete = false ; 
// Constructor, getters and setters 

如果有更多操作,请随时根据您的用例将isDelete标志更改为字符串操作标志。

  1. application.properties文件中配置 JMS 属性。pub-sub-domain表示应使用主题而不是队列。请注意,默认情况下,消息是持久的:
spring.activemq.broker-url=tcp://localhost:61616 
jms.ProductTopic=ProductT 
spring.jms.pub-sub-domain=true 
  1. 创建一个消息生产者组件,它将负责发送消息:
  • 这是基于 Spring 的JmsMessagingTemplate

  • 使用JacksonJmsMessageConverter将对象转换为消息结构

ProductMsgProducer.java文件如下:

@Component 
public class ProductMsgProducer { 

   @Autowired  
   JmsTemplate prodUpdtemplate ; 

   @Value("${jms.ProductTopic}") 
   private String productTopic ; 

@Bean 
   public MessageConverter jacksonJmsMessageConverter() { 
         MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter(); 
         converter.setTargetType(MessageType.TEXT); 
         converter.setTypeIdPropertyName("_type"); 
         return converter; 

   public void sendUpdate(Product product, boolean isDelete) { 
         ProductUpdMsg msg = new ProductUpdMsg(product, isDelete);          
         prodUpdtemplate.convertAndSend(productTopic, msg);  
   }      
} 
  1. 最后,在您的服务中,声明producer,并在完成插入、更新和删除操作之后调用它,然后返回响应。DELETE方法如下所示,其中标志isDelete为 true。其他方法的标志将为 false。ProductService.java文件如下:
@Autowired 
ProductMsgProducer producer ; 

@RequestMapping(value="/product/{id}", method = RequestMethod.DELETE) 
Product deleteProduct(@PathVariable("id") String id) { 

   // First fetch an existing product and then delete it.  
   Product existingProduct = prodRepo.findOne(id);  
   if (existingProduct == null) { 
         String errMsg = "Product Not found with code " + id ;              
         throw new BadRequestException(BadRequestException.ID_NOT_FOUND, errMsg); 
   } 

   // Return the deleted product  
   prodRepo.delete(existingProduct); 
   producer.sendUpdate(existingProduct, true); 
   return existingProduct ;             
} 

这将在主题上发送消息,您可以在管理控制台的主题部分看到。

查询模型更新

productsearch项目中,我们将不得不进行更改以更新 Elasticsearch 中的记录。

插入、更新和删除方法

这些方法与我们在 MongoDB 中设计的方法非常不同。以下是区别:

  1. MongoDB 方法有严格的验证。对于 Elasticsearch,不需要验证,因为假定主服务器(命令模型或黄金源)已更新,我们必须将更新应用到查询模型中。

  2. 更新查询模型时的任何错误都必须得到警告,不应被忽视。我们将在后面的章节中看到这一方面。

  3. 我们不分开插入和更新方法。由于我们的ProductRepository类,单个保存方法就足够了。

  4. 此外,这些方法不必暴露为 REST HTTP 服务,因为除了通过消息更新之外,可能不会直接调用它们。我们之所以在这里这样做,只是为了方便。

  5. product-nosql(MongoDB)项目中,我们从ProductService类中调用了我们的ProductMsgProducer类。在productsearch-nosql项目中,情况将完全相反,ProductUpdListener将调用服务方法。

以下是更改:

  1. Maven POM—依赖于 ActiveMQ:
<dependency> 
   <groupId>org.springframework.boot</groupId> 
   <artifactId>spring-boot-starter-activemq</artifactId> 
</dependency> 
  1. 应用程序属性包括主题和连接详细信息:
spring.activemq.broker-url=tcp://localhost:61616 
jms.ProductTopic=ProductT 
spring.jms.pub-sub-domain=true
  1. Product服务包括调用存储库保存和删除方法:
   @PutMapping("/product/{id}") 
   public void insertUpdateProduct(@RequestBody Product product) {          
         prodRepo.save(product) ;                         
   } 

   @DeleteMapping("/product/{id}") 
   public void deleteProduct(@RequestBody Product product) { 
         prodRepo.delete(product); 
   } 

JMS 相关的类和更改如下:

  1. ProductSpringApp中,包括EnableJms注解,就像在 MongoDB 项目中一样。

  2. 创建一个调用服务的ProductUpdListener类:

@Component 
public class ProductUpdListener { 

   @Autowired 
   ProductService prodService ; 

   @JmsListener(destination = "${jms.ProductTopic}", subscription = "productSearchListener") 
   public void receiveMessage(ProductUpdMsg msg) { 

         Product product = msg.getProduct() ; 
         boolean isDelete = msg.isDelete() ; 
         if (isDelete) { 
               prodService.deleteProduct(product); 
               System.out.println("deleted " + product.getId()); 
         } else { 
               prodService.insertUpdateProduct(product);        
               System.out.println("upserted " + product.getId()); 
         } 
   } 

   @Bean // Serialize message content to json using TextMessage 
   public MessageConverter jacksonJmsMessageConverter() { 
         MappingJackson2MessageConverter converter = new  
         MappingJackson2MessageConverter(); 
         converter.setTargetType(MessageType.BYTES); 
         converter.setTypeIdPropertyName("_type"); 
         return converter; 
   } 
}  

测试 CQRS 更新场景端到端

为了测试我们的场景,请执行以下步骤:

  1. 在本地计算机上启动三个服务器进程,例如 Elasticsearch、MongoDB 和 ActiveMQ,如前面所讨论的。

  2. 启动 Eureka 服务器。

  3. 启动两个应用程序,一个连接到 MongoDB(黄金源,命令模型),监听8085,另一个连接到 Elasticsearch(查询模型),监听8086

  4. 在 Elasticsearch 上测试GET请求—http://localhost:8086/products?id=1,并注意 ID 和描述。

  5. 现在,通过在 Postman 上发出以下命令来更改黄金源上的产品描述,假设服务正在端口8085上监听:

  1. 再次在 Elasticsearch 上测试GET请求——http://localhost:8086/products?id=1。您会发现 Elasticsearch 中的产品描述已更新。

摘要

在本章中,我们涵盖了许多核心概念,从添加常规关系数据库来支持我们的 GET 请求开始。我们通过本地缓存和分布式缓存 Hazelcast 增强了其性能。我们还研究了 CQRS 模式,用 MongoDB 替换了我们的关系数据库,以实现灵活的模式和 Elasticsearch 的灵活搜索和查询功能。

我们为我们的product服务添加了插入、更新和删除操作,并确保在关系项目的情况下进行必要的缓存失效。我们为我们的 API 添加了输入验证和适当的错误消息。我们涵盖了事件处理,以确保查询模型与命令模型保持最新。这是通过命令模型服务发送更改的广播,以及查询模型服务监听更改并更新其数据模型来实现的。

接下来,我们将看看如何使这些项目足够健壮,以在运行时环境中运行。

第六章:测试云原生应用程序

在本章中,我们将深入探讨测试云原生应用程序。测试从手动测试发展到使用各种测试工具、策略和模式进行自动化测试。这种方法的好处是可以频繁地进行测试,以确保云开发的重要性。

在本章中,我们将涵盖以下主题:

  • 测试概念,如行为驱动开发(BDD)测试驱动开发(TDD)

  • 测试模式,如 A/B 测试和测试替身

  • 测试工具,如 JUnit,Cucumber,JaCoCo 和 Spring Test

  • 测试类型,如单元测试、集成测试、性能测试和压力测试

  • 将 BDD 和集成测试的概念应用到我们在第二章中开发的产品服务,并在第四章中进行了增强,扩展您的云原生应用程序

在开发之前编写测试用例

在本书中,我们在第二章中开始使用 Spring Boot 开发一个简单的服务,编写您的第一个云原生应用程序,以激发您对云开发的兴趣。然而,真正的开发遵循不同的最佳实践风格。

TDD

项目始于理解需求并编写验证需求的测试用例。由于此时代码不存在,测试用例将失败。然后编写通过测试用例的代码。这个过程迭代直到测试用例和所需的代码完成以实现业务功能。Kent Beck 在这个主题上有一本优秀的书,通过示例进行测试驱动开发。在下一节中,我们将使用本章的原则重新进行第四章中的产品服务。但在此之前,让我们看看另一个重要概念,BDD。

BDD

借鉴敏捷开发原则和用户故事,BDD 鼓励我们将开发看作一系列场景,在这些场景中,给定某些条件,系统对设置的刺激以一种特定、可预测的方式做出反应。如果这些场景、条件和行为可以用业务和 IT 团队之间易于理解的共同语言来表达,这将为开发带来很多清晰度,并减少犯错的机会。这是一种编写易于测试的规范的方法。

在本章中,我们将采用 Cucumber 工具对我们的产品服务应用 BDD。

测试模式

为云端测试大型互联网应用程序需要一个有纪律的方法,其中一些模式非常有用。

A/B 测试

A/B 测试的最初目的,也称为分割测试,是为了通过实验找出少数选定用户对具有相同功能的两个不同网页的用户响应。如果用户对某种模式的响应比其他模式更好,那么就选择该模式。

这个概念可以扩展到分阶段引入新功能。功能、活动、布局或新服务被引入到一组受控的用户中,并且对其响应进行测量:

测试窗口结束后,结果被汇总以规划更新功能的有效性。

这种测试的策略是对于选定的用户组,使用 HTTP 302(临时重定向)将用户从常规网站切换到新设计的网站。这将需要在测试期间运行网站或功能服务的变体。一旦测试成功,该功能将逐渐扩展到更多用户,并合并到主网站/代码库中。

测试替身

通常,受测试的功能依赖于由其他团队独立开发的组件和 API,这具有以下缺点:

  • 它们可能在开发功能时无法进行测试

  • 它们可能并不总是可用,并且需要设置所需的数据来测试各种情况

  • 每次使用实际组件可能会更慢

因此,测试替身的概念变得流行。测试替身(就像电影中的替身演员)是一个替换实际组件并模仿其行为的组件/ API。测试替身组件通常是一个轻量级且易于更改的组件,由构建功能的团队控制,而不像可能是依赖项或外部进程的真实组件。

有许多类型的测试替身,例如虚拟、伪装、测试桩和模拟。

测试桩

当下游组件返回改变系统行为的响应时,测试桩非常有用;例如,如果我们的产品服务要调用一个决定产品服务行为的参考数据服务。参考数据服务的测试桩可以模仿导致产品服务行为改变的各种响应类型:

模拟对象

下一个测试替身类型是模拟对象,它记录系统如何与其行为,并将记录呈现以进行验证。例如,模拟数据库组件可以检查是否应该从缓存层而不是数据库中调用产品。

以下是关于模拟的生态系统的基本图表表示:

模拟 API

在云开发中,您将构建一个依赖于其他服务或主要通过这些服务访问的 API 的服务。通常,其他服务将无法立即进行测试。但您不能停止开发。这就是模拟或添加虚拟服务的有用模式来测试您的服务的地方。

服务模拟模拟了真实服务的所有合同和行为。一些示例,如WireMock.orgMockable.io,帮助我们模拟 API 并测试主要情况、边缘情况和故障情况。

确保代码审查和覆盖率

通过自动代码审查工具来增强对代码的手动审查。这有助于识别代码中可能的错误,并确保覆盖完整并测试所有路径。

我们稍后将看一下代码覆盖工具 JaCoCo。

测试类型

我们稍后在本章中讨论的各种测试类型在云计算变得流行之前就已经被了解。使用持续集成CI)和持续开发CD)的敏捷开发原则使得自动化这些测试类型变得重要,以便它们在每次代码检入和构建发生时执行。

单元测试

单元测试的目的是测试每个类或代码组件,并确保其按预期执行。JUnit 是流行的 Java 单元测试框架。

使用模拟对象模式和测试桩,可以隔离正在测试的服务的依赖组件,以便测试集中在系统正在测试的系统上。

JUnit 是执行单元测试的最流行的工具。

集成测试

组件测试的目的是检查组件(如产品服务)是否按预期执行。

诸如spring-boot-test之类的组件有助于运行测试套件并对整个组件进行测试。我们将在本章中看到这一点。

负载测试

负载测试涉及向系统发送大量并发请求一段时间,并观察其影响,如系统的响应时间和错误率。如果添加更多服务实例使系统能够处理额外的负载,则称系统具有水平可扩展性。

JMeter 和 Gatling 是流行的工具,用于覆盖这个维度。

回归测试

在引入新功能时,现有功能不应该中断。回归测试可以覆盖这一点。

Selenium 是一个基于 Web 浏览器的开源工具,在这个领域很受欢迎,用于执行回归测试。

测试产品服务

让我们将我们学到的测试原则应用于迄今为止构建的产品服务。我们从用户的角度开始,因此从验收测试开始。

通过 Cucumber 进行 BDD

第一步是回顾我们产品服务的规范。在第四章中,扩展您的云原生应用,我们构建了一些关于产品服务的功能,允许我们获取、添加、修改和删除产品,并在给定产品类别的情况下获取产品 ID 列表。

让我们在 Cucumber 中表示这些特性。

为什么选择 Cucumber?

Cucumber 允许用一种类似于普通英语的语言Gherkin表达行为。这使得领域驱动设计术语中的通用语言成为可能,从而使业务、开发和测试之间的沟通变得无缝和易于理解。

Cucumber 是如何工作的?

让我们了解一下 Cucumber 是如何工作的:

  1. Cucumber 的第一步是将用户故事表达为具有场景和Given-When-Then条件的特性:
  • 给定:为行为设置前提条件

  • 当:触发改变系统状态的操作,例如向服务发出请求

  • 然后:服务应该如何响应

  1. 这些被翻译为自动化测试用例,使用cucumber-spring翻译层,以便可以执行。

让我们从一个简单的getProduct验收测试用例开始。我们将用 Gherkin 编写一个简单的特性,如果产品 ID 存在,则获取产品,如果找不到产品 ID,则返回错误。

让我们以真正的 BDD 风格实现以下功能。产品服务上的“获取”API 返回产品细节,例如描述和类别 ID,给定产品 ID。如果找不到产品,它也可以返回错误,例如 404。让我们将这两种行为表示为我们的 Gherkin 特性文件上的两个独立场景。

特性:“获取产品”

获取产品 ID 的产品细节。

场景 1:产品 ID 有效且存在。将返回产品名称和所属类别:

  1. 给定产品服务正在运行

  2. 当使用现有产品 ID 1 调用获取产品服务时

  3. 那么我们应该得到一个带有 HTTP 状态码 200 的响应

  4. 并返回产品细节,名称为“苹果”,类别为1

场景 2:产品 ID 无效或不存在。应返回错误:

  1. 给定产品服务正在运行

  2. 当使用不存在的产品 ID 456 调用获取产品服务时

  3. 然后返回 404 未找到状态

  4. 并返回错误消息“ID 456 没有产品”

场景 1 是一个成功的场景,其中返回并验证了数据库中存在的产品 ID。

场景 2 检查数据库中不存在的 ID 的失败情况。

每个场景分为多个部分。对于正常路径场景:

  • 给定设置了一个前提条件。在我们的情况下,这很简单:产品服务应该正在运行。

  • 当改变系统的状态时,在我们的情况下,是通过提供产品 ID 向服务发出请求。

  • 然后和并是系统预期的结果。在这种情况下,我们期望服务返回 200 成功代码,并为给定产品返回有效的描述和类别代码。

正如您可能已经注意到的,这是我们的服务的文档,可以被业务和测试团队以及开发人员理解。它是技术无关的;也就是说,如果通过 Spring Boot、Ruby 或.NET 微服务进行实现,它不会改变。

在下一节中,我们将映射到我们开发的 Spring Boot 应用程序的服务。

使用 JaCoCo 进行代码覆盖

JaCoCo 是由 EclEmma 团队开发的代码覆盖库。JaCoCo 在 JVM 中嵌入代理,扫描遍历的代码路径并创建报告。

此报告可以导入更广泛的 DevOps 代码质量工具,如 SonarQube。SonarQube 是一个平台,帮助管理代码质量,具有众多插件,并与 DevOps 流程很好地集成(我们将在后面的章节中看到)。它是开源的,但也有商业版本。它是一个平台,因为它有多个组件,如服务器(计算引擎服务器、Web 服务器和 Elasticsearch)、数据库和特定于语言的扫描器。

Spring Boot 测试

Spring Boot 测试扩展并简化了 Spring 框架提供的 Spring-test 模块。让我们看一下编写我们的验收测试的基本要素,然后我们可以在本章后面重新讨论细节:

  1. 将我们在第四章中创建的项目,使用 HSQLDB 和 Hazelcast 扩展您的云原生应用,复制为本章的新项目。

  2. 在 Maven POM 文件中包含 Spring 的依赖项:

        <dependency> 
            <groupId>org.springframework.boot</groupId> 
            <artifactId>spring-boot-starter-test</artifactId> 
            <scope>test</scope> 
        </dependency> 

正如您可能已经注意到的,scope已更改为test。这意味着我们定义的依赖项不是正常运行时所需的,只是用于编译和测试执行。

  1. 向 Maven 添加另外两个依赖项。我们正在下载 Cucumber 及其 Java 翻译的库,以及spring-boot-starter-test
        <dependency> 
            <groupId>info.cukes</groupId> 
            <artifactId>cucumber-spring</artifactId> 
            <version>1.2.5</version> 
            <scope>test</scope> 
        </dependency> 
        <dependency> 
            <groupId>info.cukes</groupId> 
            <artifactId>cucumber-junit</artifactId> 
            <version>1.2.5</version> 
            <scope>test</scope> 
        </dependency> 

CucumberTest类是启动 Cucumber 测试的主类:

@RunWith(Cucumber.class) 
@CucumberOptions(features = "src/test/resources") 
public class CucumberTest { 

} 

RunWith告诉 JUnit 使用 Spring 的测试支持,然后使用 Cucumber。我们给出我们的.feature文件的路径,其中包含了前面讨论的 Gherkin 中的测试用例。

Productservice.feature文件是以 Gherkin 语言编写的包含场景的文本文件,如前所述。我们将在这里展示两个测试用例。该文件位于src/test/resources文件夹中。

CucumberTestSteps类包含了从 Gherkin 步骤到等效 Java 代码的翻译。每个步骤对应一个方法,方法根据 Gherkin 文件中的场景构造而被调用。让我们讨论与一个用例相关的所有步骤:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 
@ContextConfiguration 
public class CucumberTestSteps { 

    @Autowired 
    private TestRestTemplate restTemplate; 

    private ResponseEntity<Product> productResponse; 
    private ResponseEntity<String> errResponse; 

    @Given("(.*) Service is running") 
    public void checkServiceRunning(String serviceName) { 
         ResponseEntity<String> healthResponse = restTemplate.getForEntity("/health",String.class, new HashMap<>()); 
         Assert.assertEquals(HttpStatus.OK, healthResponse.getStatusCode()); 
    } 

    @When("get (.*) service is called with existing product id (\d+)$") 
    public void callService(String serviceName, int prodId) throws Throwable { 
         productResponse = this.restTemplate.getForEntity("/"+serviceName+"/" + prodId, Product.class, new HashMap<>()); 
    } 

    @Then("I should get a response with HTTP status code (.*)") 
    public void shouldGetResponseWithHttpStatusCode(int statusCode) { 
         Assert.assertEquals(statusCode, productResponse.getStatusCodeValue()); 
    } 

    @And("return Product details with name (.*) and category (\d+)$") 
    public void theResponseShouldContainTheMessage(String prodName, int categoryId) { 
         Product product = productResponse.getBody() ; 
         Assert.assertEquals(prodName, product.getName()); 
         Assert.assertEquals(categoryId, product.getCatId());       
    } 

@SpringBootTest注解告诉 Spring Boot 框架这是一个测试类。RANDOM_PORT表示测试服务在随机端口上启动 Tomcat 进行测试。

我们注入一个自动装配的restTemplate,它将帮助访问 HTTP/REST 服务并接收将被测试的响应。

现在,请注意带有注释@Given@When@Then的方法。每个方法使用正则表达式从特性文件中提取变量,并在方法中用于断言。我们已经通过以下方式系统地测试了这一点:

  1. 首先通过访问/health(就像我们在第二章中为 Spring Boot 执行器所做的那样)检查服务是否正在运行。

  2. 使用产品 ID 调用服务。

  3. 检查返回代码是否为200,并且响应的描述和类别是否与预期结果匹配。

  4. 运行测试。

  5. 右键单击CucumberTest.java文件,选择 Run As | JUnit Test:

您将看到控制台启动并显示启动消息。最后,JUnit 将反映测试结果如下:

作为练习,尝试向ProductService类中的插入、更新和删除产品方法添加测试用例。

集成 JaCoCo

让我们将 JaCoCo 集成到我们现有的项目中:

  1. 首先,在 POM 文件中包含包含 JaCoCo 的插件:
<plugin> 
   <groupId>org.jacoco</groupId> 
   <artifactId>jacoco-maven-plugin</artifactId> 
   <version>0.7.9</version> 
</plugin> 

第二步和第三步是将前置执行和后置执行包含到前面的插件中。

  1. 预执行准备代理配置并添加到命令行。

  2. 后置执行确保报告在输出文件夹中创建:

<executions> 
   <execution> 
         <id>pre-unit-test</id> 
         <goals> 
               <goal>prepare-agent</goal> 
         </goals> 
         <configuration> 
               <destFile>${project.build.directory}/coverage-reports/jacoco-ut.exec</destFile> 
               <propertyName>surefireArgLine</propertyName> 
         </configuration> 
   </execution> 
   <execution> 
         <id>post-unit-test</id> 
         <phase>test</phase> 
         <goals> 
               <goal>report</goal> 
         </goals> 
         <configuration> 
               <dataFile>${project.build.directory}/coverage-reports/jacoco-ut.exec</dataFile> 
   <outputDirectory>${project.reporting.outputDirectory}/jacoco-ut</outputDirectory> 
         </configuration> 
   </execution> 
</executions> 
  1. 最后,创建的命令行更改必须插入到maven-surefire-plugin中,如下所示:
<plugin> 
   <groupId>org.apache.maven.plugins</groupId> 
   <artifactId>maven-surefire-plugin</artifactId> 
   <configuration> 
         <!-- Sets the VM argument line used when unit tests are run. --> 
         <argLine>${surefireArgLine}</argLine> 
         <excludes> 
               <exclude>**/IT*.java</exclude> 
         </excludes>        
   </configuration> 
</plugin> 
  1. 现在,我们已经准备好运行覆盖报告了。右键单击项目,选择 Run As | Maven test 来测试程序,如下面的截图所示:

  1. 随着控制台填满 Spring Boot 的启动,您会发现以下行:
2 Scenarios ([32m2 passed[0m) 
8 Steps ([32m8 passed[0m) 
0m0.723s 
Tests run: 10, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 26.552 sec - in com.mycompany.product.CucumberTest......Results :Tests run: 10, Failures: 0, Errors: 0, Skipped: 0[INFO] [INFO] --- jacoco-maven-plugin:0.7.9:report (post-unit-test) @ product ---[INFO] Loading execution data file D:AppswkNeonch5-producttargetcoverage-reportsjacoco-ut.exec[INFO] Analyzed bundle 'product' with 6 classes 
  1. 这告诉我们有两种情况执行了8 步(与之前一样)。但另外,coverage-reports也生成并放置在target目录中:

  1. site文件夹中,点击index.html;您将看到覆盖报告如下:

  1. 在调查product包时,您可以看到ProductService只覆盖了24%,如下面的截图所示:

  1. 原因是我们只覆盖了服务中的getProduct API。insertProductupdateProduct没有被覆盖。这在下面的钻取报告中展示:

  1. getProduct方法上,覆盖率是完整的。这是因为在两种情况下,我们已经覆盖了正常路径以及错误条件:

  1. 另一方面,您会发现我们错过了ExceptionHandler类中的分支覆盖,如下所示:

摘要

在接下来的章节中,我们将把覆盖报告与 DevOps 管道集成,并在 CI 和 CD 期间看到它的工作。但首先,让我们看一下部署机制。

第七章:云原生应用部署

云原生应用最独特的一点是它们的部署方式。在传统的应用部署中,团队通过登录服务器并安装应用程序来部署他们的应用。但在云中通常有许多服务器,登录到每台服务器并手动安装应用程序是不可行的,而且可能非常容易出错。为了解决这些问题,我们使用云配置工具来自动部署云原生应用。

在本章中,我们将深入探讨微服务的部署模型,包括如何将应用程序打包为 Docker 容器、如何设置 CI/CD 流水线以及如何保护您的服务免受分布式拒绝服务(DDoS)等安全攻击。我们将涵盖以下内容:

  • 部署模型、打包和容器化(使用 Docker)

  • 部署模式(蓝绿部署、金丝雀发布和暗部署)

  • DDoS

  • CI/CD

部署模型

我们将涵盖在云环境中部署我们的应用程序所使用的部署模型。

虚拟化

云的基本构建块是虚拟机(从现在开始称为 VM),它相当于用户可以登录并安装或维护应用程序的物理服务器(或主机)。不同之处在于可以在单个主机上托管多个 VM,从而增加资源利用率。这是通过使用虚拟化实现的,其中在主机上安装了一个可以将物理服务器上可用的资源(如计算、内存、存储和网络)分配给托管在其上的不同 VM 的 hypervisor。云原生应用可以使用以下策略部署在这些 VM 上:

  • 每个 VM 上有多个应用程序

  • 每个 VM 上一个应用程序

在每个 VM 上运行多个应用程序时,有可能一个应用程序占用了 VM 上所有可用的资源,使其他应用程序无法运行。另一方面,每个 VM 上只运行一个应用程序可以确保应用程序被隔离,以便它们不会相互影响,但这种部署方式的缺点是资源的浪费,因为每个应用程序可能并不总是消耗所有可用的资源。

PaaS

PaaS 或平台即服务是部署云原生应用的另一个流行选项。PaaS 提供了补充开发、扩展和维护云原生应用的附加服务。通过构建包,自动化构建和部署等服务大大减少了设置额外基础设施来支持这些活动所需的时间。PaaS 还提供了一些基本的基础设施服务,如监控、日志聚合、秘密管理和负载均衡。Cloud Foundry、Google App Engine、Heroku 和 OpenShift 是 PaaS 的一些示例。

容器

为了提供所需的独立运行级别的隔离,并节约资源利用,人们开发了容器技术。通过利用 Linux 内核的特性,容器在进程级别提供了 CPU、内存、存储和网络隔离。下图展示了虚拟化的差异:

容器消除了对客户操作系统的需求,因此大大增加了可以在同一主机上运行的容器数量,与可以在同一主机上运行的虚拟机数量相比。容器的占用空间也更小,大约为 MB 级别,而虚拟机很容易超过几 GB。

容器在 CPU 和内存方面也非常高效,因为它们不必支持运行完整操作系统时必须支持的许多外围系统:

前面的图表显示了云原生应用部署策略的演变,旨在增加资源利用率和应用程序的隔离性。在堆栈的顶部是在主机上运行的 VM 中运行的容器。这允许应用程序按两个程度进行扩展:

  • 增加 VM 中容器的数量

  • 增加运行容器的 VM 数量

Docker

Docker 是一个备受欢迎的容器运行时平台,已经证明自己是部署云原生应用程序的强大平台。Docker 在 Windows、Mac 和 Linux 等所有主要平台上都可用。由于容器需要 Linux 内核,因此在 Linux 环境中更容易运行 Docker 引擎。但是,在 Windows 和 Mac 环境中有多种资源可用于舒适地运行 Docker 容器。我们将演示如何将我们迄今为止开发的服务部署为 Docker 容器,包括连接到在其自己的容器中运行的外部数据库。

在我们的示例中,我们将使用 Docker Toolbox 并使用 Docker Machine 创建一个 VM,在其中将运行 Docker 引擎。我们将使用 Docker 命令行客户端连接到此引擎,并使用提供的各种命令。

构建 Docker 镜像

我们将开始将我们当前的项目作为一组 Docker 容器进行容器化。我们将逐步介绍每个项目的步骤。

Eureka 服务器

  1. $WORKSPACE/eureka-server/.dockerignore中添加一个.dockerignore文件,内容如下:
.* 
target/* 
!target/eureka-server-*.jar 
  1. $WORKSPACE/eureka-server/Dockerfile中添加一个包含以下内容的 Dockerfile:
FROM openjdk:8-jdk-alpine 

RUN mkdir -p /app 

ADD target/eureka-server-0.0.1-SNAPSHOT.jar /app/app.jar 

EXPOSE 8761 

ENTRYPOINT [ "/usr/bin/java", "-jar", "/app/app.jar" ] 
  1. 构建可运行的 JAR,将在目标文件夹中可用:
mvn package 
  1. 构建 Docker 容器:
docker build -t cloudnativejava/eureka-server . 

上一个命令的输出如下截图所示:

  1. 在运行容器之前,我们需要创建一个网络,不同的容器可以在其中自由通信。可以通过运行以下命令来创建这个网络:
docker network create app_nw 

上一个命令的输出如下截图所示:

  1. 使用名称eureka运行容器,并将其附加到之前创建的网络:
docker run -d --network app_nw --name eureka cloudnativejava/eureka-server 

上一个命令的输出如下截图所示:

产品 API

接下来我们将在产品 API 项目上进行工作:

  1. 通过将以下内容附加到现有文件中,在application.yml中添加一个新的 Spring 配置文件docker
--- 
spring: 
  profiles: docker 
eureka: 
  instance: 
    preferIpAddress: true 
  client: 
    serviceUrl: 
      defaultZone: http://eureka:8761/eureka/ 
  1. 构建 Spring Boot JAR 以反映对application.yml的更改:
mvn clean package 
  1. 添加一个.dockerignore文件,内容如下:
.* 
target/* 
!target/product-*.jar 
  1. 添加一个包含以下内容的 Dockerfile:
FROM openjdk:8-jdk-alpine 

RUN mkdir -p /app 

ADD target/product-0.0.1-SNAPSHOT.jar /app/app.jar 

EXPOSE 8080 

ENTRYPOINT [ "/usr/bin/java", "-jar", "/app/app.jar", "--spring.profiles.active=docker" ] 
  1. 构建 Docker 容器:
docker build -t cloudnativejava/product-api . 

上一个命令的输出如下截图所示:

  1. 启动多个 Docker 容器:
docker run -d -p 8011:8080 \ 
    --network app_nw \ 
    cloudnativejava/product-api 

docker run -d -p 8012:8080 \ 
    --network app_nw \ 
    cloudnativejava/product-api 

上一个命令的输出如下截图所示:

产品 API 将在以下 URL 上可用:

  • http://<docker-host>:8011/product/1

  • http://<docker-host>:8012/product/1

连接到外部 Postgres 容器

为了将product API 连接到外部数据库而不是内存数据库,首先创建一个包含数据的容器镜像:

  1. 创建一个文件import-postgres.sql,内容如下:
create table product(id serial primary key, name varchar(20), cat_id int not null); 
begin; 
insert into product(name, cat_id) values ('Apples', 1); 
insert into product(name, cat_id) values ('Oranges', 1); 
insert into product(name, cat_id) values ('Bananas', 1); 
insert into product(name, cat_id) values ('Carrots', 2); 
insert into product(name, cat_id) values ('Beans', 2); 
insert into product(name, cat_id) values ('Peas', 2); 
commit; 
  1. 创建一个包含以下内容的Dockerfile.postgres
FROM postgres:alpine 

ENV POSTGRES_USER=dbuser  
    POSTGRES_PASSWORD=dbpass  
    POSTGRES_DB=product 

EXPOSE 5432 

RUN mkdir -p /docker-entrypoint-initdb.d 

ADD import-postgres.sql /docker-entrypoint-initdb.d/import.sql 
  1. 现在构建包含数据库初始化内容的 Postgres 容器镜像:
docker build -t cloudnativejava/datastore -f Dockerfile.postgres . 

上一个命令的输出如下截图所示:

  1. 通过将以下内容附加到现有文件中,在application.yml中添加一个新的 Spring 配置文件postgres
--- 
spring: 
  profiles: postgres 
  datasource: 
    url: jdbc:postgresql://<docker-host>:5432/product 
    username: dbuser 
    password: dbpass 
    driver-class-name: org.postgresql.Driver 
  jpa: 
    database-platform: org.hibernate.dialect.PostgreSQLDialect 
    hibernate: 
      ddl-auto: none 

确保将<docker-host>替换为适合您环境的值。

  1. 构建 Spring Boot JAR 以反映对application.yml的更改:
mvn clean package 
  1. 构建 Docker 容器:
docker build -t cloudnativejava/product-api . 

上述命令的输出如下截图所示:

  1. 如果您已经有容器在旧镜像上运行,可以停止并删除它们:
old_ids=$(docker ps -f ancestor=cloudnativejava/product-api -q) 
docker stop $old_ids 
docker rm $old_ids 
  1. 启动数据库容器:
docker run -d -p 5432:5432  
    --network app_nw  
    --name datastore  
    cloudnativejava/datastore 

上述命令的输出如下截图所示:

  1. 启动几个产品 API 的 Docker 容器:
docker run -d -p 8011:8080  
    --network app_nw  
    cloudnativejava/product-api  
    --spring.profiles.active=postgres 

docker run -d -p 8012:8080  
    --network app_nw  
    cloudnativejava/product-api  
    --spring.profiles.active=postgres 

上述命令的输出如下截图所示:

产品 API 将在以下 URL 上可用:

  • http://<docker-host>:8011/product/1

  • http://<docker-host>:8012/product/1

部署模式

在介绍了云原生应用程序的打包和部署模型之后,我们将介绍用于部署云原生应用程序的模式。传统上,应用程序在多个环境中部署,如开发、测试、暂存、预生产等,每个环境可能是最终生产环境的缩减版本。应用程序通过一系列预生产环境,并最终部署到生产环境。然而,一个重要的区别是,虽然其他环境中容忍停机时间,但在生产部署中的停机时间可能导致严重的业务后果。

使用云原生应用程序,可以实现零停机发布软件。这是通过对开发、测试和部署的每个方面严格应用自动化来实现的。我们将在后面的部分介绍持续集成CI)/ 持续部署CD),但在这里我们将介绍一些能够快速部署应用程序的模式。所有这些模式都依赖于路由器组件的存在,它类似于负载均衡器,可以将请求路由到一定数量的应用实例。在某些情况下,应用程序本身构建了隐藏在功能标志后面的功能,可以通过对应用程序配置的更改来启用。

蓝绿部署

蓝绿部署是一个分为三个阶段的模式。部署的初始状态如下图所示。所有应用流量都路由到现有实例,这些实例被视为蓝色实例。蓝绿部署的表示如下:

在蓝绿部署的第一阶段,使用新版本的应用程序的一组新实例被配置并变为可用。在这个阶段,新的绿色应用实例对最终用户不可用,并且部署在内部进行验证。如下所示:

在部署的下一个阶段,路由器上会打开一个象征性的开关,现在开始将所有请求路由到绿色实例,而不是旧的蓝色实例。旧的蓝色实例会保留一段时间进行观察,如果检测到任何关键问题,我们可以根据需要快速回滚部署到旧的应用实例:

在部署的最后阶段,应用的旧蓝色实例被废弃,绿色实例成为下一个稳定的生产版本:

蓝绿部署在切换两个稳定版本的应用程序之间以及通过备用环境确保快速恢复时非常有效。

金丝雀部署

金丝雀部署也是蓝绿部署的一种变体。金丝雀部署解决了同时运行两个生产实例时浪费资源的问题,尽管时间很短。在金丝雀部署中,绿色环境是蓝色环境的缩减版本,并且依赖路由器的能力,始终将一小部分请求路由到新的绿色环境,而大部分请求则路由到蓝色环境。以下图表描述了这一点:

当发布应用程序的新功能需要与一些测试用户进行测试,然后根据这些用户群的反馈进行全面发布时,这种方法尤其有用。一旦确定绿色环境准备好全面发布,绿色环境的实例将增加,同时蓝色环境的实例将减少。以下是一系列图表的最佳说明:

这样就避免了运行两个生产级环境的问题,并且在从一个版本平稳过渡到另一个版本的同时,还提供了回退到旧版本的便利。

暗部署

另一种常用的部署模式是暗部署模式,用于部署云原生应用程序。在这种模式下,新功能被隐藏在功能标志后,并且仅对一组特定用户启用,或者在某些情况下,用户完全不知道该功能,而应用程序模拟用户行为并执行应用程序的隐藏功能。一旦确定功能准备好并且稳定可供所有用户使用,就通过切换功能标志来启用它。

应用 CI/CD 进行自动化

云原生应用程序部署的核心方面之一在于能够有效地自动化和构建软件交付流水线。这主要是通过使用能够从源代码库获取源代码、运行测试、构建可部署构件并将其部署到目标环境的 CI/CD 工具来实现的。大多数现代的 CI/CD 工具,如 Jenkins,都支持配置构建流水线,可以根据脚本形式的配置文件构建多个构件。

我们将以 Jenkins 流水线脚本为例,演示如何配置一个简单的构建流水线。在我们的示例中,我们将简单构建两个构件,即eureka-serverproduct-api可运行的 JAR 包。添加一个名为Jenkinsfile的新文件,内容如下:

node { 
  def mvnHome 
  stage('Preparation') { // for display purposes 
    // Get some code from a GitHub repository 
    git 'https://github.com/...' 
    // Get the Maven tool. 
    // ** NOTE: This 'M3' Maven tool must be configured 
    // **       in the global configuration. 
    mvnHome = tool 'M3' 
  } 
  stage('Eureka Server') { 
    dir('eureka-server') { 
      stage('Build - Eureka Server') { 
        // Run the maven build 
        if (isUnix()) { 
          sh "'${mvnHome}/bin/mvn' -Dmaven.test.failure.ignore clean package" 
        } else { 
          bat(/"${mvnHome}binmvn" -Dmaven.test.failure.ignore clean package/) 
        } 
      } 
      stage('Results - Eureka Server') { 
        archiveArtifacts 'target/*.jar' 
      } 
    }    
  } 
  stage('Product API') { 
    dir('product') { 
      stage('Build - Product API') { 
        // Run the maven build 
        if (isUnix()) { 
          sh "'${mvnHome}/bin/mvn' -Dmaven.test.failure.ignore clean package" 
        } else { 
          bat(/"${mvnHome}binmvn" -Dmaven.test.failure.ignore clean package/) 
        } 
      } 
      stage('Results - Product API') { 
        junit '**/target/surefire-reports/TEST-*.xml' 
        archiveArtifacts 'target/*.jar' 
      } 
    } 
  } 
} 

流水线脚本的功能如下:

  1. 从 GitHub 检出源代码

  2. 配置 Maven 工具

  3. 通过在检出的源代码库的两个目录中运行 Maven 构建来构建两个构件

  4. 存储构建的测试结果和结果 JAR 包

在 Jenkins 中创建一个新的流水线作业:

在流水线配置中,指定 GitHub 仓库和 Git 仓库中Jenkinsfile的路径:

运行构建后,应该会构建出两个构件:

可以通过扩展流水线脚本来构建我们在本章中手动构建的 Docker 容器,使用 Jenkins 的 Docker 插件。

总结

在本章中,我们了解了可以用于部署云原生应用程序的各种部署模式,以及如何使用 Jenkins 等持续集成工具来自动化构建和部署。我们还学习了如何使用 Docker 容器构建和运行示例云原生应用程序。

第八章:云原生应用程序运行时

在本章中,我们将研究我们的应用程序或服务运行的运行时生态系统。我们将涵盖以下主题:

  • 全面运行时的需求,包括操作和管理大量服务中的问题的总结

  • 实施参考运行时架构,包括:

  • 服务注册表

  • 配置服务器

  • 服务前端、API 网关、反向代理和负载均衡器

  • 以 Zuul 作为反向代理的介绍

  • 通过 Kubernetes 和 Minikube 进行容器管理和编排

  • 平台即服务(PaaS)上运行:

  • PaaS 平台如何帮助实现我们在前一章讨论的服务运行时参考架构

  • 安装 Cloud Foundry 并在 Cloud Foundry 上运行我们的product服务

运行时的需求

我们已经开发了我们的服务,为它们编写了测试,自动化了持续集成,并在容器中运行它们。我们还需要什么?

在生产环境中运行许多服务并不容易。随着更多服务在生产环境中发布,它们的管理变得复杂。因此,这里是对在微服务生态系统中讨论的问题的总结,并在前一章的一些代码示例中得到解决:

  • 在云中运行的服务:传统的大型应用程序托管在应用服务器上,并在 IP 地址和端口上运行。另一方面,微服务在多个容器中以各种 IP 地址和端口运行,因此跟踪生产服务可能会变得复杂。

  • 服务像打地鼠游戏中的鼹鼠一样上下运行:有数百个服务,它们的负载被平衡和故障转移实例在云空间中运行。由于 DevOps 和敏捷性,许多团队正在部署新服务并关闭旧服务。因此,正如我们所看到的,基于微服务的云环境非常动态。

服务注册表跟踪服务来解决这两个问题。因此,客户端可以查找与名称对应的服务在哪里运行,使用客户端负载平衡模式。然而,如果我们想要将客户端与查找分离,那么我们使用服务器端负载平衡模式,其中负载均衡器(如 Nginx)、API 网关(如 Apigee)或反向代理或路由器(如 Zuul)将客户端与服务的实际地址抽象出来。

  • 跨微服务管理我的配置:如果部署单元已分解为多个服务,那么打包的配置项(如连接地址、用户 ID、日志级别等)也会分解为属性文件。因此,如果我需要更改一组服务或流程的日志级别,我是否需要在所有应用程序的属性文件中进行更改?在这里,我们将看到如何通过 Spring Config Server 或 Consul 将属性文件集中化,以按层次结构管理属性。

  • 处理如此多的日志文件:每个微服务都会生成一个(或多个)日志文件,如.out.err以及 Log4j 文件。我们如何在多个服务的多个日志文件中搜索日志消息?

解决这个问题的模式是日志聚合,使用商业工具(如 Splunk)或开源工具(如 Logstash 或 Galaxia)实现。它们也默认存在于 PaaS 提供的工具中,如 Pivotal Cloud Foundry。

另一个选项是将日志流式传输到聚合器(如 Kafka),然后可以在其中进行集中存储。

  • 来自每个服务的指标:在第二章中,编写您的第一个云原生应用程序,我们添加了 Spring 执行器指标,这些指标会暴露为端点。还有许多其他指标,如 Dropwizard 指标,可以被捕获和暴露。

要么一个代理必须监视所有服务执行器指标,要么它们可以被导出,然后在监控和报告工具中进行聚合。

另一个选项是应用程序监控工具,如 Dynatrace、AppDynamics 来监控应用程序,并在 Java 级别提取指标。我们将在下一章中介绍这些。

实施运行时参考架构

前一节讨论的问题由以下参考运行时架构解决:

所有这些组件已经在第一章中讨论过,云原生简介。现在,我们继续选择技术并展示实现。

服务注册表

运行服务注册表 Eureka 在第二章中已经讨论过,编写您的第一个云原生应用程序。请参考该章节,回顾一下product服务如何在 Eureka 中注册自己以及客户端如何使用 Ribbon 和 Eureka 找到product服务。

如果我们使用 Docker 编排(如 Kubernetes),服务注册表的重要性会稍微降低。在这种情况下,Kubernetes 本身管理服务的注册,代理查找并重定向到服务。

配置服务器

配置服务器以分层方式存储配置。这样,应用程序只需要知道配置服务器的地址,然后连接到它以获取其余的配置。

有两个流行的配置服务器。一个是 Hashicorp 的 Consul,另一个是 Spring Config Server。我们将使用 Spring Config Server 来保持堆栈与 Spring 一致。

让我们来看看启动使用配置服务器的步骤。使用外部化配置有两个部分:服务器(提供属性)和客户端。

配置服务器的服务器部分

有许多选项可以通过 HTTP 连接提供属性,Consul 和 Zookeeper 是流行的选项之一。然而,对于 Spring 项目,Spring Cloud 提供了一个灵活的配置服务器,可以连接到多个后端,包括 Git、数据库和文件系统。鉴于最好将属性存储在版本控制中,我们将在此示例中使用 Spring Cloud Config 的 Git 后端。

Spring Cloud Config 服务器的代码、配置和运行时与 Eureka 非常相似,像我们在第二章中为 Eureka 做的那样,很容易启动一个实例,编写您的第一个云原生应用程序

按照以下步骤运行服务注册表:

  1. 创建一个新的 Maven 项目,将 artifact ID 设置为config-server

  2. 编辑 POM 文件并添加以下内容:

1. 父项为spring-boot-starter-parent

2. 依赖项为spring-cloud-config-server

3. 依赖管理为spring-cloud-config

  1. 创建一个ConfigServiceApplication类,该类将有注解来启动配置服务器:

  1. 在应用程序的config-server/src/main/resources文件夹中创建一个application.yml文件,并添加以下内容:
server: 
  port: 8888 
spring: 
  cloud: 
    config: 
      server: 
        git: 
          uri: file:../.. 

端口号是配置服务器将在 HTTP 连接上监听配置请求的地方。

spring.cloud.config.server.git.uri的另一个属性是 Git 的位置,我们已经为开发配置了一个本地文件夹。这是 Git 应该在本地机器上运行的地方。如果不是,请在此文件夹上运行git init命令。

我们在这里不涵盖 Git 身份验证或加密。请查看 Spring Cloud Config 手册(spring.io/guides/gs/centralized-configuration/)了解更多详情。

  1. product.properties文件中,我们将保存最初保存在实际product项目的application.properties文件中的属性。这些属性将由配置服务器加载。我们将从一个小属性开始,如下所示:
testMessage=Hi There 

此属性文件应该存在于我们刚刚在上一步中引用的 Git 文件夹中。请使用以下命令将属性文件添加到 Git 文件夹中:

git add product.properties and then commit.
  1. 在应用程序的resources文件夹中创建一个bootstrap.yml文件,并输入此项目的名称:
spring: 
  application: 
    name: configsvr 
  1. 构建 Maven 项目,然后运行它。

  2. 您应该看到一个Tomcat started消息,如下所示:

ConfigurationServiceApplication已启动,并在端口8888上监听

让我们检查我们添加的属性是否可供使用。

打开浏览器,检查product.properties。有两种方法可以做到这一点。第一种是将属性文件视为 JSON,第二种是将其视为文本文件:

  1. http://localhost:8888/product/default:

  1. http://localhost:8888/product-default.properties:

如果您在想,默认是配置文件名称。Spring Boot 应用程序支持配置文件覆盖,例如,用于测试和用户验收测试(UAT)环境,其中可以用product-test.properties文件替换生产配置。因此,配置服务器支持以下形式的 URL 读取:http://configsvrURL/{application}/{profile}http://configsvrURL/{application-profile}.properties.yml

在生产环境中,我们几乎不太可能直接访问配置服务器,就像之前展示的那样。将是客户端访问配置服务器;我们将在下面看到这一点。

配置客户端

我们将使用先前开发的product服务代码作为基线,开始将属性从应用程序中提取到配置服务器中。

  1. 从 eclipse 中复制product服务项目,创建一个新的项目用于本章。

  2. spring-cloud-starter-config依赖项添加到 POM 文件的依赖项列表中:

  1. 我们的主要工作将在资源上进行。告诉product服务使用运行在http://localhost:8888的配置服务器。

failFast标志表示如果找不到配置服务器,我们不希望应用程序继续加载。这很重要,因为它将确保product服务在找不到配置服务器时不应假定默认值:

spring: 
  application: 
    name: product 

  cloud: 
    config: 
      uri: http://localhost:8888 
      failFast: true 
  1. product服务的resources文件夹中的application.properties部分中的所有属性转移到我们在上一节中定义的git文件夹的product.properties中。

您的product.properties文件现在将具有有用的配置,除了我们之前放入进行测试的Hi There消息之外:

server.port=8082 
eureka.instance.leaseRenewalIntervalInSeconds=15 
logging.level.org.hibernate.tool.hbm2ddl=DEBUG 
logging.level.org.hibernate.SQL=DEBUG 
testMessage=Hi There 
  1. 现在可以删除product服务的resources文件夹中存在的application.properties文件。

  2. 让我们向product服务添加一个测试方法,以检查从配置服务器设置的属性:

    @Value("${testMessage:Hello default}") 
    private String message; 

   @RequestMapping("/testMessage") 
   String getTestMessage() { 
         return message ; 
   }
  1. 启动 Eureka 服务器,就像在之前的章节中所做的那样。

  2. 确保上一节中的配置服务器仍在运行。

  3. 现在,从ProductSpringApp的主类开始启动product服务。在日志的开头,您将看到以下语句:

当 ProductSpringApp 启动时,它首先从运行在 8888 端口的外部配置服务获取配置

bootstrap.yml文件中,选择name=product的环境作为我们的应用程序名称。

product服务应该监听的端口号是从这个配置服务器中获取的,还有其他属性,比如我们现在将看到的测试消息:

ProductSpringApp在端口8082上启动,从外部化配置中获取。

使用以下两个 URL 测试应用程序:

  • http://localhost:8082/testMessage:这将返回我们配置的消息Hi There

运行其他 REST 服务之一,例如产品视图。您将看到所需的产品信息,以表明我们的服务正常运行。

  • http://localhost:8082/product/1:这将返回{"id":1,"name":"Apples","catId":1}

刷新属性

现在,如果您想要在所有服务上集中反映属性的更改,该怎么办?

  1. 您可以将product.properties文件中的消息更改为新消息,例如Hi Spring

  2. 您会注意到配置服务器在下一次读取时接收到了这一更改,如下所示:

但是,该属性不会立即被服务接收,因为调用http://localhost:8082/testMessage会导致旧的Hi There消息。我们如何在命令行上刷新属性?

这就是执行器命令/refresh派上用场的地方。我们配置这些 bean 作为@RefreshScope注解的一部分。当从 Postman 应用程序执行POST方法调用http://localhost:8082/refresh时,这些 bean 将被重新加载。查看以下日志,以查看调用刷新会导致重新加载属性的结果:

第一行显示了product服务在执行http://localhost:8082/refresh时刷新其属性的日志

您可以查看,在标记线之后,属性加载重新开始,并在调用http://localhost:8082/testMessage后反映出消息。

微服务前端

使用反向代理、负载均衡器、边缘网关或 API 网关来作为微服务的前端是一种流行的模式,复杂度逐渐增加。

  • 反向代理:反向代理被定义为使下游资源可用,就好像它是自己发出的一样。在这方面,Web 服务器前端和应用服务器也充当反向代理。反向代理在云原生应用中非常有用,因为它确保客户端无需像我们在第二章中所做的那样查找服务然后访问它们。他们必须访问反向代理,反向代理查找微服务,调用它们,并使响应可用于客户端。

  • 负载均衡器:负载均衡器是反向代理的扩展形式,可以平衡来自客户端的请求,使其分布在多个服务之间。这增加了服务的可用性。负载均衡器可以与服务注册表一起工作,找出哪些是活动服务,然后在它们之间平衡请求。Nginx 和 HAProxy 是可以用于微服务前端的负载均衡器的良好例子。

  • 边缘网关:顾名思义,边缘网关是部署在企业或部门边缘的高阶组件,比负载均衡器具有更多功能,如身份验证、授权、流量控制和路由功能。Netfix Zuul 是这种模式的一个很好的例子。我们将在本节中介绍使用 Zuul 的代码示例。

  • API 网关:随着移动和 API 的流行,这个组件提供了更复杂的功能,比如将请求分发到多个服务之间进行编排,拦截和增强请求或响应,或转换它们的格式,对请求进行复杂的分析。也可以同时使用 API 网关和负载均衡器、反向代理或边缘在一个流中。这种方法有助于责任的分离,但也会因为额外的跳跃而增加延迟。我们将在后面的章节中看到 API 网关。

Netflix Zuul

Netflix Zuul 是 Netflix 推广的一种流行的边缘网关,后来作为 Spring Cloud 的一部分提供。Zuul 意味着守门人,并执行所有这些功能,包括身份验证、流量控制,最重要的是路由,正如前面讨论的那样。它与 Eureka 和 Hystrix 很好地集成,用于查找服务和报告指标。企业或域中的服务可以由 Zuul 前端化。

让我们在我们的product服务前面放一个 Zuul 网关:

  1. 创建一个新的 Maven 项目,并将其 artifact ID 设置为zuul-server

  2. 编辑 POM 文件并添加以下内容:

  3. 将父级设置为spring-boot-starter-parent

  4. spring-cloud-starter-zuul-eureka-web项目上设置依赖管理

  5. spring-cloud-starter-netflix上设置依赖管理。

  1. 创建一个带有注释以启用 Zuul 代理的应用程序类:

application.yml中的配置信息对于 Zuul 非常重要。这是我们配置 Zuul 的路由能力以将其重定向到正确的微服务的地方。

  1. 由于 Zuul 与 Eureka 的交互良好,我们将利用这一点:
eureka: 
  client: 
    serviceUrl: 
defaultZone: http://127.0.0.1:8761/eureka/ 

这告诉 Zuul 在运行该端口的 Eureka 注册表中查找服务。

  1. 将端口配置为8080

  2. 最后,配置路由。

这些是 REST 请求中 URL 到相应服务的映射:

zuul: 
  routes: 
    product: 
      path: /product*/** 
      stripPrefix: false 

幕后发生了什么

让我们来看看幕后发生了什么:

  1. 路由定义中的product部分告诉 Zuul,配置在/product*/**之后的路径应该被重定向到product服务,如果它在 Zuul 服务器中配置的 Eureka 注册表中存在。

  2. 路径配置为/product*/**。为什么有三个*?如果您记得,我们的product服务可以处理两种类型的 REST 服务:/product/1 GET/product PUTDELETEPOST请求。/products?id=1 GET请求要求它返回给定类别 ID 的产品列表。因此,product*映射到 URL 中的/product/products

  3. stripPrefixfalse设置允许/product/传递到product服务。如果未设置该标志,则仅在/product*/之后的 URL 的其余部分将传递给微服务。我们的product微服务包括/product,因此我们希望在转发到product服务时保留前缀。

一次性运行它们

现在让我们尝试运行我们的product服务,以及其他生态系统的其余部分:

  1. 按照依赖关系的相反顺序启动服务。

  2. 通过运行项目的主类或通过 Maven 启动配置服务器和 Eureka 服务器。

  3. 启动product服务。

  4. 启动 Zuul 服务。

观察日志窗口,并等待所有服务器启动。

  1. 现在,在浏览器中运行以下请求:
  • http://localhost:8080/product/3

  • http://localhost:8080/products?id=1

您应该在第一个请求中看到产品3,在第二个请求中看到与类别1对应的产品。

让我们来看看 Zuul 和product服务的日志:

  • 在 Zuul 中,您可以看到/product*/**的映射已经解析,并且从 Eureka 注册表中获取了指向product服务的端点:

Zuul 边缘现在已注册以映射对product服务的请求,并将其转发到 Eureka 指向的服务地址

  • product服务中,通过在数据库上运行查询来执行服务:

Kubernetes - 容器编排

到目前为止,我们一直在单独部署诸如 Eureka、配置服务器、product服务和 Zuul 等服务。

从上一章中可以看出,我们可以通过 CI(如 Jenkins)自动化部署它们。我们还看到了如何使用 Docker 容器进行部署。

然而,在运行时,容器仍然独立运行。没有机制来扩展容器,或者在其中一个容器失败时重新启动它们。此外,手动决定将哪个服务部署在哪个 VM 上,这意味着服务始终部署在静态 VM 上,而不是智能地混合部署。简而言之,管理我们应用服务的编排层缺失。

Kubernetes 是一种流行的编排机制,使部署和运行时管理变得更加容易。

Kubernetes 架构和服务

Kubernetes 是由 Google 主导的开源项目。它试图实现一些在其内部容器编排系统 Borg 中实现的经过验证的想法。Kubernetes 架构由两个组件组成:主节点和从节点。主节点具有以下组件:

  • 控制器:管理节点、副本和服务

  • API 服务器:提供kubectl客户端和从节点使用的 REST 端点

  • 调度程序:决定特定容器必须生成的位置

  • Etcd:用于存储集群状态和配置

从节点包含两个组件:

  • Kubelet:与主节点通信资源可用性并启动调度程序指定的容器的代理

  • 代理:将网络请求路由到 kubernetes 服务

Kubernetes 是一个容器调度程序,使用两个基本概念,即 Pod 和 Service。Pod 是一组相关容器,可以使用特定的标签进行标记;服务可以使用这些标签来定位 Pod 并公开端点。以下图示了这个概念:

Pods are considered ephemeral in kubernetes and may be killed. However, if the Pods were created using a ReplicaSet, where we can specify how many replicas or instances of a certain Pod have to be present in the system, then the kubernetes scheduler will automatically schedule new instances of the Pod and once the Pod becomes available, the service will start routing traffic to it. As you may notice that a Pod may be targeted by multiple services provided the labels match, this feature is useful to do rolling deployments.

我们现在将看看如何在 kubernetes 上部署一个简单的 API 并进行滚动升级。

Minikube

Minikube 是一个项目,可以在虚拟机上运行一个工作的单节点 Kubernetes。

您可以按照以下说明安装 Minikube:github.com/kubernetes/minikube

对于 Windows,请确保已完成以下步骤:

在 Kubernetes 中运行产品服务

让我们将现有的product服务更改为通过 Kubernetes 容器编排运行:

  1. 您可以通过运行它来测试配置是否有效,如下面的屏幕截图所示:

  1. 设置 Docker 客户端以连接到在 Minikube VM 中运行的 Docker 守护程序,如下所示:

  1. 根据我们在前几章中创建 Docker 镜像的说明构建 Docker 镜像:

  1. 创建一个deployment文件(请注意,imagePullPolicy设置为never,因为否则,Kubernetes 的默认行为是从 Docker 注册表中拉取):

  1. 验证三个实例是否正在运行:

  1. 创建一个service.yml文件,以便我们可以访问 Pods:

现在,按照以下方式运行service.yml文件:

现在,您可以获取服务的地址:

现在,您可以访问 API,该 API 将路由请求到所有三个 Pod:

您可以使用-v来获取以下详细信息的单个命令:

  1. 更改代码如下:

  1. 使用新标签构建 Docker 镜像:

  1. 更新deployment.yml文件:

  1. 应用更改:

平台即服务(PaaS)

云原生应用程序的另一个流行运行时是使用 PaaS 平台,具体来说是应用程序 PaaS 平台。PaaS 提供了一种轻松部署云原生应用程序的方式。它们提供了额外的服务,如文件存储、加密、键值存储和数据库,可以轻松绑定到应用程序上。PaaS 平台还提供了一种轻松的机制来扩展云原生应用程序。现在让我们了解一下为什么 PaaS 平台为云原生应用程序提供了出色的运行时。

PaaS 的案例

在运行时架构实现中,我们看到许多组件,例如配置服务器、服务注册表、反向代理、监控、日志聚合和指标,必须共同实现可扩展的微服务架构。除了ProductService中的业务逻辑外,其余的服务和组件都是纯粹的支持组件,因此涉及大量的平台构建和工程。

如果我们构建的所有组件都是作为服务提供的平台的一部分,会怎么样?因此,PaaS 是对容器编排的更高级抽象。PaaS 提供了我们在容器编排中讨论的所有基本基础设施服务,例如重新启动服务、扩展服务和负载平衡。此外,PaaS 还提供了补充开发、扩展和维护云原生应用程序的其他服务。这种方法的折衷之处在于它减少了在选择和微调组件方面的选择。然而,对于大多数专注于业务问题的企业来说,这将是一个很好的折衷。

因此,使用 PaaS,开发人员现在可以专注于编写代码,而不必担心他/她将部署在哪个基础设施上。所有的工程现在都变成了开发人员和运维团队可以配置的配置。

PaaS 的另外一些优势包括:

  • 运行时:为开发人员提供各种运行时,如 Java、Go、Node.js 或.NET。因此,开发人员专注于生成部署,可以在 PaaS 环境提供的各种运行时中运行。

  • 服务:PaaS 提供应用程序服务,如数据库和消息传递,供应用程序使用。这是有益的,因为开发人员和运营人员不必单独安装或管理它们。

  • 多云:PaaS 将开发人员与基础架构(或 IaaS)抽象出来。因此,开发人员可以为 PaaS 环境开发,而不必担心将其部署在数据中心或各种云提供商(如 AWS、Azure 或 Google Cloud Platform)上,如果 PaaS 在这些基础设施上运行。这避免了对基础设施或云环境的锁定。

PaaS 环境的权衡是它们可能会限制并降低灵活性。默认选择的服务和运行时可能不适用于所有用例。然而,大多数 PaaS 提供商提供插入点和 API 来包含更多服务和配置,并提供策略来微调运行时行为,以减轻权衡。

Cloud Foundry

Cloud Foundry 是 Cloud Foundry 基金会拥有的最成熟的开源 PaaS 之一。

它主要由以下部分组成:

  • 应用程序运行时:开发人员部署 Java 或 Node.js 应用程序等应用程序工作负载的基础平台。应用程序运行时提供应用程序生命周期、应用程序执行和支持功能,如路由、身份验证、平台服务,包括消息传递、指标和日志记录。

  • 容器运行时:容器运行的运行时抽象。这提供了基于 Kubernetes 的容器的部署、管理和集成,应用程序运行在其上,它基于 Kubo 项目。

  • 应用程序服务:这些是应用程序绑定的数据库等服务。通常由第三方提供商提供。

  • Cloud Foundry 组件:有很多,比如 BOSH(用于容器运行时)、Diego(用于应用程序运行时)、公告板系统BBS)、NATS、Cloud Controller 等等。然而,这些组件负责提供 PaaS 的各种功能,并可以从开发人员中抽象出来。它们与运营和基础设施相关且感兴趣。

组织、帐户和空间的概念

Cloud Foundry 具有详细的基于角色的访问控制RBAC)来管理应用程序及其各种资源:

  • 组织:这代表一个组织,可以将多个用户绑定到其中。一个组织共享应用程序、服务可用性、资源配额和计划。

  • 用户帐户:用户帐户代表可以在 Cloud Foundry 中操作应用程序或操作的个人登录。

  • 空间:每个应用程序或服务都在一个空间中运行,该空间绑定到组织,并由用户帐户管理。一个组织至少有一个空间。

  • 角色和权限:属于组织的用户具有可以执行受限操作(或权限)的角色。详细信息已记录在:docs.cloudfoundry.org/concepts/roles.html

Cloud Foundry 实施的需求

在安装和运行原始 Cloud Foundry 中涉及了大量的工程工作。因此,有许多 PaaS 实现使用 Cloud Foundry 作为基础,并提供额外的功能,最流行的是 IBM 的 Bluemix、Redhat 的 OpenShift 和 Pivotal 的Pivotal Cloud Foundry(PCF)。

Pivotal Cloud Foundry(PCF)

Pivotal 的 Cloud Foundry 旨在提高开发人员的生产力和运营效率,并提供安全性和可用性。

尽管本书的读者可以自由选择基于 Cloud Foundry 的 PaaS 实现,但我们选择 Pivotal 有几个原因:

  • Pivotal 一直以来都支持 Spring Framework,我们在书中广泛使用了它。Pivotal 的 Cloud Foundry 实现原生支持 Spring Framework 及其组件,如 Spring Boot 和 Spring Cloud。因此,我们创建的 Spring Boot 可部署文件可以直接部署到 Cloud Foundry 的应用运行时并进行管理。

  • Pivotal 的服务市场非常丰富,涵盖了大多数合作伙伴提供的平台组件,包括 MongoDB、PostgreSQL、Redis,以及 Pivotal 开发的 MySQL 和 Cloud Cache 的原生支持服务。

  • Pivotal 在这个领域进行了多次发布,因此服务提供频繁更新。

PCF 组件

Pivotal 网站pivotal.io/platform提供了一个非常简单的 Cloud Foundry 实现图表,与我们之前的讨论相对应:

  • Pivotal 应用程序服务(PAS):这是一个应用程序的抽象,对应于 Cloud Foundry 中的应用程序运行时。在内部,它使用 Diego,但这对开发人员来说是隐藏的。PAS 对 Spring Boot 和 Spring Cloud 有很好的支持,但也可以运行其他 Java、.NET 和 Node 应用程序。它适用于运行自定义编写的应用程序工作负载。

  • Pivotal 容器服务(PKS):这是一个容器的抽象,与 Cloud Foundry 中的容器运行时相对应。它在内部使用 BOSH。它适用于运行作为容器提供的工作负载,即独立服务供应商(ISV)应用程序,如 Elasticsearch。

  • Pivotal Function Service(PFS):这是 Pivotal 在 Cloud Foundry 平台之外的新产品。它提供了函数的抽象。它推动了无服务器计算。这些函数在 HTTP 请求(同步)或消息到达时(异步)被调用。

  • 市场:这对应于 Cloud Foundry 中的应用程序服务。鉴于 PCF 的流行,市场上有很多可用的服务。

  • 共享组件:这些包括运行函数、应用程序和容器所需的支持服务,如身份验证、授权、日志记录、监控(PCF watch)、扩展、网络等。

PCF 可以在包括 Google Compute Platform、Azure、AWS 和 Open Stack(IaaS)在内的大多数热门云上运行,并托管在数据中心。

虽然 PCF 及其组件非常适合服务器端负载,但对于在本地机器上构建软件的开发人员来说可能会很麻烦。我们现在就处于这个阶段。我们已经开发了product服务,并通过各个阶段成熟,以达到云原生运行时。

整个 PCF 及其运行时组件难以适应笔记本电脑进行开发。

PCF Dev

PCF Dev 是一个精简的 PCF 发行版,可以在台式机或笔记本电脑上本地运行。它承诺能够在开发人员主要 PCF 环境上拥有相同的环境,因此当为 PCF Dev 设计的应用程序在主要 PCF 环境上运行时不会有任何差异。请参考docs.pivotal.io/pcf-dev/index.html中的表格,了解 PCF Dev 与完整 PCF 和 Cloud Foundry(CF)提供的大小和功能的确切比较:

  • 它支持 Java、Ruby、PHP 和 Python 的应用程序运行时。

  • 它具有 PAS 的迷你版本,为我们迄今为止讨论的服务开发提供了必要的功能,如日志记录和指标、路由、Diego(Docker)支持、应用程序服务、扩展、监控和故障恢复。

  • 它还内置了四个应用程序服务,它们是:Spring Cloud Services(SCS)、Redis、RabbitMQ 和 MySQL。

  • 但是,它不适用于生产。它没有 BOSH,它在基础架构层上进行编排。

如果您的台式机/笔记本内存超过 8GB,磁盘空间超过 25GB,让我们开始吧。

安装

PCF Dev 可以在 Mac、Linux 或 Windows 环境中运行。按照说明,例如,docs.pivotal.io/pcf-dev/install-windows.html for Windows,来在您的机器上运行 PCF Dev。这基本上分为三个步骤:

  • 获取 Virtual Box

  • CF 命令行界面

  • 最后,PCF Dev

启动 PCF Dev

第一次使用 cf dev start 时,下载 VM 映像(4GB)、提取它(20GB),然后启动 PCF 的各种服务需要很长时间。因此,一旦 VM 下载并运行,我们将暂停和恢复带有 Cloud Foundry 服务的 VM。

启动 PCF Dev 的命令行选项如下:

  1. 假设您有多核机器,您可以为该 VM 分配一半的核心,例如对于四核机器,可以使用-c 2

  2. SCS 版本将使用 8GB 内存;为了保持缓冲区,让我们在命令行上使用以 MB 表示的 10GB 内存。

  3. 在下一章中,我们将需要 MySQL 和 SCS 的服务。在内部,SCS 需要 RabbitMQ 来运行。因此,在运行实例时,让我们包括所有服务器。

  4. 给出域和 IP 地址是可选的,因此我们将跳过-d-i选项。

  5. 将环境变量PCFDEV_HOME设置为具有足够空间的特定驱动器上的特定文件夹,以便它不会默认为主文件夹。我们建议主文件夹是像 SSD 这样的快速驱动器,因为 Cloud Foundry 的启动和停止操作非常 I/O 密集。

因此,我们的启动命令将如下所示:

cf dev start -c 2 -s all -m 10000 

这将花费很长时间,直到您的 PCF Dev 环境准备就绪。

加快开发时间

每次启动整个 PCF Dev 环境时等待 20 分钟是很困难的。一旦您完成了当天的工作或在关闭笔记本电脑之前,您可以使用cf dev suspend来暂停 PCF Dev,并在第二天使用cf dev resume命令来恢复它。

其他有用的命令包括:

  • 默认的 PCF Dev 创建了两个用户—admin 和 user。要安装或管理应用程序,您应该登录为其中一个用户。命令cf dev target会将您登录为默认用户。

  • cf dev trust命令安装证书以启用 SSL 通信,因此您无需每次在命令行或浏览器中的应用程序管理器上登录时使用参数-skip ssl

  • cf marketplace命令(一旦您以用户身份登录)显示可以在组织和空间中安装的各种服务。

让我们看一下迄今为止讨论的命令的输出:

正如我们在市场中看到的,由于我们使用了所有服务选项启动 PCF Dev,我们可以看到市场已准备好了七项服务。

在 PCF 上创建 MySQL 服务

从列表中,在本章中,我们将配置我们的product服务以与 MySQL 数据库一起工作,并在下一章中查看 Spring Cloud 服务,例如断路器仪表板和其他服务。

运行以下命令:

cf create-service p-mysql 512mb prod-db 

检查服务是否正在运行:

在 PCF Dev 上运行产品服务

让我们创建product服务的简化版本,它只是连接到我们之前创建的 MySQL 服务以运行查询。

您可以从第三章的练习代码设计您的云原生应用程序中编写练习代码,也可以从 Git 下载文件到您的 Eclipse 环境。值得注意的工件有:

  • 在 Maven 文件中:

  • 请注意,在下面的截图中,我们已将我们的工件重命名为pcf-product

  • 一个值得注意的新依赖是spring-cloud-cloudfoundry-connector。它发现了绑定到 Cloud Foundry 应用程序的服务,比如 MySQL 配置,并使用它们。

  • 我们已经为 JPA 包含了一个 MySQL 连接器,用于连接到 MySQL 数据库:

  • application.properties文件中:

  • 请注意,我们没有提供任何 MySQL 连接属性,比如数据库、用户或密码。当应用程序上传到 Cloud Foundry 并与 MySQL 数据库服务绑定时,这些属性会被 Spring 应用程序自动获取。

  • 在 MySQL 的自动创建设置中,仅在开发目的下应该为true,因为它会在每次应用程序部署时重新创建数据库。在 UAT 或生产配置文件中,此设置将为none

management.security.enabled=false
logging.level.org.hibernate.tool.hbm2ddl=DEBUG 
logging.level.org.hibernate.SQL=DEBUG 
spring.jpa.hibernate.ddl-auto=create 
  • ProductSpringApp类被简化为一个普通的 Spring Boot 启动应用程序。我们将在下一章中增强这一点,包括指标、查找、负载平衡、监控和管理:

  • ProductRepository类只有一个名为findByCatId的方法。其余的方法,比如getsavedeleteupdate都是在存储库中自动派生的。

  • ProductServiceproduct和其他类与第三章中的相同,设计您的云原生应用程序

  • manifest.yml文件中:

  • 这是一个包含部署到云 Foundry 的说明的新文件

  • 我们将编写最基本的版本,包括应用程序名称、分配 1GB 内存空间以及与 CloudFoundry 中的 MySQL 服务绑定

  • 随机路由允许应用程序在没有冲突的情况下获取 URL 的路由,以防多个版本的情况发生:

一旦项目准备好,运行mvn install来在target目录中创建全面的.jar文件。它的名称应该与manifest.yml文件中的.jar文件的名称匹配。

部署到 Cloud Foundry

部署到 Cloud Floundry 很简单,使用命令cf push pcf-product,如下所示:

Cloud Foundry 在空间中创建应用程序、创建路由以到达应用程序,然后将各种服务与应用程序绑定时做了很多工作。如果你对底层发生的事情感兴趣,也许应该多了解一下 Cloud Foundry。

部署完成后,您将看到以下成功方法:

注意在前面截图中生成的 URL。

它是http://pcf-product-undedicated-spirketting.local.pcfdev.io。我们将在下一章中看到如何缩短这个 URL。

如果在启动时出现错误,例如配置错误或缺少一些步骤,您可以通过在命令行中输入以下命令来查看日志:

cf logs pcf-product --recent 

现在是时候测试我们的服务了。在浏览器窗口中,运行通常运行的两个服务:

  • http://pcf-product-undedicated-spirketting.local.pcfdev.io/product/1

  • http://pcf-product-undedicated-spirketting.local.pcfdev.io/products?id=1

您将看到来自数据库的响应,即输出和日志,如下所示:

这完成了将我们简单的product服务部署到 PCF 上的 PCF Dev。

总结

在本章中,我们看到了支持云原生应用程序的各种运行时组件,并在各种运行时环境中运行了我们的应用程序,比如 Kubernetes 和 Cloud Foundry。

在下一章中,我们将在 AWS Cloud 上部署我们的服务。

第九章:平台部署 - AWS

在本章中,我们将介绍亚马逊 AWS 平台提供的一些部署选项。 AWS 平台是云服务提供商中最古老和最成熟的之一。它于 2002 年推出,并自那时以来一直是该领域的领导者。 AWS 还不断创新,并推出了几项新服务,这些服务在广泛的客户群中得到了广泛的采用,从单人创业公司到企业。

在本章中,我们将涵盖以下主题:

  • AWS 平台

  • AWS 平台部署选项

AWS 平台

亚马逊 AWS 是云计算的先驱,并自那时起一直在扩展其云服务,以保持其领先地位。以下图表提供了 AWS 平台为应用程序开发人员提供的服务的指示性列表:

这只是一个指示性列表,绝不是详尽的列表;请参阅亚马逊 AWS 门户网站获取完整列表。

类别如下:

  • 基础设施:这可能是 AWS 平台的核心,使其能够提供大量其他服务。这些可以进一步分类为:

  • 计算:诸如 EC2,Lambda,ECS 和 ELB 之类的服务。我们将演示使用主要计算服务部署我们的示例应用程序,但是将它们与 AWS 提供的其他服务相结合相对容易。

  • 存储:诸如 S3,EBS 和 CloudFront 之类的服务。

  • 网络:诸如 VPC,Route53 和 DirectConnect 之类的服务。

  • 应用程序:这些服务可用作构建和支持应用程序的组件。

  • 数据库:这些服务针对数据库,提供对不同的关系数据库管理系统(RDBMS)和 NoSQL 数据存储的访问。

  • DevOps:这些服务提供了构建流水线和启用持续交付的能力。这些包括源代码托管,持续集成工具以及云和软件供应工具。

  • 安全性:这些服务为 AWS 提供了基于角色的访问控制(RBAC),并提供了一种机制来指定配额并强制执行它们,密钥管理和秘密存储。

  • 移动:这些服务旨在为移动应用程序和通知等服务提供后端。

  • 分析:这些服务包括 MapReduce 等批处理系统,以及 Spark 等流处理系统,可用于构建分析平台。

AWS 平台部署选项

在 AWS 平台提供的各种服务中,我们将重点关注本章涵盖的一些部署选项,这些选项专门针对我们一直作为示例使用的 Web API 类型。因此,我们将介绍部署到以下内容:

  • AWS Elastic Beanstalk

  • AWS 弹性容器服务

  • AWS Lambda

由于我们将在云环境中运行应用程序,因此我们将不需要直接管理基础设施,也就是说,我们将不会启动虚拟机并在其中安装应用程序,因此我们将不需要服务发现,因为弹性负载均衡器将自动路由到所有正在运行的应用程序实例。因此,我们将使用不使用 Eureka 发现客户端的“产品”API 的版本:

package com.mycompany.product;

 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;

 @SpringBootApplication
 public class ProductSpringApp {

    public static void main(String[] args) throws Exception {
       SpringApplication.run(ProductSpringApp.class, args);
    }

 }

将 Spring Boot API 部署到 Beanstalk

AWS Elastic Beanstalk(AEB)是 AWS 提供的一项服务,可在 AWS 上托管 Web 应用程序,而无需直接提供或管理 IaaS 层。 AEB 支持流行的语言,如 Java,.NET,Python,Ruby,Go 和 PHP。最近,它还提供了运行 Docker 容器的支持。我们将采用我们迄今为止在旅程中构建的“产品”服务的简化版本,并将其部署在 AEB 中作为可运行的 JAR 文件,也作为 Docker 容器。

部署可运行的 JAR

登录到 AWS 控制台,选择计算类别下的弹性 Beanstalk 服务,并点击“开始”按钮:

在下一个屏幕中填写应用程序详细信息:

上传target文件夹中的product.jar,然后点击“配置更多选项”按钮。您将看到不同的类别,可以通过选择软件,在环境属性下,添加一个名为SERVER_PORT的新环境变量,并将值设置为5000。这是必要的,因为默认情况下,AEB 环境创建的 NGINX 服务器将代理所有请求到这个端口,通过设置变量,我们确保我们的 Spring Boot 应用将在端口5000上运行:

现在,AWS 将提供一个新的环境,我们的应用程序将在其中运行:

环境创建完成后,AEB 将为应用程序生成一个 URL:

我们可以使用此 URL 访问 API 端点:

部署 Docker 容器

现在我们已经学会了如何将可运行的 JAR 部署到弹性 Beanstalk 服务,让我们也看一下相同的变体,我们将部署运行相同应用程序的 Docker 容器。使用 Docker 容器的优势在于,我们可以使用 AWS 弹性 Beanstalk 服务尚未支持的语言和平台,并且仍然可以在云中部署它们,从而获得该服务提供的好处。

对于此部署,我们将使用弹性容器服务ECS)提供的 Docker 注册表来存储我们从应用程序构建的 Docker 容器。当我们部署到 ECS 时,我们将介绍如何将本地 Docker 容器推送到 ECS 存储库。现在,让我们假设我们要部署的 Docker 容器在名为<aws-account-id>.dkr.ecr.us-west-2.amazonaws.com/product-api的存储库中可用。由于我们需要访问此存储库,我们需要将 AmazonEC2ContainerRegistryReadOnly 策略添加到默认的弹性 Beanstalk 角色 aws-elasticbeanstalk-ec2-role。

这可以在 IAM 控制台的角色部分完成:

创建一个名为Dockerfile.aws.json的文件,内容如下:

{ 
  "AWSEBDockerrunVersion": "1", 
  "Image": { 
    "Name": "<aws-account-id>.dkr.ecr.us-west-2.amazonaws.com/product-api", 
    "Update": "true" 
  }, 
  "Ports": [ 
    { 
      "ContainerPort": "8080" 
    } 
  ] 
} 

现在我们准备部署我们的 Docker 容器。在弹性 Beanstalk 控制台中,我们将选择单个 Docker 容器而不是 Java,并创建一个新的应用程序:

选择并上传Dockerfile.aws.json以创建环境:

我们可以测试我们的 API 端点,以验证我们的 Docker 容器是否正常运行。我们还可以配置容器使用 Amazon CloudWatch 日志记录和监控,以更好地监视我们的应用程序:

将 Spring Boot 应用程序部署到弹性容器服务

AWS 弹性容器服务ECS)是一项允许用户使用托管的 Docker 实例部署应用程序的服务。在这里,AWS ECS 服务负责提供虚拟机和 Docker 安装。我们可以通过以下步骤部署我们的应用程序:

  1. 启动 ECS,点击“继续”:

  1. 创建名为product-api的 ECS 存储库,然后点击“下一步”:

  1. 构建并推送 Docker 容器到存储库,按照屏幕上给出的说明进行:

  1. GUI 生成的 Docker 登录命令多了一个http://,应该去掉:

  1. 我们现在可以构建并推送 Docker 容器到创建的存储库:

  1. 在配置任务定义时,我们将使用此容器存储库:

  1. 在高级选项中,我们可以配置 AWS CloudWatch 日志记录,以捕获来自 Docker 容器的日志,在存储和日志记录部分下:

  1. 我们需要在 CloudWatch 控制台中创建相应的日志组,以捕获从我们的应用程序创建的日志:

  1. 我们可以创建一个服务映射到容器中公开的端口,即8080:

  1. 可选地,我们可以描绘 EC2 实例类型并配置密钥对,以便我们能够登录到 ECS 将为我们的应用程序创建的 EC2 实例中:

  1. 一旦我们审查配置并提交,ECS 将开始创建 EC2 实例并将我们的应用程序部署到其中:

  1. 我们可以点击自动扩展组并找到已启动的实例:

  1. 找到实例:

  1. 找到实例主机名:

  1. 通过实例主机名访问应用程序:

但是逐个通过它们的主机名访问应用程序是不可行的,因此,我们将创建一个弹性负载均衡器,它将路由请求到实例,从而允许我们在扩展或缩减时拥有稳定的端点:

  1. 我们将转到 EC2 控制台,并在应用程序负载均衡器下选择创建:

  1. 配置负载均衡器端口:

  1. 配置目标组和健康检查端点:

  1. 将目标实例注册到我们的集群定义创建的实例:

  1. 找到负载均衡器的 DNS 记录:

  1. 连接到负载均衡器端点并验证应用程序是否正常工作:

部署到 AWS Lambda

AWS Lambda 服务允许部署简单函数以在事件触发器上调用。这些事件触发器可以分为四种类型,即:

  • 数据存储(例如,AWS DyanmoDB)

  • 队列和流(例如,AWS Kinesis)

  • Blob 存储(例如,AWS S3)

  • API 数据门户:

AWS Lamda 支持的事件源的完整列表可以在docs.aws.amazon.com/lambda/latest/dg/invoking-lambda-function.html#api-gateway-with-lambda.找到

与之前讨论的其他部署选项不同,AWS Lambda 提供了最透明的扩展选项,AWS 平台根据需求自动扩展所需的实例。我们无需配置实例、负载均衡器等,而是可以专注于应用程序逻辑。

我们现在将构建一个简单的 AWS Lambda 函数,并将其绑定到 API 端点以调用它。

我们将首先创建一个新的 Spring Boot 应用程序,具有以下依赖项。我们还将使用maven-shade-plugin创建可运行的 JAR:

<project  
          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.mycompany</groupId>
   <artifactId>hello-lambda</artifactId>
   <version>0.0.1-SNAPSHOT</version>

   <dependencies>
     <dependency>
       <groupId>junit</groupId>
       <artifactId>junit</artifactId>
       <version>4.12</version>
       <scope>test</scope>
     </dependency>
     <dependency>
       <groupId>com.amazonaws</groupId>
       <artifactId>aws-lambda-java-core</artifactId>
       <version>1.1.0</version>
     </dependency>
     <dependency>
       <groupId>com.amazonaws</groupId>
       <artifactId>aws-lambda-java-events</artifactId>
       <version>2.0.1</version>
     </dependency>
     <dependency>
       <groupId>com.amazonaws</groupId>
       <artifactId>aws-lambda-java-log4j2</artifactId>
       <version>1.0.0</version>
     </dependency>
   </dependencies>

   <build>
     <finalName>hello-lambda</finalName>
     <plugins>
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-compiler-plugin</artifactId>
         <configuration>
           <source>1.8</source>
           <target>1.8</target>
         </configuration>
       </plugin>
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-shade-plugin</artifactId>
         <version>3.0.0</version>
         <configuration>
           <createDependencyReducedPom>false</createDependencyReducedPom>
         </configuration>
         <executions>
           <execution>
             <phase>package</phase>
             <goals>
               <goal>shade</goal>
             </goals>
           </execution>
         </executions>
       </plugin>
     </plugins>
   </build>

 </project>

现在创建HelloHandler.java,内容如下:

package com.mycompany;

 import com.amazonaws.services.lambda.runtime.Context;
 import com.amazonaws.services.lambda.runtime.RequestHandler;
 import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
 import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;

 import java.net.HttpURLConnection;

 public class HelloHandler implements RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {

   @Override
   public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent request, Context context) {
     String who = "World";
     if ( request.getPathParameters() != null ) {
       String name  = request.getPathParameters().get("name");
       if ( name != null && !"".equals(name.trim()) ) {
         who = name;
       }
     }
     return new APIGatewayProxyResponseEvent().withStatusCode(HttpURLConnection.HTTP_OK).withBody(String.format("Hello %s!", who));
   }

 }

由于 lambda 函数是简单的函数,我们可以通过使用函数的输入和输出很容易地测试它们。例如,一个示例测试用例可能是:

package com.mycompany;

 import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
 import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.BlockJUnit4ClassRunner;

 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;

 import static org.junit.Assert.*;

 @RunWith(BlockJUnit4ClassRunner.class)
 public class HelloHandlerTest {

   HelloHandler handler;
   APIGatewayProxyRequestEvent input;
   @Before
   public void setUp() throws Exception {
     handler = new HelloHandler();
     Map<String, String> pathParams = new HashMap<>();
     pathParams.put("name", "Universe");
     input = new APIGatewayProxyRequestEvent().withPath("/hello").withPathParamters(pathParams);
   }

   @Test
   public void handleRequest() {
     APIGatewayProxyResponseEvent res = handler.handleRequest(input, null);
     assertNotNull(res);
     assertEquals("Hello Universe!", res.getBody());
   }
   @Test
   public void handleEmptyRequest() {
     input.withPathParamters(Collections.emptyMap());
     APIGatewayProxyResponseEvent res = handler.handleRequest(input, null);
     assertNotNull(res);
     assertEquals("Hello World!", res.getBody());
   }
 }

现在我们可以使用 Maven 构建 lambda 函数:

$ mvn clean package 
[INFO] Scanning for projects... 
[WARNING] 
[WARNING] Some problems were encountered while building the effective model for com.mycompany:hello-lambda:jar:0.0.1-SNAPSHOT 
[WARNING] 'build.plugins.plugin.version' for org.apache.maven.plugins:maven-compiler-plugin is missing. @ line 35, column 15 
[WARNING] 
[WARNING] It is highly recommended to fix these problems because they threaten the stability of your build. 
[WARNING] 
[WARNING] For this reason, future Maven versions might no longer support building such malformed projects. 
[WARNING] 
[INFO] 
[INFO] ------------------------------------------------------------------------ 
[INFO] Building hello-lambda 0.0.1-SNAPSHOT 
[INFO] ------------------------------------------------------------------------ 
[INFO] 
[INFO] --- maven-clean-plugin:2.5:clean (default-clean) @ hello-lambda --- 
[INFO] Deleting /Users/shyam/workspaces/msa-wsp/CloudNativeJava/chapter-09/hello-lambda/target 
[INFO] 
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ hello-lambda --- 
[WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent! 
[INFO] skip non existing resourceDirectory /Users/shyam/workspaces/msa-wsp/CloudNativeJava/chapter-09/hello-lambda/src/main/resources 
[INFO] 
[INFO] --- maven-compiler-plugin:3.1:compile (default-compile) @ hello-lambda --- 
[INFO] Changes detected - recompiling the module! 
[WARNING] File encoding has not been set, using platform encoding UTF-8, i.e. build is platform dependent! 
[INFO] Compiling 1 source file to /Users/shyam/workspaces/msa-wsp/CloudNativeJava/chapter-09/hello-lambda/target/classes 
[INFO] 
[INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ hello-lambda --- 
[WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent! 
[INFO] skip non existing resourceDirectory /Users/shyam/workspaces/msa-wsp/CloudNativeJava/chapter-09/hello-lambda/src/test/resources 
[INFO] 
[INFO] --- maven-compiler-plugin:3.1:testCompile (default-testCompile) @ hello-lambda --- 
[INFO] Changes detected - recompiling the module! 
[WARNING] File encoding has not been set, using platform encoding UTF-8, i.e. build is platform dependent! 
[INFO] Compiling 1 source file to /Users/shyam/workspaces/msa-wsp/CloudNativeJava/chapter-09/hello-lambda/target/test-classes 
[INFO] 
[INFO] --- maven-surefire-plugin:2.12.4:test (default-test) @ hello-lambda --- 
[INFO] Surefire report directory: /Users/shyam/workspaces/msa-wsp/CloudNativeJava/chapter-09/hello-lambda/target/surefire-reports 

------------------------------------------------------- 
 T E S T S 
------------------------------------------------------- 
Running com.mycompany.HelloHandlerTest 
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.055 sec 

Results : 

Tests run: 2, Failures: 0, Errors: 0, Skipped: 0 

[INFO] 
[INFO] --- maven-jar-plugin:2.4:jar (default-jar) @ hello-lambda --- 
[INFO] Building jar: /Users/shyam/workspaces/msa-wsp/CloudNativeJava/chapter-09/hello-lambda/target/hello-lambda.jar 
[INFO] 
[INFO] --- maven-shade-plugin:3.0.0:shade (default) @ hello-lambda --- 
[INFO] Including com.amazonaws:aws-lambda-java-core:jar:1.1.0 in the shaded jar. 
[INFO] Including com.amazonaws:aws-lambda-java-events:jar:2.0.1 in the shaded jar. 
[INFO] Including joda-time:joda-time:jar:2.6 in the shaded jar. 
[INFO] Including com.amazonaws:aws-lambda-java-log4j2:jar:1.0.0 in the shaded jar. 
[INFO] Including org.apache.logging.log4j:log4j-core:jar:2.8.2 in the shaded jar. 
[INFO] Including org.apache.logging.log4j:log4j-api:jar:2.8.2 in the shaded jar. 
[INFO] Replacing original artifact with shaded artifact. 
[INFO] Replacing /Users/shyam/workspaces/msa-wsp/CloudNativeJava/chapter-09/hello-lambda/target/hello-lambda.jar with /Users/shyam/workspaces/msa-wsp/CloudNativeJava/chapter-09/hello-lambda/target/hello-lambda-0.0.1-SNAPSHOT-shaded.jar 
[INFO] ------------------------------------------------------------------------ 
[INFO] BUILD SUCCESS 
[INFO] ------------------------------------------------------------------------ 
[INFO] Total time: 2.549 s 
[INFO] Finished at: 2018-02-12T13:52:14+05:30 
[INFO] Final Memory: 25M/300M 
[INFO] ------------------------------------------------------------------------ 

我们现在已经构建了hello-lambda.jar,我们将上传到 AWS 控制台中创建的 AWS Lambda 函数。

  1. 我们将首先转到 API Gateway 控制台,该控制台出现在 AWS 控制台的网络和内容交付类别中,并创建一个新的 API:

  1. 我们将为路径/hello添加一个名为hello的新资源:

  1. 我们还将创建一个带有路径参数的子资源:

  1. 现在,我们将附加 HTTP GET方法:

  1. 创建一个具有以下详细信息的 Lambda 函数:

  1. 上传可运行的 JAR 并设置处理程序方法:

  1. 现在将此 Lambda 函数添加到 API 方法中:

  1. 确保选择使用 Lambda 代理集成,以便我们可以使用特定的RequestHandler接口,而不是使用通用的RequestStreamHandler。这也将使 API Gateway 获得对 Lambda 函数的权限:

  1. 使用 Lambda 函数调用完成 API 定义:

  1. 我们可以从控制台测试 API 端点:

  1. 现在我们可以部署 API:

  1. 成功部署 API 将导致 API 端点:

  1. 现在我们可以使用为此部署环境生成的 API 端点来访问应用程序:

总结

在本章中,我们介绍了 AWS 平台提供的一些选项,以及我们如何可以从弹性 Beanstalk 部署我们的应用程序,这是针对 Web 应用程序的。我们部署到 ECS,用于部署容器化工作负载,不限于 Web 应用程序工作负载。然后,我们部署了一个 AWS Lambda 函数,无需配置底层硬件。在接下来的章节中,我们将看一下使用 Azure 进行部署,以了解它为部署云原生应用程序提供的一些服务。

第十章:平台部署 - Azure

本章讨论了 Azure 的应用程序设计和部署——这是微软的公共云平台。云原生开发的本质是能够将您的应用程序与云提供商提供的 PaaS 平台集成。作为开发人员,您专注于创造价值(解决客户问题),并允许云提供商为您的应用程序的基础设施进行繁重的工作。

在本章中,我们将学习以下内容:

  • Azure 提供的不同类别的 PaaS 服务。我们将深入研究将被我们的样例应用程序使用的服务。

  • 将我们的样例应用程序迁移到 Azure,并了解各种可用选项。我们还将评估所有选项,并了解每个选项的利弊。

我们正在介绍 Azure 平台,目的是展示如何构建和部署应用程序。我们不打算深入研究 Azure,并期望读者使用 Azure 文档(docs.microsoft.com/en-us/azure/)来探索其他选项。

Azure 支持多种编程语言,但出于本书的目的,我们将关注 Azure 对 Java 应用程序的支持。

Azure 平台

Azure 提供了越来越多的 PaaS 和 IaaS,涵盖了各种技术领域。对于我们的目的,我们将关注直接适用于我们应用程序的子集领域和服务。

为了方便使用,我已经在最相关的技术领域中创建了这个服务分类模型:

这只是一个指示性列表,绝不是一个详尽的列表。请参考 Azure 门户以获取完整列表。

在前述分类模型中,我们将服务分为以下领域:

  • 基础设施:这是 Azure 提供的一系列服务,用于部署和托管我们的应用程序。我们已经将计算、存储和网络等服务结合在这个类别中。为了我们样例 Java 应用程序的目的,我们将研究以下一系列服务。

  • 应用服务:我们如何将现有的 Spring Boot 应用程序部署到我们的 Azure 平台?这更像是一个搬迁和部署的场景。在这里,应用程序没有重构,但依赖项被部署在应用服务上。使用其中一个数据库服务,应用程序可以被部署和托管。Azure 提供了 PostgreSQL 和 MySQL 作为托管数据库模型,还有其他各种选项。

  • 容器服务:对于打包为 Docker 容器的应用程序,我们可以探索如何将 Docker 容器部署到平台上。

  • 函数:这是无服务器平台模型,您无需担心应用程序的托管和部署。您创建一个函数,让平台为您进行繁重的工作。截至目前,基于 Java 的 Azure 云函数处于测试阶段。我们将探讨如何在开发环境中创建一个函数并进行本地测试。

  • 服务布局:服务布局是一个用于部署和管理微服务和容器应用程序的分布式系统平台。我们将探讨如何在服务布局中部署我们的样例“产品”API。

  • 应用程序:这是一个帮助构建分布式应用程序的服务列表。随着我们转向分布式微服务模型,我们需要解耦我们的应用程序组件和服务。队列、事件中心、事件网格和 API 管理等功能有助于构建一组稳健的 API 和服务。

  • 数据库:这是 Azure 平台提供的数据存储选项列表。其中包括关系型、键值、Redis 缓存和数据仓库等。

  • DevOps:对于在云中构建和部署应用程序,我们需要强大的 CI/CD 工具集的支持。Visual Studio 团队服务用于托管代码、问题跟踪和自动构建。同样,开源工具在 Azure 门户中仍然不是一流的公民。您可以随时使用所需软件的托管版本。

  • 安全:云应用程序的另一个关键因素是安全服务。在这一领域,提供了 Active Directory、权限管理、密钥保管库和多重身份验证等关键服务。

  • 移动:如果您正在构建移动应用程序,该平台提供了关键服务,如移动应用程序服务、媒体服务和移动参与服务等。

  • 分析:在分析领域,该平台通过 HDInsight 和数据湖服务提供了 MapReduce、Storm、Spark 等领域的强大服务,用于分析和数据存储库。

此外,Azure 还提供了多个其他技术领域的服务——物联网IoT)、监控、管理、人工智能AI)以及认知和企业集成领域。

Azure 平台部署选项

正如我们在前一节中看到的,Azure 提供了许多选项来构建和部署平台上的应用程序。我们将使用我们的“产品”API REST 服务的示例来检查 Azure 提供的各种选项,以部署和运行我们的应用程序。

在我们开始之前,我假设您熟悉 Azure 平台,并已经在门户中注册。

Azure 支持多种编程语言,并提供 SDK 以支持各自领域的开发。对于我们的目的,我们主要探索 Azure 平台内对 Java 应用程序的支持。

我们将在以下四个领域探索应用程序托管服务:

  • 应用服务

  • 容器服务

  • 服务织物

  • 功能

有关更多详细信息和入门,请参考以下链接:azure.microsoft.com/en-in/downloads/

将 Spring Boot API 部署到 Azure 应用服务

在本节中,我们将把我们的“产品”API 服务迁移到 Azure 应用服务。我们将查看应用程序为满足 Azure 应用服务的要求所做的额外更改。

我已经拿取了我们在第三章中构建的“产品”API REST 服务,设计您的云原生应用程序。在服务中,我们做出以下更改:

在项目的根文件夹中添加一个名为web.config的文件:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <system.webServer>
    <handlers>
      <add name="httpPlatformHandler" path="*" verb="*" 
       modules="httpPlatformHandler" resourceType="Unspecified"/>
    </handlers>
    <httpPlatform processPath="%JAVA_HOME%binjava.exe"
     arguments="-Djava.net.preferIPv4Stack=true -            
     Dserver.port=%HTTP_PLATFORM_PORT% -jar &quot;
     %HOME%sitewwwrootproduct-0.0.1-SNAPSHOT.jar&quot;">
    </httpPlatform>
  </system.webServer>
</configuration>

文件添加了以下更改,product-0.0.1-SNAPSHOT.jar,这是我们应用程序的包名称。如果您的应用程序名称不同,您将需要进行更改。

我们首先检查这里的“产品”API 代码:azure.microsoft.com/en-in/downloads/

我们运行mvn clean package命令将项目打包为一个 fat JAR:

[INFO] Scanning for projects... 
[INFO]                                                                          
[INFO] ------------------------------------------------------------------------ 
[INFO] Building product 0.0.1-SNAPSHOT 
[INFO] ------------------------------------------------------------------------ 
[INFO]  
[INFO] ...... 
[INFO]  
[INFO] --- maven-jar-plugin:2.6:jar (default-jar) @ product --- 
[INFO] Building jar: /Users/admin/Documents/workspace/CloudNativeJava/ch10-product/target/product-0.0.1-SNAPSHOT.jar 
[INFO]  
[INFO] --- spring-boot-maven-plugin:1.4.3.RELEASE:repackage (default) @ product --- 
[INFO] ------------------------------------------------------------------------ 
[INFO] BUILD SUCCESS 
[INFO] ------------------------------------------------------------------------ 
[INFO] Total time: 14.182 s 
[INFO] Finished at: 2018-01-15T15:06:56+05:30 
[INFO] Final Memory: 40M/353M 
[INFO] ------------------------------------------------------------------------ 

接下来,我们登录到 Azure 门户(portal.azure.com/)。

  1. 在左侧列中单击“应用服务”菜单项,如下截图所示:

在 Azure 门户中选择应用服务

  1. 单击“添加”链接:

  1. 接下来,单击所示的“Web 应用”链接:

通过 Azure 门户 | 应用服务 | 添加导航选择 Web 应用。

  1. 单击“创建”按钮链接,您应该会看到以下页面

  1. 我们填写我们的“产品”API 的详细信息。我已经填写了应用程序名称为ch10product,并将其他选项保留为默认。

  2. 接下来,单击页面底部的“创建”按钮。

这将创建应用服务。

  1. 我们点击 App Services 下的ch10product,这将带我们到菜单:

  1. 注意部署应用程序的 URL 和 FTP 主机名。我们需要在两个地方进行更改——应用程序设置和部署凭据:

  1. 我们点击“应用程序设置”链接,并在下拉菜单中选择以下选项:

  2. 选择 Java 8 作为 Java 版本

  3. 选择 Java 次要版本为最新

  4. 选择最新的 Tomcat 9.0 作为 Web 容器(实际上不会使用此容器;Azure 使用作为 Spring Boot 应用程序一部分捆绑的容器。)

  5. 点击保存

  1. 接下来,我们点击左侧的“部署凭据”链接。在这里,我们捕获 FTP/部署用户名和密码,以便能够将我们的应用程序推送到主机,并点击保存,如下截图所示:

  1. 连接到我们在步骤 8中看到的 FTP 主机名,并使用步骤 10中保存的凭据登录:
ftp  
open ftp://waws-prod-dm1-035.ftp.azurewebsites.windows.net 
user ch10productwrite2munish 
password *******
  1. 接下来,我们切换到远程服务器上的site/wwwroot目录,并将 fat JAR 和web.config传输到该文件夹:
cd site/wwwroot 
put product-0.0.1-SNAPSHOT.jar 
put web.config 
  1. 我们返回到概述部分并重新启动应用程序。我们应该能够启动应用程序并看到我们的 REST API 正常工作。

在本节中,我们看到了如何将现有的 REST API 应用程序部署到 Azure。这不是部署的最简单和最佳方式。这个选项更多的是一种搬迁,我们将现有的应用程序迁移到云中。对于部署 Web 应用程序,Azure 提供了一个 Maven 插件,可以直接将您的应用程序推送到云中。有关更多详细信息,请参阅以下链接:docs.microsoft.com/en-in/java/azure/spring-framework/deploy-spring-boot-java-app-with-maven-plugin

REST API 部署在 Windows Server VM 上。Azure 正在增加对 Java 应用程序的支持,但它们的长处仍然是.NET 应用程序。

如果您想使用 Linux 并部署 REST API 应用程序,您可以选择使用基于 Docker 的部署。我们将在下一节介绍基于 Docker 的部署。

将 Docker 容器部署到 Azure 容器服务

让我们部署我们的 Docker 容器应用程序。我已经为上一节中使用的“产品”API 示例创建了 Docker 镜像。可以通过以下命令从 Docker hub 拉取 Docker 镜像:

docker pull cloudnativejava/ch10productapi 

让我们开始并登录到 Azure 门户。我们应该看到以下内容:

  1. 点击左侧栏的“应用服务”菜单项。我们应该看到以下屏幕。点击“新建”,如截图所示:

  1. 在“新建”中搜索Web App for Containers

  1. 选择 Web App for Containers 后,点击“创建”如指示的那样:

通过 App Services | 添加 | Web App 导航选择创建

  1. 我们将填写我们的product API 容器的详细信息:

  2. 我已经填写了应用程序名称和资源组为ch10productContainer,并将其他选项保持默认。

  3. 在“配置容器”部分,我们选择容器存储库。如果 Docker hub 中已经有 Docker 镜像,请提供镜像拉取标签cloudnativejava/ch10productapi

  4. 点击页面底部的“确定”。它会验证图像。

  5. 接下来,我们点击页面底部的“创建”:

通过 Azure 门户导航选择创建|新建|搜索Web App for Containers

  1. 这将创建应用服务:

通过 Azure 门户导航选择新创建的应用程序容器|应用服务

  1. 我们点击 App Services 下的ch10productcontainer,这将带我们到菜单,我们可以看到标记的 URL,https://ch10productcontainer.azurewebsites.net,容器可用的地方。

主机 Docker 应用程序可以访问的 URL

  1. 我们可以在浏览器中看到我们的product API 正在运行:

这是将您的应用程序部署到云平台的一种简单方法。在前面的两种情况下,我们都没有使用任何专门的应用程序或数据存储服务。对于真正的云原生应用程序,我们需要利用提供者提供的平台服务。整个想法是应用程序的可扩展性和可用性方面的重要工作由本地平台处理。我们作为开发人员,专注于构建关键的业务功能和与其他组件的集成。

将 Spring Boot API 部署到 Azure Service Fabric

构建和部署应用程序到基础 IaaS 平台是大多数组织开始与公共云提供商合作的方式。随着云流程的舒适度和成熟度的提高,应用程序开始具备 PaaS 功能。因此,应用程序开始包括排队、事件处理、托管数据存储、安全性和其他平台服务的功能。

但是,关于非功能需求,一个关键问题仍然存在。谁会考虑应用程序的能力?

  • 如何确保有足够的应用程序实例在运行?

  • 当实例宕机时会发生什么?

  • 应用程序如何根据流量的增减而扩展/缩减?

  • 我们如何监视所有运行的实例?

  • 我们如何管理分布式有状态服务?

  • 我们如何对部署的服务执行滚动升级?

编排引擎登场。诸如 Kubernetes、Mesos 和 Docker Swarm 等产品提供了管理应用程序容器的能力。Azure 发布了 Service Fabric,这是用于应用程序/容器管理的软件。它可以在本地或云中运行。

Service Fabric 提供以下关键功能:

  • 允许您部署可以大规模扩展并提供自愈平台的应用程序

  • 允许您安装/部署有状态和无状态的基于微服务的应用程序

  • 提供监视和诊断应用程序健康状况的仪表板

  • 定义自动修复和升级的策略

在当前版本中,Service Fabric 支持两种基础操作系统——Windows Server 和 Ubuntu 16.04 的版本。最好选择 Windows Server 集群,因为支持、工具和文档是最好的。

为了演示 Service Fabric 的功能和用法,我将使用 Ubuntu 镜像进行本地测试,并使用 Service Fabric party 集群将我们的product API 示例在线部署到 Service Fabric 集群。我们还将研究如何扩展应用程序实例和 Service Fabric 的自愈功能。

基本环境设置

我使用的是 macOS 机器。我们需要设置以下内容:

  1. 本地 Service Fabric 集群设置——拉取 Docker 镜像:
docker pull servicefabricoss/service-fabric-onebox 
  1. 在主机上更新 Docker 守护程序配置,并重新启动 Docker 守护程序:
{ 
    "ipv6": true, 
    "fixed-cidr-v6": "fd00::/64" 
}
  1. 启动从 Docker Hub 拉取的 Docker 镜像:
docker run -itd -p 19080:19080 servicefabricoss/service-fabric-onebox bash 
  1. 在容器 shell 中添加以下命令:
./setup.sh      
./run.sh        

完成最后一步后,将启动一个可以从浏览器访问的开发 Service Fabric 集群,地址为http://localhost:19080

现在我们需要为容器和客户可执行文件设置 Yeoman 生成器:

  1. 首先,我们需要确保 Node.js 和 Node Package Manager(NPM)已安装。可以使用 HomeBrew 安装该软件,如下所示:
brew install node node -v npm -v 
  1. 接下来,我们从 NPM 安装 Yeoman 模板生成器:
npm install -g yo 
  1. 接下来,我们安装将用于使用 Yeoman 创建 Service Fabric 应用程序的 Yeoman 生成器。按照以下步骤进行:
# for Service Fabric Java Applications npm install -g generator-azuresfjava # for Service Fabric Guest executables npm install -g generator-azuresfguest # for Service Fabric Container Applications npm install -g generator-azuresfcontainer
  1. 要在 macOS 上构建 Service Fabric Java 应用程序,主机机器必须安装 JDK 版本 1.8 和 Gradle。可以使用 Homebrew 安装该软件,方法如下:
brew update 
brew cask install java 
brew install gradle 

这样就完成了环境设置。接下来,我们将把我们的product API 应用程序打包为 Service Fabric 应用程序,以便在集群中进行部署。

打包产品 API 应用程序

我们登录到product API 项目(完整代码可在github.com/PacktPublishing/Cloud-Native-Applications-in-Java找到),并运行以下命令:

yo azuresfguest

我们应该看到以下屏幕:

我们输入以下值:

这将创建一个包含一组文件的应用程序包:

ProductServiceFabric/ProductServiceFabric/ApplicationManifest.xml 
ProductServiceFabric/ProductServiceFabric/ProductAPIPkg/ServiceManifest.xml 
ProductServiceFabric/ProductServiceFabric/ProductAPIPkg/config/Settings.xml 
ProductServiceFabric/install.sh 
ProductServiceFabric/uninstall.sh 

接下来,我们转到/ProductServiceFabric/ProductServiceFabric/ProductAPIPkg文件夹。

创建一个名为code的目录,并在其中创建一个名为entryPoint.sh的文件,其中包含以下内容:

#!/bin/bash 
BASEDIR=$(dirname $0) 
cd $BASEDIR 
java -jar product-0.0.1-SNAPSHOT.jar 

还要确保将我们打包的 JAR(product-0.0.1-SNAPSHOT.jar)复制到此文件夹中。

Number of instances of guest binary的值应该是1,用于本地环境开发,对于云中的 Service Fabric 集群,可以是更高的数字。

接下来,我们将在 Service Fabric 集群中托管我们的应用程序。我们将利用 Service Fabric party 集群。

启动 Service Fabric 集群

我们将使用我们的 Facebook 或 GitHub ID 登录try.servicefabric.azure.com

加入 Linux 集群:

我们将被引导到包含集群详细信息的页面。该集群可用时间为一小时。

默认情况下,某些端口是开放的。当我们部署我们的product API 应用程序时,我们可以在端口8080上访问相同的应用程序:

Service Fabric 集群资源管理器可在先前提到的 URL 上找到。由于集群使用基于证书的身份验证,您需要将 PFX 文件导入到您的钥匙链中。

如果您访问该 URL,您可以看到 Service Fabric 集群资源管理器。默认情况下,该集群有三个节点。您可以将多个应用程序部署到集群中。根据应用程序设置,集群将管理您的应用程序可用性。

Azure Party 集群默认视图

将产品 API 应用程序部署到 Service Fabric 集群

要将我们的应用程序部署到为应用程序创建的 Service Fabric 脚手架的ProductServiceFabric文件夹中,我们需要登录。

连接到本地集群

我们可以使用以下命令在此处连接到本地集群:

sfctl cluster select --endpoint http://localhost:19080 

这将连接到在 Docker 容器中运行的 Service Fabric 集群。

连接到 Service Fabric party 集群

由于 Service Fabric party 集群使用基于证书的身份验证,我们需要在/ProductServiceFabric工作文件夹中下载 PFX 文件。

运行以下命令:

openssl pkcs12 -in party-cluster-1496019028-client-cert.pfx -out party-cluster-1496019028-client-cert.pem -nodes -passin pass: 

接下来,我们将使用隐私增强邮件PEM)文件连接到 Service Fabric party 集群:

sfctl cluster select --endpoint https://zlnxyngsvzoe.westus.cloudapp.azure.com:19080 --pem ./party-cluster-1496019028-client-cert.pem --no-verify 

一旦我们连接到 Service Fabric 集群,我们需要通过运行以下命令来安装我们的应用程序:

./install.sh 

我们应该看到我们的应用程序被上传并部署到集群中:

安装并启动 Docker 容器中的 Service Fabric 集群

一旦应用程序被上传,我们可以在 Service Fabric 资源管理器中看到应用程序,并且可以访问应用程序的功能:

观察在 Azure Party Cluster 中部署的应用程序

API 功能可在以下网址找到:http://zlnxyngsvzoe.westus.cloudapp.azure.com:8080/product/2

验证 API 是否正常工作

我们可以看到应用程序当前部署在一个节点(_lnxvm_2)上。如果我们关闭该节点,应用程序实例将自动部署在另一个节点实例上:

观察应用程序部署在三个可用主机中的单个节点上

通过选择节点菜单中的选项(在下面的截图中突出显示)来关闭节点(_lnxvm_2):

观察在 Azure Party Cluster 上禁用主机的选项

立即,我们可以看到应用程序作为集群的自愈模型部署在节点_lnxvm_0上:

在一个节点上禁用应用程序后,它会在 Service Fabric Cluster 的另一个节点上启动

再次,我希望读者足够好奇,继续探索集群的功能。对 Java 应用程序和多个版本的 Linux 的支持有限。Azure 正在努力增加对平台的额外支持,以支持各种类型的应用程序。

Azure 云函数

随着我们将应用程序迁移到云端,我们正在使用平台服务来提高我们对业务功能的关注,而不用担心应用程序的可伸缩性。无服务器应用程序是下一个前沿。开发人员的重点是构建应用程序,而不用担心服务器的配置、可用性和可伸缩性。

Java 函数目前处于测试阶段,不在 Azure 门户上提供。

我们可以下载并尝试在本地机器上创建 Java 函数。我们将看到功能的简要预览。

环境设置

Azure Functions Core Tools SDK 为编写、运行和调试 Java Azure Functions 提供了本地开发环境:

npm install -g azure-functions-core-tools@core 

创建一个新的 Java 函数项目

让我们创建一个示例 Java 函数项目。我们将利用以下 Maven 原型来生成虚拟项目结构:

mvn archetype:generate  -DarchetypeGroupId=com.microsoft.azure  -DarchetypeArtifactId=azure-functions-archetype 

我们运行mvn命令来提供必要的输入:

Define value for property 'groupId': : com.mycompany.product 
Define value for property 'artifactId': : mycompany-product 
Define value for property 'version':  1.0-SNAPSHOT: :  
Define value for property 'package':  com.mycompany.product: :  
Define value for property 'appName':  ${artifactId.toLowerCase()}-${package.getClass().forName("java.time.LocalDateTime").getMethod("now").invoke(null).format($package.Class.forName("java.time.format.DateTimeFormatter").getMethod("ofPattern", $package.Class).invoke(null, "yyyyMMddHHmmssSSS"))}: : productAPI 
Define value for property 'appRegion':  ${package.getClass().forName("java.lang.StringBuilder").getConstructor($package.getClass().forName("java.lang.String")).newInstance("westus").toString()}: : westus 
Confirm properties configuration: 
groupId: com.mycompany.product 
artifactId: mycompany-product 
version: 1.0-SNAPSHOT 
package: com.mycompany.product 
appName: productAPI 
appRegion: westus 
 Y: : y 

构建和运行 Java 函数

让我们继续构建包:

mvn clean package 

接下来,我们可以按以下方式运行函数:

mvn azure-functions:run 

我们可以在以下图像中看到函数的启动:

构建您的 Java 云函数

默认函数可在以下网址找到:

http://localhost:7071/api/hello 

如果我们访问http://localhost:7071/api/hello?name=cloudnative,我们可以看到函数的输出:

深入代码

如果我们深入代码,我们可以看到主要的代码文件,其中定义了默认函数hello

该方法使用@HttpTrigger进行注释,我们在其中定义了触发器的名称、允许的方法、使用的授权模型等。

当函数编译时,会生成一个function.json文件,其中定义了函数绑定。

{ 
  "scriptFile" : "../mycompany-product-1.0-SNAPSHOT.jar", 
  "entryPoint" : "productAPI.Function.hello", 
  "bindings" : [ { 
    "type" : "httpTrigger", 
    "name" : "req", 
    "direction" : "in", 
    "authLevel" : "anonymous", 
    "methods" : [ "get", "post" ] 
  }, { 
    "type" : "http", 
    "name" : "$return", 
    "direction" : "out" 
  } ], 
  "disabled" : false 
} 

您可以看到输入和输出数据绑定。函数只有一个触发器。触发器会携带一些相关数据触发函数,通常是触发函数的有效负载。

输入和输出绑定是一种声明性的方式,用于在代码内部连接数据。绑定是可选的,一个函数可以有多个输入和输出绑定。

您可以通过 Azure 门户开发函数。触发器和绑定直接在function.json文件中配置。

Java 函数仍然是一个预览功能。功能集仍处于测试阶段,文档很少。我们需要等待 Java 在 Azure Functions 的世界中成为一流公民。

这就是我们使用 Azure 进行平台开发的结束。

总结

在本章中,我们看到了 Azure 云平台提供的各种功能和服务。当我们将应用程序转移到云原生模型时,我们从应用服务|容器服务|服务布置|无服务器模型(云函数)中转移。当我们构建全新的应用程序时,我们跳过初始步骤,直接采用平台服务,实现自动应用程序可伸缩性和可用性管理。

在下一章中,我们将介绍各种类型的 XaaS API,包括 IaaS、PaaS、iPaaS 和 DBaaS。我们将介绍在构建自己的 XaaS 时涉及的架构和设计问题。

第十一章:作为服务集成

本章讨论了各种 XaaS 类型,包括基础设施即服务(IaaS)、平台即服务(PaaS)、集成平台即服务(iPaaS)和数据库即服务(DBaaS),以及在将基础设施或平台元素公开为服务时需要考虑的一切。在云原生模式下,您的应用程序可能正在集成社交媒体 API 或 PaaS API,或者您可能正在托管其他应用程序将使用的服务。本章涵盖了构建自己的 XaaS 模型时需要处理的问题。

本章将涵盖以下主题:

  • 构建自己的 XaaS 时的架构和设计问题

  • 构建移动应用程序时的架构和设计问题

  • 各种后端作为服务提供商——数据库、授权、云存储、分析等

XaaS

云计算开创了弹性、按需、IT 托管服务的分发模式。任何作为服务交付的 IT 部分都宽泛地归入云计算的范畴。

在云计算主题中,根据 IT 服务的类型,云的特定服务有各种术语。大多数术语是 XaaS 的不同变体,其中 X 是一个占位符,可以更改以代表多种事物。

让我们看看云计算的最常见交付模式:

  • IaaS:当计算资源(计算、网络和存储)作为服务提供以部署和运行操作系统和应用程序时,被称为 IaaS。如果组织不想投资于建立数据中心和购买服务器和存储,这是一种正确的选择。亚马逊网络服务(AWS)、Azure 和谷歌云平台(GCP)是 IaaS 提供商的主要例子。在这种模式下,您负责以下事项:

  • 管理、打补丁和升级所有操作系统、应用程序和相关工具、数据库系统等。

  • 从成本优化的角度来看,您将负责启动和关闭环境。

  • 计算资源的供应几乎是即时的。计算资源的弹性是 IaaS 供应商的最大卖点之一。

  • 通常,服务器镜像可以由云提供商备份,因此在使用云提供商时备份和恢复很容易管理。

  • PaaS:一旦计算、网络和存储问题解决,接下来就需要开发平台和相关环境来构建应用程序。PaaS 平台提供了整个软件开发生命周期(SDLC)的服务。运行时(如 Java 和.NET)、数据库(MySQL 和 Oracle)和 Web 服务器(如 Tomcat 和 Apache Web 服务器)等服务被视为 PaaS 服务。云计算供应商仍将管理运行时、中间件、操作系统、虚拟化、服务器、存储和网络的基础运营方面。在这种模式下,您将负责以下事项:

  • 开发人员的关注将局限于管理应用程序和相关数据。应用程序的任何更改/更新都需要由您管理。

  • PaaS 的抽象层级较高(消息传递、Lambda、容器等),使团队能够专注于核心能力,满足客户需求。

  • SaaS:接下来是您租用整个应用程序的模式。您不需要构建、部署或维护任何东西。您订阅应用程序,提供商将为您或您的组织提供一个应用程序实例供您使用。您可以通过浏览器访问应用程序,或者可以集成提供商提供的公共 API。Gmail、Office 365 和 Salesforce 等服务就是 SaaS 服务的例子。在这种模式下,提供商为所有租户提供标准版本的功能/功能,定制能力非常有限。SaaS 供应商可能提供一个安全模型,您可以使用轻量级目录访问协议LDAP)存储库与供应商集成,使用安全断言标记语言SAML)或 OAuth 模型。这种模式非常适用于定制需求较低的标准软件。Office365 和 Salesforce 是 SaaS 供应商的典范:

在构建您的组织及其应用程序组合时,您可能会订阅不同供应商提供的各种类型的服务。现在,如果您试图构建下一个 Facebook 或 Instagram 或 Uber,您将需要解决特定的架构问题,以满足全球数十亿用户的各种需求。

构建 XaaS 时的关键设计问题

让我们回顾一下在构建 XaaS 并为其提供消费服务时需要解决的关键设计问题:

  • 多租户:当您开始为公众使用设计您的服务时,首要要求之一是能够支持多个租户或客户。随着人们开始注册使用您的服务,服务需要能够为客户数据提供安全边界。通常,SaaS 是多租户设计问题的一个很好的候选者。对于每个租户,数据和应用程序工作负载可能需要进行分区。租户请求在租户数据的范围内。要在应用程序中设计多租户,您需要查看以下内容:

  • 隔离:数据应该在租户之间隔离。一个租户不应该能够访问任何其他租户的数据。这种隔离不仅限于数据,还可以扩展到底层资源(包括计算、存储、网络等)和为每个租户标记的操作过程(备份、恢复、DevOps、管理员功能、应用程序属性等)。

  • 成本优化:下一个重要问题是如何优化设计以降低云资源的总体成本,同时仍然满足各种客户的需求。您可以考虑多种技术来管理成本。例如,对于免费层客户,您可以基于租户 ID 的租赁模型。这种模型允许您优化数据库许可证、整体计算和存储成本、DevOps 流程等。同样,对于大客户,甚至可以考虑专用基础设施以提供保证的服务级别协议SLA)。有许多小公司从少数大客户那里获得数百万美元的业务。另一方面,有大公司为数百万小客户提供服务。

  • DevOps 流水线:如果您最终为客户构建同一服务的多个实例,当客户要求为他们提供特定功能时,您将遇到问题。这很快会导致代码碎片化,并成为一个难以管理的代码问题。问题在于如何平衡为所有客户推出新功能/功能的能力,同时仍能够提供每个客户所需的定制或个性化水平。DevOps 流程需要支持多租户隔离,并维护/监视每个租户的流程和数据库架构,以在所有服务实例中推出更改。除非 DevOps 得到简化,否则在整个服务中推出更改可能会变得非常复杂和令人望而却步。所有这些都会导致成本增加和客户满意度降低。

  • 可扩展性:其中一个基本要求是能够注册新客户并扩展服务。随着客户规模的增长,预期成本/服务或整体服务成本应该下降。除非我们的服务考虑到前面三种租户类型,否则服务将无法扩展并在您的业务模型周围提供人为的壕沟。

接下来,当您开始设计多租户服务时,您有以下设计选项:

    • 每个租户一个数据库:每个租户都有自己的数据库。这种模型为租户数据提供了完全隔离。
  • 共享数据库(单一):所有租户都托管在单个数据库中,并由租户 ID 标识。

  • 共享数据库(分片):在这种模型中,单个数据库被分片成多个数据库。通常,分片键是从哈希、范围或列表分区派生的。租户分布在分片中,并且可以通过租户 ID 和分片的组合访问:

  • 更快的配置:在构建 XaaS 模型时,另一个关键问题是能够为新客户提供配置的能力,这意味着客户的入职应该是自助的。注册后,客户应立即能够开始使用服务。所有这些都需要一个模型,其中新租户可以轻松快速地配置。提供基础计算资源、任何数据库架构创建和/或特定的 DevOps 流水线的能力应该非常高效和完全自动化。从客户体验的角度来看,能够为用户提供正在运行的应用程序版本也是有帮助的。对于任何旨在成为大众市场的服务,更快的配置都是必须的。但是,如果您提供的是非常特定的服务,并且需要与企业客户的本地数据中心集成,那么可能无法提供分秒级的配置。在这种情况下,我们应该构建可以尽快解决一些常见集成场景的工具/脚本,以尽快为客户提供服务。

  • 审计:安全性周围的另一个关键问题是审计对服务和基础数据存储的访问和更改的能力。所有审计跟踪都需要存储,以用于任何违规行为、安全问题或合规目的。将需要一个集中的审计存储库,用于跟踪系统中生成的事件。您应该能够在审计存储库之上运行分析,以标记任何异常行为并采取预防或纠正措施:

您可以利用 Lambda 架构,它同时使用实时流和从历史数据生成的模型来标记异常行为。一些公共云提供商提供此服务。

  • 安全性: 根据服务的性质,租户需要安全访问其数据。服务需要包含身份验证和授权的基本要求。所有客户都有安全密钥和密码短语来连接和访问其信息。可能需要企业访问和多个用户。在这种情况下,您可能需要为企业构建委托管理模型。您还可以使用 OAuth 等安全机制(通过 Google、Facebook 等)来启用对服务的访问。

  • 数据存储: 您的服务可能需要存储不同类型的数据;根据数据类型,存储需求将不同。存储需求通常分为以下几个领域:

  • 关系数据存储: 租户数据可能是关系型的,我们谈到了各种多租户策略来存储这些数据。租户特定的应用程序配置数据可能需要存储在关系模型中。

  • NoSQL 存储: 租户数据可能并非始终是关系型的;它可能是列式的、键值对的、图形的或面向文档的模型。在这种情况下,需要设计并构建适当的数据存储。

  • Blob 存储: 如果您的服务需要 Blob 存储或二进制数据存储,那么您将需要访问对象文件存储。您可以利用 AWS 或 Azure 等提供的 Blob 存储来存储您的二进制文件。

  • 监控: 需要监控整个应用程序堆栈。您可能会为客户签署严格的 SLA。在这种情况下,监控不仅仅是关于服务或系统的可用性,还涉及任何成本惩罚和声誉损失。有时,个别组件可能具有冗余和高可用性,但在堆栈级别,所有故障率可能会相互叠加,从而降低堆栈的整体可用性。跨堆栈监控资源变得重要,并且是管理可用性和定义的 SLA 的关键。监控涵盖硬件和软件。需要检测任何异常行为并自动执行纠正响应。通常,监控和自动修复需要多次迭代才能成熟。

  • 错误处理: 服务的关键方面之一将是处理故障的能力以及如何响应服务消费者。故障可能发生在多个级别;数据存储不可用、表被锁定、查询超时、服务实例宕机、会话数据丢失等都是您将遇到的一些问题。您的服务需要强大到能够处理所有这些以及更多的故障场景。诸如 CQRS、断路器、隔离、响应式等模式需要纳入到您的服务设计中。

  • 自动化构建/部署: 随着服务消费者数量的增加,推出新功能和修复错误将需要自动化的构建和部署模型。这类似于在汽车行驶时更换轮胎。升级软件并发布补丁/安全修复,而不会对消费者的调用产生任何影响,这是一门微妙的艺术,需要时间来掌握。以前,我们可以在夜间系统流量减少时寻找一些系统停机时间,但是随着来自世界各地的客户,再也没有这样的时间了。蓝绿部署是一种技术,可以帮助在对客户造成最小影响的情况下发布新变更,并降低整体风险:

  • 客户层:另一个关键问题是如何为不同的客户群建立和定价您的服务。公司一直在创建多个层次来满足众多客户的需求。这些需求帮助公司确定客户层,然后开始定价服务成本。这些因素如下:

  • 计算:限制每小时/每天/每月的调用次数。这使您能够预测租户所需的容量以及网络带宽要求。

  • 存储:另一个参数是底层数据存储所需的存储空间。这使您可以适当平衡数据库分片。

  • 安全性:对于企业客户,可能存在与 SAML 使用企业安全模型集成的单独要求。这可能需要额外的硬件和支持。

  • SLA/支持模型:这是另一个需要考虑的领域,当决定客户层时需要考虑。支持模型——社区、值班、专用等——具有不同的成本结构。根据目标市场——消费者或企业——您可以评估哪种支持模型最适合您的服务。

  • 功能标志:在构建 XaaS 模型时,一个关键问题是如何处理多个租户的代码更改、功能发布等。我应该为每个客户拥有多个代码分支,还是应该在所有客户之间使用一个代码库?如果我使用一个代码库,如何发布特定于一个租户的功能/功能?如果您的目标市场是 8-10 个客户,那么为每个客户拥有特定的代码分支是一个潜在的可行选项。但如果目标市场是数百个客户,那么代码分支是一个糟糕的选择。代码分支通常是一个糟糕的主意。为了处理不同客户的功能/功能差异或管理尚未准备发布的新功能,功能标志是处理此类要求的一个很好的方法。

功能标志允许您在生产中发布代码,而不立即为用户发布功能。您可以使用功能标志根据客户购买的服务级别为应用程序的不同客户提供/限制某些功能。您还可以与 A/B 测试结合使用功能标志,向部分用户发布新功能/功能,以检查其响应和功能正确性,然后再向更广泛的受众发布。

  • 自助服务门户:您的服务的一个关键方面将是一个自助服务门户,用户可以在那里注册、提供服务,并管理应用程序数据和服务的所有方面。该门户允许用户管理企业方面,如身份验证/授权(使用委托管理员模型)、监视已提供的服务的可用性,在服务的关键指标上设置自定义警报/警报,并解决可能在服务器端出现的任何问题。精心设计的门户有助于增加用户对服务性能的整体信心。您还可以为付费客户构建基于客户层的高级监控和分析服务。请记住,任何人都可以复制您的服务提供的功能/功能,但围绕您的服务构建附加值功能成为您服务的独特差异化因素。

  • 软件开发工具包(SDKs):作为启用用户采纳性的关键措施之一,您可能希望为您的消费者构建并提供 SDK。这不是必须的,但是是一个可取的特性,特别是当客户在应用程序代码级别与您的服务集成时。在这种情况下,SDK 应该支持多种语言,并提供良好的示例和文档,以帮助客户端的开发人员上手。如果您的应用程序或服务很复杂,那么拥有一个解释如何调用您的服务或与现有服务集成(如 SAML、OAuth 等)的 SDK 对于更快地采用您的服务至关重要。

  • 文档和社区支持:服务可采纳性的另一个方面是产品/服务的文档水平以及社区对其的支持。文档应该至少涵盖以下几点:

  • 如何注册该服务

  • 如何调用和使用服务

  • 如何将服务整合到客户的景观中以及可用于集成的 SDK

  • 如何批量导入或批量导出您的数据

  • 如何与企业 LDAP/Active Directory(AD)服务器进行安全整合进行身份验证/授权

接下来你需要考虑的是建立一个积极的社区支持。你需要为人们互动提供适当的论坛。你需要有积极的专业主题专家来回答来自各个论坛(内部和外部)的问题。像 Stack Overflow 这样的网站会收到很多问题;你应该设置警报,监控帖子,并帮助回答用户的问题/查询。一个积极的社区是对你的产品感兴趣的一个迹象。许多组织也利用这个论坛来识别早期采用者,并在产品路线图中寻求他们的反馈。

  • 产品路线图:一个好的产品可能从一个最小可行产品(MVP)开始,但通常都有一个坚实的愿景和产品路线图作为支持。当你从客户那里收到反馈时,你可以不断更新产品路线图并重新排列优先级。一个好的路线图表明了产品愿景的力量。当你遇到外部利益相关者——客户、合作伙伴、风险投资者等等——他们首先要求的是一个产品路线图。

路线图通常包括战略重点和计划发布,以及高层功能和维护/错误修复发布的计划,等等:

我们已经涵盖了一些在尝试构建您的 XaaS 模型时需要考虑的设计问题。我们已经涵盖了每个问题的基础知识。每个问题都需要至少一个章节。希望这能让您了解在尝试围绕 XaaS 构建业务模型时需要考虑的其他非服务方面。服务的实际设计和开发是基于我们从第二章开始涵盖的问题。

与第三方 API 的集成

在前一节中,我们看到了构建自己的服务提供商时的设计问题。在本节中,我们将看到,如果您正在尝试构建一个消费者应用程序,如何利用第三方公司提供的 REST 服务。例如,您正在尝试构建一个漂亮的移动应用程序,您的核心竞争力是构建视觉设计和创建移动应用程序。您不想被管理托管/管理应用程序数据的所有复杂性所拖累。该应用程序将需要包括存储、通知、位置、社交集成、用户管理、聊天功能和分析等服务。所有这些提供商都被归类为后端即服务BaaS)提供商。没有必要为这些服务注册单一供应商;您可以挑选符合您业务需求和预算的提供商。每个提供商通常都采用免费模式,每月提供一定数量的免费 API 调用,以及商业模式,您需要付费。这也属于构建无服务器应用程序的范畴,作为开发人员,您不需要维护任何运行软件的服务器。

在这方面,我们将看看构建一个完整的无服务器应用程序所需的第三方服务:

  • 身份验证服务:任何应用程序需要的第一件事情之一是能够注册用户。注册用户为应用程序开发人员提供了提供个性化服务并了解他的喜好/不喜欢的机会。这些数据使他能够优化用户体验并提供必要的支持,以从应用程序中获得最大价值。

身份验证作为服务专注于围绕用户身份验证的业务功能的封装。身份验证需要一个身份提供者。这个提供者可以映射到您的应用程序或企业,或者您可以使用一些消费者公司,如谷歌、Facebook、Twitter 等。有多个可用的身份验证服务提供商,如 Auth0、Back&、AuthRocket 等。这些提供商应该提供至少以下功能:

  • 多因素身份验证MFA)(包括对社交身份提供者的支持):作为主要要求之一,提供商应该提供身份提供者实例,应用程序可以在其中管理用户。功能包括用户注册,通过短信或电子邮件进行两因素身份验证,以及与社交身份提供者的集成。大多数提供商使用 OAuth2/OpenID 模型。

  • 用户管理:除了 MFA,身份验证提供商应该提供用户界面,允许对已注册应用程序的用户进行管理。您应该能够提取电子邮件和电话号码,以向客户发送推送通知。您应该能够重置用户凭据并通过使用安全领域或根据应用程序的需求将用户添加到某些预定义角色来保护资源。

  • 插件/小部件:最后但并非最不重要的是,提供商应该提供可以嵌入应用程序代码中以提供用户身份验证的小部件/插件作为无缝服务:

  • 无服务器服务:过去,您需要管理应用程序服务器和底层 VM 来部署代码。抽象级别已经转移到所谓的业务功能。您编写一个接受请求、处理请求并输出响应的函数。没有运行时,没有应用程序服务器,没有 Web 服务器,什么都没有。只有一个函数!提供商将自动提供运行时来运行该函数,以及服务器。作为开发人员,您不需要担心任何事情。您根据对函数的调用次数和函数运行时间的组合收费,这意味着在低谷时期,您不会产生任何费用。

通过函数,您可以访问数据存储并管理用户和应用程序特定数据。两个函数可以使用队列模型相互通信。函数可以通过提供商的 API 网关公开为 API。

所有公共云供应商都有一个无服务器模型的版本——AWS 有 Lamda,Azure 有 Azure Functions,Google 有 Cloud Functions,Bluemix 有 Openwhisk 等:

  • 数据库/存储服务:应用程序通常需要存储空间来管理客户数据。这可以是简单的用户配置文件信息(例如照片、姓名、电子邮件 ID、密码和应用程序首选项)或用户特定数据(例如消息、电子邮件和应用程序数据)。根据数据的类型和存储格式,可以选择适当的数据库/存储服务。对于二进制存储,我们有 AWS S3 和 Azure Blob Storage 等服务,适用于各种二进制文件。要直接从移动应用程序中以 JSON 格式存储数据,您可以使用 Google Firebase 等云提供商,或者您可以使用 MongoDB 作为服务(www.mlab.com)。AWS、Azure 和 GCP 提供了多种数据库模型,可用于管理各种不同的存储需求。您可能需要使用 AWS Lambda 或 Google Cloud Functions 来访问存储数据。例如,如果应用程序请求在存储数据之前需要进行一些验证或处理,您可以编写一个 Lambda 函数,该函数可以公开为 API。移动应用程序访问调用 Lambda 函数的 API,在请求处理后,数据存储在数据存储中。

  • 通知服务:应用程序通常会注册用户和设备,以便能够向设备发送通知。AWS 提供了一项名为 Amazon Simple Notification Service (SNS)的服务,可用于从您的移动应用程序注册和发送通知。AWS 服务支持向 iOS、Android、Fire OS、Windows 和基于百度的设备发送推送通知。您还可以向 macOS 桌面和 iOS 设备上的VoIP应用程序发送推送通知,向超过 200 个国家/地区的用户发送电子邮件和短信。

  • 分析服务:一旦客户开始采用该应用程序,您将想要了解应用程序的哪些功能正在使用,用户在哪些地方遇到问题或挑战,以及用户在哪些地方退出。为了了解所有这些,您需要订阅一个分析服务,该服务允许您跟踪用户的操作,然后将其汇总到一个中央服务器。您可以访问该中央存储库并深入了解用户的活动。您可以利用这些对客户行为的洞察来改善整体客户体验。Google Analytics 是这一领域中的一项热门服务。您可以跟踪用户的多个整体参数,包括位置、使用的浏览器、使用的设备、时间、会话详细信息等。您还可以通过添加自定义参数来增强它。这些工具通常提供一定数量的预定义报告。您还可以添加/设计自己的报告模板。

  • 位置服务:应用程序使用的另一个服务是位置服务。你的应用程序可能需要功能,需要根据给定的上下文进行策划(在这种情况下,位置可以是上下文属性之一)。上下文感知功能允许你个性化地将功能/服务适应最终客户的需求,并有助于改善整体客户体验。Google Play 服务位置 API 提供了这样的功能。围绕位置服务有一整套服务/应用程序。例如,像 Uber、Lyft 和 Ola(印度)这样的公司是围绕位置服务构建的商业案例的很好的例子。大多数物流企业(特别是最后一英里)都利用位置服务进行路线优化和交付等工作。

  • 社交整合服务:你的应用程序可能需要与流行的社交网络(Facebook、Twitter、Instagram 等)进行社交整合。你需要能够访问已登录用户的社交动态,代表他们发布内容,和/或访问他们的社交网络。有多种方式可以访问这些社交网络。大多数这些网络为其他应用程序提供访问,并公开一组 API 来连接它们。然后还有聚合器,允许你提供与一组社交网络的整合。

  • 广告服务:应用程序使用的另一个关键服务,特别是移动应用程序,是向用户提供广告。根据应用程序模型(免费/付费),你需要决定应用程序的货币化模式。为了向用户提供广告(称为应用内广告),你需要注册广告网络提供商并调用他们的 API 服务。谷歌的 AdMob 服务是这一领域的先驱之一。

在构建应用程序时,可能还有其他许多服务提供商值得关注。我们已经涵盖了主要突出的类别。根据你的应用程序需求,你可能想在特定需求领域搜索提供者。我相信已经有人在提供这项服务。还有一些综合性的提供商被称为 BaaS。这些 BaaS 提供商通常提供多种服务供使用,并减少了应用程序端的整体集成工作。你不必与多个提供者打交道;相反,你只需与一个提供者合作。这个提供者会满足你的多种需求。

BaaS 作为一个市场细分是非常竞争的。由于多个提供者的竞争,你会发现在这个领域也有很多的并购。最近发生了以下情况:

  • Parse:被 Facebook 收购。Parse 提供了一个后端来存储你的数据,推送通知到多个设备的能力,以及整合你的应用程序的社交层。

  • GoInstant:被 Salesforce 收购。GoInstant 提供了一个 JavaScript API,用于将实时的多用户体验集成到任何 Web 或移动应用程序中。它易于使用,并提供了所需的完整堆栈,从客户端小部件到发布/订阅消息到实时数据存储。

有提供特定领域服务或 API 的垂直和水平 BaaS 提供商。在电子商务领域、游戏领域、分析领域等都有提供者。

在注册之前记得检查提供者的可信度。记住,如果提供者倒闭,你的应用程序也会陷入困境。确保你了解他们的商业模式,产品路线图,资金模式(特别是对于初创公司),以及他们对客户的倾听程度。你希望与愿意全程帮助你的合作伙伴合作。

总结

在本章中,我们涵盖了在尝试构建您的 XaaS 提供商时的一些关键问题。我们还涵盖了光谱的另一面,我们看到了可用于构建应用程序的典型服务。

在下一章中,我们将涵盖 API 最佳实践,我们将看到如何设计以消费者为中心的 API,这些 API 是细粒度和功能导向的。我们还将讨论 API 设计方面的最佳实践,例如如何识别将用于形成 API 的资源,如何对 API 进行分类,API 错误处理,API 版本控制等等。

第十二章:API 设计最佳实践

本章讨论如何设计以消费者为中心的 API,这些 API 是细粒度的,以功能为导向的。它还讨论了 API 设计关注的各种最佳实践,例如如何识别将用于形成 API 的资源,如何对 API 进行分类,API 错误处理,API 版本控制等。我们将通过 Open API 和 RAML 来描述 API 的模型。

我们将涵盖以下主题:

  • API 设计关注点

  • API 网关部署

API 设计关注点

API 旨在被消费并定义了 API 如何被消费。API 指定了所需与 API 交互的命令/操作列表以及这些命令的格式/模式。

在定义 REST API 时,信息的关键抽象是资源。资源被定义为对一组实体的概念映射。API 设计围绕着构成设计核心的资源。统一资源标识符URI),操作(使用 HTTP 方法)和资源表示(JSON 模式)都是以资源为中心构建的。拥有正确的资源抽象对于启用 API 的消费、可重用性和可维护性非常重要。

资源可以指向单个实体或一组实体。例如,产品是一个单一的资源,而产品是一组资源。我们将在两个层面上介绍设计准则:

  • 如何确定正确的资源粒度水平

  • 如何围绕已识别的资源设计 API

API 资源识别

API 的设计与问题域的基础业务领域模型相关联。API 需要以消费者为中心,关注消费者的需求。领域驱动设计原则被应用于确定正确的粒度。有界上下文模式是帮助将问题领域划分为不同有界上下文并明确它们关系的中心模式。对于企业,资源识别也受到中央/组架构团队定义的规范模型的驱动。

此外,根据 API 的定义位置和其暴露的功能/功能,API 可以分为三个广泛的类别:

让我们在接下来的章节中详细讨论这些类别。

系统 API

关键企业资源或记录系统需要作为一组 API 对所有下游系统开放或暴露,以便这些服务周围构建逻辑/体验。对于绿地项目,系统 API 通常代表作为功能的一部分开发的记录系统或数据存储。当涉及企业时,系统 API 代表所有企业系统,例如核心企业资源规划(ERP)系统、运营数据存储、主机应用程序,或许多商业现成产品,例如客户关系管理(CRM)等,这些产品运行企业的核心流程。系统 API 的一些显著特点如下:

  • 领域驱动设计的起源源于查看核心系统领域,并创建有界上下文来定义系统 API。

  • 这些记录系统通常映射到 HTTP 资源类型—名词—并提供实体服务。例如,在银行账户的情况下,抵押贷款、证券和卡片是构建系统 API 的核心实体或名词。

  • 有界上下文模型定义了服务拥有其数据存储。但在现有系统的情况下,例如企业资源规划(ERP),服务可能共享相同的基础系统。这需要对基础业务流程进行仔细研究,识别领域(也称为名词),并将其作为系统 API 公开。账户可以是一个系统 API,但账户转账将是一个利用基础账户系统 API 提供服务的过程 API。

  • 系统 API 传统上非常稳定,并且不受渠道或过程 API 层变化的影响。这些是核心、稳定的企业端的一部分。

  • 企业系统的组合和集成机制定义了系统 API 如何与基础系统集成。例如,一个主机可能会规定使用 MQ 作为集成机制,从而使系统 API 实现 MQ 以将主机功能暴露为 API。

  • 系统 API 最大的问题是它们的正常运行时间和弹性与基础系统的稳定性相关联。如果核心应用程序频繁崩溃或出现问题,这些问题往往会传递到系统 API 层。

过程 API

纯粹主义者会说,系统 API 暴露了系统的核心功能,应用程序应该从系统 API 中整合功能,以向最终客户提供所需的功能。这对于较小的应用程序或应用程序的初始迭代可能效果很好。随着应用程序变得更大,或者开始在多个渠道或设备上公开功能,您开始看到功能开始复制的情况,这意味着缺乏重用,导致系统难以维护。过程 API 的一些显着特点如下:

  • 过程 API 提供了在系统 API 基础上构建的更丰富的功能。例如,我们可以将账户转账功能编写为过程 API,而不是每个渠道都编写账户转账功能,以便在各个渠道之间提供一致且可重用的模型。

  • 从消费者的角度来看,过程 API 提供了一个更简单的模型来访问功能,而不是尝试编排多个系统 API。这有助于改善客户端的易用性,并有助于减少 API 网关层的流量。

  • 过程 API 还可以用于为应用程序提供跨渠道/全渠道功能。此级别可以处理诸如渠道上下文切换等问题。

  • 应用程序倾向于引入过程 API 以改善整体系统的性能。如果系统 API 与速度慢或只能处理有限吞吐量的系统相关联,可以使用过程 API 来缓存来自系统 API 的数据,以避免每次都访问基础系统。如果系统记录不可用,随后系统 API 也不可用,过程 API 可以用于处理此类请求,提供替代的功能流程。

  • 过程 API 也可以充当适配器,用于外部第三方调用,而不是应用程序直接进行第三方调用。使用过程 API 可以处理第三方 API 失败不影响应用程序其余部分的情况。过程 API 可以应用模式,例如断路器和限流对外部请求进行处理以处理多种情况。

渠道 API

最终的 API 分类是通道 API。顾名思义,这些 API 是特定于通道的,并映射到作为应用程序一部分构建的客户旅程。这些也被称为体验 API 或旅程 API。例如,如果您正在使用 Angular 或 React 构建应用程序,则需要将单页应用程序SPA)的客户旅程映射到通道 API 可以提供的底层服务。通道 API 的一些显着特点如下:

  • 通道 API 映射到与通道不可避免地相关的客户旅程。有时也称为体验 API。这些 API 可以是有状态的,因为它们在客户旅程中为客户提供服务,并且需要携带会话上下文。人们可以通过将状态外部化到诸如 Redis 之类的会话存储中来构建无状态服务。

  • 每当客户旅程发生变化时,通道 API 将发生变化。通道 API 之间的可重用性系数并不是很高。通常在 10-15%之间。例如,如果类似的客户旅程映射到 Android 和 iOS 应用程序,则有可能重用相同的 API。

  • 通道 API 通常不具有业务逻辑或任何服务编排逻辑,因为这些问题通常由过程 API 层处理。

  • 诸如安全性(CQRS,CORS)、身份验证、授权、节流等问题是在 API 网关层处理的,而不是传递到通道 API 层。

  • 有时,在 API 开发过程中,人们可能对 API 进行了严格的区分和定义。但在许多应用程序迭代过程中,这些区分开始出现在 API 中,人们可以开始看到应用程序朝着这些分类发展。

  • 接下来,我们将介绍适用于我们看到的三种分类的 API 设计指南。

API 设计指南

一旦确定了正确的资源粒度级别,API 设计指南的其余部分将帮助制定合适的合同/接口,以实现可消费性、可重用性和可维护性。

RESTful 客户端应能够通过访问 URI 路径发现所有可用的操作和资源。客户端应能够处理以下内容:

  • 请求:处理发送到服务器端的入站处理消息

  • 响应:服务器提供的封装信息

  • 路径:所请求资源的唯一标识符

  • 参数:作为键/值对添加到请求中以指定操作(如过滤器、子集等)的元素

当我们开始设计 API 时,我们分享了多年来遇到的一些最佳实践。

命名和关联

资源名称通常指的是从业务领域提取的名词。一旦确定了名词,API 合同就可以被建模为针对这些名词的 HTTP 动词:

  • 资源的选择需要考虑细粒度与粗粒度模型。过于细粒度意味着过于啰嗦,而粗粒度意味着过于狭窄的焦点,导致对变化的支持。人们可以通过在一定程度上使用系统与过程 API 模型来推理。但问题在于,如果资源过于细粒度,系统 API 的数量会增加,导致难以维护的复杂性。

  • API 是通过查看消费者的需求来设计的。根据客户旅程和它们如何映射到底层数据存储来推导您的 API 需求。这意味着,使用顶层设计方法来查看 API 设计。首先进行数据建模的底层模型可能不会产生正确的平衡。如果您有现有的企业资产,您将需要执行一种中间相遇的方法,通过编写帮助弥合差距的过程 API 来平衡客户的需求。

资源的基本 URL

这取决于您如何处理资源——作为单例还是作为集合。因此,理想情况下,您将得到一个资源的两个基本 URL,一个用于集合,另一个用于实体。例如:

资源 POST(创建) GET(读取) PUT(更新) DELETE(删除)
/orders 创建新订单 订单列表 替换为新订单 错误(不想删除所有订单)
/orders/1234 错误 显示 ID 为1234的订单 如果存在则更新订单;如果不存在则创建新订单 删除 ID 为1234的订单

处理错误

利用标准 HTTP 状态代码指示问题/错误:

  • 如果使用 JSON,错误应该是一个顶级属性

  • 在出现错误时,要描述清楚、正确和有信息性

以下是一个示例错误消息片段:

{ 
   "type": "error", 
   "status":400, 
   "code": "bad_request", 
   "context_info": { 
         "errors": [ 
         { 
               "reason": "missing_argument", 
               "message": "order_id is required", 
               "name": "order_id", 
               "location": "query_param" 
         } 
         ] 
   }, 
   "help_url": "http://developers.some.com/api/docs/#error_code", 
   "message": "Bad Request" 
   "request_id": "8233232980923412494933" 
} 

以下是 HTTP 代码使用的一些示例:

  • 400 错误的请求

  • 401 未经授权

  • 403 禁止

  • 404 未找到

  • 409 冲突

  • 429 请求过多

  • 5xx API 有故障

版本控制

有多种服务版本模型:

  • URL:您只需将 API 版本添加到 URL 中,例如:https://getOrder/order/v2.0/sobjects/Account。经常使用,但不是良好的实践。

  • 接受标头:您可以修改接受标头以指定版本,例如:Accept: application/vnd.getOrders.v2+json。客户端很少使用,且繁琐。

  • 模式级别:使用模式强制执行验证,难以强制执行 JSON,与 XML 配合效果很好。良好的实践/罕见。

  • API 外观层:使用外观层来隐藏客户端的版本复杂性。

请记住,资源是一个语义模型;资源的表现形式和状态可能随时间变化,但标识符必须始终指向相同的资源。因此,只有在概念发生根本变化时才应使用新的 URI。API 外观层可以将北向 API 与底层服务和模式版本抽象出来。API 管理平台支持创建 API 外观层。

分页

使用带有分页信息的 URL 来处理结果的偏移和限制。例如,/orders?limit=25&offset=50

属性

API 应支持使用查询参数模型由消费者请求的数据属性。例如,/orders?fields=id,orderDate,total

数据格式

API 应根据消费者的要求提供多种数据格式的支持。例如,/orders/1234.json以 JSON 格式返回数据。

客户端支持有限的 HTTP 方法

根据设备及其有限的支持 HTTP 动词的能力,您可能希望使用以下方法来提供对 HTTP 方法的支持:

  • 创建/orders?method=post

  • 读取/orders

  • 更新/orders/1234?method=put&location=park

  • 删除 /orders/1234?method=delete

身份验证和授权

REST 服务在适当时使用基于角色的成员资格,并提供独立启用GETPOSTPUTDELETE的能力。

通常,这个问题应该在 API 网关级别处理。您不应该将其作为服务的一部分处理。

端点重定向

服务清单可能会因业务或技术原因随时间变化。可能无法立即替换所有对旧端点的引用。

通过采用这种设计实践,服务端点的消费者在服务清单重组时会自动适应。它会自动将访问过时端点标识符的服务消费者引用到当前标识符:

HTTP 原生支持使用 3xx 状态代码和标准标头的端点重定向模式:

  • 301 永久移动

  • 307 临时重定向

  • 位置/新 URI

内容协商

服务消费者可能会以不向后兼容的方式更改其要求。一个服务可能需要支持旧的和新的消费者,而不必为每种消费者引入特定的能力。

服务可以指定特定的内容和数据表示格式,以便在运行时作为其调用的一部分接受或返回。服务合同涉及多种标准化媒体类型。

安全

始终使用 SSL 来保护您的 URI。SSL 确保了加密通信,从而简化了身份验证的工作——不需要为每个 API 请求签名。

这涵盖了一些与 API 设计相关的最佳实践。可以从谷歌、Facebook 和亚马逊是如何定义他们的公共 API 中学习,并将其作为 API 设计的基础。

API 建模

有两种标准在竞相描述 API——开放 API 和 RESTful API。我们将在以下部分更详细地讨论它们。

开放 API

开放 API 倡议旨在创建和推广基于 Swagger 规范的供应商中立的 API 描述格式。开放 API 规范允许我们为 REST API 定义一个标准的、与语言无关的接口,这使得人类和计算机都能够在没有访问源代码的情况下发现和理解服务的能力。

在下图中,我们描述了一个基于开放 API 的示例 API 定义以及各个部分:

代码在下图中继续:

代码在下图中继续:

RESTful API 建模语言(RAML)

RESTful API 建模语言RAML)是一种描述 RESTful API 的标准语言。RAML 以与 YAML 相同的方式编写,YAML 是一种人类可读的数据序列化语言。RAML 的目标是提供描述 API 所需的所有必要信息。RAML 提供了一种可供各种 API 管理工具读取的机器可读的 API 设计。

在下图中,我们描述了一个示例 RAML 以及各个部分:

RAML 映射到完整的 API 设计生命周期,可以分为以下几类:

让我们来看一下流程:

  1. 设计:API 供应商提供编辑器作为 API 开发套件的一部分,以帮助设计/编写 API/RAML 定义,从而实现更快的开发和更少的错误。生成的 RAML 可以用模拟数据进行增强,并允许与业务所有者/消费者进行迭代,以进行验证和正确性。

  2. 构建:生成的 RAML 提供了 API 构建的规范。开发套件可以基于 RAML 生成存根,以便插入逻辑。

  3. 测试:RAML 可以用于生成测试脚本。诸如 Postman 和 Abao 之类的工具允许导入 RAML 规范并生成用于验证 API 的测试。此外,诸如 API Fortress 和 SmartBear 之类的工具还可以测试响应延迟、有效负载和错误。

  4. 文档:RAML 规范可以转换为基于 HTML 的模型。诸如 RAML2HTML for PHP、API Console 等工具提供了一种将 RAML 指定的文档公开的简单方法。该模型允许在规范中进行任何更改,并将其反映在文档中并保持同步。

  5. 集成:API 生命周期的最后阶段是能够集成或消费 API。使用 RAML,供应商/工具可以创建多种集成和消费 API 的方式。使用 RAML,可以构建特定于 API 的 SDK。供应商还提供可以利用 RAML 与客户端逻辑集成的工具。

两种标准之间的选择取决于组织选择的 API 网关产品堆栈。大多数产品都更偏好一种标准,尽管每个产品都声称支持两种标准。

API 网关部署模型

API 网关提供了一个外观模式,封装了系统的内部工作,为所有传入客户端提供了一个统一的入口点。API 网关可以为每种类型的客户端提供定制的 API,同时解决诸如安全性、身份验证、授权、限流、负载平衡等问题。

让我们来看看影响 API 如何部署在 API 网关上的因素。

  • 客户端或通道类型:根据请求的设备或通道的不同,API 可能需要为不同的数据子集提供服务。例如,服务的桌面版本可能需要更多的细节,而移动客户端则需要更少。甚至手机和平板之间的数据也可能有差异。我们如何确保同一个微服务可以为所有设备类型的请求提供服务,并且仍然处理这些变化?在这种情况下,我们为不同的设备类型创建多个 API,以满足客户端的特定需求,而不会打扰微服务。

  • 数据转换:有时,后端的服务是构建为提供 JSON 内容的。有一个要求要求提供 XML 响应或反之。在这种情况下,API 网关在网关级别进行数据转换,同时提供一个提供 XML 响应的 API,使服务能够在不改变或了解客户端需求的情况下工作。

  • 版本控制:对于公共 API 或与未在 URI 中添加版本控制的资源相关的 API,API 网关可以根据客户端和使用的版本将传入的请求路由到正确的服务。在这种情况下,API 网关可以使用多种技术解析服务版本:

  • 客户端标识符可用于识别它们是否已切换到新版本或正在使用旧版本。

  • 根据 SLA 将客户端分为多个类别。当新版本发布时,较低的类别或低使用率的客户端可以被要求切换到新版本。随着客户端升级,API 网关可以将它们重定向到正确的服务版本。

  • 编排:有时,API 可能需要调用多个后端服务并聚合结果。在这种情况下,API 网关必须同时调用多个服务并聚合结果。有时,服务调用之间可能存在依赖关系。例如,传入请求可能需要在实际服务调用之前进行身份验证,或者可能需要提取额外的客户端或会话信息以调用该调用。可以在 API 网关层编写整个编排逻辑,因为一些产品提供了运行时支持。另一个选择可能是编写一个执行跨其他服务的编排并提供一个整合 API 供消费的过程 API。这有助于减少交互并从客户端的角度提高整体性能。

我们在第三章《设计您的云原生应用程序》中介绍了编排模式。

  • 服务发现:随着服务实例的上下,服务注册表是关于服务端点在任何给定时间可用的唯一真实数据源。API 网关应该能够在运行时调用服务注册表以获取服务端点,并使用它来调用服务。服务注册表可以用作跨注册服务实例的负载平衡机制。

  • 处理超时:对于在合理时间内没有响应的服务,API 网关允许您设置超时请求。这使得网关可以处理超时失败,并为客户端提供故障模式。其中一种选择可以是提供缓存数据(如果适用并根据服务类型),或者快速失败模式,其中网关可以立即返回错误或失败,而不调用服务。

  • 数据缓存:API 网关还可以为提供静态数据或不经常更改的数据的服务调用缓存数据。这种模式可以减少服务实例上的流量,提高整体响应延迟和整体系统的弹性。缓存的数据也可以用作次要故障流程,以防主要流程失败。

  • 服务调用:部署的服务可以使用多个接口或协议。例如,您可能有使用异步消息传递机制(如 JMS、MQ、Kafka 等)的服务,或者其他服务可能使用 HTTP 或 Thrift 等同步模型。API 网关应该能够支持多种服务调用模型,并在这些调用方法之上提供编排模型。

  • 服务计量/限流:对于某些类别的客户,您可能希望限制他们可以进行的服务调用次数。例如,如果您提供了一个功能减少的免费模式服务,以及在一定时间内可以进行的调用次数限制。根据客户类型(免费或付费)对传入请求进行计量和限流的能力有助于围绕您的 API 和基础服务提供商业模式。如果您正在对另一个 SaaS 提供商进行外部 API 调用,通过 API 网关路由这些调用可以帮助预测/管理外部调用的数量,并在使用账单出现时避免不必要的冲击。

  • API 监控:另一个重要问题是监控 API 调用是否有任何偏差,无论是在各种百分位数的响应延迟、失败率、API 可用性等方面。这些指标需要在仪表板上绘制,并配备适当的警报和通知系统。根据失败类型,可以自动化恢复脚本以克服它们。

这些是可以应用于 API 网关的各种使用场景和模式,以将您的服务作为 API 向消费者公开。

总结

在本章中,我们看到了 API 如何根据其主要用途和基础资源进行分类。我们了解了关于整体 API 设计的最佳实践以及通过 Open API 或 RAML 规范对 API 进行建模的标准。接下来,我们看到了 API 网关如何利用来解决服务层未处理的问题。

在下一章中,我们将介绍云开发对企业现有格局的影响,以及它如何实现向数字化企业转型。

第十三章:数字化转型

云计算的出现正在影响企业景观的各个方面。从核心基础设施到面向客户的应用程序,企业景观正在受到变革力量的影响。一些企业是这些转型的领先者,而其他一些企业仍在努力弄清楚从何处开始以及该做什么。根据行业领域的成熟度,转型之旅可能大相径庭。一些领域是首批采纳技术趋势的(如金融服务业),而其他领域则等待技术过时后采纳新技术(制造业、公用事业)。在本章中,我们将涵盖以下内容:

  • 映射应用程序组合以进行数字化转型

  • 将现有的单片应用程序分解为分布式云原生应用程序

  • 在流程、人员和技术层面上需要的变更

  • 构建自己的平台服务(控制与委托)

应用程序组合合理化

数字化转型的决定通常与更大的应用程序组合相关联。在客户为中心、提供更好的客户体验、合规性/监管、云计算的出现、开源等外部力量的影响下,企业开始审视他们的整个应用程序景观,并确定需要改进、增强和重塑的领域。

最初的步骤是确定需要转型为云部署的机会或应用程序。在这一步中,我们通常会通过业务和技术参数进行整体组合分析。这些参数有助于提供组合的加权得分。利用这些得分,我们可以将应用程序映射到四个象限。这些象限有助于我们确定在哪里集中精力以及我们将看到最大价值的地方。

组合分析 - 业务和技术参数

应用程序根据业务和技术参数进行测量和评分。

技术价值的参数如下:

  • IT 标准合规性

  • 架构标准合规性

  • 服务质量

  • 可维护性

  • 运营考虑

  • 许可证/支持成本

  • 基础设施成本

  • 项目/变更成本

  • 应用程序维护成本

  • 采购(内部采购/外包)

业务价值的参数如下:

  • 财务影响

  • 应用用户影响

  • 客户影响

  • 关键性

  • 业务对齐

  • 功能重叠/冗余

  • 监管/合规风险

  • 服务故障风险

  • 产品/供应商稳定性

您可以按 1-5 的比例对这些参数进行评分(1 表示最低,5 表示最高)。

通过映射这些参数,我们可以确定成本和复杂性的热点,并根据业务能力领域对应用程序进行分类。这些应用程序类别进一步分析其相互依赖性、接触点、集成点和基础设施。利用所有这些,我们可以分析收益并为转型路线图提供建议。下一步基于业务价值和技术价值;我们将应用程序绘制到以下四个象限之一:

这些得分有助于我们在应用程序和组合级别提供成本效益分析。它还有助于确定功能重叠的地方(由于合并和收购活动),了解业务和 IT 的不一致之处,以及业务优先级所在。这些可以帮助确定投资机会所在以及潜在的非核心领域。

利用上述基础,每个象限中的应用程序可以进一步映射到以下图表中所示的倾向之一:

这些倾向为我们提供了在我们将在以下部分讨论的领域内的应用的合理机会。

退役

所有属于低商业价值和低技术价值的应用程序都可以被标记为退役。这些通常是在不同的商业环境中失去了相关性或实施了新功能的应用程序。

这些应用程序的使用率低,商业风险非常低。也可以通过对这些应用程序的工单和使用量进行汇总来确定这类应用程序。使用率低、工单数量较少的应用程序通常是退役的候选者。

保留

所有具有低技术价值和高商业价值的应用程序都属于这一类。技术成熟度可能较低,但它们为业务提供了重大价值。从 IT 角度来看,这些应用程序的运行成本并不高。我们可以保持这些应用程序的运行,因为它们仍然为业务提供了重大价值。

整合

所有具有高技术价值和低商业价值的应用程序都属于这一类。高技术价值可能是由于技术支持成本高、缺乏技术技能的人员、缺乏文档等。业务可以阐明这些应用程序的价值,但目前对这些应用程序的支出可能无法证明合理。这些应用程序需要迁移和整合以升级技术水平。

转换

这些是具有高技术价值和高商业价值的应用程序。这意味着这些应用程序拥有大量用户、多次发布、大量工单和高基础设施支持成本,但仍然为业务提供了重大优势。这些应用程序是需要付出努力的地方,因为它们为组织提供了重大的差异化。

使用上述方法,我们可以确定哪些应用程序适合进行转换。例如,我们可以采取一个目前在本地运行并需要转换为分布式应用程序设计模型的现有 Java/JEE 应用程序。

单片应用程序转换为分布式云原生应用程序

J2EE 规范的出现,加上应用服务器提供的必要服务,导致了单片应用程序的设计和开发:

单片应用程序及其生态系统的一些特征是:

  • 所有内容都打包到一个单一的.ear文件中。 单一的.ear文件需要进行为期数月的测试周期,这导致了生产中变更速度的降低。通常一年或两年进行一次大规模的生产推动。

  • 应用程序构建复杂性非常高,跨各种模块存在依赖关系。有时,应用程序使用的 JAR 文件版本之间会发生冲突。

  • 应用程序之间的重复使用主要通过共享.JAR文件来实现。

  • 庞大的错误和功能数据库——从积压的角度来看,各种应用程序模块中存在许多功能集/错误。有时,一些积压可能会相互冲突。

  • 用户验收标准通常未定义。有一些冒烟测试,但大多数新功能和集成大多只在生产中可见。

  • 需要多个团队的参与和重大监督(业务团队、架构团队、开发团队、测试团队、运营团队等)进行设计、开发和运营管理。在发布周期中,协调各个团队之间是一项艰巨的工作。

  • 随着时间的推移,技术债务不断积累——随着新功能/功能添加到应用程序中,原始设计从未进行任何更改/重构以适应新的需求。这导致应用程序中积累了大量死代码和重复代码。

  • 过时的运行时环境(许可证、复杂的更新)——应用程序可能在较旧版本的 JVM、较旧的应用服务器和/或数据库上运行。升级成本高且通常非常复杂。规划升级意味着在该开发周期内放弃任何功能发布。多个团队的参与需要复杂的项目管理模型。缺乏回归测试脚本使情况变得更糟。

  • 团队采用了技术设计导向的方法。架构和设计在开发开始之前就已经确定。随着应用程序的增长,新的功能/功能被添加,不再重新审视应用程序架构/设计。

  • 几乎没有使用业务组件或域。应用程序设计通常是根据层(表示层、业务层、集成层和数据库层)和客户/应用程序流程切片,进入特定的模块/模式。例如,使用 MVC 模式的应用程序将创建类似于模型、视图和控制器的包,还有值和常用包。

  • 通常,整个应用程序只有一个数据库模式。在数据库级别没有功能的分离。域通过外键相互连接,数据库遵循第三范式。应用程序设计通常是自下而上的,数据库模式决定了应用程序数据库层的设计。

  • 平均企业应用程序将有超过 50 万行代码,其中有大量样板代码。随着应用程序的增长,源代码库中将有大量死代码和重复代码。

  • 应用程序通常由笨重的基础设施支持——通过增加更多硬件来管理应用程序的能力。服务器集群用于扩展应用程序。

  • 成千上万的测试用例导致回归测试套件运行时间增加。有时,发布将跳过回归测试套件以加快周期。

  • 大多数这类项目的团队规模超过 20 人。

我们可以看到,在单片应用程序的情况下,业务速度和变化速度非常低。这种模式可能在 10-15 年前有效。在当今竞争激烈的市场中,以令人难以置信的速度发布功能/功能的能力至关重要。你不仅仅是在与其他大型企业竞争,还要与许多没有传统应用程序、技术和流程包袱的更灵活的初创企业竞争。

开源的兴起、消费者公司的增长以及移动设备的增多等因素导致了应用程序架构领域的创新,以及更多由微服务和反应性模型驱动的分布式应用程序。单片应用程序被分解成更小的应用程序/服务集。

接下来,我们将探讨与分布式应用程序相关的关键架构问题。我们将看到这些关键问题如何映射到整体应用程序的技术能力,以及应该雇用哪些能力,应该建立哪些能力。

分布式应用程序及其生态系统的一些特征包括:

  • 轻量级运行时容器:微服务的出现与笨重的 JEE 容器的消亡相关。随着应用程序变成具有单一目的和松散耦合的微服务,有必要简化管理组件生命周期的容器。Netty 的出现导致了反应性框架的发展,正好符合这一目的。

  • 事务管理:应用简化的另一个牺牲品是事务管理。有界上下文意味着服务不会与多个资源交互,并尝试进行两阶段提交事务。诸如 CQRS、事件存储、多版本并发控制(MVCC)、最终一致性等模式有助于简化并将应用程序移动到不需要锁定资源的模型。

  • 服务扩展:拆分应用程序允许单独扩展各个服务。使用帕累托法则,80%的流量由 20%的服务处理。扩展这 20%的服务的能力成为更高可用性 SLA 的重要驱动因素。

  • 负载均衡:与单体应用程序不同,单体应用程序的负载均衡是在应用服务器集群节点之间进行的,而在分布式应用程序的情况下,负载均衡是跨服务实例(在类似 Docker 的容器中运行)进行的。这些服务实例是无状态的,通常会频繁上下线。发现活动实例和非活动实例的能力成为负载均衡的关键特性。

  • 灵活部署:分布式架构的一个关键能力是从严格的集群部署模型转变为更灵活的部署模型(牛群与宠物),其中部署实例被部署为不可变实例。诸如 Kubernetes 之类的编排引擎允许最佳利用底层资源,并消除了管理/部署数百个实例的痛苦。

  • 配置:随着服务实例变得不可变,服务配置被抽象出来并保存在中央存储库(配置管理服务器)中。服务在启动时,或作为服务初始化的一部分,获取配置并以可用模式启动。

  • 服务发现:使用无状态不可变服务实例在通用硬件上运行意味着服务可以随时上下线。调用这些服务的客户端应能够在运行时发现服务实例。这一特性,连同负载均衡,有助于维护服务的可用性。一些新产品(如 Envoy)已将服务发现与负载均衡合并。

  • 服务版本:随着服务开始获得消费者,将需要升级服务契约以适应新功能/变更。在这种情况下,运行多个版本的服务变得至关重要。您需要担心将现有消费者迁移到新的服务版本。

  • 监控:与传统的单体监控侧重于基础设施和应用服务器监控不同,分布式架构需要在事务级别进行监控,因为它流经各种服务实例。应用性能管理(APM)工具如 AppDynamics、New Relic 等用于监控事务。

  • 事件处理/消息传递/异步通信:服务不是基于点对点进行通信的。服务利用事件作为一种异步通信的手段来解耦。一些关键的消息传递工具,如 RabbitMQ、Kafka 等,用于在服务之间进行异步通信。

  • 非阻塞 I/O:服务本身利用非阻塞 I/O 模型从底层资源中获得最大性能。反应式架构正在被微服务框架追求(如 Play 框架、Dropwizard、Vert.x、Reactor 等)用于构建底层服务。

  • 多语言服务:分布式应用的出现以及使用 API 作为集成允许使用最先进的技术构建服务实例。由于集成模型是 JSON over HTTP,服务可以是多语言的,允许使用正确的技术构建服务。服务还可以根据服务需求的类型使用不同的数据存储。

  • 高性能持久性:由于服务拥有自己的数据存储,读/写服务需要处理大量并发请求。诸如命令查询请求分离CQRS)的模式使我们能够分离读/写请求,并将数据存储迁移到最终一致性模型。

  • API 管理:分布式架构的另一个关键要素是能够将服务限流、身份验证/授权、转换、反向代理等问题抽象出来,并移到称为 API 管理的外部层。

  • 健康检查和恢复:服务实现健康检查和恢复,以便负载均衡器发现健康的服务实例并删除不健康的实例。服务实现心跳机制,该机制由服务发现机制用于跟踪应用程序景观中的健康/不健康服务。

  • 跨服务安全性:服务对服务的调用需要得到保护。数据在传输过程中可以通过安全通信(HTTPS)或通过加密数据来保护。服务还可以使用公钥/私钥来匹配哪些客户服务可以调用其他服务。

我们看到了构建分布式应用所需的一些架构问题。为了覆盖整体应用程序的范围,构建为一堆微服务,我们正在关注以下各个领域的关键架构问题:

为了使应用成为云原生,重要的是使用云供应商提供的 SaaS/PaaS 构建应用。这种模式使您能够专注于业务功能的转变,提高创新速度,并改善客户体验。除非技术不是组织的关键差异化因素,否则核心基础设施和平台服务的运行应该留给专家。在需求存在巨大变化的情况下,云弹性规模模型提供了一种推动力。我不想为云供应商做营销,但除非基础设施对您的业务不重要,否则您不应该运行基础设施。

唯一的缺点是您会受到云供应商提供的服务的限制。组织正在采用多云供应商策略,他们将应用程序分散开来,并利用云供应商的关键差异化因素。例如,GCP 提供了丰富的分析和机器学习功能库,具有运行分析工作负载和解密意义洞察的能力,机器学习ML)模型是使用最先进功能的一种方式。同样,对于面向消费者的应用程序,AWS 提供了丰富的 PaaS 服务集,可用于推出和转向以客户为中心的解决方案。

将单体应用转换为分布式应用

在本节中,我们将以单体应用为例,看看需要哪些步骤才能将其架构为分布式应用。

我们假设一个典型的 Java 应用在应用服务器上运行,通过集群模型进行扩展,并使用典型的关系型数据库管理系统(RDBMS)。该应用已经投入生产,并需要重构/迁移到分布式架构。

我们将讨论需要共同工作以重构/推出分布式应用程序的多个并行轨道。我们将首先涵盖各个轨道,然后看到它们如何结合在一起。在您的组织中,您可能选择为每个轨道拥有单独的团队,或者一个团队管理多个轨道。这个想法是为您提供一个实际转型单体应用程序所涉及的活动的一瞥。

客户旅程映射到领域驱动设计

开始数字化转型的关键驱动因素是定义新的客户旅程并构建新的客户体验。这种以客户为中心的方式推动业务资助数字化转型计划。对于我们的情况,我们可以假设业务已经批准了数字化转型计划,然后我们从那里开始。

从服务分解的角度来看,我们需要遵循这里提到的步骤:

  • 客户体验旅程映射:数字化转型的一个关键驱动因素是定义新的客户旅程。客户体验旅程是客户初始接触点的地图,通过过程参与模型。这个练习通常由专家完成,涉及客户关注研究、接触点、涉及的参与者/系统、业务需求和竞争分析等内容。客户旅程通常以信息图的形式创建。

客户旅程有助于确定客户互动在设备、渠道或流程之间移动时的差距。它有助于填补这些差距,并确定增强整体客户体验的手段和方法。

  • 推导领域模型:客户体验旅程地图被映射到当前和未来的需求。这些需求然后形成用户故事的基础。对于新应用程序,需求可以形成系统的功能分解的基础。在现有应用程序的情况下,系统可能已经分解为可识别的领域/子领域。

一旦我们有了需求,我们就可以开始识别系统内的各个子领域。领域模型使用普遍语言进行记录。整个想法是使用业务和技术团队都能理解的语言。

领域围绕实体及其功能进行建模。我们还考虑在这些功能之间相互操作的依赖关系。通常,首次尝试时,我们得到一个大块泥巴,其中已经识别出所有已知的实体和功能。对于较小的应用程序,领域模型可能是合适的大小,但对于较大的应用程序,大块泥巴需要进一步分解,这就是有界上下文发挥作用的地方。

  • 定义有界上下文:大块泥巴需要分解成更小的块,以便更容易采用。每个这些更小的块或有界上下文都有其围绕特定责任构建的业务上下文。上下文也可以围绕团队的组织方式或现有应用程序代码库的结构进行建模。

定义上下文的规则没有固定的定义,但非常重要的是每个人都理解边界条件。您可以创建上下文地图来绘制领域景观,并确保有界上下文得到清晰定义和映射。有各种模式(例如,共享内核、顺应者、生产者/供应者等),可以应用于绘制有界上下文。

  • 服务分解:使用有界上下文,我们可以确定将作为一个有界上下文部分工作的团队。他们将专注于需要生产/消费以作为有界上下文一部分提供功能的服务。业务能力被分解为单独的微服务。服务可以根据以下原则进行分解:

  • 单一责任:首先是服务的范围和服务将公开的能力

  • 独立:功能/特性需求的更改应该限制在一个服务中,允许一个团队拥有并完成相同的需求。

  • 松耦合:服务应该松散耦合,允许它们独立演进

  • 映射上/下游服务依赖:随着在每个领域中识别出的服务,这些服务可以根据依赖关系进行映射。封装记录系统的核心实体服务是上游服务。来自上游服务的更改被发布为事件,由下游服务订阅或消费。

定义架构跑道

业务应用程序需要建立在一个平台之上。平台可以根据业务和应用程序的需求进行构建或购买。组织需要定义一个有意识的架构模型,并定义铁路护栏,以确保团队在给定的技术约束条件下构建服务。平台团队拥有这个全面的架构,选择架构和技术组件,并帮助构建应用服务成功运行所需的任何共同关注点。

  • 平台架构:成功的分布式架构的关键要素之一是底层平台。可以选择使用现成的、开源/商业软件(如 Red Hat OpenStack、Cloud Foundry 等)来构建平台,也可以选择战略性的云提供商(如 AWS、Azure)来开始构建平台。底层基础设施(计算、网络和存储)的弹性特性为平台提供了基本的构建模块。

  • 技术选择、验证和集成:为了构建平台服务,您可能希望评估多组技术,以确定在您的生态系统中哪种技术最有效。技术堆栈评估通常是一个多步骤的过程,其中需求被映射到可用的技术/产品,并进行详细的验证步骤,最终形成一个关于技术集成的矩阵。

  • 设计决策:技术评估的结果被映射到基本需求,形成一个矩阵。这个矩阵用于确定最佳匹配,并帮助做出设计决策。这一步与前一步密切配合。

  • 环境设置:一旦关键的设计决策就位,我们需要开始进行环境设置。根据选择是在本地还是云端,设置和相关步骤会有所不同。您可以从开发、测试、预生产和生产环境的设置开始。环境按复杂性顺序构建,并经历多次迭代(从手动到脚本/自动化)。

  • DevOps/Maven 原型:接下来,我们开始着手应用构建和部署的持续集成CI)/ 持续部署CD)部分。对于在敏捷模型中开发的应用程序,CI/CD 模型有助于一天内进行多次发布,并为整个流程带来更高的速度。我们还可以开发加速器来辅助 CI/CD 流程。例如,Maven 原型带有用于创建可部署构件的必要绑定。

  • 平台服务构建:接下来是需要构建/提供给平台用户的一系列平台服务。

服务包括应用程序开发(例如排队、工作流、API 网关、电子邮件服务等)、数据库(例如 NoSQL、RDBMS、缓存等)、DevOps 工具(例如 CI/CD 工具、服务注册表、代码存储库等)、安全性(例如目录服务、密钥管理服务、证书管理服务、硬件安全模块(HSM)等)、数据分析(例如认知服务、数据管道、数据湖等)。

您可以从多个供应商那里购买这些服务(例如,作为 Pivotal Cloud Foundry(PCF)的一部分提供的 Tiles,Iron.io 平台),或者订阅云供应商提供的服务,或者在产品的基础上创建自己的平台服务。

  • 非功能性需求(NFR)问题:一旦关键平台服务就位,并且第一批应用程序开始接入平台,我们需要开始担心如何处理应用程序的 NFR 问题。应用程序如何根据传入负载进行扩展,如何检测故障,如何保持应用程序的最低阈值等等。同样,您可能希望将现有产品集成到您的平台,以提供/支持这些 NFR 问题。

  • 生产问题:最后,我们需要开始担心生产问题,如服务管理、监控、安全等。我们需要从运营角度构建服务和必要的门户,以监视、检测并在偏离/定义规则的情况下采取适当的行动。这些服务通常是根据组织标准构建的。随着更多用例的识别,服务会不断成熟。其目的是自动化所有可能的操作,以确保平台始终运行,无需任何人为干预。

开发人员构建

数字转型的另一个关键方面是专注于您现有团队管理/维护现有应用程序。团队需要在技能和技术方面进行升级,以便能够重构/构建/部署现有应用程序为分布式应用程序。我们将介绍重新培训团队处理分布式应用程序故事所需的步骤。

  • 开发人员再培训/培训:首要任务是教导开发人员新的应用架构技术和设计模式。这意味着课堂培训、在线技术培训、供应商产品会议/培训等等。提升团队技能的另一种方法是雇佣具有相关技能的人,并让他们在现有开发团队的支持下带头进行整体开发。

有时,您可能希望有两个团队——一个改变业务,另一个运行业务。在这种情况下,第一个业务团队为团队带来新的技能。另一个业务团队在转型期间管理和操作现有应用程序。

  • 开发机器升级和设置:新的技术栈需要升级开发人员的机器。如果机器运行在 4GB RAM 上,我们可能需要将它们升级到至少 8GB RAM,最好是 16GB RAM。新的技术栈需要虚拟机、Docker 引擎、集成开发环境和其他开发和单元测试软件。较慢的机器会增加构建/测试代码的时间。没有足够的性能,开发人员就无法高效工作。

  • 实验室/概念验证:一旦机器升级和开发人员培训完成,开发人员可以开始使用新技术栈进行实验室操作和/或概念验证,以熟悉新的开发技术。开发人员可以被分配小项目,或者参与技术栈评估的一部分,以使他们熟悉技术栈。

开发团队完成的工作应该由该领域的 SME 评估,以指出他们做错了什么以及正确的做法。拥有外部顾问(无论是 SME 还是供应商顾问团队)有助于弥合这一差距。

  • 代码分支和配置:一旦开发团队准备开始开发分布式应用程序,下一步就是从单体应用程序中分支出代码。您可能还希望分支配置数据。

请记住,即使进行了分支,现有的应用程序维护也会继续在主代码主干上进行。分支版本用于重构代码。我们将在下一节中看到更多细节。

  • 开发/构建微服务:一旦代码分支和重构完成,开发人员应该开始将它们打包为微服务。团队还可以开始创建新的微服务,以满足应用程序的新需求。

分支上的代码定期与主干同步,以确保对主干进行的更改在分支代码中可用。

移动到云供应商提供的特定 PaaS 服务也是这个阶段的一部分。如果您想使用诸如排队或通知等服务,或者任何其他服务,那么这个阶段就是您进行相关更改的阶段。

  • 微服务的 CI/CD 流程:开发人员将开始为微服务创建持续集成和部署的流水线。服务依赖关系被映射出并考虑在内。各种代码分析检查作为 CI 流程的一部分运行,以确保代码的生产准备性。额外的服务治理流程可以内置到流水线的各个阶段中。

  • 功能/集成测试:最后,开发人员将编写功能和集成测试套件,以验证服务的正确性。这些测试套件作为 CI 流水线的一部分进行集成。在部署新代码时,这些测试作为回归的一部分运行,以确保功能的正确性。

打破单体应用

数字化转型的关键步骤之一是实际重构单体应用程序。在这种情况下,我们假设需要将基于 Java 的应用程序重构/拆分为分布式应用程序:

  • 初始状态:在我们开始之前,我们先了解一下单体应用的初始状态。在这种状态下,应用由部署单元(如 WAR 文件)组成,内部由多个 JAR 文件组成。代码以逻辑方式布局,跨展示、业务和数据层具有一定的逻辑结构。每个层次都根据模块或基于模块的子包进行了进一步的分叉。如果没有,也可以根据类名的区别来识别模块。配置存储为一组外部属性文件。代码覆盖率不错(超过 60%),有潜力编写更多的测试用例。

  • 代码重构:下一步是从单体应用程序中切割出可能一起的代码片段。例如,跨模块的类可以打包为单独的 Java 项目。常见文件或实用程序类可以打包为单独的 JAR 文件。在从单一代码项目重构代码时,将创建多个相互依赖的 Java 项目。将 JAR 文件打包为更大的 WAR 或 EAR 文件的一部分。请记住,我们正在处理代码基础的主干。更改被集成并同步回分支代码。

除了代码,您还需要重构应用程序配置。在重构代码的同时,需要将配置映射到相应的 Java 项目。配置可能特定于项目/模块,跨模块共享,或者是全局的,用于整个应用程序。

  • 构建过程更新:在进行代码重构的过程中,创建较小的独立 Java 项目,您需要更新项目构建过程。Java 项目需要按照它们相互依赖的顺序进行构建。随着项目的划分,构建过程不断进行迭代。构建过程与代码重构步骤一起更新。

随着代码的重构,更新的 WAR/EAR 需要部署到生产环境。这确保了代码重构的有效性,并考虑了其他指标——代码覆盖率、单元测试、回归测试等。这确保了您的工作每天都会被纳入生产。

  • Java 版本更新:我们多次看到项目中使用的 JVM 版本可能不是最新的。一些较新的响应式框架通常需要 Java 1.7 及更高版本。这意味着基本的 JVM 版本需要升级。这可能需要对应用程序代码进行重构,以适应已弃用的功能。某些代码片段可能需要升级以适应新功能。重构后的代码需要与升级后的 JVM 版本一起投入生产。

  • 引入断路器/响应式模式:代码重构的下一步是升级代码以实现弹性模式。您可以通过实现 Java 库(如 Hystrix)引入断路器等模式。您还可以通过实现异步消息传递、引入响应式框架(如 Spring Boot、Vert.x、Dropwizard 等)以及改进并发性(如 Akka、RxJava 等)来改进模块间的代码,并将所有更改应用到生产代码并与分支代码集成。

  • 特性标志实施:有时,您可能正在集成来自分支的代码。在这种情况下,您可能不希望某些代码立即上线。您可以在代码中引入特性标志,并通过配置进行控制。因此,您可以将可能在特性准备上线之前处于停用状态的代码投入生产。

  • 持续的功能更新:应用程序将不断进行功能性的更改/更新。更改将应用到代码中,并定期与分支代码同步。

将所有内容整合在一起

我们看到四个轨道在各自的能力上在应用程序中运作。现在我们以协作的方式将所有四个轨道结合起来。随着单片应用程序的转变,其他轨道为划分界限上下文和相关微服务奠定了基础平台:

我们可以看到两个轨道如何改变业务,运行业务重叠,并为从单片模型迁移到分布式应用程序模型提供完美的平衡。

这类似于在行驶中更换汽车轮胎。

构建自己的平台服务(控制与委托)

企业面临的另一个关键决定是如何选择平台:

  • 我应该构建自己的平台吗?

  • 我应该订阅现有平台并在其上开发我的应用程序吗?

这个决定归结为如何看待技术,是作为一种促进因素(控制)还是作为一种差异化因素(委托)?

从根本上讲,所有公司都是科技公司。但问题是,控制技术是否能为您提供与竞争对手的额外优势,或者帮助构建一个可能阻止新参与者加入的壕沟。让我们举几个例子,看看它是如何发挥作用的:

  • 如果您计划在零售领域与亚马逊等公司竞争,您需要有雄厚的资金实力。亚马逊零售的低利润业务是由 AWS 的盈利业务支持的。因此,除非您有一个资金雄厚的支持者或替代收入模式,与亚马逊竞争将不会容易。但是假设您有雄厚的资金实力,您可以开始在 AWS 或任何云提供商上建模您的零售平台吗?是的!您可以从任何公共云平台开始,一旦您有可预测的需求,您可以转向私有云模型。这种模型可以节省您的前期资本支出。

  • 让我们以销售实体产品的制造领域为例。他们可以潜在地利用物联网设备来增强他们的产品,这些设备提供关于产品性能和使用情况的定期数据流。公司收集这些数据,并提供围绕这些产品的数字服务(如预测性维护)的分析服务。现在,您可以在任何云提供商上建模和构建分析模型。平台的选择可以由认知或数据处理能力的选择来确定。您可以从平台选择认知服务,甚至创建您自己的认知服务。基础平台能力委托给云提供商。您专注于构建正确的模型来进行预测。

没有正确或错误的模型。您可以从代表(选择公共云提供商)开始,然后转向控制模型(私有云),在那里您可以完全控制应用程序的功能/功能。在没有大量前期投资和锁定的情况下,很容易在云提供商模型上进行转变。关键是要确定您的差异化所在!

总结

这就结束了数字转型。我们看到了我们需要评估应用程序组合以寻找转型机会。我们看到了单片应用程序对实现业务目标的阻碍原因。

一旦确定了转型机会,我们可以将现有的单片应用程序转移到分布式应用程序模型。我们看到需要在人员、流程和技术层面采取各种步骤。

这也结束了在 Java 中构建云原生应用程序的整体旅程。我们看到了构建基于微服务的新时代应用程序的各种工具/技术,如何构建它们,如何将这些应用程序投入生产,如何监视它们,以及我们如何将这些应用程序用于 AWS 和 Azure 等云提供商。我们还看到了构建基于 API 的平台的一些最佳实践,以及如何将现有的单片应用程序转变为分布式微服务应用程序。

posted @ 2025-09-11 09:45  绝不原创的飞龙  阅读(4)  评论(0)    收藏  举报