购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

第1章
云原生简介

本章内容:

■ 云和云计算模型

■ 云原生的定义

■ 云原生应用的属性

■ 支撑云原生的文化和实践

■ 何时以及为何要考虑采用云原生方式

■ 云原生应用的拓扑结构和架构

云原生应用是高度分布式系统,它们存在于云中,并且能够对变化保持韧性。系统是由多个服务组成的,服务之间通过网络进行通信,并且会部署到一个一切都在不断变化的动态环境中。

在深入研究技术之前,很重要的一件事就是定义云原生到底是什么。就像我们这个领域中其他的流行词(比如敏捷、DevOps或微服务)一样,云原生有时会被误解,并且成为混乱的根源,因为对不同的人,它意味着不同的东西。

本章将介绍一些理念性工具,它们都是本书后续内容所需要的。我们首先定义云原生意味着什么,以及要采取哪些行动才能使应用可以称为是云原生的。我将会阐述云原生应用的属性,审视云计算模型的特征并讨论何时以及为何要将应用转移到云中。我还会展示云原生拓扑结构和架构的基本理念。图1.1展示了我将在本章中定义和鉴别云原生系统的所有元素。在本章结束时,我们将会为后续的旅程做好准备,以便于使用Spring构建云原生应用并将其部署到Kubernetes中。

图1.1 云原生是一种旨在利用云技术的应用开发方式

bt2-L 1.1 什么是云原生

2010年5月25日,云计算领域的资深人士Paul Fremantle撰写了一篇名为“云原生”的博客文章 。他是最早使用云原生这个术语的人之一。在微服务、Docker、DevOps、Kubernetes和Spring Boot等概念和技术尚未出现的年代,Fremantle和他在WSO2的团队讨论了“应用和中间件要在云中良好运行”所需的条件,也就是所谓的云原生。

Fremantle所阐述的核心理念是应用要针对云环境进行专门的设计,并且要充分利用云环境和云计算模型的特点。我们可以将一个传统的(按照在本地运行所设计的)应用直接转移到云中,这种方式通常被称为“提升并转移”(lift and shift),但这并不能让应用“原生”适应云环境。接下来,我们看一下如何才能做到这一点。

云原生的3P

什么样的应用才能算是专门为云环境设计的呢?云原生计算基金会(Cloud Native Computing Foundation,CNCF)在对云原生的定义中回答了这个问题

云原生技术有利于各组织在公有云、私有云和混合云等新型动态环境中,构建和运行可弹性扩展的应用。云原生的代表技术包括容器、服务网格、微服务、不可变基础设施和声明式API。

这些技术能够构建容错性好、易于管理和便于观测的松耦合系统。结合可靠的自动化手段,云原生技术使工程师能够轻松地对系统做出频繁和可预测的重大变更。

从这个定义中,我识别出了三组信息,并将其称为“云原生的3P”:

■ 平台(Platform):云原生应用运行在基于动态化、分布式环境的平台上,也就是云(公有云、私有云和混合云)中。

■ 属性(Property):按照设计,云原生应用是可扩展、松耦合、有韧性、可管理和可观测的。

■ 实践(Practice):围绕云原生应用的实践包括可靠的自动化,以及频繁且可预测的变更,即自动化、持续交付和DevOps。

什么是云原生计算基金会?

云原生计算基金会(CNCF)是Linux基金会的一部分,致力于“构建可持续的生态系统和培育社区,以支持云原生开源软件的健康发展”。CNCF托管了许多云原生技术和项目,以实现云的可移植性,避免被供应商锁定。如果你想了解解决云原生各方面问题的项目,建议查阅CNCF的云原生交互式全景图(CNCF Cloud Native Interactive Landscape)

在后续章节中,我将进一步阐述这些概念。但首先,我想让你注意的是,云原生的定义与任何具体的实现细节或技术没有关联。CNCF在定义中提到了一些技术,如容器和微服务,但它们只是示例。在向云迁移时,一个常见的误解是我们必须要采用微服务架构、构建容器并将其部署至Kubernetes中。这是不对的。Fremantle在2010年的博客文章就证明了这一点,他并没有提及这些技术,因为当时它们根本就不存在。然而,他所描述的应用不仅仍然被认为是云原生的,而且还符合多年后CNCF给出的定义。

bt2-L 1.2 云和云计算模型

在关注我们的主角(云原生应用)之前,我想要先介绍一下我们这个旅程的发生地,也就是云原生应用的运行环境——云(如图1.2所示)。在本节中,我将定义云及其主要特征。毕竟,如果云原生应用按照设计要在云环境中运行,我们应该知道这是一个什么样的环境。

图1.2 云是一种IT基础设施,其主要特征是具有不同的计算模型,供应商会按照消费者所需的控制程度以服务的形式提供

云是一种能够按照云计算模型向消费者提供计算资源的IT基础设施。美国国家标准与技术研究院(National Institute of Standards and Technology,NIST)是这样定义云计算的

云计算是一种模型,能够实现按需在任意位置对可配置的计算资源(如网络、服务器、存储、应用和服务)共享池进行便利的网络访问,这些计算资源可以快速获取和释放,并且要尽可能减少管理成本以及与服务供应商的沟通交流。

就像我们会从供应商那里获取电力,而不是自己发电一样,借助云,我们就能够以商品的形式获取计算资源(例如服务器、存储和网络)。

云供应商管理底层的计算基础设施,所以消费者不需要操心像机器或网络这样的物理资源。迁移到云的公司可以通过网络(通常是互联网)获取需要的所有计算资源,借助一组API,这些公司能够自助式地按需获取和扩展资源。

弹性是该模型的主要特点之一:计算资源可以根据需要动态获取和释放。

弹性指的是一个系统能够在多大程度上自主地获取和减少资源以适应工作负载的变化,确保在每个时间点可用的资源与当前的需求都是相互匹配的

传统的IT基础设施无法提供弹性。公司必须要计算出所需的最大计算能力,并建立能够支持该能力的基础设施,即便其中大多数的计算能力只是偶尔才会用到。在云计算模型下,计算资源的使用会受到监控,消费者只需要为实际使用的资源付费。

对于云基础设施应该放在哪里,以及由谁来管理,并没有严格的要求。交付云服务的部署模型有多种,主要是私有云、公有云和混合云。

■ 私有云:提供的云基础设施只能由一个组织使用。它可以由组织自身或第三方进行管理,可以托管在企业内部或企业外部。对于处理敏感数据和高度关键系统的组织来说,私有云通常是首选方案。如果要完全控制基础设施的合规性,以符合特定的法律和要求,比如,通用数据保护条例(General Data Protection Regulation,GDPR)或加利福尼亚消费者隐私法案(California Consumer Privacy Act,CCPA),私有云也是一个常见的选择。例如,银行和医疗机构很可能会建立自己的云基础设施。

■ 公有云:提供的云基础设施可公开使用。它通常属于某个组织,并由其进行管理,也就是所谓的云供应商,基础设施由供应商托管。公有云服务提供商如Amazon Web Services(AWS)、Microsoft Azure、Google Cloud、Alibaba Cloud和DigitalOcean。

■ 混合云:由上述任意类型的两个或更多不同的云基础设施组合而成,并且在提供服务的时候,就像是来自一个环境一样。

图1.3描述了5种主要的云计算模型、在每种模型下平台提供了什么内容,以及向消费者提供了哪些抽象。例如,在基础设施即服务(IaaS)模型下,平台提供并管理计算、存储和网络资源,消费者则会设置和管理虚拟机。至于该选择哪种服务模型,取决于消费者需要对基础设施的控制程度以及他们需要管理的计算资源类型。

图1.3 云计算模型的差异在于提供的抽象层级以及由谁(平台或消费者)来管理不同的层级

