深解CloudNative架构,12-Factor(一套流行的应用程序开发原则 )

这篇文章详细介绍了12-Factor原则,包括自动配置、跨平台可移植性、云部署优化、开发环境一致性、依赖管理、配置分离、后端服务管理、构建发布流程、无状态设计、端口绑定、并发处理、开发环境与线上环境的等价性以及日志管理,这些都是构建和部署现代应用程序的关键要素。
摘要由CSDN通过智能技术生成

12-Factor

12-Factor(Twelve-Factor),也称为“十二要素”,是一套流行的应用程序开发原则。12-Factor的目标在于以下5点。

·使用标准化流程自动配置,从而使新的开发者花费最少的学习成本加入项目。

·和底层操作系统之间尽可能地划清界限,在各个系统中提供最大的可移植性。

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

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

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

12-Factor可以适用于任意语言和后端服务(数据库、消息队列、缓存等)开发的应用程序,自然也适用于Cloud Native。在构建CloudNative应用时,也需要考虑这12个方面的内容。

 基准代码

代码是程序的根本,什么样的代码最终会表现为什么样的程序软件。从源代码编写到产品发布,中间会经历多个环节,比如开发、编译、测试、构建、部署等,这些环节可能都有自己不同的部署环境,而不同的环境相应的责任人关注产品的不同阶段。比如,测试人员主要关注测试的结果,而业务人员可能关注生产环境的最终的部署结果。但不管是在哪个环节,部署到怎么样的环境中,他们所依赖的代码是一致的,即所谓的“一份基准代码(Codebase),多份部署(Deploy)”。现代的代码管理,往往需要进行版本的管理。即便是个人的工作,采用版本管理工具进行管理,对于方便查找特定版本的内容,或者是回溯历史修改内容都是极其必要的。版本控制系统就是帮助人们协调工作的工具,它能够帮助我们和其他小组成员监测同一组文件,比如软件源代码,升级过程中所做的变更,也就是说,它可以帮助我们轻松地将工作进行融合。

版本控制工具发展到现在已经有几十年了,简单地可以将其分为4代。

·文件式版本控制系统,比如SCCS、RCS。

·树状版本控制系统——服务器模式,比如CVS。

·树状版本控制系统——双服务器模式,比如Subversion。

·树状版本控制系统——分布式模式,比如Bazaar、Mercurial、Git。

目前,企业广泛采用服务器模式的版本控制系统,但越来越多的企业开始倾向于采用分布式模式版本控制系统。

依赖

应该明确声明应用程序依赖关系(Dependency),这样,所有的依赖关系都可以从工件的存储库中获得,并且可以使用依赖管理器(例如Apache Maven、Gradle)进行下载。

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

比如,项目组统一采用Gradle来进行依赖管理,那么可以使用Gradle Wrapper。Gradle Wrapper免去了用户在使用Gradle进行项目构建时需要安装Gradle的烦琐步骤。每个Gradle Wrapper都绑定到一个特定版本的Gradle,所以当你第一次在给定Gradle版本下运行下面的命令之一时,它将下载相应的Gradle发布包,并使用它来执行构建。GradleWrapper的发布包默认指向官网的Web服务地址,相关配置记录在了gradle-wrapper.properties文件中。我们查看下Spring Boot提供的这个Gradle Wrapper的配置,参数“distributionUrl”就是用于指定发布包的位置。

distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.5.1-bin.zip

这个gradle-wrapper.properties文件是作为依赖项而被纳入代码存储库的。

 配置

相同的应用,在不同的部署环境(如预发布、生产环境、开发环境等)下,可能有不同的配置内容,如下。

·数据库、Redis以及其他后端服务的配置。

·第三方服务的证书。

·每份部署特有的配置,如域名等。

这些配置项不可能硬编码在代码中,因为我们必须保证同一份基准代码能够多份部署。一种解决方法是使用配置文件,但不把它们纳入版本控制系统,就像Rails的config/database.yml。这相对于在代码中硬编码常量,已经是长足进步,但仍然有缺点。

·不小心将配置文件签入了代码库。

·配置文件可能会分散在不同的目录,并有着不同的格式,不方便统一管理。

·这些格式通常是语言或框架特定的,不具备通用性。

所以,推荐的做法是将应用的配置存储于环境变量中。好处在于如下3点。

·环境变量可以非常方便地在不同的部署文件上做修改,却不改动一行代码。

·与配置文件不同,不小心把它们签入代码库的概率微乎其微。

·与一些传统的解决配置问题的机制(比如Java的属性配置文件)相比,环境变量与语言和系统无关。

 后端服务

后端服务(Backing Services)是指程序运行所需要的通过网络调用的各种服务,如数据库(MySQL、CouchDB)、消息/队列系统(RabbitMQ、Beanstalkd)、SMTP邮件发送服务(Postfix),以及缓存系统(Memcached、Redis)。

这些后端服务,通常由部署应用程序的系统管理员一起管理。除了本地服务之外,应用程序有可能使用了第三方发布和管理的服务。示例包括SMTP(例如Postmark)、数据收集服务(例如New Relic、Loggly)、数据存储服务(如Amazon S3),以及使用API访问的服务(例如Google Maps)。

