涨知识了!用这 12 要素来构建你的应用程序

1,849 阅读14分钟

前奏曲

12 要素应用程序(12-factor-app)是 Heroku 开发团队在吸取了大量的SaaS(Soft as a Service) 经验后,所整理出来的 12 条准则。这 12 条准则提供了最佳的实践和有条理的方法来开发现代复杂的应用程序,这些准则不限于任何的开发语言。遵守这些准则可以增进你应用程序的稳定性及扩展性,减少开发与生产之间的差异。

由 12 要素所创建的应用程序需要实现的目标:

  • 通过声明性语法配置自动流程,最小化新开发人员加入项目的时间和成本;
  • 与操作系统解耦,在不同的执行环境之间提供最大的可移植性;
  • 适合在现代云平台上进行部署,消除对服务器和系统管理的需求;
  • 支持持续部署以实现最大的敏捷性,最小化开发与生产之间的差异;
  • 无需重大更改即可进行拓展

接下来就是具体用 12 要素来体现这些目标。

⚠️注意:此文多是文字阐述,所以文字会多一点,但是希望你能坚持读下去并在阅读的时候加入你的思考,相信到最后你会得到些什么。

代码库(Codebase)

在版本控制中去跟踪一个代码库,做多次部署

**
1、代码库始终由版本控制系统 (VCS) 来跟踪管理
**
比如 Git 与 SVN。 代码的跟踪和版本控制在开发中是至关重要的一环,同时这一环还简化了应用程序进行协同开发的工作。相信现在应该没有不使用 (VCS) 的开发团队了吧。如果有,我只能送上一个大写的“服”!

2、代码库和应用程序之间始终要保持一对一的关系

如果有多个代码库,那么这就不是应用程序,而是分布式系统,分布式系统的每一个组件都是一个应用程序,而每一个应用程序都可以遵守 12 要素。

3、代码库不应该在应用程序之间共享代码

应该把在整个应用程序中通用的代码创建为可通过依赖项管理器包含的库。

4、不同环境共享相同的代码库

前面有提到代码库与应用程序之间应保持一对一关系,如果需要部署应用程序到不同的环境中,比如 development 、 staging 、 production 那么不应该去创建不同的代码库,在实现上可以使用Subversion控制系统,如GithubGitLab等去创建不同的分支,这样也有利于查看团队成员的进度与监管成员代码的质量。如果你想知道如何更好的去管理多个版本的代码库,建议去看看 Git-flow

依赖性(Dependecies)

明确声明和隔离依赖项。

对于任何应用程序,都不应该把任何依赖项复制到代码库中,而应该使用依赖管理工具去获取所需要的依赖项。

比如使用 npm 或者 yarn 与 package.json 来对前端项目或者 Node 项目的依赖项进行管理。

比如使用 Maven 与 pom.xml 来对 Java 项目的依赖项进行管理。

这样一来,所有的第三方依赖库都明确的定义了出来,并统一管理,这也有助于新加入项目的开发者随时都可以通过依赖管理工具来安装到自己的环境中,而且在任何平台都不会有冲突。

你之后将要使用 CI/CD 流程使其进行自动化,稍后的内容会提到。

配置项 (Config)

将配置存储在环境中。

在现代应用程序开发中都需要某种形式的配置,这些配置通常包括端口号、服务账户凭证,数据库连接信息等。而且在不同的环境下,比如 development 、 staging 、 production 都有不同的配置信息。

⚠️注意:将配置存储为代码中的常量是不可取的!各个环境中的部署配置差异是很大的,而代码却没有差异。而且这样会面临暴露应用中的私密凭证。

检查一个应用程序是否正确地将所有配置从代码中分解出来的一个测试方法就是:想想是否可以在不暴露任何私密凭证的情况下随时把代码库设为开源。

最好的方法就是把配置存储在环境变量中,这样很容易在运行时针对特定环境进行修改。

建议以下特定的方法:

  • 使用不受版本控制的 .env 文件进行本地开发,Docker 在运行时支持加载这些文件;
  • 将所有的 .env 文件保存在安全的存储系统(例如 Vault)中,以提供开发人员使用,不提交到 Git;
  • 对于运行时可以更改的任何内容以及不应提交给共享存储库的任何私密凭证,请使用环境变量;
  • 将应用程序部署到交付平台后(例如:AWS、阿里云、腾讯云),请使用交付平台的机制来管理环境变量。

相信有前端工程化和 Node 项目经验的人会知道 dotenv 这个库在 Node.js 环境中的表现。而在 dotenv 的 README 中也提到:将配置与代码库分开存储在环境中是基于 “12 要素应用程序” 方法的。

