云原生-容器-函数-数据和-Kubernetes-指南-全-

云原生:容器、函数、数据和 Kubernetes 指南(全)

原文:zh.annas-archive.org/md5/fff0b37ef17af81234321abccc82fca7

译者:飞龙

协议:CC BY-NC-SA 4.0

序言

各公司和行业的思想领袖们一直在重申沃茨·亨弗里(Watts Humphrey)的声明,“每个企业都将成为软件企业。”他说得没错。软件正在改变世界,并挑战现有公司的现状。Netflix 已经彻底改变了我们获取和消费电视节目和电影的方式,Uber 改变了交通运输行业,Airbnb 挑战了酒店行业。几年前这是不可想象的,但是软件使新公司得以进入各行业并建立新的思维和商业模式。

前面提到的公司通常被称为“云原生公司”,这意味着它们的产品基础是在云中运行的服务。这些服务的构建方式使公司能够快速响应市场和客户需求,在短时间内发布更新和修复,使用最新技术,并利用云提供的改进经济条件。以云原生方式构建的服务还使公司能够重新思考其商业模式并转向新模式,如订阅模型。这种服务通常被称为云原生应用

云原生应用的成功和普及使许多企业采用了云原生架构,甚至将许多概念引入到本地应用程序中。

云原生应用的核心是容器函数数据。市面上有许多专注于这些具体技术的书籍。云原生应用利用所有这些技术,并利用云计算提供的所有优势和利益。我们作为作者,看到许多客户在拼凑所有这些技术以设计和开发云原生应用时遇到困难,因此我们决定撰写这本书,旨在提供能使开发人员和架构师开始设计云原生应用的基础知识。

这本书从奠定基础开始,帮助读者理解分布式计算的基本原理以及它们与云原生应用的关系,同时深入探讨容器和函数。此外,书中还涵盖了服务通信模式、可靠性和数据模式,以及在何时使用何种模式的指导。该书最后解释了 DevOps 方法、可移植性考虑因素,以及我们认为在成功的云原生应用中非常有用的一些最佳实践。

本书并非特定要求的云原生应用程序逐步实施指南。阅读本书后,您应具备设计、构建和操作成功云原生应用程序的理解和知识。教程适用于处理非常具体的需求,但对云原生应用程序的基础理解为团队提供了运送成功云原生应用程序所需的必要技能。

本书中使用的约定

本书中使用以下排版约定:

Italic

指示新术语、URL、电子邮件地址、文件名和文件扩展名。

Constant width

用于程序清单,以及段落内部用来指代程序元素,如变量或函数名、数据库、数据类型、环境变量、语句和关键字。

Constant width bold

显示用户应该按照字面意义输入的命令或其他文本。

Constant width italic

显示应由用户提供值或由上下文确定值的文本。

小贴士

此元素表示提示或建议。

注释

此元素表示一般备注。

警告

此元素指示警告或注意事项。

O’Reilly 在线学习

注释

几乎 40 年来,O’Reilly Media 一直为公司提供技术和商业培训、知识和见解,帮助它们取得成功。

我们独特的专家和创新者网络通过书籍、文章、会议以及我们的在线学习平台分享他们的知识和专长。O’Reilly 的在线学习平台提供即时访问活动培训课程、深入学习路径、交互式编码环境,以及来自 O’Reilly 和其他 200 多家出版商的大量文本和视频。更多信息,请访问http://oreilly.com

如何联系我们

请将关于本书的评论和问题发送至出版社:

  • O’Reilly Media, Inc.

  • 1005 Gravenstein Highway North

  • Sebastopol, CA 95472

  • 800-998-9938(美国或加拿大)

  • 707-829-0515(国际或本地)

  • 707-829-0104(传真)

我们为本书设立了一个网页,在那里列出勘误、示例和任何额外信息。您可以访问这个页面http://bit.ly/cloud-native-1e

如需对本书发表评论或提出技术问题,请发送电子邮件至bookquestions@oreilly.com

有关我们的书籍、课程、会议和新闻的更多信息,请访问我们的网站http://www.oreilly.com

在 Facebook 上找到我们:http://facebook.com/oreilly

在 Twitter 上关注我们:http://twitter.com/oreillymedia

观看我们的 YouTube 频道:http://www.youtube.com/oreillymedia

致谢

我们要感谢我们的编辑尼科尔·塔谢在 O’Reilly,以及技术审阅者和 beta 读者对这本书的宝贵贡献。此外,我们还要感谢白海石和布尚·内尼对书籍质量进行彻底的审阅和提出的建议。

Boris 感谢他的妻子,克里斯蒂娜,以及他的孩子,玛丽和安东,因为在他写书期间如此耐心和支持。

Trent 感谢他的妻子,丽莎,以及他的儿子,马克,在他写这本书期间的支持和耐心。

Peter 感谢他的妻子,尼维斯,在他晚上和周末写书期间的支持、鼓励和理解。

第一章:云原生简介

什么是云原生应用程序?使它们如此吸引人的原因是什么,以至于云原生模型现在不仅被认为适用于云端,还适用于边缘?最后,你如何设计和开发云原生应用程序?这些都是本书将在全书中回答的问题。但在我们深入探讨什么、为什么和如何之前,我们想先简要介绍一下云原生世界及其一些构建现代云原生应用程序和环境基础的基本概念和假设。

分布式系统

开发人员在首次构建云原生应用程序时面临的最大障碍之一是,他们必须处理不在同一台机器上的服务,并且需要处理考虑到机器之间网络的模式。甚至在不知不觉中,他们已经进入了分布式系统的世界。分布式系统是指通过网络连接的个体计算机看起来像单个计算机的系统。能够在一群机器上分配计算能力是实现可伸缩性、可靠性和更好经济性的好方法。例如,大多数云提供商正在使用更便宜的通用硬件,并通过软件解决方案解决高可用性和可靠性等常见问题。

分布式系统的谬误

大多数开发人员和架构师在进入分布式系统的世界时会有一些不正确或无根据的假设。彼得·德意志(Peter Deutsch)是 Sun Microsystems 的一位研究员,在 1994 年就已经指出了分布式计算的谬误,那时还没有人考虑到云计算。因为云原生应用程序在其核心上就是分布式系统,这些谬误至今仍然有效。以下是德意志描述的谬误列表,以及它们在云原生应用程序中的含义:

网络是可靠的

即使在云中,你也不能假设网络是可靠的。因为服务通常放置在不同的机器上,你需要以一种能够应对潜在网络故障的方式开发软件,这一点我们稍后在本书中讨论。

延迟是零

延迟和带宽经常被混淆,但理解它们的区别非常重要。延迟是数据接收所需的时间,而带宽则表示在给定时间窗口内可以传输多少数据。由于延迟对用户体验和性能有很大影响,你应该注意以下几点:

  • 避免频繁的网络调用和引入网络通信的冗余。

  • 设计你的云原生应用程序,使数据通过使用缓存、内容传递网络(CDN)和多区域部署尽可能接近客户端。

  • 使用发布/订阅(pub/sub)机制来通知有新数据并将其存储在本地以便立即可用。 第三章 更详细地涵盖了诸如 pub/sub 之类的消息模式。

存在无限带宽

如今,网络带宽似乎不再是一个大问题,但是新技术和边缘计算等领域开辟了需要更多带宽的新场景。例如,预计自动驾驶汽车每天会产生约 50TB 的数据。这种数据量要求您在设计云原生应用时考虑带宽的使用情况。领域驱动设计(DDD)和数据模式,如命令查询责任分离(CQRS),在这种对带宽需求严苛的情况下非常有用。 第四章 和 第六章 更详细地介绍了在云原生应用中处理数据的方法。

网络是安全的

对于开发人员来说,两件事经常被忽视:诊断和安全性。假设网络是安全的这一假设可能是致命的。作为开发人员或架构师,您需要把安全性作为设计的优先考虑因素;例如,采用深度防御的方法。

拓扑结构不会改变

宠物与牲畜之间的对比是一个随着容器的出现而流行起来的梗。它意味着你不再将任何机器视为已知实体(宠物),具有其自身的属性,如静态 IP 等等。相反,你将机器视为没有特殊属性的群体成员。这个概念在云原生应用中非常重要。由于云环境旨在提供弹性,可以根据资源消耗或每秒请求等条件添加和移除机器。

存在一个管理员

在传统软件开发中,一个人负责环境的安装、升级应用程序等是相当普遍的。现代云架构和 DevOps 方法改变了软件构建的方式。现代云原生应用是许多服务的组合,这些服务需要协同工作,并由不同团队开发。这使得一个人几乎不可能全面了解和理解应用程序,更不用说解决问题了。因此,您需要确保有治理机制使故障排除变得容易。在本书的整个过程中,我们向您介绍了诸如发布管理解耦日志与监控等重要概念。 第五章 提供了云原生应用常见 DevOps 实践的详细介绍。

运输成本为零

从原生云的角度看,可以从两个方面看待这个谬误。首先,传输发生在网络上,大多数云提供商并不免费。例如,大多数云提供商不收取数据入口费用,但却会收取数据出口费用。另一个看待这个谬误的方式是,将任何有效载荷转换为对象的成本不是免费的。例如,序列化和反序列化通常是非常昂贵的操作,您需要考虑这些操作的延迟,而不仅仅是网络调用的延迟。

网络是同构的

这几乎不值得列出,因为几乎每个开发者和架构师都明白,在构建应用程序时必须考虑不同的协议。

如前所述,尽管这些谬论早在很久以前就有记录,但它们仍然是一个很好的提醒,告诉人们在进入原生云时会做出的错误假设。在本书中,我们教授您考虑到分布式计算的所有谬误的模式和最佳实践。

CAP 定理(CAP Theorem)

CAP 定理通常与分布式系统一起提到。CAP 定理指出,任何网络共享数据系统最多只能具备以下三种理想属性中的两种:

  • 一致性(C)等同于保持数据的单一最新副本

  • 高可用性(A)(用于更新)的数据

  • 对网络分区的容错性(P)

实际情况是,您总会有网络分区(记住,“网络是可靠的”是分布式计算谬误之一)。这让您只有两个选择——您可以优化一致性,也可以优化高可用性。许多 NoSQL 数据库(如 Cassandra)优化为高可用性,而遵循 ACID 原则(原子性、一致性、隔离性和持久性)的基于 SQL 的系统则优化为一致性。

十二要素应用(The Twelve-Factor App)

在基础设施即服务(IaaS)和平台即服务(PaaS)早期,很快就明显地需要一种新的应用程序开发方式。例如,传统的扩展往往通过垂直扩展来实现,即向机器添加更多资源。另一方面,在云中,扩展通常是水平的,即添加更多机器以分配负载。这种类型的扩展需要无状态应用程序,这是十二要素应用宣言描述的因素之一。十二要素应用方法可以被认为是原生云应用程序的基础,并由 Heroku 的工程师首次引入,源自云中应用程序开发的最佳实践。自从十二要素宣言引入以来,云开发已经发展,但原则仍然适用。以下是适用于原生云应用程序的 12 个因素及其含义:

  1. 代码库(Codebase)

    一种代码库在版本控制中跟踪;多次部署

    每个应用程序只有一个代码库,但可以部署到多个环境,例如开发、测试和生产。在云原生架构中,这直接转化为每个服务或函数一个代码库,每个都有自己的持续集成/持续部署(CI/CD)流程。

  2. 依赖关系

    明确声明和隔离依赖关系

    声明和隔离依赖关系是云原生开发的重要方面。许多问题源于缺少依赖关系或依赖关系版本不匹配,这些问题来自本地和云环境之间的环境差异。通常情况下,您应始终为诸如 Maven 或 npm 等语言使用依赖管理器。容器大大减少了基于依赖关系的问题,因为所有依赖关系都打包在容器内,并且应在 Dockerfile 中声明。Chef、Puppet、Ansible 和 Terraform 是管理和安装系统依赖项的优秀工具。

  3. 配置

    将配置存储在环境中

    配置应严格与代码分离。这样可以轻松地根据环境应用配置。例如,您可以有一个测试配置文件,其中存储了测试环境中使用的所有连接字符串和其他信息。如果您希望将同一应用程序部署到生产环境中,只需替换配置即可。许多现代平台支持外部配置,无论是 Kubernetes 中的配置映射还是云环境中的托管配置服务。

  4. 支持服务

    将支持服务视为附加资源

    支持服务被定义为“应用程序在正常操作中通过网络消耗的任何服务”。在云原生应用程序的情况下,这可能是托管的缓存服务或作为服务的数据库实现。建议是通过存储在外部配置系统中的配置设置访问这些服务,这允许松耦合,这也是云原生应用程序适用的原则之一。

  5. 构建、发布、运行

    严格分离构建和运行阶段

    正如您将在第五章中了解到的关于 DevOps 的内容,建议采用 CI/CD 实践实现完全自动化的构建和发布阶段。

  6. 进程

    在一个或多个无状态进程中执行应用程序

    正如前面提到的,在云中计算应该是无状态的,这意味着数据只应保存在进程外部。这样做可以实现弹性,这也是云计算的一个承诺之一。

  7. 数据隔离

    每个服务管理其自己的数据

    这是微服务架构的关键原则之一,这是云原生应用程序中常见的模式。每个服务管理其自己的数据,只能通过 API 访问,这意味着应用程序中的其他服务不允许直接访问另一个服务的数据。

  8. 并发性

    通过进程模型扩展

    Improved scale and resource usage are two of the key benefits of cloud native applications, meaning that you can scale each service or function independently and horizontally; thus, you’ll achieve better resource usage.

  9. Disposability

    通过快速启动和优雅关闭来最大化健壮性

    Containers and functions already satisfy this factor given that both provide fast startup times. One thing that is often neglected is to design for a crash or scale in scenario, meaning that the instance count of a function or a container is decreased, which is also captured in this factor.

  10. Dev/Prod Parity

    保持开发、演示和生产尽可能相似

    Containers allow you to package all of the dependencies of your service, which limits the issues with environment inconsistencies. There are scenarios that are a bit trickier, especially when you use managed services that are not available on-premises in your Dev environment. 第五章 介绍了保持环境尽可能一致的方法和技术。

  11. Logs

    Treat logs as event streams.

    Logging is one of the most important tasks in a distributed system. There are so many moving parts and without a good logging strategy, you would be “flying blind” when the application is not behaving as expected. The Twelve-Factor manifesto states that you should treat logs as streams, routed to external systems.

  12. Admin Processes

    将管理和管理任务作为一次性进程运行

    This basically means that you should execute administrative and management tasks as short-lived processes. Both functions and containers are great tools for that.

本书全程你会发现许多这些因素,因为它们对于云原生应用仍然非常重要。

Availability and Service-Level Agreements

Most of the time, cloud native applications are composite applications that use compute, such as containers and functions, but also managed cloud services such as DbaaS, caching services, and/or identity services. What is not obvious is that your compound Service-Level Agreement (SLA) will never be as high as the highest availability of an individual service. SLAs are typically measured in uptime in a year, more commonly referred to as “number of nines.” 表格 1-1 显示了云服务常见的可用性百分比及其对应的停机时间。

表格 1-1. 可用性百分比和服务停机时间

可用性 % 每年停机时间 每月停机时间 每周停机时间
99% 3.65 天 7.20 小时 1.68 小时
99.9% 8.76 小时 43.2 分钟 10.1 分钟
99.99% 52.56 minutes 4.32 minutes 1.01 minutes
99.999% 5.26 分钟 25.9 秒 6.05 秒
99.9999% 31.5 seconds 2.59 seconds 0.605 seconds

Following is an example of a compound SLA:

  • 服务 1(99.95%)+ 服务 2(99.90%):0.9995 × 0.9990 = 0.9985005

复合 SLA 为 99.85%。

总结

许多开发人员在开始为云端开发时感到困难。简而言之,开发人员面临三个主要挑战:首先,他们需要理解分布式系统;其次,他们需要了解诸如容器和函数等新技术;第三,他们需要了解在构建云原生应用程序时使用的模式。具备一些基础知识,如分布式系统的谬论、十二因素宣言和复合 SLA,将使过渡更加容易。本章介绍了云原生的一些基本概念,这使您能够更好地理解本书中讨论的一些架构考虑和模式。

第二章:基础知识

正如在第一章中讨论的,云原生应用是分布式的,并利用云基础设施。有许多技术和工具用于实现云原生应用,但从计算角度看,主要是函数容器。从架构角度来看,微服务架构已经非常流行。往往这些术语被误用,并经常被认为是同一回事。事实上,函数和容器是不同的技术,每种技术都有其特定的用途,而微服务则描述了一种架构风格。因此,了解如何最佳地使用函数和容器,以及事件消息传递技术,使开发者能够以最高效和敏捷的方式设计、开发和运行新一代基于云原生微服务的应用程序非常重要。为了做出正确的架构决策来设计这些类型的应用程序,理解基础术语和技术的基础知识至关重要。本章解释了与云原生应用程序一起使用的重要技术,并通过概述微服务架构风格来结束。