1.2.1 基础设施即服务

在基础设施即服务(Infrastructure as a Service,IaaS)模型中,消费者可以直接控制和获取像服务器、存储和网络这样的资源。例如,它们可以获取虚拟机并安装软件,比如操作系统和库。尽管这种模型已经存在很久了,但直到2006年,亚马逊通过Amazon Web Services(AWS)才使其流行起来,并得到了广泛的使用。IaaS产品包括AWS Elastic Compute Cloud(EC2)、Azure Virtual Machines、Google Compute Engine、Alibaba Virtual Machines和DigitalOcean Droplets。

1.2.2 容器即服务

使用容器即服务(Container as a Service,CaaS)模型时,消费者无法控制原始的虚拟化资源。相反,他们会设置并管理容器。云供应商负责提供满足这些容器需求的底层资源,例如,通过启动新的虚拟机和配置网络使其能够通过互联网进行访问。Docker Swarm、Apache Mesos和Kubernetes都是用来构建容器平台的示例工具。所有主流的云供应商都提供了托管的Kubernetes服务,它已经成为CaaS的事实标准技术,比如Amazon Elastic Kubernetes Service(EKS)、Microsoft Azure Kubernetes Service(AKS)、Google Kubernetes Engine(GKE)、Alibaba Container Service for Kubernetes(ACK)和DigitalOcean Kubernetes。

1.2.3 平台即服务

在平台即服务(Platform as a Service,PaaS)模型中,平台会给开发人员提供构建和部署应用所需的基础设施、工具和API。例如,作为开发人员,我们可以构建一个Java应用,打包为JAR文件,然后将其部署到按照PaaS模式运行的平台上。平台会提供Java运行时和其他所需的中间件,还可以提供额外的服务,比如数据库或消息系统。PaaS产品有Cloud Foundry、Heroku、AWS Elastic Beanstalk、Azure App Service、Google App Engine、Alibaba Web App Service和DigitalOcean App Platform。在过去的几年间,供应商一直在向Kubernetes靠拢,为开发人员和运营商构建了新的PaaS体验。这种新一代服务的示例如VMware Tanzu Application Platform和RedHat OpenShift。

1.2.4 函数即服务

函数即服务(Function as a Service,FaaS)模型依赖Serverless计算,让消费者专注于应用业务逻辑的实现(通常遵循函数的方式),而平台负责提供服务器和其他基础设施。例如,你可能会编写一个函数,即每当消息队列中有数据集时,该函数就会分析该数据集并按照一些算法计算出结果。FaaS产品包括Amazon AWS Lambda、Microsoft Azure Functions、Google Cloud Functions和Alibaba Functions Compute。开源FaaS产品包括Knative和Apache OpenWhisk。

1.2.5 软件即服务

具有最高抽象水准的模型是软件即服务(Software as a Service,SaaS)。在这种模型下,消费者以用户的形式访问应用,云供应商管理整个软件栈和基础设施。很多公司会构建应用并使用CaaS或PaaS模型运行它们,然后将它们的使用权以SaaS的形式出售给终端客户。SaaS应用的消费者一般会以瘦客户端(如Web浏览器或移动设备)的形式来访问它们。SaaS的示例应用包括Salesforce、ProtonMail、GitHub、Plausible Analytics和Microsoft Office 365。

平台与PaaS

在云原生相关讨论中,“平台”这个术语可能会产生一些混淆。所以,我们来澄清一下。一般来讲,平台是一个用来运行和管理应用的运维环境。Google Kubernetes Engine(GKE)是一个按照CaaS模型提供云服务的平台。Microsoft Azure Functions是一个按照FaaS模型提供云服务的平台。在更低的层级上,如果我们直接在Ubuntu机器上部署应用,那么这就是我们的平台。在本书后文,当我使用“平台”这个术语时,我指的就是刚刚所述的这个更宽泛的概念,除非另有说明。

bt2-L 1.3 云原生应用的属性

场景已经搭建好了,那就是要在云中。我们该如何设计应用以充分利用云的特点呢?

CNCF定义了云原生应用应该具备的五个主要属性,即可扩展性、松耦合、韧性、可观测性和可管理性。云原生是一种构建和运行具有这些属性的应用的方法论。Cornelia Davis这样总结:“云原生软件是由如何计算定义的,而不是由在何处计算定义的。” 换句话说,云是关于“在何处计算”这个问题的,而云原生是关于“如何计算”这个问题的。

我已经介绍了“在何处计算”这个问题,也就是在云中。现在,我们继续讨论“如何计算”的问题。作为快速参考,图1.4列出了这些属性以及它们的简单描述。

图1.4 云原生应用的主要属性

1.3.1 可扩展性

云原生应用是可扩展的,这意味着如果提供额外的资源,它们能够支持增加工作负载。根据这些额外资源的特点,我们可以将其划分为垂直可扩展性和水平可扩展性。

■ 垂直可扩展性:垂直扩展,或者称为向上/向下扩展,意味着在计算节点添加/移除硬件资源,如CPU或内存。这种方式是有限制的,因为我们不能无限地增加硬件资源。另外,应用不需要按照特定的方式进行设计,就能实现向上或向下扩展。

■ 水平可扩展性:水平扩展,或者称为向外/向内扩展,意味着向系统中添加或移除计算节点或容器。这种方式没有垂直扩展那样的限制,但它需要应用具有可扩展性。

传统系统在面临工作负载增加的时候通常会采用垂直可扩展方式。对于要支持更多用户的应用来说,添加CPU和内存是一种常见的方式,不需要针对可扩展性进行重新设计。在特定的场景中,这依然是一个很好的选择,但是对于云环境来说,我们需要一些其他东西。

在云中,所有的内容都是动态和不断变化的,水平可扩展是首选方案。借助云计算模型所提供的抽象等级,为应用补充新的实例是非常简单的,而不应该为已经处于运行状态的机器增加计算能力。因为云是有弹性的,我们可以在很短的时间内动态地扩展和收缩应用的实例。我之前已经讨论过,弹性是云的主要特征之一:计算资源可以根据需要主动提供和释放。可扩展性是实现弹性的先决条件。

图1.5展示了垂直可扩展性和水平可扩展性之间的差异。在垂直可扩展性中,我们通过向现有的虚拟机添加更多的资源来进行扩展。在水平可扩展性中,我们添加了另外一个虚拟机,它会帮助现有的虚拟机处理额外的工作负载。

图1.5 当需要支持不断增加的工作负载时,垂直可扩展性模型会向计算节点中增加硬件资源,
而水平可扩展性模型则会增加更多的计算节点

当讨论Kubernetes的时候,你将会看到,平台(可以是CaaS、PaaS或其他类型的平台)会根据上文所述的定义动态地扩展和收缩应用。作为开发人员,你有责任设计可扩展的应用。可扩展性的最大障碍是应用的状态,这将决定应用最终是有状态的还是无状态的。在本书中,我将介绍构建无状态应用的技术,并让它们没有任何问题地进行扩展。除此之外,我将会展示如何将应用的状态从Spring推送到像PostgreSQL和Redis这样的数据存储中。

1.3.2 松耦合

松耦合是系统的一个基本属性,根据该属性,系统的各个组成部分之间对彼此的了解要尽可能的少。它的目标是每个部分都能独立演进,这样当某一部分发生变化的时候,其他组成部分无须相应地变化。

几十年来,耦合及其伴生的内聚理念在软件工程中发挥了重要的作用。将系统分解成模块(模块化),这些模块对其他部分的依赖达到最小(松耦合),并且将联系紧密的代码封装在一起(高内聚),这是一种良好的设计实践。根据不同的架构风格,模块可以建模成一个单体组件或一个独立的服务(例如,微服务)。不管采用哪种方式,我们都应该以实现松耦合和高内聚的恰当模块化为目标。