image.png

将支撑服务视为附加资源

支撑服务是应用程序在操作过程中通过网络访问的任何服务。例如:数据库、SMTP、数据 API、缓存系统之类的都应作为服务访问。本地服务和第三方服务应该是没有区别的,都视为附加的资源,在切换服务的时候不应该去更改代码库,而是更改URL、账户密码者其他的连接信息。比如把 Mysql 从本地切换到AWS提供的服务,只需要更改连接的信息就行了。而这些都是通过配置信息传递的,配置信息的管理方式遵从上一条准则。

构建、发行与运行(Build, release, run)

严格分开的构建和运行阶段

将应用程序部署分为三个阶段很重要:

1、构建
这个阶段会将代码库与其依赖的程序包一起编译打包,开发人员可以完全控制这个阶段,可以标记新版本并修复所有错误。最好只在构建阶段更改代码,而不在其他阶段进行更改。

2、发行
获取上一阶段构建好的程序包将其与特定环境上的配置进行组合,并准备在执行环境中立即执行。每一个发行版本都有一个特定的唯一辨识编号(软体版本号),如有问题可方便快速的回滚到之前的版本。

3、运行
针对特定发布版本,在特定环境中启动执行,这是运行应用程序的最后阶段,不应被任何其他阶段干预。

为了支持严格分开构建、发行、运行阶段,建议使用持续集成 / 连续交付(CI/CD)工具来自动化构建,Docker也使得构建分离和运行阶段变得容易。

进程 (Process)

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

应用程序应该是无状态和无共享的,任何需要保留的数据,都必须存储在有状态的支持服务中,通常是数据库。

比如一个web系统依赖 “粘性会话”,即在应用程序的进程内存中缓存用户会话数据,并期望将来来自同一访问者的请求将路由到同一进程,这是不可取的,会话状态数据非常适合提供时间到期的数据存储,例如 MemcachedRedis

端口绑定 (Port binding)

通过端口提供对外服务

应用程序服务对外公开通过 URL 并由PORT 环境变量指定的端口号是一种体系结构最佳实践。这样,你应用程序服务可以在需要时充当其他服务的资源。这可以促进使应用程序自成体系。