12-Factor应用不会区别对待本地或第三方服务。对应用程序而言,本地或第三方服务都是附加资源,都可以通过一个URI或是其他存储在配置中的服务定位或服务证书来获取数据。12-Factor应用的任意部署,都应该可以在不进行任何代码改动的情况下,将本地MySQL数据库换成第三方服务(例如Amazon RDS)。类似地,本地SMTP服务应该也可以和第三方SMTP服务(例如Postmark)互换。比如,在上述两个例子中,仅需修改配置中的资源地址。

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

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

 构建、发布、运行

基准代码进行部署需要以下3个阶段。

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

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

·运行阶段:是指针对选定的发布版本,在执行环境中启动一系列应用程序进程。

应用严格区分构建、发布、运行这3个阶段。举例来说,直接修改处于运行状态的代码是非常不可取的做法,因为这些修改很难再同步回构建阶段。

部署工具通常都提供了发布管理工具,必要时还是可以退回至较旧的发布版本。

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

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

进程

12-Factor应用推荐以一个或多个无状态进程运行应用。这里的“无状态”是与REST中的无状态是一个意思,即进程的执行不依赖于上一个进程的执行。

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

一些互联网应用依赖于“黏性Session”,这是指将用户Session中的数据缓存至某进程的内存中,并将同一用户的后续请求路由到同一个进程。黏性Session是12-Factor极力反对的,12-Factor认为Session中的数据应该保存在诸如Memcached或Redis这样的带有过期时间的缓存中。

与有状态的应用相比,无状态具有更好的可扩展性。

 端口绑定

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

12-Factor应用完全具备自我加载的能力,而不依赖于任何Web服务器就可以创建一个面向网络的服务。互联网应用通过端口绑定(PortBinding)来提供服务,并监听发送至该端口的请求。

举例来说,Java程序完全能够在程序中内嵌一个Tomcat,从而自己就能启动并提供服务,省去了将Java应用部署到Tomcat中的烦琐过程。

在这方面,Spring Boot框架的倡导者Josh Long有句名言“Make JAR notWAR”,即Java应用程序应该被打包为可以独立运行的JAR文件,而不是传统的WAR包。

以Spring Boot为例,构建一个具有内嵌容器的Java应用是非常简单的,只需要引入以下依赖。

// 依赖关系
dependencies {
// 该依赖用于编译阶段
compile('org.springframework.boot:spring-boot-starter-web')
}

这样,该Spring Boot应用就包含了内嵌Tomcat容器。

如果想使用其他容器,比如Jetty、Undertow等,只需要在依赖中加入相应Servlet容器的Starter就能实现默认容器的替换。

·spring-boot-starter-jetty:使用Jetty作为内嵌容器,可以替换
spring-boot-starter-tomcat。

·
spring-boot-starter-undertow:使用Undertow作为内嵌容器,可以替换spring-boot-starter tomcat。

可以使用Spring Environment属性配置常见的Servlet容器的相关设置。通常你将在application.properties文件中来定义属性。

常见的Servlet容器设置包括如下5点。

·网络设置:监听HTTP请求的端口(server.port),绑定到server.address的接口地址等。

·会话设置:会话是否持久(
server.session.persistence)、会话超时(server.session.timeout)、会话数据的位置(server.session.store-dir)和会话cookie配置(server.session.cookie.*)。

·错误管理:错误页面的位置(server.error.path)等。

·SSL。

·HTTP压缩。

Spring Boot尽可能地尝试公开这些常见公用设置,但也会有一些特殊的配置。对于这些例外的情况,Spring Boot提供了专用命名空间来对应特定于服务器的配置(比如server.tomcat、server.undertow)。

 并发

在12-Factor应用中,进程是“一等公民”。由于进程之间不会共享状态,这意味着应用可以通过进程的扩展来实现并发。

类似于UNIX守护进程模型,开发人员可以运用这个模型设计应用架构,将不同的工作分配给不同的进程。例如,HTTP请求可以交给Web进程来处理,而常驻的后台工作则交由worker进程负责。

在Java中,往往通过多线程的方式来实现程序的并发。线程允许在同一个进程中同时存在多个线程控制流。线程会共享进程范围内的资源,例如内存句柄和文件句柄,但每个线程都有各自的程序计数器、栈以及局部变量。线程还提供了一种直观的分解模式来充分利用操作系统中的硬件并行性,而在同一个程序中的多个线程也可以被同时调度到多个CPU上运行。毫无疑问,多线程编程使得程序任务并发成为可能。而并发控制主要是为了解决多个线程之间资源争夺等问题。并发一般发生在数据聚合的地方,只要有聚合,就有争夺发生,传统解决争夺的方式采取线程锁机制,这是强行对CPU管理线程进行人为干预,线程唤醒成本高,新的无锁并发策略来源于异步编程、非阻塞I/O等编程模型。

并发的使用并非没有风险。多线程并发会带来以下问题。

