企业级云原生架构:技术、服务与实践
上QQ阅读APP看书,第一时间看更新

1.2.5 云原生应用十二要素

“十二要素”英文全称为The Twelve-Factor App,其定义了一个优雅的互联网应用,在设计过程中,需要遵循的一些基本原则和云原生有异曲同工之处。十二要素由Heroku创始人Adam Wiggins首次提出并开源,由众多经验丰富的开发者共同完善。这综合了他们关于SaaS应用几乎所有的经验和智慧,是开发此类应用的理想实践标准和方法论指导。

(1)使用标准化流程自动配置,从而使新加入的开发者花费最低的学习成本快速上手这个项目。

(2)和操作系统之间尽可能地划清界限,在各个系统中提供最强的可移植性。

(3)适合部署在现代的云计算平台,从而在服务器和系统管理方面节省资源。

(4)将开发环境和生产环境的差异降至最小,并使用持续交付实施敏捷开发。

(5)可以在工具、架构和开发流程不发生明显变化的前提下实现扩展。

这套理论适用于任意语言和后端服务(数据库、消息队列、缓存等)开发的应用程序。通过强化详细配置和规范,类似“约定优于配置”的原则,特别在大规模的软件生产实践中,这些约定非常重要,从无状态到水平横向扩展的过程,从松耦合架构关系到部署环境。如图1-9所示,基于十二要素的上下文关联,软件生产就变成一个个单一的部署单元;多个联合部署的单元组成一个应用,多个应用就可以组成一个复杂的分布式应用系统。

▲图1-8 某互联网公司内部研发体系与工作流程

▲图1-9 云原生应用十二要素

下面介绍十二要素的具体说明(这部分内容参考十二要素官网),很多开发人员在实际工作过程中可能也是按照其中的一些原则规范进行开发的,因此读者看到这些要素说明会觉得很熟悉和自然,事物原本就是这样的,只不过我们平时没有意识到这些抽象概念而已。

1.基准代码

一份基准代码(codebase),多份部署(deploy)。

在类似于SVN等集中式版本控制系统中,“基准代码”是指控制系统中的代码仓库;而在Git等分布式版本控制系统中,“基准代码”则是指最上游的代码仓库。

基准代码和应用之间总是保持如下一一对应的关系。

(1)一旦有多个基准代码,就不能称为一个应用,而是一个分布式系统。分布式系统中的每一个组件都是一个应用,每一个应用可以分别使用十二要素进行开发。

(2)多个应用共享一份基准代码是有悖于十二要素原则的。解决方案是将共享的代码拆分为独立的类库,然后使用“依赖管理”策略去加载它们。

尽管每个应用只对应一份基准代码,但可以同时存在多份部署。每份“部署”相当于运行了一个应用的实例。通常会有一个生产环境,一个或多个预发布环境。此外,每个开发人员都会在自己本地环境运行一个应用实例,这些都相当于一份部署。

所有部署的基准代码相同,但每份部署可以使用其不同的版本。比如,开发人员可能有一些提交还没有同步至预发布环境,预发布环境也有一些提交没有同步至生产环境。但它们都共享一份基准代码,我们就认为它们只是相同应用的不同部署而已。

部署的环境性质多种多样,如应用环境既有开发环境、测试环境、生产环境等,也有某种环境的多个介质,如容器、虚拟机、物理服务器等。运行环境和配置参数有较大差别,但是程序源码只有一份基准的版本。

在云原生应用架构中,所有的基础设施都是代码配置(Infrastructure as Code),即可编程式基础设施,整个应用通过配置文件就可以编排出来,而不再需要手动干预,做到基础服务也可以通过版本跟踪管理。

2.依赖

显式声明依赖关系(dependency)。

十二要素规则下的应用程序不会隐式依赖系统级的类库,它一定通过“依赖清单”确切地声明所有依赖项。此外,在运行过程中,通过“依赖隔离”工具来确保程序不会调用系统中存在但清单中未声明的依赖项。这一做法会统一应用到生产环境和开发环境中,比如通过合适的工具(Maven、Bundler、NPM),应用可以很清晰地对部署环境公开和隔离依赖,而不是模糊地对部署环境产生依赖。无论用什么工具,依赖声明和依赖隔离都必须一起使用,否则无法满足十二要素原则。

在容器应用中,应用的依赖、环境的依赖和软件的安装等,都是通过Dockerfile来完成声明的,通过配置能明确把依赖关系(包括版本)都图形化地明确展示出来,不存在黑盒。

显式声明依赖的优点之一是为新进开发者简化了环境配置流程。新进开发者可以检出应用程序的基准代码,安装编程语言环境和它对应的依赖管理工具,只需通过一个“构建命令”来安装所有的依赖项,即可开始工作。

十二要素应用同样不会隐式依赖某些系统工具(如ImageMagick或curl)。虽然这些工具存在于几乎所有的系统中,但终究无法保证所有未来的系统都能支持应用顺利运行,或是能够和应用兼容。如果应用必须使用某些系统工具,那么这些工具应该被包含在应用之中。

3.配置

在环境中存储配置。

通常,应用的配置在不同部署环境(预发布环境、生产环境、开发环境等)会有很大差异,这其中包括如下内容。

(1)数据库、Memcached,以及其他后端服务的配置。

(2)第三方服务的证书。

(3)每份部署特有的配置,如域名以及所依赖的外部服务地址等。

有些应用在代码中使用常量保存配置,这与十二要素所要求的代码和配置严格分离显然大相径庭。配置文件在各部署间存在巨大差异,代码却完全一致。

判断一个应用是否正确地将配置排除在代码之外,一个简单的方法是看该应用的基准代码是否可以立刻开源,而不用担心会暴露任何敏感的信息。

十二要素推荐将应用的配置存储于环境变量(env vars、env)中。环境变量可以非常方便地在不同的部署中做修改,而不改变一行代码;与配置文件不同,不小心把它们签入代码仓库的概率微乎其微;与一些传统的解决配置问题的机制(如Java的属性配置文件)相比,环境变量与语言和系统无关。

实例根据不同的环境配置而运行在不同的环境中,此外,实现配置即代码(Configuration as Code)。在云环境中,无论是统一的配置中心,还是分布式的配置中心,都有很好的实践方式,比如Nacos通过命名空间实现不同环境的配置隔离,以及Docker的环境变量使用。

在十二要素应用中,环境变量的粒度要足够小,且相对独立。它们永远不会组合成一个所谓的“环境”,而是独立存在于每个部署之中。当应用程序不断扩展,需要更多种类的部署时,这种配置管理方式能够做到平滑过渡。

4.后端服务

把后端服务(backing service)当作附加资源。

后端服务是指程序运行所需的通过网络调用的各种服务,如数据库、消息队列、简单邮件传输协议(Simple Mail Transfer Protocal,SMTP)邮件服务,以及缓存等。

十二要素应用不会区别对待本地或第三方服务。对应用程序而言,两种都是附加资源,通过一个统一资源定位器(Uniform Resource Locator,URL)或其他存储在配置中的服务定位/服务证书来获取数据。十二要素应用的任意部署都应该可以在不进行任何代码改动的情况下,将本地MySQL数据库换成第三方服务[例如云数据库关系型数据库服务(Relational Database Service,RDS)]。类似地,本地SMTP服务应该也可以和第三方SMTP服务互换。在上述两个例子中,仅需修改配置中的资源地址。

每个不同的后端服务是一份资源。例如,一个MySQL数据库是一个资源,两个MySQL数据库(用来数据分区)就被当作两个不同的资源。十二要素应用将这些数据库都视作附加资源,这些资源和它们附属的部署保持松耦合。

部署可以按需加载或卸载资源。例如,如果应用的数据库服务由于硬件问题出现异常,管理员可以从最近的备份中恢复,即卸载当前的数据库,然后加载新的数据库,整个过程都不需要修改代码。

5.构建、发布、运行

严格分离构建和运行。

将基准代码转化为一份部署(非开发环境)需要经过以下3个阶段。

(1)构建阶段:是指将代码仓库转化为可执行包的过程。构建时会使用指定版本的代码,获取和打包依赖项,编译成二进制文件和资源文件。

(2)发布阶段:会将构建的结果和当前部署所需配置相结合,并能够立刻在运行环境中投入使用。

(3)运行阶段(或者说“运行时”):是指针对选定的发布版本,在执行环境中启动一系列应用程序进程。

十二要素应用严格区分构建、发布、运行3个阶段。举例来说,直接修改处于运行状态的代码是非常不可取的做法,因为这些修改很难再同步回构建阶段。部署工具通常提供了发布管理工具,最引人注目的功能是退回至较旧运行稳定的发布版本。在云原生应用中,基于容器的Build-Ship-Run和这3个阶段完全吻合,也是Docker对本原则的最佳实践。

每一个发布版本必须对应一个唯一的发布ID,例如可以使用发布时的时间戳(2011-04- 06-20:32:17)或是一个自增的数字(v100)。发布的版本就像一本只能追加的账本,一旦发布就不可修改,任何的变动都应该产生一个新的发布版本。

新的代码在部署之前,需要开发人员触发构建操作。但是,运行阶段不一定需要人为触发,而是可以自动进行,如服务器重启,或进程管理器重启一个崩溃的进程。因此,运行阶段应该保持尽可能少的模块,这样假设半夜发生系统故障,即使缺少开发人员,也不会引起太大问题。构建阶段是可以相对复杂一些的,因为错误消息能够立刻显示在开发人员面前,从而得到妥善处理。

6.进程

以一个或多个无状态进程运行应用。

在运行环境中,应用程序通常是以一个和多个进程运行的。十二要素应用的进程必须无状态且无共享。任何需要持久化的数据都要存储在“后端服务”内,如数据库。

所有的应用在设计时就被认为随时随地会失败,面向失败而设计,因此进程可能会被随时拉起或消失,特别是在弹性扩缩容阶段。

内存区域或磁盘空间可以作为进程在做某种事务型操作时的缓存,例如要下载一个很大的文件,对其操作并将结果写入数据库的过程。十二要素应用根本不用考虑这些缓存的内容是否可以保留给之后的请求来使用,这是因为应用启动了多种类型的进程,将来的请求多半会由其他进程来服务。即使在只有一个进程的情形下,先前保存的数据(内存或文件系统中)也会因为重启(如代码部署、配置更改或运行环境将进程调度至另一个物理区域执行)而丢失。

一些互联网系统依赖于“黏性session”(sticky session),这是指将用户 session 中的数据缓存至某进程的内存中,并将同一用户的后续请求路由到同一个进程。黏性session是十二要素极力反对的,session中的数据应该保存在诸如Memcached或Redis等带有过期时间的缓存中。

7.端口绑定

通过端口绑定(port binding)来提供服务。

互联网应用有时会运行于服务器的容器之中。例如PHP经常作为Apache httpd的一个模块来运行,正如Java运行于Tomcat中。

十二要素应用完全自我加载,不依赖任何网络服务器即可创建一个面向网络的服务。互联网应用通过端口绑定来提供服务,并监听发送至该端口的请求。

超文本传输协议(HyperText Transfer Protocol,HTTP)并不是唯一一个可以由端口绑定提供的服务,其实几乎所有服务器软件都可以通过进程绑定端口来等待请求。还要指出的是,端口绑定这种方式也意味着一个应用可以成为另一个应用的后端服务,调用方将服务方提供的相应URL当作资源存入配置,以备将来调用。

在容器应用中,应用统一通过暴露端口来提供服务,尽量避免通过本地文件或进程来通信,每种服务通过服务发现来路由调用。

8.并发