例如,你希望开发的 Web 应用程序中的一些功能可供其他服务访问,你可以向他们提供API URL(例如:https://www.web/api/module:5000)和一些请求对方访问的数据令牌。

并发 (Concurrency)

进程可以进行横向扩展

在满足十二要素的应用程序中,进程是一等公民,应用的进程主要借鉴于 unix 守护进程模型,可以运用这个模型去设计应用架构,将不同的工作分配给不同的进程类型 ,来构建其应用程序以处理各种工作负载。

例如:HTTP请求可以给Web进程处理,而长时间运行的后台任务可以由工作进程进行处理。

应用程序中的每个进程都应能够根据需要进行横向扩展,而不要过多地依赖应用程序中的线程,因为垂直缩放可能会限制服务器上运行的进程。不要为应用程序守护进程或者编写PID,而是依靠操作系统的进程程管理工具。

易处理 (Disposability)

Disposability 这个词语是表示可处置性或者用完即可丢弃的,但是这里理解为便利性或者易处理会更好一点。

应用程序的进程应该在最短的时间内启动或停止,减少耗时。这有利于快速弹性扩展,代码或者配置更改的快速部署以及生产部署的健壮性

当进程收到 SIGTERM  信号应优雅的关闭,对于工作进程的终止,应该将未完成的任务退回到队列中,并且在系统故障的时候能够处理,保持程序的健壮性,比如Beanstalkd

环境相似

保证各个环境尽可能一致

传统的应用程序开发环境(dev)和生产环境(production)存在很大不差异,这些差异主要体现在:

  • 时间差异:从开发到生产环境需要很长的时间,有时候要持续几周甚至几个月才可以发行新的版本;
  • 人员差异:开发人员编写代码,由运维工程师进行部署;
  • 工具差异:开发时可能会使用NginxSQLitewindows等技术栈,而生产部署则使用ApacheMySQLLinux

而十二要素应用程序旨在通过尽可能的缩小开发与生产之间的差距来进行连续部署,从上面的差异性来说:

  • 时间差异:从开发到生产的时间间隔可以在数小时甚至数分钟;
  • 人员差异:由开发人员来进行部署发行新版本,不需要通过运维工程师;
  • 工具差异:使开发与生产环境中的工具尽可能的相似;

如果在开发和生产之间使用不同的服务和工具,那么你会遇见这样的情况:在开发过程中成功运行并通过测试的代码,而在生产中出现不可预见的问题。如果要确保持续部署稳定性,就要尽量在各环境都使用与生产环境一样的工具或后端服务。使用现代一些工具、服务会让这些更容易,而且成本更低。例如,使用 Docker 和 Docker Compose,可以确保应用程序在整个环境中保持一致性。

日志处理(Log)

将日志作为事件流处理

日志在应用程序终止前都会不断的记录着开发者认为重要的讯息,例如错误资讯或是未来可以用来分析的数据资料。日志可以查看到正在运行的应用程序的行为,通常被写入磁盘上的文件,但这只是一种输出格式。

日志的收集,处理和分析与应用程序的核心逻辑分离开来是很重要的。当应用程序需要动态扩展并在公共云上运行时,解耦日志记录特别有用,因为不用考虑管理日志的存储位置以及来自分布式(通常是临时性)VM的聚合的开销,仅应进行相应打印以检查您的应用程序流程。例如你的应用程式在多个主机上执行,造成记录档案很可能处于不连续的状态,所以需要有额外的服务来将记录整合起来,就需要把日志记录解耦。另外通过可视化分析的方式(比如 Elasticsearch、Kinbana)将一些数据展示出来更直观一些,并设置一些报警的阀值,自动触发报警操作。

管理流程 (Admin processes)

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

**
管理流程通常由一次性任务或者定时的可重复的任务组成,比如:数据库迁移、生成报告,执行批处理脚本等等。这些管理流程应该与应用程序的环境相对应 ,比如在 development 中执行 development 的管理流程,在 production 中执行 production 的管理流程,而且这些管理流程的代码应该与应用程序本身的代码应一起以同一个版本发行 (Release) 到同一个环境,配合该环境中的配置项 (config) 来执行,以避免同步问题。

额外可考虑的因素

API

应用程序使用API进行通信,在构建应用程序的时候,要多考虑应用程序的生态系统将如何使用该应用程序,并从设计API策略开始。良好的 API 设计使该 API易于被应用程序开发人员或者其他外部利益相关者使用,在实现代码之前,最好先使用 OpenAPI 规范你的 API 。

API 抽取来底层功能,一个经过精心设计的 API 应将使用的应用程序和提供服务的应用程序基础结构进行分离,这种解耦可以让你独立更改基础服务与基础结构,而不会使用应用程序的使用者。

对你的 API 进行分类记录发布也很重要,这样 API 的使用者才能更好的使用和发现这些 API 。

安全

安全的领域非常广泛,包括操作系统、网络、防火墙、数据库安全、应用程序安全性以及身份和访问管理。

在整理这片文章时候,看到一条消息:一个国外的开发者绕过了 GitHub 的 OAuth  流程,然后给 Github 提交了报告,修复后,Github 奖励了 $25000 赏金。想详细了解的请点击这里

从应用程序的角度看, API 提供了生态系统中的应用程序的访问,因此,你应该确保在应用程序的设计和构建过程中,这些构建的基础模块可以解决安全问题。有一些可以帮助保护你的应用程序的访问:

  • 传输层安全性(TLS):使用 TLS  帮助保护传输中的数据,在某些用例中,基于 IP 地址创建允许列表和拒绝列表作为附加安全层也是很常见的。传输安全还涉及保护您的服务免受 DDos 和 Bot 攻击。
  • 应用和用户的安全:传输安全性有助于为传输中的数据提供安全性并建立信任,但是最佳实践是添加应用程序级别的安全,以根据应用程序的使用者是谁来控制对应用的访问。使用者可以是其他应用程序,员工,合作伙伴或者用户。你可以使用 API 密钥(用于消费类应用程序),基于证书的身份认证和授权, JWT(JSON Web Token)  交换或者 SAML (安全性声明标记语言)来保证安全性。

最后

通过以上可以看到十二个要素如何让你的应用程序构建发布变得更少的忧虑且具有更高的可扩展性和可预测性。另外花在理解和实施这些准则上的时间可以帮助我们节省大量软件成本。在某些情况下,偏离一些要素(例如支撑服务(Backing service)和日志 (log))也是可取的,但是最好尽可能地遵守所有十二要素。

如果你正在设计构建你的应用,请考虑并认真对待这 12 要素。

当你在多个不同的环境中运行多种服务时,现在看起来微不足道的内容可能之后会非常重要。请查看是否缺少一些东西,也许它们可以帮助您解决您之前无法注意到的问题。

总而言之,这些要素为构建你的应用程序和服务奠定了良好的基础,从长远来看,这种方法可以帮助你顺利扩展和维护你的应用程序。

最后,希望你能从中获益或者得到一些思考与启发。

引用与参考: