云原生时代应用开发之12-Factor的理解与实践

前言

在云与移动互联网的时代,应用会更多的部署在云端,基于云端的架构设计和开发模式需要一套新的理论或者标准,在头部的云厂商们的推动下,云原生的思想四面开花,迅速占领开发人员的心智。

云原生(Cloud Native) 是由Pivotal 的Matt Stine在2013年提出的一个概概念,是他多年架构经验和咨询服务总结出来的思想集合。

维基百科的定义:

Cloud Native Computing is an approach in software development that utilizes cloud computing to “build and run
scalable applications in modern, dynamic environments such as public, private, and hybrid clouds”.[1]
Technologies such as containers, microservices, serverless functions and immutable infrastructure,
deployed via declarative code are common elements of this architectural style.[2][3]

These techniques enable loosely coupled systems that are resilient, manageable, and observable. Combined
with robust automation, they allow engineers to make high-impact changes frequently and predictably with
minimal toil.

那么问题来了,开发云原生应用或者云平台用有没有一些放之四海皆准的方法论呢? 巧了,12-Factor为构建云原生应用就提供了如下的方法论:

  • 使用标准化流程自动配置,从而使新的开发者花费最少的学习成本加入这个项目。
  • 和操作系统之间尽可能的划清界限,在各个系统中提供最大的可移植性。
  • 适合部署在现代的云计算平台,从而在服务器和系统管理方面节省资源。
  • 将开发环境和生产环境的差异降至最低,并使用持续交付实施敏捷开发。
  • 可以在工具、架构和开发流程不发生明显变化的前提下实现扩展。

维基百科定义:

The Twelve-Factor App methodology is a methodology for building software-as-a-service applications.
These best practices are designed to enable applications to be built with portability and resilience
when deployed to the web.[1]

12-Factor方法论产生于PAAS云厂商Heroku的开发人员,第一次被大众熟知是由Heroku的联合创始人Adam Wiggins在2011年的介绍。它定义
了一个优雅的云原生应用在设计过程中需要遵循的一些基本原则,切记这只是一个云原生的基线。

十二原则应用与实践

12-Factors 综合了Heroku资深开发人员的经验和智慧,是开发此类应用的理想踏实标准,并特别关注于应用程序如何保持良性成长,开发者
之间如何进行有效的代码协作,以及如何避免软件污染。

1. 基准代码 (Code Base)

份基准代码(Codebase),多份部署(deploy)。
在大多数的软件开发团队通常都会使用版本控制系统加以管理,如Git, Subversion, TFS 或者Mercurial,用于跟踪所有代码的更改记录。
基准代码和应用之间总是保持一一对应的关系:

  • 一个应用只能有一个基准代码。且多个应用共享一份基准代码是悖于12-Factor原则,解决方案是将共享代码拆分为独立类库,使之一一对应。
  • 虽然每个应用对应一份基准代码,但是可以同时存在多份部署,相当每份部署运行了一个应用的实例。可以是多个环境,也可以某个环境的多个实例(节点).

如下图示意(来自官网):
One codebase tracked in revision control, many deploys

2. 依赖(Dependencies)

显式声明依赖关系( dependency )

大多数编程语言都会提供一个打包系统,用来为各个类库或者应用提供打包服务,就像 Java 的 Maven 或Gradle。
12-Factor规则下的应用程序不会隐式依赖系统级的类库。 它一定通过 依赖清单 ,确切地声明所有依赖项。此外,在运行过程中通过依赖隔离工具来
确保程序不会调用系统中存在但清单中未声明的依赖项。这一做法会统一应用到生产和开发环境。

例如,Java中的Maven/Gradel使用pom.xml或者libraries 作为依赖项声明清单,同时将依赖包按照manifest file打包成一个独立的可运行包Jar/War.
无论用什么工具,依赖声明和依赖隔离必须一起使用,否则无法满足 12-Factor 规范.

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

3. 配置(Config)

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

  • 数据库,Memcached,以及其他 后端服务 的配置
  • 第三方服务的证书,如 Amazon S3、Twitter 等
  • 每份部署特有的配置,如域名等

12-Factor推荐将应用的配置存储于 环境变量 中( env vars, env )。环境变量可以非常方便地在不同的部署间做修改,却不动一行代码;
与配置文件不同,不小心把它们签入代码库的概率微乎其微;与一些传统的解决配置问题的机制(比如 Java 的属性配置文件)相比,
环境变量与语言和系统无关。通过将配置信息存储于环境变量中,同时可避免一些敏感信息在代码库中泄露。