通过进程模型进行扩展,以支持并发。

任何计算机程序,一旦启动,就会生成一个或多个进程。互联网应用采用多种进程运行方式。例如,PHP进程作为Apache的子进程存在,随请求按需启动。Java进程则采取了相反的方式,在程序启动之初Java虚拟机(Java Virtual Machine,JVM)就提供了一个超级进程储备了大量的系统资源(CPU和内存),并通过多线程实现内部的并发管理。在上述两个例子中,进程是开发人员可以操作的最小单位。

在十二要素应用中,进程是“一等公民”。十二要素应用的进程主要借鉴于Unix守护进程模型。开发人员可以运用这个模型去设计应用架构,将不同的工作分配给不同的进程类型。例如,HTTP请求可以交给web进程来处理,而常驻的后台工作则交由worker进程负责。

上述进程模型会在系统急需扩展时大放异彩。十二要素应用的进程所具备的无共享、水平分区的特性意味着添加并发会变得简单而稳妥。这些进程的类型和每个类型中进程的数量被称作进程构成。

在互联网的服务中,业务的爆发随时可能发生,因此不太可能通过硬件扩容来随时提供扩容服务,而需要依赖横向扩展能力进行扩容。

9.易处理

快速启动和优雅终止可使健壮性最大化。

十二要素应用的进程是易处理的(disposable),即它们可以瞬间开启或停止。这有利于快速、弹性地伸缩应用,迅速部署变化的代码或配置,稳健地部署应用。

进程应当追求最短启动时间。在理想状态下,进程从输入命令到真正启动并等待请求应该只需很短的时间。更短的启动时间提供了更敏捷的发布和扩展过程,此外还增加了健壮性,因为进程管理器可以在授权情形下容易地将进程移到新的物理机器上。

一旦接收终止信号(即SIGTERM),进程就会优雅地终止。就网络进程而言,优雅终止是指停止监听服务的端口,即拒绝所有新的请求,并继续执行当前已接收的请求,然后退出。此类型的进程所隐含的要求是HTTP请求大多很短(不会超过几秒),而在长时间轮询中,客户端在丢失连接后应该马上尝试重连。

对worker进程来说,优雅终止是指将当前任务退回队列,有锁机制的系统则需要确定释放了系统资源。此类型的进程所隐含的要求是,任务都应该“可重复执行”,这主要通过将结果包装进事务或使用重复操作幂等来实现。

进程还应当在面对突然死亡时保持健壮,例如底层硬件故障。虽然这种情况比优雅终止发生的概率小得多,但终究有可能发生。十二要素应用应该可以设计能够应对意外的、不优雅的终结。

云最大的优势就是弹性及分布式,根据业务的高低峰值,经常需要能实现快速灵活、弹性的伸缩应用。由于不可控的硬件等因素,应用可能随时会发生故障,因此需要应用在架构设计上尽可能无状态化,应用既能随时随地被拉起,也能随时随地销毁,同时保证进程最短启动时间和架构的可弃性,也可以提供更敏捷的发布及扩展过程。

10.环境等价

尽可能地保持开发环境、预发布环境和线上环境相同。

从以往经验来看,开发环境和线上环境之间存在着很大差异,主要表现在以下3个方面。

(1)时间差异:开发人员正在编写的代码可能需要几天、几周,甚至几个月才会上线。

(2)人员差异:开发人员编写代码,运维人员部署代码。

(3)工具差异:开发环境或许使用Nginx、SQLite、Windows,而线上环境使用Apache、MySQL以及Linux。

十二要素应用想要做到持续部署,就必须缩小本地与线上差异,对应上面所述的3个方面如下。

(1)缩小时间差异:开发人员可以在几小时,甚至几分钟内就部署代码。

(2)缩小人员差异:开发人员不仅要编写代码,还应该密切参与部署过程以及关注代码在线上的表现。

(3)缩小工具差异:尽量保证开发环境与线上环境的一致性。