容器

起初,容器是由创业公司和云原生公司推广的,但在过去几年中,容器已成为应用现代化的代名词。如今,几乎没有公司不在使用容器或至少考虑将来使用容器,这意味着架构师和开发者都需要理解容器提供和不提供的内容。

如今,人们在谈论容器时,大多数时候指的是“Docker 容器”,因为 Docker 确实使容器变得流行起来。然而,在 Linux 操作系统(OS)世界中,容器已有十多年的历史。容器最初的想法是将操作系统切片,以便可以安全地运行多个应用程序而彼此不干扰。这种所需的隔离是通过命名空间和控制组来实现的,这些是 Linux 内核的功能。命名空间允许切片化操作系统的不同组件,从而创建隔离的工作空间。控制组则允许对资源利用进行精细化控制,有效地防止一个容器占用所有系统资源。

由于与内核特性的交互并不完全符合我们所说的开发者友好,Linux 容器(LXC)被引入以抽象化组合现在通常称为“容器”的各种技术基础的复杂性。最终,Docker 通过引入开发者友好的内核特性打包,使容器成为主流。Docker 将容器定义为“标准化的软件单元”。“软件单元”——或者更准确地说,运行在容器内的服务或应用程序——具有对自己独立的操作系统结构的全面私密访问。换句话说,您可以将容器视为封装的、可单独部署的组件,在同一内核上以隔离的实例运行,并在操作系统级别进行虚拟化。

clna 0201

图 2-1. 单个主机上的 VM 和容器

此外,容器采用写时复制文件系统策略,允许多个容器共享相同的数据,操作系统将数据的副本提供给需要修改或写入数据的容器。这使得容器在内存和磁盘空间使用方面非常轻量化,从而导致更快的启动时间,这是使用容器的重要好处之一。其他好处包括确定性部署,允许在不同环境间的可移植性、隔离性以及更高的密度。对于现代云原生应用来说,容器镜像已成为封装应用或服务代码、运行时、依赖关系、系统库等的部署单元。由于它们快速的启动时间,容器成为规模化应用场景的理想技术。图 2-1 展示了单个主机上虚拟机(VMs)和容器之间的区别。

容器隔离级别

因为容器基于操作系统虚拟化,在同一主机上运行时它们共享相同的内核。尽管这对大多数场景来说提供了足够的隔离,但它无法达到基于硬件虚拟化选项(例如 VM)提供的隔离级别。以下是将 VM 作为云原生应用基础的一些不足之处:

  • VM 的启动可能需要相当长的时间,因为它们会启动完整的操作系统。

  • VM 的大小可能是一个问题。一个 VM 包含整个操作系统,其大小可能轻易达到几个千兆字节。跨网络复制此映像(例如,如果它们存储在中央映像库中)将需要大量时间。

  • VM 的扩展具有其挑战性。扩展(增加更多资源)需要配置和启动一个新的、更大的 VM(更多的 CPU、内存、存储等)。扩展输出可能不足以快速响应需求;新实例的启动需要时间。

  • VMs 拥有更多的开销,例如内存、CPU 和磁盘资源。这限制了密度,即在单个主机上运行的 VM 数量。

需要在硬件虚拟化级别上实现高隔离的最常见场景是敌对多租户场景,在这些场景中,通常需要防止对同一主机或共享基础设施上其他目标的恶意逃逸和突破尝试。云提供商一直在内部使用技术来提供 VM 级别的隔离,同时保持容器的预期速度和效率。这些技术被称为Hyper-V 容器沙箱容器MicroVMs。以下是最流行的 MicroVM 技术(无特定顺序):

Nabla 容器

这些技术利用 unikernel 技术实现更好的隔离,具体来自Solo5 项目,限制容器对主机内核的系统调用。Nabla 容器运行时(runc)是一个符合 Open Container Initiative(OCI)标准的运行时。稍后本章节将更详细地解释 OCI。

Google's gVisor

这是一个用 Go 语言编写的容器运行时和用户空间内核。这个新内核是一个“用户空间”进程,处理容器的系统调用需求,防止直接与主机操作系统交互。gVisor 运行时(runSC)是一个符合 OCI 规范的运行时,它也支持 Kubernetes 编排。

Microsoft 的 Hyper-V 容器

几年前引入的 Microsoft 的 Hyper-V 容器基于 VM Worker Process(vmwp.exe)。这些容器提供完整的 VM 级隔离,并且符合 OCI 规范。至于在生产环境中在 Kubernetes 中运行Hyper-V 容器,你需要等待 Kubernetes 在 Windows 上的普及。

Kata 容器

Kata 容器结合了 Hyper.sh 和 Intel 的 clear 容器技术,提供经典的硬件辅助虚拟化。Kata 容器兼容 OCI 规范的 Docker 容器和 Kubernetes 的 CRI。

亚马逊的 Firecracker

Firecracker 正在为亚马逊的 Lambda 基础设施提供动力,并且已经在 Apache 2.0 许可下开源。Firecracker 是一个用户态 VM 解决方案,建立在 KVM API 之上,旨在以类似于 Kata 容器等更隔离的容器技术的方式运行现代 Linux 内核。请注意,截至目前,无法在 Kubernetes、Docker 或 Kata 容器中使用 Firecracker。

Figure 2-2 提供了这些技术的隔离级别概述。

clna 0202

图 2-2. 虚拟机、容器和进程的隔离级别

容器编排

要管理规模化的容器生命周期,您需要使用容器编排器。容器编排器的任务如下:

  • 向集群节点上的容器进行供应和部署

  • 容器资源管理,即将容器放置在提供足够资源的节点上,或者在节点资源限制达到时将容器移动到其他节点

  • 对容器和节点进行健康监控,以便在容器或节点级别出现故障时进行重新启动和重新调度

  • 在集群中缩放容器的数量

  • 提供容器连接网络的映射

  • 容器间的内部负载均衡

当前存在多种容器编排器,但毫无疑问 Kubernetes 是管理集群和调度容器工作负载的最受欢迎选择。

Kubernetes 概述

Kubernetes(通常简称为 k8s)是一个用于运行和管理容器的开源项目。Google 在 2014 年开源了该项目,Kubernetes 通常被视为容器平台、微服务平台和/或云可移植性层。所有主要的云供应商今天都提供托管的 Kubernetes 服务。

Kubernetes 集群运行多个组件,可以分为三类:主控组件节点组件插件。主控组件提供集群控制平面。这些组件负责做出整个集群范围的决策,如在集群中调度任务或响应事件,例如如果一个任务失败或不符合所需副本数量,则启动新任务。主控组件可以运行在集群的任何节点上,但通常部署在专用的主节点上。云供应商提供的托管 Kubernetes 服务将处理控制平面的管理,包括按需升级和补丁。

Kubernetes 主控组件包括以下内容:

kube-apiserver

提供 Kubernetes API,是 Kubernetes 控制平面的前端

etcd

用于存储所有集群数据的键/值存储

kube-scheduler

监视新创建的pods(Kubernetes 中特定管理容器的封装,稍后在本章节详细解释)是否已分配到节点,并找到可用节点

kube-controller-manager

管理一些控制器,负责响应宕机节点或维持正确数量的副本

cloud-controller-manager

运行与底层云提供商交互的控制器

节点组件在集群中的每个节点上运行,也称为数据平面,负责维护运行中的 Pod 和部署到节点的环境。

Kubernetes 节点组件包括以下内容:

kubelet

在集群中每个节点上运行的代理,负责根据其 pod 规范在 pods 中运行容器

kube-proxy

在节点上维护网络规则并执行连接转发

容器运行时

负责运行容器的软件(参见 “Kubernetes and Containers”)

图 2-3 展示了 Kubernetes 主控节点和工作节点组件。

clna 0203

图 2-3. Kubernetes 主控节点和工作节点组件

Kubernetes 通常与由主控节点和工作节点组件管理的插件一起部署。这些插件包括域名系统(DNS)和管理用户界面(UI)等服务。

本书不涉及 Kubernetes 的深入讨论。然而,有一些基本概念对您理解是很重要的:

Pods

Pod 基本上是围绕一个或多个容器、存储资源或唯一网络 IP 的管理包装器,管理容器的生命周期。尽管 Kubernetes 支持每个 pod 多个容器,但大多数情况下每个 pod 只有一个应用程序容器。也就是说,“sidecar 容器”模式非常流行,它扩展或增强应用程序容器的功能。像 Istio 这样的服务网格大量依赖 sidecars,如您可以在 第三章 中看到的那样。

服务

Kubernetes 服务为集群上运行的一组 pods 提供稳定的端点。Kubernetes 使用标签选择器标识服务的目标 pods。

ReplicaSets

最简单的方式来理解 ReplicaSets 就是将其视为服务实例。您基本上定义了需要多少个 pod 的副本,Kubernetes 确保在任何给定时间内都有这些副本在运行。

Deployments

Kubernetes 部署文档说明,“您可以使用 Deployment 对象描述所需的状态,部署控制器以受控速率将实际状态更改为所需状态。” 换句话说,您应该使用 Deployments 来逐步推出和监控 ReplicaSets,扩展 ReplicaSets,更新 pods,回滚到早期的 Deployment 版本,并清理旧的 ReplicaSets。

图 2-4 展示了基本 Kubernetes 概念的逻辑视图及其相互作用。

clna 0204

图 2-4. 基本 Kubernetes 概念

Kubernetes 和容器

Kubernetes 只是容器的编排平台,因此需要一个容器运行时来管理容器的生命周期。从一开始,Kubernetes 就支持 Docker 运行时,但市场上并非只有 Docker 运行时可用。因此,Kubernetes 社区推动了一种通用的方法来集成容器运行时到 Kubernetes 中。接口已被证明是在两个系统之间提供契约的良好软件模式,因此社区创建了 容器运行时接口 (CRI)。CRI 避免了将特定的运行时需求硬编码到 Kubernetes 代码库中的问题,因此当容器运行时发生更改时,总是需要更新 Kubernetes 代码库。相反,CRI 描述了容器运行时必须实现的功能。CRI 描述的功能包括处理容器 pod 的生命周期(启动、停止、暂停、杀死、删除)、容器镜像管理(例如从注册表下载镜像)以及一些辅助功能,如日志和指标收集以及网络功能。图 2-5 展示了 Docker 和 Kata 容器的高级 CRI 示例架构。

clna 0205

图 2-5. Docker versus Kata container on Kubernetes

以下列表提供了其他可能有用的与容器相关的技术:

OCI

OCI 是一个由 Linux Foundation 推动的项目,旨在设计容器镜像和运行时的开放标准。许多容器技术实现了兼容 OCI 的运行时和镜像规范。

containerd

containerd 是一个行业标准的容器运行时,被 Docker 和 Kubernetes CRI 使用,仅举两个最流行的例子。它作为 Linux 和 Windows 的守护程序可用,可以管理其主机系统的完整容器生命周期,包括容器镜像管理、容器执行、底层存储和网络附加。

Moby

Moby 是由 Docker 创建的一组开源工具,旨在实现和加速软件容器化。这套工具包括容器构建工具、容器注册表、编排工具、运行时等等,你可以将其作为其他工具和项目的构建模块。Moby 使用 containerd 作为默认的容器运行时。

无服务器计算

无服务器计算意味着规模和基础架构由云提供商管理;也就是说,您的应用程序自动驱动资源的分配和释放,您无需担心管理底层基础设施。所有管理和操作都被从用户身上抽象出来,并由微软 Azure、亚马逊 AWS 和谷歌云平台等云提供商管理。从开发者的角度来看,无服务器通常添加了事件驱动的编程模型;从经济学的角度来看,您只需按执行付费(消耗的 CPU 时间)。

许多人认为函数即服务(FaaS)是无服务器的。从技术上讲,这是正确的,但 FaaS 只是无服务器计算的一种变体。微软 Azure 的容器实例(ACI)和 Azure SF Mesh,以及 AWS Fargate 和 GCP 的云函数上的无服务器容器,都是很好的例子。ACI 和 AWS Fargate 也是被称为容器即服务(CaaS)的无服务器容器提供,允许您部署容器化应用程序而无需了解底层基础设施。无服务器提供的其他示例包括 API 管理和机器学习服务,基本上,任何让您消耗功能而不必管理底层基础设施,并且采用按使用量付费模式的服务都属于无服务器提供。

函数

当谈论函数时,人们通常会谈论诸如 AWS Lambda、Azure Functions 和 Google Cloud Functions 等 FaaS 提供的服务,这些服务是在无服务器基础设施上实现的。无服务器计算的优势——快速启动和执行时间,以及简化应用程序——使 FaaS 提供非常吸引开发者,因为它允许他们专注于编写代码。

从开发的角度来看,函数是工作的单元,这意味着您的代码有一个开始和一个结束。函数通常由其他函数或平台服务发出的事件触发。例如,可以通过向数据库服务或事件服务添加条目来触发函数。当您希望建立一个仅仅依靠函数构建的大型复杂应用程序时,需要考虑的事情还有很多。您将需要管理更多独立的代码,确保状态被处理,如果函数必须相互依赖,您将需要实现模式,这只是其中的几个。容器化微服务共享很多相同的模式,因此关于何时使用 FaaS 或容器已经进行了很多讨论。表 2-1 提供了 FaaS 和容器之间的一些高级指导,而第 3 章更详细地讨论了其中的权衡。

表 2-1. FaaS 和容器化服务的比较

FaaS 容器化服务
只做一件事 做多于一件事
无法部署依赖 可以部署依赖
必须响应一种事件 可以响应多种事件

使用 FaaS 提供的服务可能并不总是理想的两种情况。尽管它提供了最好的经济性,但首先,你需要避免供应商锁定。因为你需要根据 FaaS 提供的服务来开发你的函数,并且使用提供商的更高级云服务,这使得整个应用程序变得不太可移植。其次,你可能希望在本地或自己的集群上运行函数。有许多开源的 FaaS 运行时可用,并且可以在任何 Kubernetes 集群上运行。Kubeless、OpenFaaS、Serverless 和 Apache OpenWhisk 是最受欢迎的可安装 FaaS 平台,Azure Functions 自开源以来也越来越受欢迎。可安装的 FaaS 平台通常通过容器部署,并允许开发人员简单地部署小段代码(函数),而不必担心底层基础设施。许多可安装的 FaaS 框架使用 Kubernetes 资源进行路由、自动缩放和监控。

任何 FaaS 实现的关键方面,无论它是在云提供商的无服务器基础设施上运行还是安装在你自己的集群上,都是启动时间。通常情况下,你希望函数在被触发后能够非常快速地执行,这意味着它们的底层技术需要提供非常快的启动时间。正如前面讨论的,容器提供良好的启动时间,但并不一定提供最佳的隔离性。

从虚拟机到云原生

要理解我们是如何进入下一代云原生应用的,值得看一看应用程序是如何从运行在虚拟机上到函数上演变的过程。描述这一旅程应该能够给你一个很好的想法,即 IT 行业正在如何转变以提升开发者的生产力,以及如何利用所有新技术的优势。云原生世界真的有两条不同的路径。第一条主要用于现有场景,这意味着你有一个现有的应用程序,并且通常遵循搬迁和升级,最终进行优化的过程。第二条是新建场景,在这种情况下,你可以从头开始创建你的应用程序。

搬迁和升级

直接在云中的机器上安装软件仍然是许多客户迁移到云的第一步。主要好处在于资本和运营费用领域,因为客户不需要运营自己的数据中心,或者至少可以减少运营成本。从技术角度来看,将应用程序迁移到基础设施即服务(IaaS)中可以让您对整个堆栈拥有最大的控制权。控制权伴随责任,直接在机器上安装软件通常会导致由于缺少依赖项、运行时版本冲突、资源争用和隔离而导致的错误。下一个合乎逻辑的步骤是将应用程序迁移到平台即服务(PaaS)环境中。在容器变得流行之前,PaaS 已经存在很久;例如,Azure 云服务可以追溯到 2010 年。在大多数过去的 PaaS 环境中,对底层虚拟机的访问受到限制,或者在某些情况下被禁止,因此迁移到云端需要对应用程序进行一些重写。对开发人员的好处在于不再需要担心底层基础设施。云提供商会处理操作任务,如打补丁操作系统,但一些问题,如缺少依赖项,仍然存在。由于许多 PaaS 服务基于虚拟机,因此在突发情况下进行扩展仍然是一个挑战,这是由于之前讨论过的虚拟机的缺点,以及出于经济原因。

应用现代化

除了提供超快的启动时间外,容器还极大地消除了缺少依赖项的问题,因为应用程序所需的一切都打包在容器内。开发人员很快就开始喜欢容器作为一种打包格式的概念,现在几乎每个新应用程序都在使用容器,越来越多的单体遗留应用程序正在被容器化。许多客户将现有应用程序的容器化视为一个机会,也可以迁移到更适合云原生环境的架构。微服务是显而易见的选择,但正如您将在本章后面看到的,迁移到这样的架构也带来了一些缺点。然而,有几个非常明显的原因,您希望拆分您的单体应用:

  • 部署时间更快。

  • 某些组件需要比其他组件更频繁地更新。

  • 某些组件需要不同的规模要求。

  • 某些组件应该采用不同的技术进行开发。

  • 代码库变得太庞大和复杂。