当今流行的Java开发框架Spring Framework已经完美的在配置中支持环境变量当作配置方式之一。

在配置类里声明

@Value("${DB_URL}") private String dbUrl;

或者
在applicaton.properties中

db.url=${DB_URL}

4. 后端服务(Backing Services)

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

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

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

衡量一个应用是否满足12-Factor, 应用部署可以按需加载或者卸载后端服务,在加载或者卸载的整个过程不需要修改代码。
如下图示意(来自官网):

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-flgNc5jK-1615207258241)(https://12factor.net/images/attached-resources.png "把后端服务(backing services)]当作附加资源 ")

5. 构建,发布与运行(Build, Release and Run)

严格分离构建和运行

基准代码 转化为一份部署(非开发环境)需要以下三个阶段:

  • 构建阶段 是指将代码仓库转化为可执行包的过程。构建时会使用指定版本的代码,获取和打包 依赖项,编译成二进制文件和资源文件。
  • 发布阶段 会将构建的结果和当前部署所需 配置 相结合,并能够立刻在运行环境中投入使用。
  • 运行阶段 (或者说“运行时”)是指针对选定的发布版本,在执行环境中启动一系列应用程序 进程。

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

针对这个原则,市场上主流CI/CD工具均能良好的支持从构建到运行的自动化,引入这些工具大大的提高团队的效率。下面列举几个常用的工具:

Jenkins

Jenkins 是一个开源软件项目,是基于Java开发的一种持续集成工具,用于监控持续重复的工作,旨在提供一个开放易用的软件平台,使软件的持续集成变成可能。
Jenkins应该说是目前最好用的持续集成工具之一,它的插件非常多,安装也很方便,功能相当的强大、灵活,最大的缺点就是学习成本较高。

Microsoft Visual Studio

微软DevOps产品的基础之一是 Visual Studio 允许用户定义版本定义,自动化运行,跟踪版本等等。

DeployBot

DeployBot 可以链接任何Git存储库,并且允许手动或自动部署到多种环境。DeployBot提供大量集成,包括通过Slack部署的能力。

TeamCity

TeamCity 是一个来自Jet Brains的CI服务器。TeamCity 有智能的配置功能和拥有官方Docker镜像服务器和代理。

Capistrano

Capistrano是一个开源部署工具,使用Ruby开发。Capistrano 文档具有脚本语言和“理智的,富有表现力的API。”

6. 进程(Process)

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

运行环境中,应用程序通常是以一个和多个 进程 运行的。
12-Factor 应用的进程必须无状态且 无共享。 任何需要持久化的数据或者状态都要存储在后端服务内,比如数据库服务, 缓存服务或者对象存储服务。

文中提到一个互联网系统依赖于常见于负载均衡服务中的"粘性session", 是12-Factor极力反对的,Session中的数据应该保存在诸如 Memcached 或
Redis 这样的带有过期时间的缓存中。

7. 端口绑定(Port Binding)

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

市场上开发的大多数互联网应用或者企业级应用都是运行于服务器的容器中,如Tomcat, Jetty或者Undertow。

12-Factor 应用完全自我加载 而不依赖于任何网络服务器就可以创建一个面向网络的服务。互联网应用 通过端口绑定来提供服务 ,并监听发送至该端口的请求。
例如本地环境中,开发人员通过类似 http://localhost:8080/ 的地址来访问服务。在线上环境中,请求统一发送至公共域名而后路由至绑定了端口的网络进程。
常见的是通过负载均衡服务LVS或者轻量级Web服务器Nginx进行端口的转发。

8. 并发(Concurrency)

通过进程模型进行扩展

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

在Java的世界是通过虚拟机的线程处理并发的内部运算,或是使用诸如 EventMachine, Twisted, Node.js 的异步/事件触发模型。开发人员可以运用UNIX
守护进程模型去设计应用架构,将不同的工作分配给不同的线程类型,比如说接收请求,处理请求,输出日志,垃圾回收等。但一台独立的虚拟机的扩展有
瓶颈(垂直扩展),所以应用程序必须可以在多台物理机器间跨进程工作。

9. 易处理(Disposability)

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

12-Factor 应用的 进程 是 易处理(disposable)的,意思是说它们可以瞬间开启或停止。 这有利于快速、弹性的伸缩应用,迅速部署变化的 代码 或 配置 ,稳健的部署应用。

进程应当追求 最小启动时间 。 理想状态下,进程从敲下命令到真正启动并等待请求的时间应该只需很短的时间。更少的启动时间提供了更敏捷的 发布 以及扩展过程,
此外还增加了健壮性,因为进程管理器可以在授权情形下容易的将进程搬到新的物理机器上。 最近几年新出现的云原生Java开发框架Micronaut
Quarkus 都在启动时间和初始资源进行优化,具体的性能指标可参考以下链接:

进程 一旦接收 终止信号(SIGTERM) 就会优雅的终止 。就网络进程而言,优雅终止是指停止监听服务的端口,即拒绝所有新的请求,并继续执行当前
已接收的请求,然后退出。此类型的进程所隐含的要求是HTTP请求大多都很短(不会超过几秒钟),而在长时间轮询中,客户端在丢失连接后应该马上尝试重连。
此类型的进程所隐含的要求是,任务都应该 可重复执行 , 这主要由将结果包装进事务或是使重复操作 幂等 来实现。

10. 开发环境与线上环境等价(Dev Prod Parity)

尽可能的保持开发,预发布,线上环境相同

从以往经验来看,开发环境(即开发人员的本地 部署)和线上环境(外部用户访问的真实部署)之间存在着很多差异。这些差异表现在以下三个方面:

  • 时间差异: 开发人员正在编写的代码可能需要几天,几周,甚至几个月才会上线。
  • 人员差异: 开发人员编写代码,运维人员部署代码。
  • 工具差异: 开发人员或许使用 Nginx,SQLite,OS X,而线上环境使用 Apache,MySQL 以及 Linux。

12-Factor 应用想要做到 持续部署 就必须缩小本地与线上差异。 再回头看上面所描述的三个差异:

  • 缩小时间差异:开发人员可以几小时,甚至几分钟就部署代码。
  • 缩小人员差异:开发人员不只要编写代码,更应该密切参与部署过程以及代码在线上的表现。
  • 缩小工具差异:尽量保证开发环境以及线上环境的一致性。

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

借助于Docker类似现代的系统和工具,将后端服务像MySQL, Redis, Tomcat, Kafka, Memcached, JRE置入基准的镜像里,可以使得开发人员的本地
环境和线上环境无线接近。

11. 日志(Logs)

把日志当作事件流

日志 使得应用程序运行的动作变得透明。在基于服务器的环境中,日志通常被写在硬盘的一个文件里,但这只是一种输出格式。
在预发布或线上部署中,每个进程的输出流由运行环境截获,并将其他输出流整理在一起,然后一并发送给一个或多个最终的处理程序,用于查看或是长期存档。这些存档路径对于应用来说不可见也不可配置,而是完全交给程序的运行环境管理。类似 Logplex 和 Fluentd 的开源工具可以达到这个目的。
这些事件流可以输出至文件,或者在终端实时观察。最重要的,输出流可以发送到 Splunk 这样的日志索引及分析系统,或 Hadoop/Hive 这样的通用数据存储系统。这些系统为查看应用的历史活动提供了强大而灵活的功能,包括:

  • 找出过去一段时间特殊的事件。
  • 图形化一个大规模的趋势,比如每分钟的请求量。
  • 根据用户定义的条件实时触发警报,比如每分钟的报错超过某个警戒线。

另外在阿里巴巴Java开发手册 也对应用日志提出了更具体的要求。

  • 日志框架应依赖SLF4J中的API, 不可直接使用log4j, logback或者JCL.
  • 日志文件存储时间至少保存15天
  • 应用中的扩展日志命令方式参照appName_logType_logName.

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

12. 管理进程(Admin Processes)

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

进程构成(process formation)是指用来处理应用的常规业务(比如处理 web 请求)的一组进程。与此不同,开发人员经常希望执行一些管理或维护应用的一次性任务
一次性管理进程应该和正常的 常驻进程 使用同样的环境。这些管理进程和任何其他的进程一样使用相同的 代码 和 配置 ,基于某个 发布版本 运行。后台管理代码应该随其他应用程序代码一起发布,从而避免同步问题。
正如我们在日常开发中碰到的修数或者归档任务,应该当作常规的一个应用功能包含在基准代码库中,随应用程序代码一起发布。

总结

12-Factor指出了软件开发过程经常发现的一些系统性问题,并对这些问题进行归纳总结与探讨,同时使用相关术语给出一套这些问题的广义解决方案。
希望每一位开发者都能领会并理解其中的实践,为自己的软件开发过程更少的为故障而熬夜,提升生活品质。

参考文档

Logo

一站式 AI 云服务平台

更多推荐