十二要素云原生应用与传统应用在环境及部署运维方面的差异如表1-1所示。

表1-1 云原生应用和传统应用部署运维的对比分析

后端服务是保持开发与线上等价的重要部分,例如数据库、队列系统,以及缓存。许多语言提供了简化获取后端服务的类库,例如不同类型服务的适配器。开发人员有时会认为在本地环境中使用轻量级的后端服务具有很强的吸引力,而那些重量级的、健壮的后端服务应该在生产环境中使用。

十二要素应用的开发人员应该反对在不同环境间使用不同的后端服务,即使适配器已经几乎可以消除使用上的差异。这是因为,不同的后端服务意味着会突然出现不兼容,从而导致测试、预发布时都表现正常的代码在线上可能会出现问题。这些问题会给持续部署带来阻力。从应用程序的生命周期来看,消除这种阻力需要付出很大的代价。

不同后端服务的适配器仍然是有用的,因为它们可以使移植后端服务变得简单。但应用的所有部署(包括开发、预发布以及线上环境),都应该使用同一个后端服务的相同版本。

在容器化应用中,通过运行Dockerfile文件构建的环境能做到版本化,因此可保证各个不同环境的差异性,同时能大大减少环境不同带来的排错等沟通成本问题。

11.日志

把日志当作事件流。

日志使应用程序运行的动作变得透明。在基于服务器的环境中,日志通常被写在硬盘的一个文件里,但这只是一种输出格式。

日志应该是事件流的汇总,将所有运行中进程和后端服务的输出流按照时间顺序收集起来。尽管在回溯问题时可能需要看很多行,但日志最原始的格式确实是一个事件占一行。日志没有确定开始和结束,但随着应用在运行会持续增加。

十二要素应用本身从来不考虑存储自己的输出流,不应该试图去写或者管理日志文件。相反,每一个运行的进程都会直接地标准输出(stdout)事件流。在开发环境中,开发人员可以通过这些数据流实时地在终端看到应用的活动。

在预发布或线上部署中,每个进程的输出流由运行环境截获,并将其与其他输出流整理在一起,然后一并发送给一个或多个最终的处理程序,用于查看或长期存档。对应用来说,这些存档路径不可见也不可配置,而是完全交给程序的运行环境管理。

这些事件流可以输出至文件,或在终端实时观察。最重要的是,输出流可以发送到Splunk等日志索引及分析系统或Hadoop/Hive等通用数据存储系统。这些系统为查看应用的历史活动提供了如下强大而灵活的功能。

(1)找出过去一段时间内特殊的事件。

(2)图形化一个大规模的趋势,比如每分钟的请求量。

(3)根据用户定义的条件实时触发警报,比如每分钟的报错超过某个警戒线。

日志是系统运行状态的部分体现,无论是系统诊断、业务追踪,还是后续大数据的分析处理服务。Docker提供标准的日志服务,用户可以根据需求进行自定义的插件开发来处理日志。

12.管理进程

后台管理任务当作一次性进程运行。

进程构成(process formation)是指用来处理应用的常规业务(比如处理Web请求)的一组进程。与此不同,开发人员经常希望执行一些管理或维护应用的一次性任务,举例如下。

(1)运行数据移植。

(2)运行一个控制台(也被称为REPL shell)来执行一些代码,或针对线上数据库做一些健康检查或安全巡检。

(3)运行一些提交到代码仓库的一次性脚本。

一次性管理进程应该和正常的常驻进程使用同样的环境。这些管理进程和任何其他的进程一样使用相同的代码和配置,基于某个发布版本运行。后台管理代码应该随其他应用程序代码一起发布,从而避免同步问题。

十二要素尤其青睐那些提供了 REPL shell 的语言,因为那会让运行一次性脚本变得简单。在本地部署中,开发人员直接在命令行使用 shell 命令调用一次性管理进程。在线上部署中,开发人员依旧可以使用ssh或运行环境提供的其他机制来运行这样的进程。