尽管拆分单体应用的方法论超出了本书的范围,但值得一提的是从单体应用迁移到微服务的两种主要模式。

Strangler 模式

使用“Strangler”模式,你逐步重构单体应用程序。新服务或现有组件被实现为微服务。一个门面或者网关将用户请求路由到正确的应用程序。随着时间的推移,越来越多的功能被转移到新的架构中,直到单体应用程序完全转变为微服务应用程序。

防腐层模式

当新服务需要访问旧应用程序时,类似于 Strangler 模式。该层将现有应用程序的概念转换为新的概念,反之亦然。

我们在第六章中更详细地描述了这些模式。

随着应用程序被打包为容器镜像,编排器开始扮演更重要的角色。尽管一开始有几种选择,但 Kubernetes 如今已成为最流行的选择;事实上,它被视为新的云操作系统。然而,编排器为开发和运维团队增加了另一个变量。环境管理部分变得更好了,因为几乎每个云供应商现在都提供“编排器即服务”。与任何云提供商一样,“托管” Kubernetes 意味着 Kubernetes 服务的设置和运行时部分是由供应商管理的。从经济学角度来看,用户通常按计算小时付费,这意味着只要集群节点运行,即使应用程序可能处于空闲状态或低资源利用率,也会产生费用。

从开发者的角度来看,如果你想在其上构建微服务应用程序,你仍然需要了解 Kubernetes 的工作原理,因为 Kubernetes 默认不提供任何平台即服务(PaaS)或容器即服务(CaaS)功能。

例如,Kubernetes 服务并不真正代表容器内的服务代码,它只是为其提供一个端点,以便始终可以通过相同的端点访问容器内的代码。除了需要理解 Kubernetes 外,开发人员还开始接触处理弹性、诊断和路由等分布式系统模式。

诸如 Istio 或 Linkerd 的服务网格因将部分分布式系统复杂性移到平台层而日益流行。第三章详细介绍了服务网格,但现在你可以将服务网格看作是一个专用的网络基础设施层,用于处理服务间的通信。服务网格除了其他功能外,还支持重试、熔断器、分布式跟踪和路由等弹性特性。

应用演进的下一步是使用无服务器基础设施来处理容器化工作负载,即 CaaS 提供的解决方案,如 Azure 容器实例或 AWS Fargate。微软 Azure 在将其托管的 Kubernetes 服务(AKS)与其 CaaS 提供的 ACI 融合方面做得非常出色,通过使用虚拟节点。虚拟节点基于微软的开源项目 Virtual Kubelet,它允许任何计算资源充当 Kubernetes 节点并使用 Kubernetes 控制平面。对于 AKS 虚拟节点而言,您可以将应用程序调度到 AKS 并在需要扩展的情况下无需设置额外的节点即可扩展到 ACI。图 2-6 显示了现有的单片应用程序(Legacy App)如何被拆分为更小的微服务(Feature 3)。遗留应用程序和新的微服务(Feature 3)位于 Kubernetes 上的服务网格中。在这种情况下,Feature 3 具有独立的扩展需求,并且可以使用 Virtual Kubelet 扩展到 CaaS 提供的环境中。

clna 0206

图 2-6. 使用 Virtual Kubelet 将 Feature 3 扩展到 CaaS 中的现代化应用程序

应用程序优化

下一步是不仅在成本优化方面进一步优化应用程序,还包括代码优化。函数在短期计算场景(如更新记录、发送电子邮件、转换消息等)中表现出色。要利用函数,您可以在服务代码库中识别出短期计算功能,并使用函数来实现。一个很好的例子是订单服务,其中容器化微服务执行所有的创建、读取、更新和删除(CRUD)操作,而函数则发送成功下单的通知。为了触发函数,通常使用事件或消息系统。最终,您可以决定使用函数构建整个订单服务,每个函数执行 CRUD 操作中的一个。

微服务

"微服务" 是一个常用术语,用来指代微服务架构风格或微服务架构中的各个服务。微服务架构是一种面向服务的架构,其中应用程序根据功能领域被分解为小型、松耦合的服务。服务保持相对较小、松耦合,并围绕业务能力进行分解是非常重要的。

微服务架构通常与单体架构进行比较和对比。与单体架构管理单一代码库和共享数据存储和数据结构不同,在微服务架构中,一个应用程序由由独立团队创建和管理的更小的代码库组成。每个服务由一个小团队拥有和操作,服务的所有元素都贡献于一个单一明确定义的任务。服务在单独的进程中运行,并通过同步或异步基于消息的 API 进行通信。

每个服务都可以被视为具有独立团队、测试、构建、数据和部署的独立应用程序。图 2-7 展示了微服务架构的概念,以库存服务为例。

clna 0207

图 2-7. 微服务架构中的库存服务

微服务架构的优点

一个正确实施的微服务架构将增加大型应用程序的发布速度,使企业能够更快、更可靠地为客户提供价值。

敏捷性

大型的单体应用程序可能会面临快速、可靠的部署挑战。对一个特性区域的模块进行小改动的部署可能会受到对另一个特性的改动的影响。随着应用程序的增长,应用程序的测试将增加,并且要向利益相关者交付新的价值可能需要相当长的时间。对一个特性的更改将要求整个应用程序被重新部署并前进或后退,如果出现问题。通过将应用程序分解为更小的服务,可以减少验证和发布变更所需的时间,并更可靠地进行部署。

持续创新

企业需要更快地移动,以保持今天的相关性。这要求组织具备敏捷性,并能够快速适应快速变化的市场条件。企业不能再等待数年或数月来为客户提供新的价值:他们通常必须每天交付新的价值。微服务架构可以更容易地以可靠的方式向利益相关者交付价值。小而独立的团队能够发布功能,并在繁忙时期进行 A/B 测试以改进转化率或用户体验。

进化设计

在大型单体应用中,很难采用新技术或技术,因为这通常要求整个应用程序被重写,或者需要确保某些新依赖可以与之前的依赖并行运行。松散耦合和高功能内聚对于能够通过变化的技术进行演变的系统设计非常重要。通过将应用程序按特性分解为小型、松散耦合的服务,可以更轻松地修改单个服务而不影响整个应用程序。如果需要支持业务,可以在不同服务之间使用不同的语言、框架和库。

小而专注的团队

在规模化构建工程团队并保持他们专注和高效可能是具有挑战性的。如果你正在构建的内容与其他人正在构建的内容紧密相连,那么让人们负责设计、运行和操作他们构建的内容也可能会很具有挑战性。新团队成员有时需要花费几天、几周,甚至几个月的时间才能迅速上手并开始贡献,因为他们需要理解与他们关注领域无关的系统方面。通过将应用程序分解为较小的服务,小敏捷团队能够专注于较小的关注点并快速迭代。新成员加入时也会更容易,因为他们只需关注较小的一个服务。团队成员可以更容易地运行和对他们构建的服务负责。

故障隔离

在单体应用中,一个单一的库或模块可能会对整个应用程序造成问题。一个模块中的内存泄漏不仅会影响整个应用程序的稳定性和性能,而且往往很难隔离和识别。通过将应用程序的特性分解为独立服务,团队可以将一个服务中的缺陷隔离到该服务中。

改进的规模和资源使用

应用通常是通过增加机器的大小或类型来进行扩展,也可以通过增加部署的实例数量并在这些实例之间路由用户来进行扩展。应用程序的不同特性有时会有不同的资源需求;例如内存、CPU、磁盘等。应用程序的不同特性通常会有不同的规模需求。一些特性可能可以轻松地通过很少的资源在每个实例上进行扩展,而其他特性可能需要大量内存,且能力有限以进行扩展。通过将这些特性解耦为独立服务,团队可以配置这些服务在最符合服务、个体资源和规模需求的环境中运行。

改进的可观察性

在单体应用程序中,如果不对整个应用程序进行仔细和详细的仪表化,很难测量和观察应用程序的各个组件。通过将应用程序的特性分解为单独的服务,团队可以使用工具更深入地了解各个特性的行为以及与其他特性的交互。例如,系统指标如进程利用率和内存使用现在可以轻松地与特性团队关联起来,因为它们运行在单独的进程或容器中。

微服务架构的挑战

尽管微服务架构带来了诸多好处,但也存在权衡和挑战。工具和技术已经开始解决其中一些挑战,但许多挑战仍然存在。对于所有应用程序来说,微服务架构可能并非今天的最佳选择,但我们仍然可以将许多概念和实践应用于其他架构中。最佳方法通常介于两者之间。

复杂性

分布式系统本质上复杂。当我们将应用程序分解为单独的服务时,网络调用是这些服务之间通信的必要手段。网络调用会增加延迟,并可能经历暂时性故障,而且运行在不同机器上的操作可能具有不同的时钟,每个时钟对当前时间有稍微不同的感知。我们不能假设网络是可靠的,延迟为零,带宽是无限的,网络是安全的,拓扑结构不会改变,有一个管理员,传输成本为零,并且网络是同质化的。许多开发人员对分布式系统不熟悉,并且在进入这个领域时常常做出错误的假设。分布式计算的谬误,正如在第一章中讨论的那样,是一组描述开发人员常犯的这些错误假设的声明。这些错误首次由 L. Peter Deutsch 和其他 Sun Microsystems 工程师记录,并在众多博客文章中进行了讨论。第六章提供了处理分布式系统复杂性的最佳实践、工具和技术的更多信息。

数据完整性和一致性

分散化的数据意味着数据通常存在于多个地方,并且跨不同系统存在关系。在这些系统之间执行事务可能会很困难,我们需要采用不同的数据管理方法。一个服务可能与另一个服务中的数据存在关联;例如,订单服务可能会引用帐户服务中的客户。为了满足某些性能要求,数据可能已经从帐户服务复制过来。如果客户被删除或禁用,则订单服务更新指示此状态可能很重要。处理数据将需要不同的方法。第四章涵盖了处理这些情况的模式。

性能

网络请求和数据序列化会增加额外开销。在基于微服务的架构中,网络请求的数量将会增加。请记住,组件是不再直接调用的库;而是通过网络进行调用。对一个服务的调用可能会导致对其他依赖服务的调用。为了满足原始请求,可能需要向多个服务发送多个请求。我们可以实施一些模式和最佳实践,以减轻微服务架构中潜在的性能开销,这些内容将在第六章中介绍。

开发和测试

开发可能会更具挑战性,因为今天使用的工具和实践方法不适用于微服务架构。考虑到变化的速度以及存在更多外部依赖的事实,运行与生产环境中运行的依赖服务版本完全一致的完整测试套件可能会很具挑战性。我们可以采用不同的测试方法来解决这些挑战,而且将需要一个适当的持续集成/持续部署(CI/CD)流水线。多年来,开发工具和测试策略已经在演变,以更好地适应微服务架构。第五章涵盖了许多工具、技术和最佳实践。

版本控制和集成

在单体应用程序中更改接口可能需要进行一些重构,但更改通常作为一个单一的统一单元构建、测试和部署。在微服务架构中,服务的依赖关系是在独立演化和变化的。在处理服务版本控制时,需要特别注意前向和后向兼容性。除了与服务更改保持前向和后向兼容性之外,还可能需要在一段时间内与之前版本并存并在旁边运行完全新的服务版本。第五章探讨了服务版本控制和集成策略。

监控和日志记录

许多组织在监视和记录单体应用程序时遇到困难,即使它们使用了共享的日志记录库。名称、数据类型和值的不一致性使得相关日志事件的关联变得困难。在微服务架构中,当相关事件跨越多个服务时——每个服务可能使用不同的日志记录实现——关联这些事件可能会更加具有挑战性。计划并早期关注日志记录和监视的重要性可以帮助解决其中大部分问题,我们将在第五章进行详细探讨。

服务依赖管理

使用单体应用程序时,通常会将对库的依赖项编译到单个包中并进行测试。在微服务架构中,服务依赖关系的管理方式不同,需要特定于环境的路由和发现。在解决这些挑战方面,服务发现和路由工具和技术已经取得了长足的进步。第三章深入探讨了这些内容。

可用性

尽管微服务架构可以帮助将故障隔离到单个服务,但如果其他服务或整个应用程序无法在没有该服务的情况下运行,则应用程序将无法使用。随着服务数量的增加,某个服务遇到故障的可能性也增加。服务将需要实施弹性设计模式,或在服务故障时降低某些功能。第六章涵盖了构建高可用应用程序的模式和最佳实践,并详细讨论了具体的挑战。

总结

每个应用程序,无论是云原生还是传统的,都需要基础设施来托管,技术来解决开发和部署中的痛点,以及能够帮助实现业务目标的架构风格,如市场投放时间。本章的目标是为云原生应用程序提供基础知识。到目前为止,您应该了解到有各种容器技术具有不同的隔离级别,函数如何与容器相关联,以及无服务器基础设施并不总是需要函数即服务。此外,您应该对微服务架构有基本的理解,以及如何将现有应用程序迁移到云原生应用程序。

即将到来的章节将在此基础上深入探讨如何设计、开发和运营云原生应用程序。

第三章:云原生应用设计

应用架构是独特业务需求的产物,这使得制定通用适用的架构蓝图变得困难。云原生应用也不例外。设计云原生应用的一个好方法是,在初始设计阶段考虑五个关键领域:运营卓越、安全性、可靠性、可扩展性和成本。从实际实施的角度来看,有一些已被证明在解决特定问题时非常有用的构建模块、模式和技术。除了讨论这五个关键领域外,本章还涵盖了最常见的架构构建模块。

本章的目标是为您提供设计和构建云原生架构所需的知识,使其更加高效。

云原生应用基础

所有主要的云提供商都提供了如何构建针对其各自云环境的应用程序的指导。Microsoft Azure 有其云应用架构和云模式指南,Amazon Web Services(AWS)有其良好架构框架,Google 提供了关于如何构建云原生应用的各种指南。虽然这些指南更具体地适用于每个环境中提供的服务,但无论您选择的云提供商是哪个,您都可以识别出五个通常适用的支柱,无论如何都应该记住。

运营卓越……

第四章:数据处理

云计算已经对我们今天的软件构建和操作方式产生了重大影响,包括我们处理数据的方式。存储数据的成本显著降低,使得公司可以更便宜、更可行地保留大量数据。随着托管和无服务器数据存储服务的出现,数据库系统的运营开销大幅减少。这使得数据更容易分布到不同的数据存储类型中,将数据放入更适合管理存储的系统中。微服务架构的趋势鼓励数据的去中心化,将应用程序的数据分布到多个服务中,每个服务都有自己的数据存储。数据复制和分区是扩展系统的常见做法。图 4-1 显示了典型架构将包含多个数据存储系统,并在它们之间分布数据。在一个数据存储中存储的数据通常是从另一个存储中的数据复制而来,或者与另一个存储中的数据有某种关系。

云原生应用充分利用托管和无服务器数据存储和处理服务。所有主要的公共云提供商都提供多种不同的托管服务来存储、处理和分析数据。除了云提供商提供的托管数据库服务外,一些公司还在您选择的云提供商上提供托管数据库。例如,MongoDB 提供了一种名为 MongoDB Atlas 的云托管数据库服务,可在亚马逊云服务(AWS)、微软 Azure 和谷歌云平台(GCP)上使用。通过使用托管数据库,团队可以专注于构建使用数据库的应用程序,而不是花费时间进行底层数据系统的预配和管理。

clna 0401

图 4-1. 数据通常分布在多个数据系统中
注意

无服务器数据库 是一种术语,用来指代一种按使用量计费的托管数据库类型,客户根据存储和处理的数据量收费。这意味着如果数据库没有被访问,用户仅需支付存储数据的费用。当数据库执行操作时,用户要么按特定操作收费,要么在操作处理过程中从零开始并进行扩展。

云原生应用充分利用云的所有优势,包括使用的数据系统。以下是用于数据的云原生应用程序特征列表:

  • 更喜欢托管的数据存储和分析服务。

  • 使用多语言持久性、数据分区和缓存。

  • 接受最终一致性,在必要时使用强一致性。

  • 更喜欢云原生数据库,它们能够横向扩展、容忍故障,并优化云存储。

  • 处理分布在多个数据存储中的数据。

云原生应用程序通常需要处理数据的数据孤立,这需要一种与数据处理不同的方法。使用多语言持久性、分散式数据和数据分区有许多好处,但也有权衡和考虑因素。

数据存储系统

有越来越多的选项可用于存储和处理数据。在构建应用程序时,确定使用哪些产品可能很难。团队有时会进行多次迭代评估语言、框架和将在应用程序中使用的数据存储系统。许多人仍然不确定他们是否做出了正确的决定,这些存储系统通常会随着应用程序的演变而被替换或添加新的存储系统。

在决定使用哪些产品时,了解各种类型的数据存储和它们优化的工作负载可能会有所帮助。然而,许多产品都是多模型的,并且设计为支持多个数据模型,属于多个数据存储分类。应用程序通常会利用多个数据存储系统,将文件存储在对象存储中,将数据写入关系数据库,并使用内存键/值存储进行缓存。

对象、文件和磁盘

每个公共云服务提供商都提供一个廉价的对象存储服务。对象存储服务将数据管理为对象。对象通常存储有对象的元数据和作为对象引用的键。文件存储服务通常通过传统的文件共享模型提供文件的共享访问,具有分层目录结构。磁盘或块存储提供计算实例使用的磁盘卷的存储。决定存储图像、文档、内容和基因组数据文件等文件的位置,很大程度上取决于访问它们的系统。以下每种存储类型更适合不同类型的文件:

注意

你应该优先选择对象存储来存储文件数据。对象存储相对便宜、极其耐用且高度可用。所有主要的云服务提供商都提供不同的存储层级,可根据数据访问需求实现成本节约。

对象/块存储

  • 当访问数据的应用程序支持云服务提供商的 API 时,应与文件一起使用。

  • 它价格低廉且可以存储大量数据。

  • 应用程序需要实现云提供商的 API。如果应用程序可移植性是一个要求,请参阅第七章。

文件存储

  • 当设计支持网络附加存储(NAS)的应用程序时,应与之一起使用。

  • 当使用需要共享文件访问的库或服务时,请使用它。

  • 它比对象存储更昂贵。

磁盘(块)存储

  • 将其用于假设持久本地存储磁盘的应用程序,例如 MongoDB 或 MySQL 数据库。

除了各种云服务提供商管理的文件和对象存储选项外,您还可以提供分布式文件系统。Hadoop 分布式文件系统(HDFS)在大数据分析中非常流行。分布式文件系统可以使用云提供商的磁盘或块存储服务。许多云提供商还为流行的分布式文件系统提供了管理服务,其中包括使用的分析工具。在使用与它们兼容的分析工具时,您应考虑这些文件系统。

数据库

数据库通常用于存储更结构化、有明确定义格式的数据。在过去几年中发布了许多数据库,并且可供我们选择的数据库数量每年都在增加。许多这些数据库都是为特定类型的数据模型和工作负载设计的。其中一些支持多个模型,并经常被标记为多模型数据库。在考虑在应用程序中使用哪个数据库时,将数据库组织成一组或分类是有帮助的。

键/值

经常情况下,应用程序数据需要仅使用主键或甚至部分主键来检索。键/值存储可以被视为一个简单存储一些值在唯一键下的大型哈希表。可以非常有效地使用键或在某些情况下使用部分键来检索值。因为值对于数据库来说是不透明的,消费者需要逐条记录扫描以查找基于值的项目。键/值数据库中的键可以由多个元素组成,甚至可以进行排序以实现高效的查找。一些键/值数据库允许使用键前缀进行查找,从而可以使用复合键。如果数据可以基于某些简单键的嵌套进行查询,这可能是一个合适的选项。如果我们在键/值存储中存储客户 xyz 的订单,我们可以使用客户 ID 作为键前缀,后跟订单号“xyz-1001”。可以使用整个键来检索特定订单,而使用“xyz”前缀可以检索客户 xyz 的订单。

注意

键/值数据库通常是价格便宜且非常可扩展的数据存储。键/值数据存储服务能够根据键来分区甚至重新分区数据。在使用这些数据存储时选择键是很重要的,因为它将对数据存储的规模和性能读写产生显著影响。

文档

文档数据库类似于键/值数据库,它通过主键存储文档(值)。与键/值数据库不同的是,文档数据库中的文档需要符合某些定义的结构。这样可以实现维护辅助索引和根据文档查询数据等功能。文档数据库中通常存储的值是哈希映射(JSON 对象)和列表(JSON 数组)的组合。JSON 是文档数据库中常用的格式,尽管许多数据库引擎使用更高效的内部存储格式,如 MongoDB 的 BSON。

提示

当从关系数据库过渡到面向文档的数据库时,您需要以不同的方式考虑如何组织数据。许多人需要时间来适应这种不同的数据建模方法。

您可以使用这些数据库存储传统上存储在关系数据库(如 PostgreSQL)中的许多数据。它们在流行度上正在增长,并且与关系数据库不同,文档在编程语言中的对象映射得很好,不需要对象关系映射(ORM)工具。这些数据库通常不强制执行模式,在软件变更要求数据模式更改的持续交付(CD)方面具有一些优势。

注意

不强制执行模式的数据库通常被称为“读取时模式”,因为尽管数据库不强制执行模式,但应用程序中存在隐含的模式,并且需要知道如何处理返回的数据。

关系

关系数据库将数据组织成称为表的二维结构,由列和行组成。一个表中的数据可以与另一个表中的数据有关联,数据库系统可以强制执行这种关系。关系数据库通常强制执行严格的模式,也称为写入时模式,即向数据库写入数据的消费者必须符合数据库中定义的模式。

关系数据库存在已久,许多开发人员有使用它们的经验。截至今天,最受欢迎和常用的数据库仍然是关系数据库。这些数据库非常成熟,对于包含大量关系的数据非常有效,并且有大量的工具和应用程序生态系统知道如何处理它们。多对多关系在文档数据库中可能难以处理,但在关系数据库中非常简单。如果应用程序数据有许多关系,特别是那些需要事务的关系,这些数据库可能非常适合。

图数据库存储两种类型的信息:节点。边定义节点之间的关系,你可以将节点视为实体。节点和边都可以具有描述特定边或节点的属性。边通常定义关系的方向或性质。图数据库在分析实体之间的关系方面表现良好。图数据可以存储在其他任何数据库中,但当图遍历变得越来越复杂时,在其他存储类型中满足图数据的性能和规模要求可能会有挑战。

列族

列族数据库将数据组织成行和列,最初可能看起来与关系数据库非常相似。你可以把列族数据库看作是带有行和列的表格数据,但列被划分为称为列族的组。每个列族包含一组逻辑相关的列,通常作为一个单元进行检索或操作。可以将单独访问的其他数据存储在单独的列族中。在列族中,可以动态添加新列,行可以是稀疏的(即,行不需要为每一列都有值)。

时间序列

时间序列数据是针对时间进行优化的数据库,根据时间存储值。这些数据库通常需要支持非常高数量的写入操作。它们通常用于实时从大量来源收集大量数据。对数据的更新很少,删除通常是批量完成的。写入时间序列数据库的记录通常非常小,但通常有大量记录。时间序列数据库非常适合存储遥测数据。常见用途包括物联网(IoT)传感器或应用程序/系统计数器。时间序列数据库通常包含用于数据保留、降采样以及根据配置数据使用模式将数据存储在不同介质中的功能。

搜索

搜索引擎数据库通常用于搜索其他数据存储和服务中保存的信息。搜索引擎数据库可以索引大量数据,并几乎实时访问索引。除了搜索类似网页中的非结构化数据外,许多应用程序还使用它们在另一个数据库的数据上提供结构化和特定查询的搜索功能。某些数据库具有全文索引功能,但搜索数据库还能通过词干提取和规范化将单词减少到它们的根形式。

流和队列

流和队列是存储事件和消息的数据存储系统。虽然它们有时用于相同的目的,但它们是非常不同类型的系统。在事件流中,数据以不可变的事件流形式存储。消费者能够在特定位置读取流中的事件,但无法修改事件或流。您不能从流中删除或删除单个事件。消息队列或主题将存储可以更改(变异)的消息,并且可以从队列中删除单个消息。流非常适合记录一系列事件,流处理系统通常能够存储和处理大量数据。队列或主题非常适合不同服务之间的消息传递,这些系统通常设计用于可以更改和随机删除的消息的短期存储。本章更多地关注流,因为它们在数据系统中更常用,而队列更常用于服务通信。有关队列的更多信息,请参见第三章。

注意

主题是发布-订阅消息模型中使用的概念。主题和队列唯一的区别在于队列上的消息发送给一个订阅者,而主题上的消息将发送给多个订阅者。您可以将队列视为只有一个订阅者的主题。

区块链

记录在区块链上以一种不可变的方式存储。记录被分组在一个区块中,每个区块包含数据库中的若干记录。每次创建新记录时,它们被组合成一个单独的区块并添加到链中。使用哈希将区块链接在一起,以确保它们不被篡改。对区块中数据的最轻微更改都会改变哈希值。每个区块的哈希存储在下一个区块的开头,确保没有人可以改变或移除链中的区块。尽管区块链可以像任何其他集中式数据库一样使用,但通常是去中心化的,从而削弱了中央组织的权力。

选择数据存储

在选择数据存储时,您需要考虑一些要求。选择数据存储技术和服务可能非常具有挑战性,特别是考虑到不断出现的新型数据库和我们构建软件的方式的变化。首先从架构上重要的要求——也称为非功能要求——开始考虑系统,然后再考虑功能性要求。

根据您的需求选择适当的数据存储可以是一个重要的设计决策。在 SQL 和 NoSQL 数据库中有数百种实现可供选择。数据存储通常根据它们如何结构化数据以及它们支持的操作类型进行分类。开始的好地方是考虑哪种存储模型最适合需求。然后,根据功能集、成本和管理易用性等因素,考虑该类别内的特定数据存储。

尽可能收集有关您数据需求的以下信息。

功能需求

数据格式

您需要存储哪些类型的数据?

读和写

数据需要如何被消耗和写入?

数据大小

存储数据的项目有多大?

规模和结构

您需要多少存储容量,是否预计需要对数据进行分区?

数据关系

您的数据是否需要支持复杂的关系?

一致性模型

您是否需要强一致性,或者最终一致性可以接受?

模式灵活性

您的数据将应用哪种模式?固定的或强制执行的模式重要吗?

并发性

应用程序是否会从多版本并发控制中受益?是否需要悲观和/或乐观的并发控制?

数据移动

您的应用程序是否需要将数据移动到其他存储或数据仓库?

数据生命周期

数据是否写入一次,读取多次?是否可以通过归档或通过降采样减少数据的保真度?

变更流

是否需要支持变更数据捕获(CDC)并在数据变化时触发事件?

其他支持的功能

是否需要其他特定功能,如全文搜索、索引等?

非功能性需求

团队经验

选择特定数据库解决方案的一个最大原因可能是经验。

支持

有时,对于一个应用程序而言,在技术上最合适的数据库系统可能不是最适合项目的,因为支持选项的问题。考虑可用的支持选项是否满足组织的需求。

性能和可伸缩性

您的性能要求是什么?工作负载是否主要是摄入、查询和分析?

可靠性

您的可用性要求是什么?需要哪些备份和恢复功能?

复制

数据是否需要在多个区域或区域间复制?

限制

数据大小和规模是否有任何硬性限制?

可移植性

是否需要部署在本地或多个云服务提供商上?

管理和成本

托管服务

在可能的情况下,请使用托管数据服务。然而,有时需要的功能是不可用的。

区域或云服务提供商的可用性

是否有可用的托管数据存储解决方案?

许可证

组织内对许可类型是否有任何限制?您对专有软件与开源软件(OSS)许可证有偏好吗?

总体成本

在您的解决方案中使用服务的总体成本是多少?选择托管服务的一个好理由是降低操作成本。