·安全性问题。在没有充分同步的情况下,多个线程中的操作执行顺序是不可预测的,甚至会产生奇怪的结果。线程间的通信主要是通过共享访问字段及其字段所引用的对象来实现的。这种形式的通信是非常有效的,但可能导致两种错误:线程干扰(Thread Interference)和内存一致性错误(Memory Consistency Errors)。

·活跃度问题。一个并行应用程序的及时执行能力被称为它的活跃度(Liveness)。安全性的含义是“永远不发生糟糕的事情”,而活跃度则关注于另外一个目标,即“某件正确的事情最终会发生”。当某个操作无法继续执行下去,就会发生活跃度问题。在串行程序中,活跃度问题形式之一就是无意中造成的无限循环(死循环)。而在多线程程序中,常见的活跃度问题主要有死锁、饥饿以及活锁。

·性能问题。在设计良好的并发应用程序中,线程能提升程序的性能,但无论如何,线程总是带来某种程度的运行时开销。而这种开销主要是在线程调度器临时关闭活跃线程并转而运行另外一个线程的上下文切换操作(Context Switch)上,因为执行上下文切换,需要保存和恢复执行上下文,丢失局部性,并且CPU时间将更多地花在线程调度而不是线程运行上。当线程共享数据时,必须使用同步机制,而这些机制往往会抑制某些编译器优化,使内存缓存区中的数据无效,以及增加贡献内存总线的同步流量。所以这些因素都会带来额外的性能开销。

易处理

12-Factor应用的进程是易处理(Disposable)的,这意味着它们可以瞬间启动或停止。比如,Spring Boot应用,它可以无须依赖容器,而采用内嵌容器的方式来实现自启动。这有利于迅速部署变化的代码或配置,保障系统的可用性,并在系统负荷到来前快速实现扩展。

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

进程一旦接收终止信号(SIGTERM)就会“优雅”地终止。就网络进程而言,优雅终止是指停止监听服务的端口,即拒绝所有新的请求,并继续执行当前已接收的请求,然后退出。

对于worker进程来说,优雅终止是指将当前任务退回队列。例如,RabbitMQ中,worker可以发送一个NACK信号。Beanstalkd中,任务终止并退回队列会在worker断开时自动触发。有锁机制的系统诸如DelayedJob则需要确定释放了系统资源。

开发环境与线上环境等价

我们期望一份基准代码可以部署到多个环境,但如果环境不一致,最终也可能导致运行程序的结果不一致。

比如,在开发环境,我们采用了MySQL作为测试数据库,而在线上生产环境,则采用了Oracle。虽然MySQL和Oracle都遵循相同的SQL标准,但两者在很多语法上还是存在细微的差异。这些差异非常有可能导致两者的执行结果不一致,甚至某些SQL语句在开发环境能够正常执行,而在生产环境根本无法执行。这都给调试增加了复杂性,同时,也无法保障最终的测试效果。

所以,一个好的指导意见是,不同的环境设置尽量保持一样。开发环境、测试环境与线上环境设置成一样,以便更早发现测试问题,而不至于在生产环境才暴露出问题。

日志

在应用程序中输出日志是一个好习惯。日志使得应用程序运行的动作变得透明。日志是在系统出现故障时排查问题的有力帮手。

日志应该是事件流的汇总,它将所有运行中进程和后端服务的输出流按照时间顺序收集起来。尽管在回溯问题时可能需要看很多行,日志最原始的格式确实是一个事件一行。日志没有确定开始和结束,但随着应用在运行时会持续增加。对于传统的Java EE应用程序而言,有许多框架和库可用于日志记录。Java Logging(JUL)是Java自身所提供的现成选项。除此之外,Log4j、Logback和SLF4J是其他一些流行的日志框架。

对于传统的单块架构而言,日志管理本身并不存在难点,毕竟所有的日志文件都存储在应用所部署的主机上,获取日志文件或者搜索日志内容都比较简单。但在Cloud Native应用中,情况则有非常大的不同。

分布式系统,特别是微服务架构所带来的部署应用方式的重大转变,都使得微服务的日志管理面临很多新的挑战。一方面随着微服务实例的数量的增长,日志文件递增。另一方面,日志被散落在各自的实例所部署的主机上,不方便整合和回溯。

在这种情况下,将日志进行集中化的管理变得意义重大。

管理进程

开发人员经常希望执行以下一些管理或维护应用的一次性任务。

·运行数据移植(Django中的manage.py migrate、Rails中的rakedb:migrate)。

·运行一个控制台(也被称为REPL shell),来执行一些代码或是针对线上数据库做一些检查。大多数语言都通过解释器提供了一个REPL工具(Python、Perl),或是其他命令(Ruby使用irb,Rails使用railsconsole)。

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

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

所有进程类型应该使用同样的依赖隔离技术。例如,如果Ruby的Web进程使用了命令bundle exec thin start,那么数据库移植应使用bundleexec rake db:migrate。同样如果一个Python程序使用了Virtualenv,则需要在运行Tornado Web服务器和任何manage.py管理进程时引入bin/python。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值