Parnas指出了模块化的三个收益

■ 管理方面:鉴于每个模块都是松耦合的,所以负责团队应该不需要与其他团队进行过多的沟通和协调。

■ 产品的灵活性:每个模块都可以独立于其他模块演进,所以这会形成一个非常灵活的系统。

■ 可理解性:人们应该能够理解和运作单个模块,而不必学习整个系统。

上面所述的收益通常也是微服务所带来的部分收益。事实上,要实现它们,并不一定要采用微服务。在最近几年中,很多组织决定从单体迁移至微服务。但是,其中有些组织因为缺乏恰当的模块化而宣告失败。单体由紧耦合、非内聚的组件组成,当进行迁移的时候,会产生一个紧耦合、非内聚的微服务系统,它有时候也被称为分布式单体。我认为这并不是一个好的名字,因为它暗示着,按照定义单体是由紧耦合、非内聚的组件组成。而事实并非如此。架构风格并不重要:糟糕的设计就是糟糕的设计,与架构风格无关。事实上,我喜欢Simon Brown提出的“模块化单体”这个术语,它可以提高人们的认知,那就是单体也可以提升松耦合和高内聚,而且单体和微服务最终都有可能成为“大泥球”。

在本书中,我将会展示一些如何在应用中强制实现松耦合的技术。尤其是,我将会采用面向服务的架构,专注于构建具有明确接口的服务,以便于服务间的相互通信,并将与其他服务的依赖降至最低从而实现高内聚。

1.3.3 韧性

如果一个系统能够在出现故障或环境变化的情况下依然能够提供服务,那么我们就说它是有韧性(resilience)的。韧性是指“在面临故障以及挑战正常运维的情况下,硬件-软件网络提供和保持可接受的服务水平的能力”

在构建云原生应用时,我们的目标应该是,不管是基础设施还是我们的软件出现故障,都要确保应用始终是可用的。云原生应用在一个动态的环境中运行,在这种环境中所有的事情都在不断地发生变化,故障在所难免,这是无法预防的。过去,我们习惯于将变化和故障视为异常情况。但是,对于像云原生这样的高度分布式系统来说,变化不是异常情况,它们是常态。

当讨论韧性的时候,我们有必要定义三个基本概念,即过错(fault)、错误(error)和故障(failure)。

■ 过错:指的是在软件或基础设施中会产生不正确的内部状态的缺陷。例如,某个方法调用返回了一个空值,但是规范要求它必须返回非空的值。

■ 错误:指的是系统的预期行为和实际行为的差异。例如,因为上面所述的过错,抛出了NullPointerException异常。

■ 故障:当出现过错并导致错误时,有可能会产生故障,这将使得系统无反应并且无法按照其规范行事。例如,如果这个NullPointerException没有被捕获的话,这个错误就会引发故障——系统对任何请求都会产生500响应码。

过错可能会变成错误,进而引发故障,所以我们应该设计能够容错(fault tolerant)的应用。韧性的一个重要组成部分就是要确保故障不会级联到系统的其他组件中,而是在修复时保持它是隔离的。我们可能还希望系统是自我修复(self-repairing)或自我治愈(self-healing)的,云模型实际上可以做到这一点。

在本书中,我将会展示一些容错技术,并且能够阻止故障的影响传播到系统的其他组成部分,导致故障传播。例如,我们将会使用断路器、重试、超时和限流器。

1.3.4 可观测性

可观测性(observability)是来自控制理论领域的一个属性。在考虑一个系统的时候,可观测性指的是我们能够在多大程度上根据它的外部输出推断其内部状态。在软件工程方面,系统可能是单个应用,也可能是作为一个整体的分布式系统。外部输出可以是度量指标、日志或跟踪信息。图1.6展示了可观测性是如何运行的。

图1.6 可观测性指的是从应用的外部输出推断其内部状态。可管理性指的是通过外部输入改变其内部状态和输出。在这两种情况下,应用制品没有发生任何变化,它是不可变的

Twitter的可观测性工程团队识别了可观测性的四大支柱

■ 监控:监控指的是测量应用的特定方面,以获取其整体的健康状况并识别故障。在本书中,我们将会利用Spring Boot Actuator提供的监控特性,并将Prometheus与Spring集成,以导出应用的相关度量指标。

■ 告警/可视化:只有将收集到的系统状态数据用于采取某些行动时,我们才能说这些数据是真正有用的。在监控应用的时候,如果识别出了故障,应该触发告警并采取一些操作来处理它。我们会使用特定的仪表盘将收集到的数据进行可视化,并将其绘制在相关的图表中,目标是在一个良好的画面中展示系统的行为。在本书中,我们将会学习如何使用Grafana来可视化从云原生应用中收集到的数据。

■ 分布式系统的跟踪基础设施:在分布式系统中,仅仅跟踪每个子系统的行为是不够的,重要的是要跟踪流经不同子系统的数据。在本书中,我们将会集成Spring和OpenTelemetry,并使用Grafana Tempo来收集和可视化跟踪信息。

■ 日志聚合/分析:对于推断软件的行为以及在出现问题时对其进行调试来说,跟踪应用的主要事件是非常重要的。在云原生系统中,日志应该进行聚合和收集,以便更好地了解系统行为,并且这样才有可能运行分析工具以挖掘这些数据中的信息。在本书中,我将会详细讨论日志。我们将使用Fluent Bit、Loki和Grafana来收集和可视化日志,并学习在云原生场景下使用日志的最佳实践。

1.3.5 可管理性

在控制理论中,与可观测性对应的概念是可控制性,它表示在有限的时间间隔内,外部输入改变系统的状态或输出的能力。这个概念将我们带到了云原生的最后一个主要属性,也就是可管理性。

再次借鉴控制理论,我们可以说可管理性是衡量外部输入改变系统状态和输出的便利程度和效率。用更少的数学术语来讲,它是在不改变代码的情况下修改应用行为的能力。不要将它与“可维护性”混淆,可维护性衡量了从内部通过修改代码来改变系统的便利程度和效率。图1.6展示了可管理性是如何运作的。

可管理性所涉及的一个方面就是在部署和更新应用的时候,要保持整个系统的正常运行。可管理性的另一个元素就是配置,我将在本书中深入探讨这个问题。我们希望让云原生应用具有可配置性,这样就可以在不改变代码和构建新发布版本的情况下改变它们的行为。将数据源URL、服务凭证和证书等设置信息变成可配置的是很常见的。例如,根据不同的环境,我们可以使用不同的数据源,分别用于开发、测试和生产环境。其他类型的配置还包括特性标记,它们用来决定是否在运行时启用特定的特性。在本书中,我将会展示配置应用的不同策略,包括使用Spring Cloud Config Server、Kubernetes ConfigMaps与Secret,以及Kustomize。

可管理性不仅涉及变更本身,还包括如何便利和高效地应用这些变更。云原生系统是非常复杂的,所以必须要设计出能够适应功能、环境和安全性变化的应用。鉴于其复杂性,我们要尽可能地通过自动化来进行管理,这样我们就来到了上文所述“云原生3P”的最后一项,也就是实践。

bt2-L 1.4 支撑云原生的文化与实践

在本节中,我们将讨论CNCF所提供的定义中的最后一句话:“结合可靠的自动化手段,云原生技术使工程师能够轻松地对系统做出频繁和可预测的重大变更。”在这里,我将讨论三个理念,即自动化、持续交付和DevOps(如图1.7所示)。

图1.7 云原生开发的文化和实践

1.4.1 自动化

自动化是云原生的一个核心原则。它的理念是将重复性的人工任务进行自动化,以加快云原生应用的交付和部署。很多任务都可以实现自动化,从应用的构建到部署,从基础设施的供应到配置管理均是如此。自动化最重要的优势在于,它能够将流程和任务变成可重复的,这样系统整体会更加稳定可靠。手动执行任务容易出错,并且会增加成本。通过将任务自动化,我们可以得到更加稳定和高效的结果。

在云计算模型中,计算资源会以自动化、自服务的模式供应,并且能够弹性增加或减少资源。云计算自动化的两个重要方面就是基础设施供应和配置管理,我们分别将其称为基础设施即代码(infrastructure as code)和配置即代码(configuration as code)。

Martin Fowler将基础设施即代码定义为“通过源代码的方式来定义计算和网络基础设施,就像任何其他软件系统代码一样”

云供应商提供了便利的API来创建和供应服务器、网络和存储。通过使用像Terraform这样的工具将这些任务进行自动化,将代码进行源码控制并采用与应用开发相同的测试和交付实践,我们可以得到一个更可靠和更可预测的基础设施,它是可重复、更高效并且风险更低的。比如,一个自动化此类任务的样例可能是创建一个新的虚拟机,它具有8个CPU、64GB的内存,并且安装了Ubuntu 22.04操作系统。

在供应完计算资源之后,我们就可以管理它们并对它们的配置实现自动化。套用前面的定义,配置即代码指的是通过源代码的方式来定义计算资源的配置,就像任何其他软件系统代码一样。

通过使用像Ansible这样的工具,我们能够声明服务器或网络该如何进行配置。例如,按照上文所述提供了Ubuntu服务器之后,我们可以自动完成安装Java Runtime Environment(JRE)17,并在防火墙上打开8080和8443端口的任务。配置即代码的理念也适用于应用的配置。

通过将基础设施供应和配置管理相关的所有任务自动化,我们可以避免不稳定、不可靠的“雪花服务器”(snowflake server)。当每台服务器都是以手动的方式进行供应、管理和配置的时候,其结果就是服务器像雪花一样,即该服务器是脆弱且独一无二的,无法复制,并且变更起来也有风险。自动化能够避免出现雪花服务器,并形成“凤凰服务器”(phoenix server),即作用于这些服务器的所有任务都是自动化的,每个变更都可以在源码中进行跟踪,降低了风险,并且每个设置过程都是可重复的。如果将这种理念发挥到极致,我们就会实现所谓的不可变服务器(immutable server),CNCF在其云原生定义中也提到了不可变基础设施。

注意 在对比传统的雪花基础设施(需要很多的关注和照料,就像宠物一样)和不可变基础设施或容器(其特点是可拆卸和可替换,就像牲畜一样)时,你可能听过“宠物与牲畜”这种表述方式。在本书中,我不会使用这种表述,但是在关于该话题的讨论中,有人可能会用到这种方式,所以你需要注意一下。

在最初的供应和配置完成之后,不可变服务器不允许再进行任何变更,也就是说它们是不可变的。如果有必要进行改变的话,会以代码的方式进行定义和交付。最终,会根据新的代码供应和配置新的服务器,而之前的服务器则会被销毁。

例如,如果现在的基础设施包含Ubuntu 20.04服务器,你想要将其升级到22.04,那么你有两个方案。第一种方案是通过代码定义升级,并在现有的机器(凤凰服务器)上运行自动化脚本以执行操作。第二种方案是自动供应带有Ubuntu 22.04的新机器,并开始使用它们(不可变服务器),而不是在现有的机器上进行升级。

在下一节中,我们将会讨论构建和部署应用的自动化问题。

1.4.2 持续交付

持续交付(Continuous Delivery,CD)是“软件开发的一种理念,按照这种方式构建的软件能够在任意时间发布到生产环境”。 借助持续交付,团队能够在短周期内实现特性,确保软件在任意时间都能可靠地发布。它是确保“轻松地对系统做出频繁和可预测的重大变更”(来自CNCF的云原生定义)的关键。

持续集成(Continuous Integration,CI)是持续交付中的一个基础实践。开发人员会持续(至少每天一次)将代码提交至主线(main分支)。在每次提交时,软件会自动编译、测试和打包成可执行制品(比如JAR文件或容器镜像)。它的理念是在每次新的变更后,得到软件状态的快速反馈。如果探测到错误的话,应该立即修正,确保主线是一个稳定的基础,以便于后续开发。

持续交付构建在CI之上,它的关注点在于,确保主线始终是健康的,处于可发布状态。在与主线集成所形成的可执行制品生成之后,软件会部署到一个类生产环境中。它会经历额外的测试以验证可发布性,比如用户验收测试、性能测试、安全性测试、合规测试,以及有助于增加软件可发布信心的其他测试。如果主线始终处于可发布状态,那么发布软件的新版本将会是一个业务决策,而不是技术决策。

正如Jez Humbleh和David Farley在合著的基础性图书 Continuous Delivery (Addison-Wesley Professional,2010)中所述,持续交付鼓励整个过程通过“部署流水线”(也称为持续交付流水线)实现整个过程的自动化。部署流水线会从代码提交开始,一直延续到可发布的输出,它是通向生产环境的唯一方式。在本书中,我们将会构建部署流水线以保证main分支始终处于可发布状态。最后,我们会使用它将应用自动部署到Kubernetes生产环境中。

有时候,人们会将持续交付与持续部署(continuous deployment)相混淆。持续交付会确保在每次变更之后,软件都能处于一种可部署至生产环境的状态。至于何时进行真正的部署,这是一个业务方面的决策。而持续部署则是在部署流水线中添加最后一个步骤,在每次变更发生之后,将新的发布版本自动部署到生产环境中。

持续交付并不是工具,它是涉及组织中文化和结构变化的理念。搭建自动化的流水线来测试和交付应用并不意味着你在践行持续交付。与之类似,使用CI服务器来自动化构建并不意味着你在践行持续集成。 这就把我们引入到了下一个话题,它也通常被认为仅仅是一些工具而已。

持续交付与CI/CD

由于持续集成是持续交付的基础实践,该组合通常被称为CI/CD。因此,部署流水线经常被称为CI/CD流水线。我对这个术语有一些保留意见,因为持续集成并不是持续交付的唯一实践。例如,测试驱动开发(TDD)、自动化配置管理、验收测试和持续学习同样重要。

Jez Humble和Dave Farley在他们合著的 Continuous Delivery 一书中没有使用CI/CD这个术语,在他们写的关于这个主题的任何其他书中也没有使用。此外,它还会造成困惑。CD是代表持续交付还是持续部署呢?在本书中,我将把“更快交付更好的软件” [1] 这一整体方法称为持续交付,而不是CI/CD。

1.4.3 DevOps

DevOps是最近另外一个非常流行的热词,但有时候它被人误解了。当转向云原生时,DevOps是一个需要掌握的重要概念。

DevOps的起源是非常奇特的。非常有意思的一点是,这个概念的创始人拒绝为其提供一个定义。这带来的结果就是很多人都使用它们自己的解释,当然,最终我们都在使用DevOps这个词来表示不同的东西。

注意 如果你有兴趣了解关于DevOps起源的更多信息,建议你观看Ken Mugrage的演讲,题目为“DevOps and DevOpsDays—Where it started, where it is, where it’s going”(http://mng.bz/Ooln)。

在所有关于DevOps的定义中,我发现Ken Mugrage(ThoughtWorks的首席技术专家)提出的定义特别有参考价值和有趣,他强调了DevOps的真正含义

DevOps是一种文化,在这种文化中人们不分头衔或背景,共同想象、开发、部署和运维一个系统。

所以,DevOps是一种文化,它指的是为了一个共同的目标而协作。开发人员、测试人员、运维人员、安全专家以及其他人,无论头衔或背景,团结协作,将理念带入生产中并产生价值。

这意味着孤岛(silo)时代的结束,特性团队、QA团队和运维团队之间不再有壁垒。DevOps通常被认为是敏捷(agile)的自然延续,敏捷是DevOps的助推器,其概念是以小团队的形式频繁地向客户提供价值。简洁描述DevOps可引用亚马逊的CTO Werner Vogels在2006年发布的一句名言,当时DevOps这个词尚不存在:“如果你负责构建它的话,那就要负责运行它。”

定义了DevOps是什么之后,我们再简单看一下它不是什么。

■ DevOps并不意味着没有运维(NoOps)。一个常见的错误就是认为开发人员负责运维,运维人员的角色就消失了。但实际上,现在是一种合作的形式。团队将包含这两种角色,他们都会向团队贡献技能,从而将产品从最初的想法带到生产环境中。

■ DevOps并不是一种工具。像Docker、Ansible、Prometheus这样的工具通常被称为DevOps工具,但这是错误的。DevOps是一种文化。我们无法仅通过工具就将一个组织变成DevOps组织。换句话说,DevOps不是一个产品,但工具是重要的推动者。

■ DevOps不是自动化。即便自动化是DevOps的重要组成部分,但是这并不是DevOps的定义。DevOps指的是开发人员和运维人员协同工作,从最初的理念阶段直至生产环境,在这个过程中,可能会将一些过程自动化,比如持续交付。

■ DevOps不是一个角色。如果我们将DevOps视为一种文化和思维方式,那么DevOps是一个角色的说法就不攻自破了。然而,我们对DevOps工程师的需求却越来越多。通常情况下,当招聘人员寻找DevOps工程师的时候,他们寻找的是熟练掌握自动化工具、脚本和IT系统等技能的人。

■ DevOps不是一个团队。如果组织没有完全理解上文所述内容,他们很可能最终依然像以前那样保持孤岛状态,只会有一个变化:使用名为DevOps的孤岛取代原来的Ops孤岛,或者仅仅增加了一个DevOps孤岛而已。

在迈向云原生时,开发人员和运维人员的合作是最重要的。你可能已经注意到,在设计和构建云原生应用的时候,需要始终记住一点,那就是这些应用的部署地点是在云中。与运维人员协同工作,能够让开发人员设计和构建更高质量的产品。

虽然它被称为DevOps,但是我们要注意,这个定义不只涉及开发人员和运维人员。相反,它涉及所有人,无论他们的头衔或背景是什么。这意味着协作也会涉及其他角色,比如测试人员和安全专家(不过,我们并不需要DevSecOps、DevTestOps、DevSecTestOps或DevBizSecTestOps这样的新术语)。他们一起对整个产品的生命周期负责,是实现持续交付目标的关键。

bt2-L 1.5 云是最佳方案吗

在我们的行业中,有一个最大的错误就是决定采用某项技术或方式仅仅因为它是新出现的,而且所有人都在谈论它。公司从单体迁移至微服务,最终以惨烈失败而告终的故事层出不穷。我已经阐述了云和云原生应用的属性。它们应该能够为你提供一些指导。如果你的系统不需要这些属性,因为你的系统根本不存在这些技术所试图解决的问题,那么对你的项目来说,“迈向云原生”可能并不是最佳选择。

作为技术人员,我们很容易被最新、最流行、最闪亮的技术所吸引。这里的关键在于,要弄清楚某项特定的技术或方式是否能够解决你的问题。我们将想法变成软件,然后将其交付给客户并为其提供价值,这才是我们的终极目标。如果某项技术或方式能够帮助我们为客户提供更多的价值,那么我们就应该考虑采用它。如果根本不值得这样做,你却一意孤行要采取这种方式的话,很可能最终会面临更高的成本和众多的问题。

迁移至云原生的最佳时机是什么时候呢?为什么公司要采用云原生方式?采用云原生的主要目标如图1.8所示,也就是速度、扩展、韧性和节省成本。如果你的业务愿景包含这些目标,并且要面对云技术所试图解决的问题,那么考虑迁移至云并采用云原生方式是很不错的。否则,请保持原样,在本地运行会更好一些。例如,如果你的公司通过一个单体应用来提供服务,而且该应用已经处于维护阶段,不会进一步扩展新的功能,在过去的几十年间该应用运行良好,那么就没有必要将其迁移到云中,更不用说将其变成云原生应用了。

图1.8 迈向云原生能够帮助我们实现速度、韧性、扩展和节省成本相关的目标

1.5.1 速度

对于当今企业来讲,能够更快地交付软件是一个重要的目标。尽可能快地将理念投入生产,从而缩短产品的上市时间是一个关键的竞争优势。能否在正确的时间将正确的理念投入生产,可能就决定了企业的成败。

客户希望得到越来越多的特性实现或缺陷修复,而且他们希望立即就要,根本不愿意等待六个月之后才能看到我们软件的下一个版本。他们的期望在不断地提高,我们需要有一种方法来跟上他们的节奏。归根到底,这一切都是为了给客户提供价值,并确保他们对结果感到满意。否则,我们的企业难以在激烈的竞争中生存下来。

更快、更频繁地交付不仅关乎竞争和客户的最后期限,它还能够缩短反馈周期。频繁和小规模的发布意味着我们能够更快地获取客户的反馈。更短的反馈周期反过来又会减少新特性相关的风险。与其花费几个月的时间实现完美的特性,还不如快速推出,从客户那里得到反馈,并对其进行调整以符合他们的期望。同时,较小的版本会包含更少的变更,因此发生故障的组件数量也会随之减少。

我们还需要灵活性,因为客户期望我们的软件能够不断演化。例如,它应该有足够强的灵活性以支持新类型的客户端。如今,日常生活中越来越多的物品都已经能够连接至互联网,比如各种移动和物联网(IoT)系统。我们希望能够对未来任何类型的扩展和客户端类型保持开放,从而能够以新的方式提供业务服务。

传统的软件开发方式并不支持这一目标。传统方式的典型特点是大规模的发布、微乎其微的灵活性以及漫长的发布周期。云原生方式与自动化任务、持续交付工作流和DevOps实践相结合,有助于实现业务更快速地发展,并缩短上市时间。

1.5.2 韧性

万事万物都在不断地发生着变化,故障也是一直存在的。试图预测故障并将其视为异常情况的时代已经成为历史。正如我在前文所述,变更并不是异常情况,它们是常态。

客户希望软件是7×24小时可用的,而且一旦有新特性,就能立即升级。停机或故障会导致金钱方面的直接损失以及客户满意度的下降,这甚至可能会影响到声誉,导致组织在未来的市场机会方面蒙受损失。

无论基础设施还是软件出现了故障,我们的目标都是确保系统的可用性和稳定性。哪怕是在降级的运维模式下,我们也希望能够继续为用户提供服务。为了保证可用性,我们需要采取一些措施来应对故障的发生,以对故障进行处理,从而确保整个系统依然能够为用户提供服务。在处理故障和执行升级这样的任务时,所有的操作都应该在零停机的情况下完成。客户的期望就是这样的。

我们希望云原生应用是具有韧性的,同时云技术提供了实现韧性基础设施的策略。如果你的业务需求包括始终可用、安全和韧性,那么云原生方式就非常适合你。软件系统的韧性反过来又能推进它的交付速度:系统越稳定,就能越频繁地安全发布新特性。

1.5.3 扩展

弹性指的是能够根据负载情况对软件进行扩展。我们可以扩展一个弹性系统,确保为所有的客户提供足够的服务水平。如果系统的负载比往常高,那么我们需要生成更多的服务实例来支持额外的流量。或者发生一些严重的事情,有些服务出现了故障,这样我们就需要生成新的实例来替换它们。

问题在于,预见会出现什么样的状况是很困难的,甚至是不可能实现的。仅仅构建可扩展的应用还不够,我们还需要它们能够动态扩展。每当出现高负载的时候,系统能够动态、快速且毫不费力地进行扩展。当高峰期结束的时候,它应该能够再次收缩回来。

如果你的业务需要快速、有效地适应新的客户,或者需要灵活支持新类型的客户端(这会增加服务器的工作负载),那么云的本质特点再结合云原生应用(按照定义,它就是可扩展的)能够为你提供所需的所有弹性。

1.5.4 节省成本

作为软件开发人员,我们可能不会直接和钱打交道,但是在设计解决方案的时候,我们有责任将成本考虑在内。凭借其弹性和按需付费的策略,云计算模型有助于优化IT基础设施的成本。我们不再有永远在线的基础设施:在需要的时候,我们会供应资源,并为实际使用付费;当不再需要的时候,我们就将其销毁。

在此基础之上,采用云原生方式会进一步优化成本。云原生应用被设计为可扩展的,所以它们可以充分利用云的弹性。它们是有韧性的,所以与停机时间和生产环境故障相关的成本都会更低。鉴于应用是松耦合的,它们能够让团队行动更快速,加快上市时间,从而具备明显的竞争优势。这样的例子不胜枚举。

迁移到云的隐性成本

在决定迁移到云之前,我们还必须考虑其他类型的成本。一方面,如上所述,我们可以优化成本,只为使用的资源付费。但另一方面,我们还应该考虑迁移的成本及其影响。

迁移到云需要特定的技术能力,而员工很可能还不具备这样的能力。这就意味着要对他们进行教育投资,以获得必要的技能,我们也许还需要聘请专业人员作为顾问,帮助我们往云中进行迁移。根据所选择的解决方案,组织可能还需要担负一些额外的责任,这又需要特定的技能(例如,处理云安全方面的问题)。除此之外,还有其他的一些考虑因素,比如迁移期间的业务中断、重新培训终端用户、更新文档和支持材料等。

bt2-L 1.6 云原生拓扑结构

我对云原生的阐述并不涉及特定的技术或架构。CNCF在其定义中提到了一些技术,比如容器和微服务,但是它们只是示例。要将应用变成云原生的,并不一定要使用Docker容器。比如,我们想一下Serverless或PaaS方案。为AWS Lambda平台编写的函数或部署到Heroku中的应用并不需要我们构建容器。但是,它们依然是云原生应用。

在本节中,我将会描述一些通用的云原生拓扑结构(参见图1.9)。首先,我将会介绍容器和编排的概念,当我们在后文讨论Docker和Kubernetes的时候,还会对它们进行详细介绍。随后,我将会介绍Serverless和函数(FaaS)技术。在本书中,我不会过多关注FaaS模型,但是会介绍如何使用Spring Native和Spring Cloud Function构建Serverless应用的基础知识。

图1.9 主要的云原生应用都基于容器(由编排器进行管理)和Serverless

1.6.1 容器

假设你加入了某个团队,并且要参与一个应用相关的工作。你所做的第一件事情就是按照指南搭建与其他同事类似的本地开发环境。你开发完了一个新的特性,并且在质量保证(Quality Assurance,QA)环境进行了测试。验证完成之后,应用就可以部署到staging环境进行额外的测试,并最终部署到生产环境。应用要在具有一定特征的环境中运行,我们在构建时也会考虑到这一点,所以上述所有环境尽可能相似是至关重要的。那么,如何实现这一点呢?这就是容器的用武之地了。

在容器出现之前,我们需要依赖虚拟机来保证环境的可重复性、隔离性和可配置性。虚拟化的原理是使用一个Hypervisor组件来抽象硬件,从而能够在同一台机器上以隔离的方式运行多个操作系统。Hypervisor直接运行在机器硬件(type 1)或宿主机操作系统(type 2)之上。

而操作系统容器是一个轻量级的可执行包,容器中包含了应用以及运行该应用所需的所有内容。容器间共享同一个内核,因此要添加新的隔离上下文时,没有必要启动完整的操作系统。在Linux上,这是通过Linux内核所提供的一些特性来实现的:

■ Namespace用来在进程之间划分资源,所以每个进程(或进程组)只能看到机器上可用资源的一个子集。

■ Cgroups用来控制和限制进程(或进程组)的资源使用。

注意 当仅使用虚拟化的时候,硬件是共享的,但是容器还会共享相同的操作系统内核。这两种情况都提供了隔离运行软件的计算环境,尽管隔离的程度有所不同。

图1.10展示了虚拟化和容器技术的差异。

图1.10 虚拟化和容器技术的差异在于隔离上下文之间所共享的内容。虚拟机只会共享硬件,
容器还会共享操作系统内核,后者更加轻量级和可移植

对于云原生应用,容器为什么这么流行呢?按照传统的方式,要使应用运行起来,我们需要在虚拟机上安装和维护Java运行时环境(JRE)以及中间件。相反,容器几乎可以在任何计算环境中可靠地运行,独立于应用及其依赖或中间件。应用是什么类型、使用哪种语言编写、使用了哪些库,都无关紧要。从外边来看,所有容器的外形都一样,就如同货运中的集装箱。

因此,容器实现了敏捷性、跨不同环境的可移植性以及部署的可重复性。鉴于其轻量级和较低的资源需求,它们非常适合在云中运行,因为云中的应用是用完即废弃的,需要能够动态和快速扩展。相比之下,建立和销毁虚拟机的成本要高得多,并且会更加耗时。

容器!无处不在的容器!

“容器”这个词在不同的语境中有不同的含义。有时候,这种模糊性可能会产生一些混乱,所以我们看一下在不同语境中它分别是什么意思。

■ 操作系统:操作系统容器是一种在与系统其他部分隔离的环境中运行一个或多个进程的方法。在本书中,我们会主要关注Linux容器,但是需要注意Windows容器也是存在的。

■ Docker:Docker容器是Linux容器的一个实现。

■ OCI:OCI容器是由开放容器计划(Open Container Initiative,OCI)实现的Docker容器标准。

■ Spring:Spring容器指的是应用上下文,对象、属性和其他应用资源都会在这里被管理和执行。

■ Servlet:Servlet容器为使用Java Servlet API的Web应用提供了一个运行时。Tomcat服务器的Catalina组件就是Servlet容器的一个示例。

虚拟化和容器并不是互斥的。实际上,我们会在云原生环境中同时使用它们,也就是在虚拟机组成的基础设施上运行容器。IaaS(基础设施即服务)模型提供了一个虚拟层,我们可以使用它来引导新的虚拟机。在此基础上,我们可以直接安装容器运行时和运行容器。

一个应用通常是由不同的容器组成的,在开发阶段或执行一些早期的测试时,它们可以在同一台机器上运行。但是,我们很快就会遇到管理许多容器变得越来越复杂的问题,当我们需要复制它们以实现可扩展性以及跨不同的机器进行分布式部署时,这种问题就变得尤为突出。这时,我们就会开始依赖CaaS(容器即服务)模型所提供的更高层次的抽象,该模型提供了在机器集群中部署和管理容器的功能。需要注意的是,在幕后,它依然有一个虚拟化层。

即便在使用Heroku或Cloud Foundry这样的PaaS平台时,也会涉及容器。我们在这些平台上部署应用时,只需提供JAR制品,因为它们会负责处理JRE、中间件、操作系统和所需的依赖。不过,在幕后,它们会基于这些组件建立一个容器,并最终运行它。所以,区别在于,不再是我们负责建立容器,而是平台本身为我们实现这一点。一方面,这对开发人员来说是很方便的,可以减少相关的职责。但另一方面,我们放弃了对运行时和中间件的控制,并可能面临供应商锁定的问题。

在这本书中,我们将学习如何使用Cloud Native Buildpacks(它是一个CNCF项目)实现Spring应用的容器化,并使用Docker在本地环境中运行这些应用。

1.6.2 编排

你应该已经决定采用容器技术了,那就太好了!我们可以利用它们的可移植性,将其部署到任意提供容器运行时的基础设施中。我们还可以实现可重复性,所以把容器从开发环境转移至staging环境,再到生产环境时,我们不会遇到糟糕的意外情况。我们还可以对其进行快速扩展,因为它们是轻量级的,从而获取应用的高可用性。你是不是已经准备好在下一个云原生系统中就采用这项技术了呢?

在单台机器上供应和管理容器是非常简单的。但是,当我们开始处理几十或几百个容器,并在多台机器上进行扩展和部署时,我们就需要其他技术的辅助了。

当从虚拟服务器(IaaS模型)转换到容器集群(CaaS模型)时,我们也在转换自己的视角 。在IaaS中,我们关注单个计算节点,也就是虚拟机。在CaaS中,底层的基础设施已经被抽象了,我们所关注的是节点的集群。

伴随CaaS方案所提供的新视角,部署的目标不再是一台机器,而是一个机器集群。像Kubernetes这样的CaaS平台提供了很多特性来解决我们在云原生环境中所面临的所有重大问题,也就是跨集群编排容器。图1.11展示了这两种不同的拓扑结构。

图1.11 容器的部署目标是单台机器,而编排器的部署目标是一个集群

容器编排能够帮助我们实现很多任务的自动化:

■ 管理集群,在必要的时候启动和关闭机器。

■ 在集群中,将容器调度和部署到能够满足其CPU和内存需求的机器上。

■ 利用健康监控,动态扩展容器,以实现高可用性和韧性。

■ 为容器之间的通信搭建网络,定义路由、服务发现和负载均衡。

■ 将服务暴露到互联网中,建立端口和网络。

■ 根据特定的标准,为容器分配资源。

■ 配置在容器中运行的应用。

■ 确保安全,执行访问控制策略。

编排工具的指令是以声明式的方式实现的,例如,借助YAML文件。借助特定工具定义的格式和语言,我们通常会描述出想要达成的状态,比如我们想要在集群中部署Web应用容器的三个副本,并将它的服务暴露到互联网上。

容器编排的样例包括Kubernetes(它是一个CNCF项目)、Docker Swarm和Apache Mesos。在本书中,我们将学习如何使用Kubernetes来编排Spring应用的容器。

1.6.3 Serverless

继从虚拟机发展到容器之后,我们可以进一步抽象基础设施,这就是Serverless技术。借助该计算模型,开发人员只须关注应用的业务逻辑实现即可。

Serverless这个名字可能会有一定误导性:当然,服务器是存在的。不同的是,既不是我们在管理它,也不是由我们将应用部署编排到对应的服务器上。现在,这变成了平台的责任。当使用像Kubernetes这样的编排器时,我们依然需要考虑基础设施供应、容量规划和扩展的问题。而Serverless平台会负责搭建应用所需的底层基础设施,包括虚拟机、容器和动态扩展。

Serverless架构通常会与函数关联,但是它们包含两种经常一起使用的主要模型。

■ 后端即服务(Backend as a Service,BaaS):在这种模型中,应用严重依赖于云供应商提供的第三方服务,比如数据库、认证服务和消息队列。它的关注点在于减少后端服务相关的开发和运维成本。开发人员可以只实现前端应用(比如单页应用或移动应用),而将大部分甚至全部的后端功能转移至BaaS供应商。例如,可以使用Okta来认证用户,使用Google Firebase来持久化数据,并使用Amazon API Gateway来发布和管理REST API。

■ 函数即服务(Function as a Service,FaaS):在这种模型中,应用是无状态的,由事件触发并且完全由平台来管理。它的关注点在于减少编排和扩展应用相关的部署和运维成本。开发人员只须实现应用的业务逻辑,平台负责处理其他内容。Serverless应用并非必须要以函数的方式来实现并归类。目前主要有两种FaaS方案。第一种是特定供应商的FaaS平台,比如AWS Lambda、Azure Functions或Google Cloud Functions。另一种方案是选择基于开源项目的Serverless平台,它们可以运行在公有云或内建基础设施上,从而解决供应商锁定和缺乏控制的问题。这种项目的样例是Knative和Apache Open- Whisk。Knative在Kubernetes之上提供了一个Serverless运行时环境,我们会在第16章对其进行介绍。它被用来作为一些企业级Serverless平台的基础,包括VMware Tanzu Application Platform、RedHat OpenShift Serverless和Google Cloud Run。

Serverless应用一般是事件驱动的,仅在有事件(比如HTTP请求或消息)需要处理的时候才会运行。事件可以是外部的,也可以是由另一个函数生成的。例如,当一个消息添加到队列中时,某个函数可能会被触发,该函数处理事件,然后退出执行。

当没有任何要处理的事件时,Serverless平台就会关闭所有与该函数相关的资源,因此,我们可以真正为实际使用付费。在其他云原生拓扑结构中,如CaaS或PaaS,总会有一台服务器在7×24小时运行。与传统系统相比,它们提供了动态可扩展性的优势,以减少任意时刻资源供应的数量。不过,总会有一些资源在始终运行,这也是有成本的。而在Serverless模型中,只有在必要时才会供应资源。如果没有要处理的事件,所有的资源都会被关闭。这就是我们所说的伸缩至零(scaling to zero),这是Serverless平台提供的主要特性之一。

除了成本优化,Serverless技术还将一些额外的职责从应用转移到了平台中。这可能是一个优势,因为它能让开发人员完全专注于业务逻辑。但同样重要的是,我们必须要考虑控制权,以及如何处理供应商锁定的问题。

每个FaaS以及通用的Serverless平台都有自己的特性和API。一旦我们开始为某个特定的平台编写函数,就无法轻易地将它们转移到另一个平台上了,而对于容器,我们是能够实现这一点的。与其他方式相比,FaaS是以在控制权和可移植性方面的妥协换取职责方面的收益。这也是Knative得以迅速流行起来的原因,它构建在Kubernetes之上,这意味着我们可以很容易地在平台和供应商之间转移Serverless工作负载。归根到底,这就是一种权衡。

bt2-L 1.7 云原生应用的架构

在前面我们介绍了云原生的主要特点,这都是我们学习本书后面内容所需要用到的,现在我们到了定义云原生旅程的最后一站。在上一节中,我们熟悉了云原生的主要拓扑结构,尤其是容器,它将会是我们的计算单元。现在,我们看一下容器里面是什么,并探讨一些关于架构和设计云原生应用的高层次原则。图1.12展示了本节将要涵盖的主要概念。

图1.12 云原生架构元素

1.7.1 从多层架构到微服务和其他架构

IT基础设施一直在影响着软件应用的架构和设计方式。最初,我们曾将单体应用以单一组件的形式部署在庞大的大型机上。当互联网和PC流行起来之后,我们开始按照客户端/服务器的范式设计应用。依赖该范式的多层架构广泛用于桌面和Web应用中,该架构会将代码分解为展现层、业务层和数据层。

随着应用复杂性的增加以及敏捷性的需要,人们在探索进一步分解代码的新方式,一种新的架构风格应运而生,那就是微服务。在过去的几年中,这种架构风格变得越来越流行,许多公司决定按照这种风格重构它们的应用。微服务通常会拿来与单体应用进行对比,如图 1.13所示。

图1.13 单体应用与微服务。单体架构通常是多层的,微服务是由不同的组件组成的,
这些组件可以独立部署

两者之间的主要差异在于应用是如何分解的。单体应用会使用较大的三层,而基于微服务的应用则会使用众多组件,每个组件实现一部分功能。目前有很多关于如何将单体分解为微服务,并处理众多组件所带来的复杂性的模式。

注意 本书不是关于微服务的,因此,我不会讨论细节。如果你对这个话题感兴趣的话,可以参阅Sam Newman的 Building Microservices (第2版)(O’Reilly, 2021年)以及Chris Richardson的 Microservice Patterns (Manning, 2018年)。在Spring方面,可以参阅John Carnell和Illary Huaylupo Sanchez合著的 Spring Microservices in Action (Manning, 2021年)。如果你不熟悉微服务的话也不要担心,要学习后面的内容,这些知识并不是必备的。

在经历了多年的热度和诸多失败的迁移之后,开发者社区围绕这种流行的架构风格展开了激烈的讨论。有些开发人员建议转向宏服务(macroservice),以减少组件的数量,从而降低管理的复杂性。“宏服务”这个术语是由Cindy Sridharan提出的,最初带有讽刺意味,但是它已经被行业所采用,Dropbox和Airbnb这样的公司已使用它来描述新的架构。 有些人则提议采用城堡式(citadel)架构风格,该风格由一个被微服务包围的中心化单体组成。还有人主张以模块化单体的形式重回单体应用架构。

归根到底,我认为最重要的是选择一个能够支撑我们为客户和业务提供价值的架构。这是我们最初要开发应用的原因所在。每种架构风格都有其使用场景。没有所谓的“银弹”或者放之四海而皆准的方案。大多数与微服务相关的负面体验都是由其他问题导致的,比如糟糕的代码模块化或不匹配的组织结构。这不应该是单体和微服务之间的一场论战。

在本书中,我主要关注如何使用Spring构建云原生应用并将其以容器的形式部署到Kubernetes中。云原生应用是分布式系统,就像微服务一样。你会发现一些在微服务语境下讨论的话题,但它们实际上属于分布式系统领域,比如路由和服务发现。根据定义,云原生应用是松耦合的,这也是微服务的一个特点。

即便有一些相似之处,但很重要的一点是,我们需要明白云原生应用与微服务是不一样的。我们当然可以为云原生应用采用微服务风格。实际上,很多开发人员就是这么做的。但是,这并非必备条件。在本书中,我将会采用称为“基于服务”的架构风格。可能这并不是一个很响亮的名字,也不花哨,但对于我们的目的来讲,这已经足够了。我们会处理服务。它们的大小是随意的,可以根据不同的原则来封装逻辑。这并不重要,我们想要追求的是根据开发、组织和业务需求来设计服务。

1.7.2 基于服务架构的云原生应用

在本书中,我们将会按照基于服务的架构来设计和构建云原生应用。

我们的主要工作单元是服务,它能够以不同的方式与其他服务交互。按照Cornelia Davis在 Cloud Native Patterns (Manning, 2019年)一书中所提出的划分方式,我们可以在这种架构中识别出两个元素,分别是服务和交互。

■ 服务:一个能够向其他组件提供任意类型的服务的组件。

■ 交互:为完成系统的需求,服务之间所产生的通信。

服务是一个非常通用的组件,它们可以是任何东西。根据是否存储任意类型的状态,我们可以将其区分为应用服务(无状态)和数据服务(有状态)。

图1.14展示了云原生架构的元素。用来管理图书馆库存的应用应该是应用服务。存储图书信息的PostgreSQL数据库应该是数据服务。

1.应用服务

应用服务是无状态的,负责实现各种类型的逻辑。它们不必像微服务那样遵循特定的规则,只需要具备我们在前文所述的所有云原生属性即可。

最重要的是,在设计每个服务的时候,我们要考虑到松耦合和高内聚。服务应该尽可能独立。分布式系统是很复杂的,所以在设计阶段要格外小心。增加服务的数量也会导致问题数量的增加。

图1.14 基于服务架构的云原生应用。主要的元素是服务(应用或数据),它们会以不同的方式进行交互

我们可能会开发和维护系统中大多数的应用服务,但也可能会使用云供应商提供的一些服务,如认证和支付服务。

2.数据服务

数据服务是有状态的,负责存储各种类型的状态。状态指的是关闭服务和启动新实例时,应该保存下来的所有内容。

它们可以是像PostgreSQL这样的关系型数据库,像Redis这样的键/值存储,或者像RabbitMQ这样的消息代理。我们可以自己管理这些服务。由于保存状态需要存储,所以这比管理云原生应用更具挑战性,但这能够获得对自己数据的更多控制权。另一个可选方案是使用云提供商提供的数据服务,它将负责管理所有与存储、韧性、可扩展性和性能相关的问题。在后一种方案中,我们可以使用很多专门为云构建的数据服务,如Amazon DynamoDB、Azure Cosmos DB和Google BigQuery。

云原生数据服务是一个非常有吸引力的话题,但在本书中,我们将主要处理应用。与数据有关的问题,如集群、复制、一致性或分布式事务,在本书中不会有太多的详细阐述。尽管我很想这样做,但这些话题应该有专门的图书来充分地介绍。

3.交互

云原生服务需要相互沟通以满足系统的需求。如何进行通信将影响系统的整体属性。例如,选择请求/响应模式(同步的 HTTP 调用)而不是基于事件的方法(通过 RabbitMQ 实现消息流)会为应用带来不同的韧性。在本书中,我们会使用不同类型的交互,并学习它们的差异,以及每种方式适合什么样的场景。

bt2-L 1.8 小结

■ 云原生应用是高度分布式系统,专门为云环境设计,而且会在云中运行。

■ 云是一种IT基础设施,以商品的形式提供计算、存储和网络资源。

■ 在云中,用户只须为实际使用的资源付费。

■ 云供应商以不同的抽象层次提供服务:基础设施(IaaS)、容器(CaaS)、平台(PaaS)、函数(FaaS)或软件(SaaS)。

■ 云原生应用具有水平可扩展性、松耦合和高内聚性,并且对故障有韧性、可管理、可观测。

■ 云原生开发得到了自动化、持续交付和DevOps的支持。

■ 持续交付是一种综合的工程实践,可快速、可靠和安全地交付高质量的软件。

■ DevOps是一种文化,能够让不同的角色进行协作,共同交付业务价值。

■ 现代企业采用云原生来生产软件,这些软件可以快速交付,能够根据需要动态扩展,并且在优化成本的同时保证始终可用、对故障有韧性。

■ 可以将容器(比如Docker容器)作为计算单元来设计云原生系统。它们比虚拟机更加轻量级,并提供了可移植性、不变性和灵活性。

■ 有专门的平台(比如Kubernetes)提供管理容器的服务,这样我们就不需要直接处理底层的问题。它们提供容器编排、集群管理、网络服务和调度功能。

■ 在Serverless计算模型中,平台(比如Knative)负责管理服务器和底层基础设施,开发人员只关注业务逻辑。后端功能是按使用量付费的,以实现成本优化。

■ 微服务架构可用于开发云原生应用,但这并不是必需的。

■ 为了设计云原生应用,我们将使用基于服务的风格,其特点是服务以及服务间的交互。

■ 云原生服务可以进一步分类为应用服务(无状态)和数据服务(有状态)。

[1] D.Farley, Continuous Delivery Pipelines ,2021。 O3AtoxB4zcG92pepEjRtwklKaTppuDy5Qg3ypQrnKWMk6lw5a5ZGD5QsSldXdtHE

点击中间区域
呼出菜单
上一章
目录
下一章
×