在今天众多可用的数据库和市场上不断推出的新数据库中,选择一个数据库可能会有些令人生畏。一个追踪数据库流行度的网站,db-engines(https://db-engines.com),截至本文撰写时列出了 329 种不同的数据库。在许多情况下,团队的技能组成是选择数据库的主要驱动因素。管理数据系统可能会给团队增加显著的操作负担,因此云原生应用通常更喜欢托管数据系统,这会显著减少选择的选项。部署一个简单的数据库可能很容易,但需要考虑补丁、升级、性能调优、备份以及高可用数据库配置等操作负担。然而,在某些情况下,管理数据库是必要的,你可能更喜欢一些为云而建的新数据库,如 CockroachDB 或 YugaByte。同时,也要考虑可用的工具:如果这可以避免构建用于消费数据的软件,如仪表板或报告系统,可能会有意义部署和管理某些数据库。

多个数据存储中的数据

无论您是在分区、数据库还是服务之间工作,多个数据存储中的数据可能会引入一些数据管理挑战。传统的事务管理可能不可行,分布式事务会对系统的性能和规模产生不利影响。以下是分布数据的一些挑战:

  • 数据存储中的数据一致性

  • 多个数据存储中的数据分析

  • 数据存储的备份和恢复

跨多个数据存储中保持数据的一致性和完整性可能会很具挑战性。当一个系统中的相关记录更新以反映另一个系统的变化时,如何确保?如何管理数据的副本,无论是在内存中缓存、物化视图中,还是存储在另一个服务团队的系统中?如何有效分析存储在多个隔离区中的数据?很多这些问题通过数据移动来解决,市场上出现了越来越多的技术和服务来处理这些问题。

变更数据捕获

当今提供的许多数据库选项都提供数据变更事件流 (变更日志),并通过易于消费的 API 公开这些事件。这可以使得可以在事件上执行某些操作,例如在文档更改时触发功能或更新材料化视图。例如,成功添加包含订单的文档可以触发事件来更新报告总数,并通知会计服务已创建了客户的订单。鉴于向多语言持久性和分散的数据存储迁移,这些事件流在跨这些数据孤岛维护一致性方面非常有帮助。CDC 的一些常见用例包括:

通知

在微服务架构中,另一个服务希望在服务中的数据更改时得到通知并不少见。为此,您可以使用 Webhook 或订阅来为其他服务发布事件。

材料化视图

材料化视图可在系统上进行高效且简化的查询。更改事件可用于更新这些视图。

缓存失效

缓存对于提高系统的规模和性能非常有帮助,但是在后端数据发生更改时使缓存失效是一个挑战。可以使用变更事件来删除缓存项或更新缓存项,而不是使用生存时间 (TTL)。

审计

许多系统需要维护数据更改的记录。可以使用这些更改日志来跟踪何时进行了更改以及更改了什么。通常需要了解进行更改的用户,因此可能需要确保也捕获此信息。

搜索

许多数据库在处理搜索时效果不佳,并且搜索数据存储系统不提供其他数据库所需的所有功能。您可以使用变更流来维护搜索索引。

分析

组织的数据分析需求通常需要跨多个不同的数据库进行视图。将数据移动到中央数据湖、数据仓库或数据库中可以实现更丰富的报告和分析需求。

变更分析

数据更改的几乎实时分析可以与数据访问关注点分离,并在数据更改上执行。

存档

在某些应用中,需要维护状态的存档。这些存档很少被访问,通常最好将其存储在成本较低的存储系统中。

传统系统

替换传统系统有时需要在多个位置维护数据。可以使用这些变更流来更新传统系统中的数据。

在图 4-2 中,我们看到一个应用程序向记录更改的数据库写入数据。然后,该更改被写入更改日志流并由多个消费者处理。许多数据库系统维护一个内部更改日志,可以订阅以便在特定位置恢复检查点。例如,MongoDB 允许您订阅部署、数据或集合上的事件,并提供一个标记以在特定位置恢复。许多云提供商的数据库处理观察过程,并将为每次更改调用一个无服务器函数。

clna 0402

图 4-2. 用于同步数据更改的 CDC

应用程序本可以将更改写入流和数据库,但如果两个操作中的一个失败,可能会出现一些问题,并且可能会创建竞争条件。例如,如果应用程序正在更新数据库中的一些数据,如帐户的运输偏好设置,然后未能写入事件流,那么数据库中的数据将已更改,但其他系统未被通知或更新,如运输服务。另一个问题是,如果两个进程几乎同时对同一记录进行更改,则事件顺序可能成为问题。根据更改的内容及其处理方式,这可能不是问题,但需要考虑。关键是,我们要么记录某事物变更的事件,而事实上并未发生变更,要么更改了某事物却没有记录事件。

通过使用数据库更改流,我们可以将文档的更改或变异以事务的形式写入并记录该更改的日志。尽管数据系统在一段时间后消费事件流后最终一致,但重要的是它们变得一致。图 4-3 显示了已更新的文档及其作为事务一部分记录的更改。这确保了更改事件与实际更改本身的一致性,因此现在我们只需要消费和处理那些事件到其他系统中。

clna 0403

图 4-3. 在事务范围内对记录进行的更改和操作日志

许多托管数据服务使这种实现变得非常容易,并且可以快速配置以在数据存储中发生更改时调用无服务器函数。您可以配置 MongoDB Atlas 来调用 MongoDB Stitch 服务中的函数。Amazon DynamoDB 或 Amazon Simple Storage Service (Amazon S3) 中的更改可以触发 lambda 函数。当 Azure Cosmos DB 或 Azure Blob Storage 中发生更改时,可以调用 Microsoft Azure Functions。Google Cloud Firestore 或对象存储服务的更改可以触发 Cloud Function。使用流行的托管数据存储服务进行实现通常非常简单。这已成为大多数数据存储中受欢迎且必要的功能。

将更改作为事件写入更改日志

正如我们刚刚看到的,在涉及多个数据存储的操作中发生应用程序故障可能导致数据一致性问题。当操作涉及多个数据库时,另一种方法是将一组更改写入更改日志,然后应用这些更改。一组更改可以按顺序写入流中,如果在应用更改时发生故障,则可以轻松重试或恢复操作,如图 4-4 所示。

clna 0404

图 4-4. 每次写入更改之前保存一组更改

事务监督员

您可以使用监督服务确保事务成功完成或进行补偿。当您执行涉及外部服务的交易时特别有用——例如,将订单写入系统并处理信用卡,信用卡处理可能会失败,或保存处理结果。正如图 4-5 所示,结账服务接收订单,处理信用卡付款,然后未能将订单保存到订单数据库中。大多数客户会感到不安,因为他们的信用卡已经被扣款,但却没有订单记录。这是一个相当常见的实现。

clna 0405

图 4-5. 在处理订单后未能保存订单详细信息

另一种方法可能是以处理中的状态保存订单或购物车,然后调用支付网关进行信用卡支付,最后更新订单状态。如果未能更新订单状态,图 4-6 显示了我们至少有一个已提交订单的记录和处理意图。如果支付网关服务提供像 webhook 回调这样的通知服务,我们可以配置它以确保状态准确。

clna 0406

图 4-6. 未能更新订单状态

在图 4-7 中,添加了一个监督员来监视订单数据库中未完成的处理交易并对状态进行调和。监督员可以是在特定间隔触发的简单函数。

clna 0407

图 4-7. 监控事务错误的监督服务

您可以使用这种方法——使用监督服务并设置状态——以多种不同的方式监视系统和数据库的一致性,并采取纠正措施或生成问题通知。

补偿性交易

在今天的云原生应用程序中,传统的分布式事务并不常用,也不总是可用。有些情况下需要事务以保持服务或数据存储一致性。例如,消费者通过 API 向文件发送一些数据,要求应用程序将文件写入对象存储并将一些数据写入文档数据库。如果我们将文件写入对象存储,然后在写入数据库时失败(无论何种原因),则如果唯一找到文件的方法是通过对数据库和引用的查询,则对象存储中可能会有一个孤立的文件。这种情况下,我们希望将写文件和数据库记录的操作视为一个事务;如果其中一个失败,则两者都应失败。然后应删除文件以补偿失败的数据库写入。这本质上就是补偿事务所做的。一组逻辑操作需要完成;如果其中一个操作失败,则可能需要补偿成功的操作。

注意

应避免服务协调。在许多情况下,可以通过设计事件一致性并使用 CDC 等技术避免复杂的事务协调。

提取、转换和加载

将数据移动和转换以进行业务智能(BI)的需求非常普遍。企业长期以来一直在使用提取、转换和加载(ETL)平台将数据从一个系统移动到另一个系统。数据分析正在成为每个大大小小企业的重要组成部分,因此并不奇怪 ETL 平台变得越来越重要。数据已经分布在更多的系统中,分析工具也变得更加易于获取。每个人都可以利用数据分析,因此有将数据移动到用于执行数据分析的位置(如数据湖或数据仓库)的增长需求。可以使用 ETL 将这些操作数据系统中的数据获取到要分析的系统中。ETL 是一个包括以下三个不同阶段的过程:

提取

数据是从业务系统和数据存储系统、遗留系统、运营数据库、外部服务以及企业资源规划(ERP)或客户关系管理(CRM)系统中提取或导出的。从各种来源提取数据时,重要的是确定速度,即每个来源的数据提取频率以及在各种来源之间的优先级。

转换

接下来,提取的数据进行转换;这通常涉及多个数据清洗、转换和增强任务。数据可以通过流进行处理,并经常存储在中间临时存储中以进行批处理处理。

加载

转换后的数据然后加载到目标位置,可以进行业务智能分析。

所有主要的云服务提供商都提供托管的 ETL 服务,例如 AWS Glue、Azure Data Factory 和 Google Cloud DataFlow。 在今天的云原生应用程序中,从一个源移动和处理数据变得越来越重要和普遍。

微服务和数据湖

在微服务架构中处理分散的数据的一个挑战是需要跨多个服务的数据执行报告或分析。 某些报告和分析需求将需要来自服务的数据在一个共同的数据存储中。

注意

为了执行所需的所有数据分析和报告,可能并不需要移动数据。 可以在每个单独的数据存储中执行部分或全部分析,同时结合一些集中的分析任务对结果进行分析。

然而,让每个服务从一个共享或共同的数据库工作,可能会违反微服务原则之一,并且可能引入服务之间的耦合。 处理这个的常见方法是通过数据移动和将数据聚合到一个位置供报告或分析团队使用。 在图 4-8 中,来自多个微服务数据存储的数据被聚合到一个集中数据库中,以满足必要的报告和分析需求。

clna 0408

图 4-8. 多个微服务的数据聚合在一个集中的数据存储中

数据分析或报告团队将需要确定如何从各个服务团队获取所需的数据以进行报告,而不引入耦合。 有多种方法可以解决这个问题,重要的是要确保保持松散耦合,使团队能够保持敏捷并快速交付价值。

服务团队可以给数据分析团队对数据库的只读访问权限,并允许他们复制数据,如图 4-9 所示。 这将是一种非常快速和简便的方法,但服务团队无法控制数据提取对存储的负载和影响,可能会导致性能问题。 这也引入了耦合,并且很可能服务团队在进行内部模式更改时需要与数据分析团队协调。 为了解决数据库上的 ETL 负载对服务性能的不利影响,可以让数据分析团队访问只读复制品而不是主数据。 另外,也可能让数据分析团队访问数据的视图而不是原始文档或表格。 这有助于减轻一些耦合问题。

clna 0409

图 4-9. 数据分析团队直接从服务团队的数据库消耗数据

在应用早期阶段,这种方法可以处理少量服务,但随着应用和团队的增长,将会变得具有挑战性。另一种方法是使用集成数据存储。服务团队为内部集成配置和维护数据存储,如 图 4-10 所示。这使得服务团队可以控制集成存储库中的数据及其数据形式。集成存储库应该像 API 一样进行管理、文档化和版本控制。服务团队可以运行 ETL 作业来维护数据库,也可以使用 CDC 并将其视为物化视图。服务团队可以对其运营存储进行更改,而不影响其他团队。服务团队将负责集成存储。

clna 0410

图 4-10. 数据库作为 API

这可以转变为服务消费者(如数据分析团队)请求服务团队将数据导出或写入数据湖,如 图 4-11 所示,或者写入到临时存储,如 图 4-12 所示。服务团队支持数据复制、日志或数据导出到客户提供的位置作为服务功能和 API 的一部分。数据分析团队将为每个服务团队在数据存储中配置存储或位置。然后数据分析团队订阅所需的数据进行聚合分析。

clna 0411

图 4-11. 服务团队数据导出服务 API

clna 0412

图 4-12. 服务团队写入到临时存储

服务支持数据导出并非罕见。服务实现将定义其 API 的导出格式和协议。例如,配置对象存储位置和凭据以发送夜间导出,或者是发送变更批次的 Webhook。数据分析团队等服务消费者可以访问服务 API,允许其订阅数据变更或导出。团队可以发送位置和凭据,以便要么倒入导出文件,要么发送事件。

客户端访问数据

在今天的大多数应用程序中,客户端应用程序通常无法直接访问数据存储。数据通常通过负责进行授权、审计、验证和数据转换的服务来访问。尽管在许多数据中心的应用程序中,服务实现的大部分工作只是处理数据的读写操作。

一个简单的数据中心应用通常需要您构建和操作一个服务,该服务执行身份验证、授权、日志记录、数据转换和验证。但是,它确实需要控制数据存储中谁能访问什么,并验证正在写入的内容。图 4-13 显示了一个典型的前端应用程序调用后端服务,后者读取和写入单个数据库。这是今天许多应用程序的常见架构。

clna 0413

图 4-13. 带有后端服务和数据库的客户端应用程序

受限客户端令牌(代客钥匙)

服务可以创建并返回一个对消费者有限使用的令牌。实际上,可以使用 OAuth 甚至自定义的加密签名策略来实现此功能。代客钥匙通常用作解释 OAuth 工作原理的隐喻,并且是常用的云设计模式。返回的令牌可能只能访问数据存储中的特定数据项,且仅在有限时间内或将文件上传到特定位置。这可以是一种从服务中卸载处理的便捷方式,降低服务的成本和规模,并提供更好的性能。在图 4-14 中,文件上传到将文件写入存储的服务中。

clna 0414

图 4-14. 客户端上传通过服务传递的文件

与通过服务流式传输文件不同,将一个位置令牌返回给客户端可能更有效,以便客户端读取或上传文件到特定位置。在图 4-15 中,客户端请求服务的令牌和位置,然后服务生成带有某些策略的令牌。令牌策略可以限制文件上传的位置,并且最佳实践是设置过期时间,以便令牌在稍后任何时间都无法使用。令牌应遵循最小权限原则,仅授予完成任务所需的最低权限。在 Microsoft Azure Blob Storage 中,该令牌也称为共享访问签名,在 Amazon S3 中,这将是预签名 URL。文件上传后,可以使用对象存储功能更新应用程序状态。

clna 0415

图 4-15. 客户端从服务获取令牌和路径直接上传到存储

数据库服务与细粒度访问控制

一些数据库提供对数据库中数据的精细访问控制。这些数据库服务有时称为后端即服务 (BaaS) 或移动后端即服务 (MBaaS)。一个功能齐全的 MBaaS 通常不仅提供数据存储,因为移动应用程序通常还需要身份管理和通知服务。这几乎感觉像是我们回到了旧的厚客户端应用程序的时代。值得庆幸的是,数据存储服务已经发展,所以情况并非完全相同。图 4-16 展示了一个移动客户端连接到数据库服务,而无需部署和管理额外的 API。如果不需要提供客户 API,这是一个快速推出应用程序的好方法,操作开销低。需要注意发布更新和测试安全规则,以确保只有适当的人员能够访问数据。

clna 0416

图 4-16. 移动应用连接到数据库

诸如 Google 的 Cloud FireStore 等数据库允许您应用安全规则,提供访问控制和数据验证。您可以编写安全规则和验证,而不是构建一个控制访问和验证请求的服务。用户需要对接到 Google Firebase 认证服务进行身份验证,该服务可以联合其他身份提供者,如微软的 Azure Active Directory 服务。用户认证通过后,客户端应用程序可以直接连接到数据库服务,并读取或写入数据,只要操作符合定义的安全规则。

GraphQL 数据服务

您可以部署和配置一个 GraphQL 服务器来为客户端提供数据访问,而不是构建和操作自定义服务来管理客户端对数据的访问。在 图 4-17 中,部署和配置了一个 GraphQL 服务来处理数据的授权、验证、缓存和分页。像 AWS AppSync 这样的完全托管的 GraphQL 服务极大地简化了为客户端服务部署基于 GraphQL 的后端的过程。

GraphQL 不是数据库查询语言或存储模型;它是一个 API,根据完全独立于数据存储方式的模式返回应用程序数据。

clna 0417

图 4-17. GraphQL 数据访问服务

GraphQL 通过 GraphQL 规范变得灵活且可配置。您可以配置它与多个提供者一起使用,并且甚至可以配置它执行在容器中运行的多个服务,或者作为请求时调用的函数,如图 4-18 所示。GraphQL 非常适合于以数据为中心的后端,偶尔需要调用的服务方法。像 GitHub 这样的服务实际上正在将其整个 API 转移到 GraphQL,因为这为 API 的消费者提供了更大的灵活性。GraphQL 在解决基于 REST 的 API 中有时常见的过度获取和过多请求的问题方面非常有帮助。

GraphQL 使用基于模式的方法,将节点(对象)和边(关系)定义为图结构的模式定义的一部分。消费者可以查询模式以获取有关对象之间类型和关系的详细信息。GraphQL 的一个好处是它可以轻松定义您想要的数据,仅获取您想要的数据,而无需进行多次调用或获取不需要的数据。该规范支持授权、分页、缓存等功能。这使得快速且轻松地创建处理大部分数据中心应用程序所需功能的后端成为可能。有关更多信息,请访问GraphQL 网站

clna 0418

图 4-18. 具有多个提供者和执行的 GraphQL 服务

快速可扩展的数据

应用程序的大部分扩展和性能问题可以归因于数据库。这是一个常见的争议点,挑战在满足应用程序数据质量要求的同时进行扩展。过去,将逻辑以存储过程和触发器的形式放入数据库中太容易了,这增加了系统的计算需求,而该系统本来就昂贵且难以扩展。我们学会在应用程序中处理更多事务,减少对数据库的依赖,除了用来存储数据之外的其他事务。

小贴士

将逻辑放入数据库几乎没有多少理由。不要这样做。如果你非要这么做,请确保你理解了其中的权衡。在少数情况下这么做可能是有道理的,它可能会提高性能,但很可能会牺牲可扩展性。

通过复制和分区可以实现任何事物的扩展。将数据复制到缓存、物化视图或只读副本可以帮助提高数据系统的可扩展性、可用性和性能。通过水平分片、基于数据模型的垂直分区或基于功能的功能分区数据将有助于通过系统分布负载来提高可扩展性。

数据分片

数据分片是将数据存储区划分为水平分区,称为分片。每个分片包含相同的模式,但持有数据的子集。通过将负载分布到多个数据存储系统中,分片通常用于通过扩展系统来实现扩展。

在分片数据时,确定使用多少分片以及如何在分片之间分配数据是非常重要的。决定如何在分片之间分配数据在很大程度上取决于应用程序的数据。重要的是以这样的方式分配数据,使得单个分片不会过载并接收所有或大部分负载。由于每个分片或分区的数据通常位于单独的数据存储中,因此应用程序能够连接到适当的分片(分区或数据库)是非常重要的。

缓存数据

数据缓存对于扩展应用程序和提高性能至关重要。缓存实际上只是将数据复制到更快的存储介质(如内存),通常更靠近消费者。甚至可能存在多个层次的缓存;例如,数据可以在客户端应用程序的内存中缓存,并在后端的共享分布式缓存中缓存。

在使用缓存时,最大的挑战之一是保持缓存数据与源数据的同步。当源数据发生变化时,通常需要使缓存中的数据失效或更新。有时,数据很少更改;事实上,在某些情况下,数据在应用程序进程的生命周期内不会更改,因此可以在应用程序启动时将这些静态数据加载到缓存中,然后无需担心失效。以下是一些常见的缓存失效和更新方法:

  • 通过设置一个值来依赖 TTL 配置,该值在可配置的过期时间后移除缓存项。当应用程序或服务层在缓存中找不到项目时,应负责重新加载数据。

  • 使用 CDC 更新或使缓存失效。一个进程订阅数据存储的变更流,并负责更新缓存。

  • 当应用程序逻辑对源数据进行更改时,负责使缓存失效或更新缓存。

  • 使用透传缓存层来管理缓存数据。这可以减少应用程序对数据缓存实现的关注。

  • 后台服务以配置的间隔运行,更新缓存。

  • 使用数据库或其他服务的数据复制功能将数据复制到缓存中。

  • 缓存层根据访问和可用缓存资源更新缓存项。

内容传送网络

内容传送网络(CDN)是一组地理分布的数据中心,也称为点对点(POP)。CDN 通常用于将静态内容缓存在消费者附近,从而减少消费者与所需内容或数据之间的延迟。以下是一些常见的 CDN 使用案例:

  • 通过将内容放置在靠近消费者的地方来改善网站加载时间。

  • 通过在接近消费者的地方终止流量来提高 API 的应用程序性能。

  • 加快软件下载和更新。

  • 增加内容可用性和冗余性。

  • 通过像亚马逊云前的 CDN 服务加速文件上传。

内容被缓存,因此它的副本存储在边缘位置,并且将用于代替源内容。在图 4-19 中,客户端从附近的 CDN 获取文件,延迟较低,仅为 15 毫秒,而不是客户端和文件源位置之间的 82 毫秒延迟,也称为。缓存和 CDN 技术使内容更快地获取,并通过减少源负载来扩展。

clna 0419

图 4-19. 客户访问在 CDN 中缓存的内容,离客户端更近。

缓存在 CDN 中的内容通常配置有过期日期时间,也称为TTL 属性。超过过期日期时间后,CDN 将从源或来源重新加载内容。许多 CDN 服务允许根据路径显式地使内容失效;例如,*/img/**。另一种常见的技术是通过为内容添加一个小哈希来更改内容的名称,并更新消费者的引用。此技术通常用于 Web 应用程序捆绑包,如 Web 应用程序中使用的 JavaScript 和 CSS 文件。

在 CDN 缓存管理方面需要考虑以下几点:

  • 使用内容过期来在特定间隔刷新内容。

  • 通过将哈希或版本附加到内容来更改资源的名称。

  • 明确地通过管理控制台或 API 使缓存过期。

CDN 供应商继续添加更多功能,使得将更多内容、数据和服务推送到消费者更加可能,从而提高性能、规模、安全性和可用性。图 4-20 展示了客户通过 CDN 调用后端 API,请求通过数据中心之间的云服务提供商的骨干连接路由。这是到 API 的更快路径,具有更低的延迟,同时改善了客户端与 CDN 之间以及 API 请求之间的安全套接字层(SSL)握手。

clna 0420

图 4-20. 加速访问后端 API

在使用 CDN 技术时,考虑以下几个额外的功能:

规则或行为

可能需要配置路由、添加响应头或根据请求属性(如 SSL)启用重定向。

应用逻辑

一些 CDN 供应商如亚马逊云前允许您在边缘运行应用逻辑,这样可以为消费者个性化内容。

自定义名称

通常需要使用 SSL 的自定义名称,特别是通过 CDN 提供网站时。

文件上传加速

一些 CDN 技术能够通过减少到消费者的延迟来加速文件上传。

API 加速

与文件上传类似,可以通过 CDN 加速 API,从而减少到消费者的延迟。

注意

尽可能多地使用 CDN,通过 CDN 推送尽可能多的内容。

分析数据

创建和存储的数据继续以指数速率增长。用于从数据中提取信息的工具和技术继续发展,以支持从数据中获取洞察的不断增长的需求,使复杂分析通过数据变得对甚至最小的企业可用。

企业需要减少洞察时间,以在今天竞争激烈的快速市场中获得优势。实时分析数据流是减少这种延迟的一个好方法。流式数据处理引擎专为无界数据集设计。与传统数据存储系统中在特定时间点上对数据的整体视图不同,流式数据有一个随时间变化的实体逐个实体的视图。一些数据,如股市交易、点击流或设备传感器数据,以事件流的形式持续不断地传入。流处理可用于检测模式、识别序列并查看结果。例如,传感器中突然的转变事件在发生时可能更有价值,并随时间减少,或使企业能够更快速和立即地对这些重要变化作出反应。例如,检测库存突然下降允许公司订购更多库存并避免一些销售机会的错失。

批处理

与流处理不同,批处理通常是在数据到达时执行的实时处理,是探索数据科学假设的一部分,或在特定间隔内以获取业务洞察。批处理能够处理全部或大部分数据,并可能需要几分钟或几小时才能完成,而流处理则在几秒钟内完成。批处理非常适合处理大量可能已存储了很长时间的数据。这可以是来自传统系统的数据或仅仅是你正在寻找数月或数年内模式的数据。

数据分析系统通常使用批处理和流处理的组合。处理流和批处理的方法已被一些众所周知的架构模式所捕捉。Lambda 架构是一种方法,应用程序将数据写入不可变流中。多个消费者独立地从流中读取数据。一个消费者关注快速处理数据,几乎是实时的,而另一个消费者关注批处理和较低速度在较大数据集上的处理或将数据存档到对象存储中。

对象存储上的数据湖

数据湖是大型、可扩展且通常是集中的数据存储,允许您存储结构化和非结构化数据。它们通常用于运行映射和减少作业,以分析大量数据。分析作业高度可并行化,因此可以轻松地在整个存储中分布数据分析。Hadoop 已成为数据湖和大数据分析的流行工具。数据通常存储在 Hadoop 分布式文件系统(HDFS)的计算机群集上,Hadoop 生态系统中的各种工具用于分析数据。所有主要的公共云供应商都提供托管的 Hadoop 群集,用于存储和分析数据。这些群集可能变得昂贵,需要大量非常大的机器。即使没有作业运行在群集上,这些机器可能也在运行。可以关闭这些群集以节省成本,并在不使用时保持状态,并在数据加载或分析期间恢复群集。

越来越普遍地使用完全托管的服务,允许您按加载到服务中的数据付费并按作业执行付费。这些服务不仅可以减少与管理这些服务相关的运营成本,而且在运行偶尔的分析作业时也可以节省大量费用。云供应商已开始提供与服务器无关成本模型对齐的服务,用于设置数据湖。Azure 数据湖和基于 Amazon S3 的 AWS Lake Formation 是其中的一些示例。

数据湖和数据仓库

数据湖经常与数据仓库进行比较和对比,因为它们相似,尽管在大型组织中同时使用两者并不罕见。数据湖通常用于存储原始和非结构化数据,而数据仓库中的数据已经经过处理并组织成了明确定义的模式。通常会将数据写入数据湖,然后从数据湖中处理到数据仓库中。数据科学家能够探索和分析数据,发现能帮助业务专业人士定义数据仓库处理的趋势。

分布式查询引擎

分布式查询引擎越来越受欢迎,支持快速分析存储在多个数据系统中的数据的需求。分布式查询引擎将查询引擎与存储引擎分离,并使用技术将查询分布到一个工作池中的多个工作者中。市场上有许多开源查询引擎变得流行:比如 Presto、Spark SQL、Drill 和 Impala。这些查询引擎利用提供程序模型访问各种数据存储系统和分区。

Hadoop 作业旨在通过运行几分钟甚至几小时的作业来处理大量数据。虽然工具如 HIVE 中存在类似结构化查询语言(SQL)的界面,但查询会被转换为提交到作业队列并进行调度的作业。客户端不会期望作业的结果在几分钟或几秒钟内返回。然而,分布式查询引擎如 Facebook 的 Presto 可以在几分钟甚至几秒钟内返回查询结果。

在高层次上,客户端向分布式查询引擎提交查询。协调器负责解析查询并将工作调度到一组工作节点。工作节点连接到需要满足查询的数据存储,获取结果,并将来自每个工作节点的结果合并。查询可以针对多种数据存储运行:关系型、文档型、对象型、文件型等等。图 4-21 描述了从 MongoDB 数据库和存储在像 Amazon S3、Azure Blob Storage 或 Google Object Storage 这样的对象存储中的逗号分隔值(CSV)文件中获取信息的查询。

云计算使得快速和简单地扩展工作节点成为可能,从而使分布式查询引擎能够处理查询需求。

clna 0421

图 4-21. 分布式查询引擎概述

Kubernetes 上的数据库

Kubernetes 动态环境可能会使在 Kubernetes 集群中运行数据存储系统变得具有挑战性。Kubernetes Pod 可以创建和销毁,并且集群节点可以添加或移除,强制 Pod 移动到新节点。运行像数据库这样的有状态工作负载与无状态服务有很大不同。Kubernetes 具有诸如有状态集和对持久卷的支持等特性,有助于在 Kubernetes 集群中部署和操作数据库。大多数持久数据存储系统需要磁盘卷作为底层持久存储机制,因此在在 Kubernetes 上部署数据库时,了解如何将存储附加到 Pod 和存储卷的工作原理非常重要。

除了提供底层存储卷外,数据存储系统还具有不同的路由和连接需求,以及硬件、调度和操作需求。一些新型的云原生数据库已经为这些更动态的环境构建,并且可以利用这些环境来进行扩展和容忍瞬态错误。

注意

有越来越多的操作者可用来简化在 Kubernetes 上部署和管理数据系统。Operator Hub 是操作者的目录列表(https://www.operatorhub.io)。

存储卷

像 MongoDB 这样的数据库系统在 Kubernetes 上运行在容器中,通常需要一个与容器不同的生命周期的持久卷。管理存储与管理计算有很大的不同。Kubernetes 卷通过持久卷、持久卷声明和底层存储提供程序挂载到 Pod 中。以下是一些基本的存储卷术语和概念:

持久卷

持久卷是代表实际物理存储服务的 Kubernetes 资源,例如云提供商的存储磁盘。

持久卷声明

持久卷存储声明是一个存储请求,Kubernetes 将为其分配并关联一个持久卷。

存储类

存储类定义了用于动态配置持久卷的存储属性。

群集管理员将配置捕获存储的持久卷。这可以是持久卷到网络附加文件共享或云提供商的持久性磁盘。当使用云提供商的磁盘时,很可能定义一个或多个存储类,并使用动态配置。存储类将以可用于引用资源的名称创建,并定义一个提供者以及传递给提供者的参数。云提供商提供多种具有不同价格和性能特征的磁盘选项。通常会创建不同的存储类,以便在群集中可用不同的选项。

将创建一个需要持久存储卷的 Pod,以便当 Pod 被删除并在另一个节点上重新启动时数据仍然存在。在创建 Pod 之前,会创建一个持久卷声明,指定工作负载的存储要求。创建持久卷声明时,如果引用了特定的存储类,则将使用存储类中定义的提供者和参数来创建满足持久卷声明请求的持久卷。创建引用持久卷声明的 Pod,并在 Pod 指定的路径上挂载卷。图 4-22 显示了一个引用持久卷声明的 Pod,并引用了一个持久卷的 Pod。持久卷资源和插件包含了附加底层存储实现所需的配置和实现。

clna 0422

图 4-22. Kubernetes Pod 持久卷关系
注意

一些数据系统可能使用临时存储在群集中部署。不要将这些系统配置为将数据存储在容器中;而是使用映射到节点临时磁盘的持久卷。

StatefulSets

StatefulSets 旨在解决在 Kubernetes 上运行类似数据存储系统等有状态服务的问题。StatefulSets 管理一组基于容器规范的 Pod 的部署和扩展。StatefulSets 提供有关 Pod 顺序和唯一性的保证。从规范创建的 Pod 具有跨任何重新调度维护的持久标识符。唯一的 Pod 标识包括 StatefulSet 名称和从零开始的序数。因此,一个名为“mongo”的 StatefulSet 和“3”的副本设置将创建三个名为“mongo-0”、“mongo-1”和“mongo-2”的 Pod,每个 Pod 都可以使用这个稳定的 Pod 名称进行访问。这很重要,因为客户端通常需要能够访问存储系统中的特定副本,并且副本通常需要彼此之间进行通信。StatefulSets 还为每个单独的 Pod 创建持久卷和持久卷声明,并且它们被配置为当“mongo-0” Pod 重新调度时,为“mongo-0” Pod 绑定创建的磁盘。

注意

目前 StatefulSets 需要一个无头服务,负责 Pod 的网络标识,并且必须在 StatefulSet 之外创建。

亲和性和反亲和性是 Kubernetes 的一个功能,允许您约束 Pod 将在哪些节点上运行。Pod 反亲和性可以通过确保副本不在同一个节点上运行来提高在 Kubernetes 上运行的数据存储系统的可用性。如果主节点和备份节点运行在同一个节点上,并且该节点发生故障,则数据库将不可用,直到 Pod 重新调度并在另一个节点上启动。

云提供商提供许多不同类型的计算实例类型,这些类型更适合不同类型的工作负载。数据存储系统通常在优化了磁盘访问的计算实例上运行得更好,尽管有些可能需要更高内存的实例。然而,运行在集群中的无状态服务不需要这些专门的实例,这些实例通常成本更高,可以在通用的普通实例上正常运行。您可以向 Kubernetes 集群添加一组优化了存储的节点池,以运行可以从这些资源中受益的存储工作负载。您可以使用 Kubernetes 节点选择以及污点和容忍来确保数据存储系统被调度在优化了存储的节点池上,并且其他服务不被调度到这些节点上。

鉴于大多数数据存储系统不具备 Kubernetes 感知能力,通常需要创建一个适配器服务,与数据存储系统 Pod 一起运行。这些服务通常负责向数据存储系统注入配置或集群环境设置。例如,如果我们部署了一个 MongoDB 集群,并且需要通过另一个节点扩展集群,MongoDB 的 sidecar 服务将负责将新的 MongoDB Pod 添加到 MongoDB 集群中。

DaemonSets

当一组节点运行单个 pod 的副本时,DaemonSet 确保了系统的一部分需要成为集群的一部分并且使用专门用于存储系统的节点。集群中会创建一个节点池,用于运行存储系统。使用节点选择器可以确保只将存储系统调度到这些专用节点上。使用污点和容忍性可以确保其他进程不会被调度到这些节点上。在决定使用守护程序集和有状态集之间时,以下是一些权衡和考虑因素:

  • Kubernetes StatefulSets 就像任何其他 Kubernetes pod 一样工作,允许根据需要利用集群资源调度它们。

  • StatefulSets 通常依赖于远程网络附加存储设备。

  • 当在专用节点池上运行数据库时,DaemonSets 提供了更自然的抽象。

  • 发现和通信会增加一些需要解决的挑战。

总结

在云中迁移和构建应用程序需要一种不同的方法来设计和构建应用程序数据相关的需求。云服务提供商提供了丰富的托管数据存储和分析服务,降低了数据系统的运营成本。这使得考虑同时运行多个不同类型的数据系统变得更加容易,可以使用更适合任务的存储技术。随着云服务提供商在这些领域持续创新和竞争,数据存储的成本和规模发生了变化,使得以更低的价格存储大量数据变得更加容易。

第五章:DevOps

开发、测试和部署云原生应用与传统的开发和运维实践有很大区别。在本章中,您将学习 DevOps 的基础知识以及经过验证的实践,包括开发、测试和运行云原生应用的所有好处和挑战。此外,我们将介绍设计云原生应用时应考虑的运维和快速、可靠的开发流程。本章介绍的大多数概念和模式适用于容器化服务和函数。如果不适用,我们会明确指出差异。

什么是 DevOps?

DevOps 是一个广泛的概念,涵盖了软件开发人员与其他 IT 专业人员之间多个方面的协作和沟通。定义 DevOps 最简单的方法是谈论其目标。DevOps 旨在改善开发和运维团队在整个软件开发过程中的协作,从规划到交付,以提高部署频率,实现更快的上市时间,降低新版本的失败率,缩短修复时间,提高恢复时间。

在谈论 DevOps 时,您可以使用的模型之一称为 CALMS,代表协作(Collaboration)、自动化(Automation)、精益(Lean)、测量(Measurement)和共享(Sharing)。CALMS 模型是我们用来评估、分析和比较 DevOps 团队成熟度的一种方法。

协作...

第六章:最佳实践

本书全程学习了云原生应用程序的基础知识——如何设计、开发和运行它们,以及如何处理数据。总结来说,本章旨在提供一份清单,涵盖了构建和管理反应式云原生应用程序的建议、验证技术和最佳实践。

迁移到云原生

在第二章中,您学习了许多客户在将传统应用程序迁移到云端时遵循的过程。在将现有应用程序迁移到云端时,您应考虑许多最佳实践和经验教训。

为了正确的理由分解单体架构

“永远不要改变正在运行的系统”是软件开发中广泛使用的声明,当您考虑将应用程序迁移到云端时也适用。如果您的唯一需求是将应用程序迁移到云端,您可以考虑首先将其移至基础设施即服务(IaaS)——事实上,这应该是您的第一步。也就是说,重新设计应用程序为云原生也有其好处,但您需要权衡利弊。以下是一些指导方针,表明重新设计是有意义的:

  • 您的代码库已经增长到更新版本需要很长时间,并且因此无法迅速响应新市场或客户需求。

  • 应用程序的各个组件具有不同的规模要求。一个很好的例子是传统的三层应用程序,包括前端、业务和数据层。只有前端层可能会经历用户请求的大量负载,而业务和数据层仍然可以轻松处理负载。正如在第二章和第三章中所提到的,云原生应用程序允许您独立扩展服务。

  • 出现了更好的技术选择。技术领域不断创新,一些新技术可能更适合您应用的某些部分。

一旦决定重新设计应用程序,您需要考虑很多事情。在接下来的章节中,我们全面探讨这些考虑事项。

首先解耦简单服务

从分离提供简单功能的组件开始,因为它们通常没有太多依赖,因此不会深度集成在单体架构中。

学会小规模运作

使用第一个服务作为学习路径,了解如何在云原生世界中操作。从一个简单的服务开始,您可以专注于设置自动化以提供基础架构和 CI/CD 管道,以便熟悉开发、部署和操作云原生服务的过程。拥有一个简单的服务和最小的基础设施将使您能够提前学习、练习和改进新的流程,而不会对单片和最终用户造成重大影响。

使用防腐层模式

没有什么是完美的,特别是在软件开发领域,所以最终您可能会得到一个向单片做出调用的新服务。在这种情况下,您可能需要使用防腐层模式。此模式用于在不共享相同语义的组件之间实现外观或适配器。防腐层的目的是将一个组件的请求转换为另一个组件的请求;例如,实现协议或模式的转换。

要实施此操作,您需要在单片应用程序中设计并创建一个新的 API,通过新服务中的防腐层进行调用,如图 6-1 所示。

clna 0601

图 6-1. 防腐层 模式

当您使用此方法时,有几个考虑因素。如图 6-1 所示,防腐层本身是一个服务,因此您需要考虑如何扩展和操作该层。此外,您需要考虑在完全将单片应用程序移至云原生应用程序之后是否要废弃防腐层。

使用 Strangler 模式

当您将单片拆解以迁移到微服务和函数时,您可以使用网关和Strangler模式。Strangler 模式的理念是使用网关作为外观,同时逐步将后端单片移至新架构——服务、函数或两者的组合。随着您分解单片并将其功能实现为服务或函数,您更新网关以重定向请求到新功能,如图 6-2 所示。

clna 0602

图 6-2. 使用Strangler模式从单片迁移

注意,如果您无法拦截发送到支持单片的请求,则 Strangler 模式可能不适用。如果您有一个较小的系统,更容易和更快地替换整个系统,而不是逐步移动,则此模式可能也没有意义。

防腐层和 Strangler 模式在多次证明中被证明是将单片遗留应用程序移动到云原生应用程序的良好方法,因为两者都促进了逐步的方法。

制定数据迁移策略

在单体应用中,通常会使用一个中心共享的数据存储,多个地方和服务从中读取和写入数据。要真正转向云原生架构,您还需要解耦数据。您的数据迁移策略可能包含多个阶段,特别是如果无法同时迁移所有内容。然而,在大多数情况下,您需要进行增量迁移,同时保持整个系统运行。逐步迁移可能会在一段时间内将数据写入两次(到新旧数据存储)。在两个地方的数据都同步后,您需要修改数据的读取位置,然后从新存储中读取所有数据。最后,您应该能够完全停止向旧存储写入数据。

重写任何样板代码

单体应用通常会有大量处理配置、数据缓存、数据存储访问等内容的代码,并且可能使用较旧的库和框架。在将功能移动到新服务时,您应该重写此代码。最好的选择是放弃旧代码,从头开始重写,而不是修改现有代码并将其塑造成适合新服务的样子。

重新考虑框架、语言、数据结构和数据存储

移向微服务为您提供重新思考现有实现的选项。是否有新的框架或语言可以用来重写当前代码,以提供更好的功能和功能?如果重写代码有意义,那就去做吧!此外,重新考虑当前代码中的任何数据结构。当转移到服务时,它们是否仍然合理?您还应该评估是否要使用不同的数据存储。第四章概述了哪些数据存储最适合特定的数据结构和查询模式。

淘汰代码

创建新服务并将所有流量重定向到该服务后,您需要淘汰和删除单体中的旧代码。使用这种方法,您正在缩小单体并扩展您的服务。

确保弹性

弹性是系统从故障中恢复并继续运行和提供请求的能力。弹性不是避免故障,而是响应故障的方式,以避免重大停机时间或数据丢失。

处理瞬态故障并进行重试

请求可能因网络延迟、连接中断或超时(如果下游服务繁忙)等多种原因而失败。如果重试请求,您可以避免大部分这些失败。重试还可以提高应用程序的稳定性。然而,在盲目重试所有请求之前,您需要实现一些逻辑来确定是否应该重试请求。如果故障不是暂时的,或者重试可能不会成功,最好是组件取消请求并返回适当的错误消息。例如,因为密码错误而重试失败的登录是徒劳的,重试也不会有所帮助。如果故障是由于罕见的网络问题引起的,可以立即重试请求,因为同样的问题可能不会持续。最后,如果故障是由于下游服务繁忙或者速率限制,例如,应该在延迟后进行重试。以下是一些常见的重试操作之间延迟的策略:

常量

在每次尝试之间等待相同的时间。

线性

按照每次重试之间逐步增加的时间。例如,可以从一秒开始,然后是三秒、五秒等。

指数退避

按照每次重试之间指数增加的时间。例如,从 3 秒开始,然后是 12 秒、30 秒等。

根据您处理的故障类型,您还可以立即重试操作一次,然后使用前面列表中提到的延迟策略之一。您可以通过使用许多服务 SDK 提供的重试和暂时性故障逻辑来处理组件源代码中的重试,或者如果您正在使用像 Istio 这样的服务网格,则可以在基础设施层处理重试。

使用有限次数的重试

无论使用哪种重试策略,请确保使用有限次数的重试。无限次重试将对系统造成不必要的负担。

用于非暂时故障的断路器

断路器的目的是防止组件执行可能失败且不是暂时的操作。断路器监视故障次数,并根据该信息决定是否继续请求或者是否应该返回错误而不调用下游服务。如果断路器跳闸,则失败次数已超过预定义值,断路器将自动在预设时间内返回错误。在预设时间结束后,它将重置故障计数并允许请求再次通过到下游服务。一个实现断路器模式的知名库是 Netflix 的 Hystrix。如果您正在使用像 Istio 或 Envoy 代理这样的服务网格,您可以利用这些解决方案中的断路器实现。

优雅降级

服务应优雅地降级,因此即使它们失败,如果有意义的话,它们仍然提供可接受的用户体验。例如,如果无法检索数据,可以显示数据的缓存版本,一旦数据源恢复,就显示最新的数据。

使用分隔模式

分隔模式指的是将系统的不同部分分组隔离,以便如果一个部分失败,其他部分仍将继续运行而不受影响。以这种方式对服务进行分组允许你隔离故障,并在发生故障时继续提供请求服务。

实施健康检查和就绪检查

对于你部署的每个服务实施健康检查和就绪检查。平台可以使用这些来确定服务是否健康且正确执行,以及服务何时准备好接受请求。在 Kubernetes 中,健康检查被称为探针。存活探针用于确定何时重新启动容器,而就绪探针确定是否应该开始向 pod 发送流量。

初始延迟定义了容器启动后多少秒之后活跃探针或就绪探针开始工作,而周期则定义了探针的执行频率。还有额外的设置,如成功/失败阈值和超时,可以用来微调探针。

为你的容器定义 CPU 和内存限制

你应该定义 CPU 和内存限制来隔离资源,防止某些服务实例消耗过多资源。在 Kubernetes 中,你可以通过在 pod 定义中定义内存和 CPU 限制来实现这一点。

实施速率限制和节流

你可以使用速率限制和节流来限制服务的入站或出站请求数量。实施这些措施可以帮助你保持服务在请求突然增加的情况下仍然响应。另一方面,节流通常用于出站请求。考虑在希望控制发送到外部服务的请求数量以减少成本或确保你的服务不像是拒绝服务攻击来源时使用它。

确保安全性

云原生世界中的安全性基于共享责任模型。云提供商不单独负责其客户解决方案的安全性;相反,他们与客户分享这一责任。从应用程序的角度来看,你应考虑采纳深度防御的概念,该概念在第三章中有讨论。本节列出的最佳实践将帮助你确保安全性。

将安全要求与其他任何要求视为同等重要

拥有完全自动化的流程符合云原生开发的精神。为了实现这一点,所有安全要求必须像其他任何要求一样被视为开发流水线中的一部分。

在您的设计中融入安全性

在规划和设计云原生解决方案时,您需要考虑安全性,并在设计中融入安全功能。作为设计的一部分,您还应该指出在组件开发期间需要解决的任何额外安全问题。

授予最小特权访问

如果您的服务或函数需要访问任何资源,它们应该被授予具有最少权限的特定权限。例如,如果您的服务只从数据库读取,它不需要使用具有写权限的账户。

使用单独的账户/订阅/租户

根据您的云服务提供商的术语,您的云原生系统应该使用单独的账户、订阅和/或租户。至少,您需要为每个将使用的环境设置一个独立的账户;这样,您可以确保各个环境之间得到适当的隔离。

安全存储所有机密信息

系统内部的任何机密信息,无论是由您的组件还是持续集成/持续开发(CI/CD)流水线使用,都需要被加密并安全地存储。这听起来是理所当然的,但绝不要以明文形式存储任何机密信息:始终加密它们。最好使用现有和经过验证的机密管理系统来处理这些事务。最简单的选择是使用 Kubernetes Secrets 来存储集群内服务使用的机密信息。机密信息存储在 etcd 中,这是一个分布式键/值存储。然而,受管理和集中化的解决方案在几个方面都比 Kubernetes Secrets 有多个优势:所有内容都存储在一个集中位置,您可以定义访问控制策略,机密信息被加密,提供审计支持等等。一些受管理的解决方案的例子包括 Microsoft Azure Key Vault,Amazon Secrets Manager 和 HashiCorp Vault。

数据混淆

您的组件使用的任何数据都需要适当地混淆。例如,您绝不希望以明文形式记录任何被分类为个人身份信息(PII)的数据;如果需要记录或存储它,确保它要么被混淆(如果记录)或加密(如果存储)。

加密传输中的数据

在传输中加密数据可以保护您的数据,以防通信过程中被拦截。为了实现这种保护,您需要在传输之前加密数据,验证端点的身份,最后在达到端点后解密并验证数据。传输层安全协议(TLS)用于加密传输中的数据,以实现传输安全。如果您正在使用服务网格,TLS 可能已经在网格中的代理之间实现。

使用联合身份管理

使用现有的联合身份管理服务(例如 Auth0)来处理用户的注册、登录和退出,允许您将用户重定向到第三方页面进行身份验证。您的组件应尽可能地委派认证和授权。

使用基于角色的访问控制

基于角色的访问控制(RBAC)已经存在很长时间了。RBAC 是一种围绕角色和权限的访问控制机制,正如您所学到的,它可以成为防御深度策略的重要组成部分,因为它允许您为用户提供对他们所需资源的精细化访问。例如,Kubernetes 的 RBAC 控制对 Kubernetes API 的权限。使用 RBAC,您可以允许或拒绝特定用户创建部署或列出 Pod 等操作。在 Kubernetes 中,通过命名空间来限定 RBAC 权限是一个良好的实践,而不是使用集群角色。

隔离 Kubernetes Pods

在 Kubernetes 集群中运行的任何 Pod 都不是隔离的,可以接受来自任何来源的请求。在 Pod 上定义网络策略可以使其隔离,并拒绝任何未经策略允许的连接。例如,如果系统中的某个组件受到了威胁,网络策略将阻止恶意行为者与您不希望其通信的服务进行通信。在 Kubernetes 中使用 NetworkPolicy 资源,您可以定义 Pod 选择器以及详细的入站和出站策略。

处理数据

大多数现代应用程序都需要存储和处理数据。越来越多的数据存储和分析服务作为云提供商管理的服务可用。云原生应用程序设计旨在充分利用云提供商管理的数据系统,并设计成可以逐步利用增多功能。在云中处理数据时,许多标准的数据最佳实践仍然适用:拥有灾难恢复计划、将业务逻辑从数据库中分离、避免过度获取或过度聊天式的 I/O、使用防止 SQL 注入攻击的数据访问实现等等。

使用托管数据库和分析服务

尽可能使用托管数据库。在虚拟机(VM)上或 Kubernetes 集群中部署数据库通常是一个快速且简单的任务。需要备份和副本的生产数据库可以快速增加操作数据存储系统的时间和负担。通过卸载部署和管理数据库的运营负担,团队能够更多地专注于开发工作。

在某些情况下,数据存储技术可能没有作为托管服务提供,或者可能需要访问一些在系统的托管版本中不可用的配置。

使用最适合数据需求的数据存储

在设计本地应用程序时,架构师通常会尝试避免使用多个数据库。每种数据库技术的使用都需要具有部署和管理数据库技能的数据库管理员,这显著增加了应用程序的运营成本。云托管数据库的降低运营成本使得可以使用多种不同类型的数据存储来放置最适合数据类型、读取和写入要求的系统中的数据。云原生应用程序充分利用这一点,使用多种数据存储技术。

将数据存储在多个区域或区域中。

存储应用程序的生产数据跨多个区域或区域。数据如何存储在区域或区域中将取决于应用程序的可用性要求;例如,数据可以是备份或复制数据库。如果云服务提供商经历某个区域或区域的故障,数据可以用于恢复或故障转移。

使用数据分区和复制进行扩展。

云原生应用程序的设计是为了扩展而不是扩展。通过增加可用于数据库实例的资源,例如增加更多的核心或内存来实现数据库的扩展。这最终会遇到一个硬性限制并且成本高昂。通过在多个数据库实例之间分布数据来实现数据库的扩展。数据库被分区或分割,并存储在多个数据库中。

避免过多获取和交互式 I/O。

过多获取是指应用程序从数据库请求数据,但仅需要数据操作的一小部分。例如,应用程序可能会显示订单列表和简单摘要,但请求整个订单和订单详细信息而不需要这些详细信息。另一方面,喋喋不休的应用程序会进行大量的小型调用以完成操作,而单个请求可以向数据库发出。

不要将业务逻辑放在数据库中。

太多应用程序扩展问题的根源在于将太多逻辑放在数据库中。数据库通过支持标准开发语言,使得在数据库内部执行业务逻辑变得容易,并且执行这些任务在数据库中变得方便。这通常会引入扩展性问题,因为数据库通常是一种昂贵的共享资源。

使用类似于生产环境的数据进行测试。

创建自动化流程对生产数据进行匿名化处理,并随着数据变更更新新规则。应用程序应该使用类似于生产环境的数据进行测试。有时会从生产系统中提取数据,进行清洗,并加载到测试系统中,以提供类似于生产环境的数据。您应该自动化此过程,以便随着数据变更而轻松更新。

处理瞬时故障。

如本章的弹性部分所述,调用数据库时可能会发生故障。在进行调用时预期故障,并准备处理它们。许多数据库客户端库已经支持暂时性故障处理。重要的是要了解它们是否支持以及如何支持。

性能和可伸缩性

性能(Performance)指示一个系统在特定时间框架内执行操作的能力,而可伸缩性(scalability)则是指系统如何处理负载增加而不影响性能。预测系统活动增加的时期可能会很困难,因此组件需要能够根据需要进行伸缩,以满足增加的需求,然后在需求减少后进行缩减。接下来的子节提供了一些最佳实践,以帮助您实现最佳性能和可伸缩性。

设计能够横向扩展的无状态服务

应设计服务以进行横向扩展。横向扩展是通过增加服务的更多实例来增加服务的规模的一种方法。纵向扩展是通过增加内存或核心等资源来扩展服务的方法,但这种方法通常有硬性限制。通过设计服务以进行横向扩展和缩回,您可以扩展服务以处理负载变化,而不影响服务的可用性。

有状态应用程序本质上难以扩展,应尽量避免。如果有状态服务是必需的,通常最好将功能与应用程序分开,并使用分区策略和可管理的服务(如果有的话)。

使用平台自动缩放功能

在可能的情况下,在实施自己的自动缩放之前,请使用平台内置的任何自动缩放功能。Kubernetes 提供水平 Pod 自动缩放器(HPA)。 HPA 根据 CPU、内存或自定义指标扩展 Pod。您可以指定指标(例如,CPU 的 85% 或 16 GB 内存)以及 Pod 副本的最小和最大数量。达到目标指标后,Kubernetes 会自动缩放 Pod。类似地,集群自动缩放会根据 Pod 规范中请求的资源来确定是否应该添加节点,如果无法调度 Pod,则会缩放集群节点数量。

使用缓存

缓存是一种技术,可以通过在靠近组件的存储中临时存储经常使用的数据来帮助提高组件的性能。这样做可以减少组件访问原始数据源的时间。最基本的缓存类型是单进程使用的内存存储。如果您有多个组件实例,则每个实例将具有自己独立的内存缓存副本。如果数据不是静态的,这可能会导致一致性问题,因为不同的实例将具有不同版本的缓存数据。为了解决这个问题,您可以使用共享缓存,确保不同的组件实例使用相同的缓存数据。在这种情况下,缓存通常是单独存储的,通常位于数据库之前。

使用分区来扩展服务限制之外的规模

云服务通常会有一些定义的规模限制。了解每个使用的服务的可伸缩性限制及其可扩展的程度非常重要。如果单个服务无法扩展以满足应用程序的要求,请创建多个服务实例并将工作分区到这些实例中。例如,如果一个托管网关能够处理应用程序预期负载的 80%,则创建另一个网关并在网关之间分配服务。

函数

虽然软件开发生命周期(SDLC)和一般服务器架构最佳实践对无服务器架构也适用。然而,由于无服务器是一种不同的操作模型,因此有一些专门针对函数的最佳实践。

撰写单一目的的函数

遵循单一职责原则,只编写具有单一职责的函数。这将使您的函数更易于理解、测试,并且在需要时更易于调试。

不要链式调用函数

通常情况下,函数应该将消息/数据推送到队列或数据存储中,以触发其他函数的执行。通常认为一个或多个函数调用其他函数是一个反模式,会增加成本并使调试更加困难。如果您的应用程序需要函数串联,您应考虑使用如 Azure Durable Functions 或 AWS Step Functions 之类的函数提供。

保持函数轻量和简单

每个函数应只做一件事,并仅依赖最少数量的外部库。函数中额外和不必要的代码会增加函数的大小,从而影响其启动时间。

使函数无状态

不要在函数中保存任何数据,因为新的函数实例通常在其自己的隔离环境中运行,不与其他函数或同一函数的调用共享任何内容。

将函数的入口点与函数逻辑分离开来

函数将由函数框架调用入口点。通常,框架特定的上下文将传递给函数入口点,以及调用上下文。例如,如果函数通过类似 API 网关的 HTTP 请求调用,则上下文将包含 HTTP 特定的细节。入口方法应将这些入口点细节与代码的其余部分分开。这将提高函数的可管理性、可测试性和可移植性。

避免长时间运行的函数

大多数函数即服务(FaaS)提供的函数执行时间有上限。因此,长时间运行的函数可能会导致诸如增加的加载时间和超时等问题。在可能的情况下,将大函数重构为更小的函数,这些函数共同工作。

使用队列进行跨函数通信

函数之间应该使用队列而不是互相传递信息。其他函数可以基于队列上发生的事件(添加、删除、更新等)来触发和执行。

运维

DevOps 实践为组织充分利用云技术提供了必要的基础。云原生应用程序利用详细说明的 DevOps 原则和最佳实践,详见 第五章。

部署和发布是分开的活动

很重要的是要区分部署和发布。部署是将构建的组件放置在环境中的行为 —— 组件已完全配置且准备就绪;然而,此时尚未向其发送流量。作为组件发布的一部分,我们开始允许流量访问已部署的组件。这种分离允许您以受控的方式进行渐进式发布、A/B 测试和金丝雀部署。

保持小型部署

每个组件部署应该是一个小事件,可以由一个团队在短时间内完成。关于部署的大小和时间没有通用规则,因为这高度依赖于组件、您的流程和对组件的更改。一个好的方法是能够在一天内推出关键修复。

CI/CD 定义与组件同在

您需要存储和版本化与组件一起的任何 CI/CD 配置和依赖关系。每次推送到组件分支都会触发流水线,并执行在 CI/CD 配置中定义的作业。为了控制组件部署到不同环境(开发、演示、生产),您可以使用 Git 分支名称,并配置流水线仅将主分支部署到生产环境,例如。

一致的应用程序部署

通过一个持续可靠且可重复的部署过程,您可以最小化错误。尽可能自动化进程,并确保在部署失败时定义了回滚计划。

使用零停机时间发布

为了在发布期间最大化系统的可用性,考虑使用蓝/绿或金丝雀等零停机时间发布。使用这些方法之一还允许您在出现故障时快速回滚更新。

不要修改已部署的基础设施

基础设施应该是不可变的。修改已部署的基础设施可能会迅速失控,并且跟踪更改内容可能会变得复杂。如果需要更新基础设施,请重新部署它。

使用容器化构建

为了避免配置构建环境,请将您的构建过程打包到 Docker 容器中。考虑使用多个图像和容器进行构建,而不是创建单个的、单块的构建图像。

使用代码描述基础设施

基础设施应该使用云提供商的声明性模板或编程语言或脚本来描述和提供基础设施。

使用命名空间组织 Kubernetes 中的服务

在 Kubernetes 集群中,每个资源都属于一个命名空间。默认情况下,新创建的资源将进入一个名为default的命名空间。为了更好地组织服务,使用描述性名称并将服务分组到有界上下文是一个好的实践。

隔离环境

使用专用的生产集群,并在您的开发、预备或测试环境中物理分离生产集群。

分离功能源代码

每个功能必须独立进行版本控制并具有其自己的依赖关系。如果不是这种情况,您将得到一个单体和紧密耦合的代码库。

将部署与提交相关联

选择一个分支策略,使您能够将部署与分支中的特定提交相关联,并且还允许您确定部署的源代码版本。

日志记录、监控和警报

应用程序和基础设施日志记录可以提供比仅仅根本原因分析更多的价值。一个适当的日志记录解决方案将为应用程序和系统提供宝贵的洞察,并且通常对监控应用程序的健康和警报操作的重要事件是必要的。随着云应用程序变得更加分布式,日志记录和仪表化变得越来越具有挑战性和重要性。

使用统一的日志系统

使用能够捕获系统中所有服务和层级的日志消息并将其存储在集中存储中的统一日志系统。无论您将所有日志移动到集中存储以进行分析和搜索,还是在机器上留下它们,并配备有必要的工具来运行分布式查询,工程师都能够找到并分析日志而不必从一个系统到另一个系统。

使用关联 ID

包含一个通过所有服务传递的唯一关联 ID(CID)。如果其中一个服务失败,关联 ID 用于跟踪请求通过系统并准确定位故障发生的位置。

在日志条目中包含上下文

每个日志条目应包含在调查问题时有助的额外上下文。例如,包括所有异常处理、重试尝试、服务名称或 ID、图像版本、二进制版本等等。

常见且结构化的日志格式

确定所有组件将使用的常见和结构化日志格式。这将使你能够快速搜索和解析日志。另外,请确保所有组件使用相同的时区信息。一般来说,最好遵循协调世界时(UTC)等常见的时间格式。

为你的度量标记适当的标签

除了使用清晰且唯一的度量标签外,确保存储任何额外信息,例如组件名称、环境、函数名称、区域等等。有了标签,你可以使用多维度(例如,特定区域的平均延迟或特定函数的多个区域的延迟)创建查询、仪表板和报告。

避免警报疲劳

大量的度量标准使得设置警报和何时发出警报变得困难。如果触发了太多警报,最终人们将停止关注它们并不再认真对待。此外,调查一堆警报可能会变得很压倒,甚至可能是你的团队唯一在做的事情。重要的是按严重性对警报进行分类:低、中和高。低严重性警报的目的是在高严重性警报的根本原因分析时可能会用到。你可以用它们来发现某些模式,但在触发时不需要立即采取行动。中等严重性警报应该创建通知或开启工单。这些是你想要查看的警报,但不是高优先级并且不需要立即采取行动。它们可能代表一个临时条件(例如需求增加),最终会消失。它们还可以提前警告可能的高严重性警报。最后,高严重性警报会在半夜唤醒人们并要求立即采取行动。最近,基于机器学习的自动分类问题和发出警报的方法越来越受欢迎,甚至引入了 AIOps 这个术语。

定义并在关键性能指标上发出警报

云原生系统会发出和监控大量信号。你需要筛选出其中最重要和有价值的信号。这些关键性能指标(KPI)可以让你了解系统的健康状况。例如,一个 KPI 是延迟,用于衡量处理请求所需的时间。如果看到延迟增加或偏离可接受范围,可能是时候发出警报并请人查看了。除了 KPI,你还可以使用其他信号和度量来确定某些事情失败的原因。

生产中的持续测试

使用持续测试可以生成发送到系统各处并模拟真实用户的请求。您可以利用这些流量对组件进行测试覆盖率、发现潜在问题,并测试您的监控和警报。以下是一些常见的持续测试实践:

  • 蓝绿部署

  • 金丝雀测试

  • A/B 测试

这些实践在第五章中讨论。

从基本指标开始

确保始终收集系统中每个组件的流量(组件承受的需求量)、延迟(服务请求所需的时间)和错误(请求失败率)。

服务通信

服务通信是云原生应用程序的重要组成部分。无论是客户端与后端的通信,服务与数据库的通信,还是分布式架构中的各个服务之间的通信,这些交互都是云原生应用程序的重要组成部分。根据需求使用多种不同形式的通信。以下各小节提供了一些服务通信的最佳实践。

设计向后和向前兼容

通过向后兼容性,确保添加到服务或组件的新功能不会破坏任何现有服务。例如,在图 6-3 中,服务 A v1.0 与服务 B v1.0 兼容。向后兼容性意味着发布的服务 B v1.1 不会破坏服务 A 的功能。

clna 0603

图 6-3. 向后兼容性

为了确保向后兼容性,应向 API 添加的任何新字段都应是可选的或具有合理的默认值。任何现有字段不应更名,因为这将破坏向后兼容性。

注意

并行更改,也称为扩展和收缩模式,可用于安全地引入向后不兼容的更改。例如,假设服务所有者想要在接口上更改属性或资源。服务所有者将通过新的属性或资源扩展接口,然后在所有消费者有机会迁移服务接口之后,删除先前的属性。

如果您的系统或组件需要确保回滚功能,那么在修改服务时,需要考虑其向前兼容性。向前兼容性意味着您的组件与未来版本兼容。您的服务应能够接受“未来”数据和消息格式,并适当处理。向前兼容性的一个很好的例子是 HTML:当它遇到未知标签或属性时,不会导致失败;它只会跳过它们。

定义不泄漏内部细节的服务契约

暴露 API 的服务应定义契约,并在发布更新时进行契约测试。例如,基于 REST 的服务通常会以 OpenAPI 格式或文档的形式定义契约,而服务的消费者会根据这个契约进行开发。只要更新不会对 API 契约造成破坏性变更,服务的更新就可以推送,这样就不会影响消费者。泄露服务的内部实现可能会导致难以进行更改并引入耦合。不要假设消费者不会使用通过 API 公开的某些数据。

注意

发布消息到队列或流的服务也应以同样的方式定义契约。通常会发布事件的服务会拥有这个契约。

优先使用异步通信

尽可能使用异步通信。它与分布式系统很搭配,并解耦了两个或更多服务的执行。在实现此方法时通常会使用消息总线或流,但也可以通过像 gRPC 这样的直接调用来实现。两者都使用消息总线作为通道。

使用高效的序列化技术

像使用微服务架构构建的分布式应用程序更依赖于服务之间的通信和消息传递。数据序列化和反序列化在服务通信中会增加很多额外开销。

注意

在某个案例中,序列化和反序列化被发现占据了所有服务中近 40% 的 CPU 利用率。将标准的 JSON 序列化库替换为自定义库将这一开销减少到大约总 CPU 利用率的 15%。

使用诸如协议缓冲区这样的高效序列化格式,它在 gRPC 中被广泛使用。了解不同序列化格式的权衡是很重要的,因为工具和消费者的要求可能不会使这成为可行的选择。您还可以通过将一些数据放入头部来减少某些服务中序列化的需求。例如,如果一个服务接收到请求并在大量消息负载中仅操作少量字段然后将其传递给下游服务,通过将这些字段放入头部,该服务就无需对负载进行反序列化或重新序列化。该服务读取和写入头部,然后简单地将整个负载传递给下游服务。

使用队列或流处理大量负载和流量峰值

组件之间的队列或流作为缓冲区,存储消息直到检索。使用队列允许组件以自己的节奏处理消息,无论传入的数量或负载如何。因此,这有助于最大化服务的可用性和可扩展性。

批量请求以提高效率

队列可以用于批处理多个请求并执行一次操作。例如,将 1,000 个批处理条目写入数据库比每次写入一次 1,000 次更有效率。

拆分大消息

发送、接收和操作大消息需要更多资源,并可能减慢整个系统。Claim-Check 模式讨论了将大消息拆分为两部分的方法。您可以将整个消息存储在外部服务中(例如数据库),并仅发送消息的引用。任何感兴趣的消息接收者可以使用引用从数据库中获取完整的消息。

容器

可以很容易地在 Docker 容器中运行大多数应用程序。然而,在生产环境中运行容器和优化构建、部署和监控时,可能会遇到一些潜在的问题。已经确定了一些最佳实践,以帮助避免这些问题并改善结果。

将镜像存储在受信任的注册表中

平台上运行的任何镜像都应来自受信任的容器镜像注册表。Kubernetes 提供了一个 webhook(验证接入),可以用来确保 Pod 只能使用来自受信任注册表的镜像。如果您使用 Google Cloud,可以利用二进制授权安全措施,确保仅在集群上部署受信任的镜像。

利用 Docker 构建缓存

使用构建缓存在构建 Docker 镜像时可以加快构建过程。所有镜像都是由层次构成的,每个 Dockerfile 中的命令都为最终镜像贡献了一层。在构建过程中,Docker 将尝试重用上一次构建的层次而不是再次构建它。然而,只有当所有前面的构建步骤也使用了缓存的层次时,它才能重新使用缓存的层次。为了充分利用 Docker 构建缓存,将更经常变化的命令(例如将源代码添加到镜像中、构建源代码)放在 Dockerfile 的末尾。这样,任何前面的步骤都将被重用。

不要在特权模式下运行容器

在特权模式下运行容器允许访问主机上的所有内容。使用 Pod 上的安全策略阻止容器在特权模式下运行。如果某个容器确实因某种原因需要特权模式以对主机环境进行更改,则考虑将该功能从容器分离并放入基础设施配置中。

使用明确的容器镜像标签

始终使用与打包在镜像中的代码紧密关联的特定标签标记容器镜像。为了正确标记图像,您可以使用唯一标识代码版本的 Git 提交哈希(例如,1f7a7a472)或使用语义版本(例如,1.0.1)。如果未提供标签,则默认使用latest标签;然而,由于它与特定代码版本的紧密联系不足,应避免在生产环境中使用。永远不应在生产环境中使用latest标签,因为它可能导致不一致的行为,难以排查故障。

保持容器镜像小巧

将镜像尽可能地缩小,不论是在容器注册表还是主机系统中使用镜像运行容器,都能减少空间占用。小型镜像提高了镜像的推送和拉取性能。这进一步提升了在部署或扩展服务时启动容器的性能。应用及其依赖关系会影响镜像大小,但通过使用精简基础镜像并确保不包含不必要的文件,可以大幅减少镜像大小。例如,Alpine 3.9.4 镜像仅有 3 MB,Debian Stretch 镜像为 45 MB,CentOS 7.6.1810 镜像则为 75 MB。这些发行版通常提供了精简版本,从基础镜像中删除可能不需要的内容,这不被应用所需。保持镜像精简需要牢记两点:

  • 选择精简基础镜像开始

  • 仅包含应用程序操作所需的文件

可以使用容器构建模式通过将用于构建工件的图像与用于运行应用程序的基础图像分离,创建精简镜像。Docker 的多阶段构建通常用于实现此目的。您可以创建 Docker 构建文件,从不同的图像开始执行构建和测试工件的命令,然后在创建运行应用程序镜像的过程中定义另一个基础图像。

提示

使用.dockerignore文件可以通过排除 Docker 构建中不需要的文件来提高构建速度。

每个容器内运行一个应用程序

每个容器内仅运行单一应用程序。容器设计为仅运行单一应用程序,容器与其中运行的应用程序具有相同的生命周期。在同一容器内运行多个应用程序会增加管理难度,并可能导致容器中某些进程崩溃或无响应。

使用受信任仓库的验证图像

有大量且不断增长的公开可用镜像,在处理容器时非常有用。Docker 仓库标签是可变的,因此理解镜像可能会变化至关重要。当使用外部仓库中的镜像时,最好将其从外部仓库复制或重新创建到组织管理的仓库中。组织的仓库通常更接近 CI 服务,这种方法可以消除可能影响构建的其他服务依赖。

在镜像上使用漏洞扫描工具

你需要注意影响你的镜像的任何漏洞,因为这可能会危及系统的安全性。如果发现漏洞,需要重新构建镜像,并包含补丁和修复程序,然后重新部署它。一些云提供商在其镜像注册解决方案中提供漏洞扫描功能,因此确保你充分利用这些功能。

提示

尽可能频繁地扫描镜像,因为每天都会发布新的网络安全漏洞和暴露(CVE)。

不要在容器中存储数据

容器是临时的 —— 它们可以停止、销毁或替换而不会丢失数据。如果运行在容器中的服务需要存储数据,请使用卷挂载来保存数据。卷中的内容存在于容器的生命周期之外,并且卷不会增加容器的大小。如果容器需要临时非持久性写入,请使用 tmpfs 挂载,这将通过避免向容器的可写层写入来提高性能。

永远不要在镜像内部存储机密或配置信息

在镜像内部硬编码任何类型的机密是应该避免的。如果你的容器需要任何机密信息,请将其定义为环境变量或文件,并通过卷挂载到容器中。

总结

针对云原生应用程序的最佳实践有很多技术涉及,我们可以轻松撰写一整本书来覆盖这些内容。然而,在客户交流中反复出现的特定领域,本章节已经涵盖了一系列云原生应用程序的最佳实践、技巧和验证的模式。你应该对你可能需要考虑的因素有更深入的理解。

第七章:可移植性

在构建云原生应用程序时,可移植性有时是一个关注点。该应用程序可能需要在多个云提供商或本地环境中部署。这些需求通常由利益相关者驱动,无论是使用应用程序的客户还是构建应用程序的企业。可能的情况是,应用程序由客户部署,可以是在他们自己的硬件上或者在他们选择的云提供商账户中。无论出于何种原因,可移植性的要求都应该像任何其他架构重要的要求一样对待。应当由业务驱动,并仔细考虑成本和权衡。

为什么要做应用程序的可移植性?

有许多制作应用程序可移植性的充分理由。可移植性应当是一个要求,应当考虑与此功能相关的权衡和成本。以下是软件供应商制作应用程序可移植性的一些原因:

  • 构建一个部署到客户环境中的应用程序,并且有一个要求,即能够选择将其部署到客户选择的云提供商或本地环境中。

  • 构建一个混合应用程序,在云中和本地环境中运行,在应用程序中的某些服务同时在这两种环境中运行。

  • 服务需要靠近客户的应用程序,以减少延迟。例如,这些服务可能是存储或分析数据的服务。…

posted @ 2025-11-24 09:16  绝不原创的飞龙  阅读(5)  评论(0)    收藏  举报