简介
如今,软件通常会作为一种服务来交付,它们被称为网络应用程序,或软件即服务(SaaS)12-Factor 为构建如下的 SaaS 应用提供了方法论:
- 使用标准化流程自动配置,从而使新的开发者花费最少的学习成本加入这个项目。
- 在各个系统中提供最大的可移植性。(不受各个操作系统的限制)
- 容易部署在云计算机上,从而节省资源
- 开发环境和生产环境之间的差异降低到最小,并持续交付实施敏捷开发
- 在开发流程不发生明显的变化下实现扩展、在架构不发生明显的变化下实现扩展、在工具不发生明显的变化下实现扩展
1.One Codebase / Many Deploy 基准代码
通常会使用版本控制系统加以管理,如 Git, Mercurial, Subversion。一份用来跟踪代码所有修订版本的数据库被称作 代码库(code repository, code repo, repo)。 在类似 SVN 这样的集中式版本控制系统中,基准代码 就是指控制系统中的这一份代码库;而在 Git 那样的分布式版本控制系统中,基准代码 则是指最上游的那份代码库. 基准代码和应用之间总是保持一一对应的关系: 一旦有多个基准代码,就不能称为一个应用,而是一个分布式系统。分布式系统中的每一个组件都是一个应用,每一个应用可以分别使用 12-Factor 进行开发。 多个应用共享一份基准代码是有悖于 12-Factor 原则的。解决方案是将共享的代码拆分为独立的类库,然后使用 依赖管理 策略去加载它们。尽管每个应用只对应一份基准代码,但可以同时存在多份部署。每份 部署 相当于运行了一个应用的实例。通常会有一个生产环境,一个或多个预发布环境。此外,每个开发人员都会在自己本地环境运行一个应用实例,这些都相当于一份部署。 所有部署的基准代码相同,但每份部署可以使用其不同的版本。比如,开发人员可能有一些提交还没有同步至预发布环境;预发布环境也有一些提交没有同步至生产环境。但它们都共享一份基准代码,我们就认为它们只是相同应用的不同部署而已。
2.显式声明依赖关系( dependency )
显式声明依赖关系( dependency ) 通过打包系统安装的类库可以是系统级的(称之为 “site packages”),或仅供某个应用程序使用,部署在相应的目录中(称之为 “vendoring” 或 “bunding”)。规则下的应用程序不会隐式依赖系统级的类库.Ruby 的 Bundler 使用 Gemfile 作为依赖项声明清单,使用 bundle exec 来进行依赖隔离。Python 中则可分别使用两种工具 – Pip 用作依赖声明, Virtualenv 用作依赖隔离。显式声明依赖的优点之一是为新进开发者简化了环境配置流程。新进开发者可以检出应用程序的基准代码,安装编程语言环境和它对应的依赖管理工具,只需通过一个 构建命令 来安装所有的依赖项,即可开始工作。
3.配置 在环境变量中存储配置
应用的 配置 在不同 部署 (预发布、生产环境、开发环境等等)间会有很大差异。这其中包括:
- 数据库,Memcached,以及其他 后端服务 的配置.
- 第三方服务的证书,如 Amazon S3、Twitter 等
- 每份部署特有的配置,如域名等
有些应用在代码中使用常量保存配置,这与 12-Factor 所要求的代码和配置严格分离显然大相径庭。配置文件在各部署间存在大幅差异,代码却完全一致。
一个解决方法是使用配置文件,但不把它们纳入版本控制系统,就像 Rails 的 config/database.yml 。这相对于在代码中使用常量已经是长足进步,但仍然有缺点:总是会不小心将配置文件签入了代码库;配置文件的可能会分散在不同的目录,并有着不同的格式,这让找出一个地方来统一管理所有配置变的不太现实。更糟的是,这些格式通常是语言或框架特定的。
环境变量可以非常方便地在不同的部署间做修改,却不动一行代码;与配置文件不同,不小心把它们签入代码库的概率微乎其微;与一些传统的解决配置问题的机制(比如 Java 的属性配置文件)相比,环境变量与语言和系统无关。
环境变量的粒度要足够小,且相对独立。它们永远也不会组合成一个所谓的“环境”,而是独立存在于每个部署之中。当应用程序不断扩展,需要更多种类的部署时,这种配置管理方式能够做到平滑过渡
4.把后端服务(backing services)当作附加资源
后端服务是指程序运行所需要的通过网络调用的各种服务,如数据库(MySQL,CouchDB),消息/队列系统(RabbitMQ,Beanstalkd),SMTP 邮件发送服务(Postfix),以及缓存系统(Memcached)。本地服务之外,应用程序有可能使用了第三方发布和管理的服务。
每个不同的后端服务是一份 资源 。例如,一个 MySQL 数据库是一个资源,两个 MySQL 数据库(用来数据分区)就被当作是 2 个不同的资源。12-Factor 应用将这些数据库都视作 附加资源 ,这些资源和它们附属的部署保持松耦合。部署可以按需加载或卸载资源。例如,如果应用的数据库服务由于硬件问题出现异常,管理员可以从最近的备份中恢复一个数据库,卸载当前的数据库,然后加载新的数据库 – 整个过程都不需要修改代码。
5.严格分离构建和运行
基准代码 转化为一份部署(非开发环境)需要以下三个阶段:
- 构建阶段 是指将代码仓库转化为可执行包的过程。构建时会使用指定版本的代码,获取和打包 依赖项,编译成二进制文件和资源文件。
- 发布阶段 会将构建的结果和当前部署所需 配置 相结合,并能够立刻在运行环境中投入使用。
- 运行阶段 (或者说“运行时”)是指针对选定的发布版本,在执行环境中启动一系列应用程序 进程。
新的代码在部署之前,需要开发人员触发构建操作。但是,运行阶段不一定需要人为触发,而是可以自动进行。如服务器重启,或是进程管理器重启了一个崩溃的进程。因此,运行阶段应该保持尽可能少的模块,这样假设半夜发生系统故障而开发人员又捉襟见肘也不会引起太大问题。构建阶段是可以相对复杂一些的,因为错误信息能够立刻展示在开发人员面前,从而得到妥善处理。
6.以一个或多个无状态进程运行应用
简单的场景下,代码是一个独立的脚本,运行环境是开发人员自己的笔记本电脑,进程由一条命令行(例如 python my_script.py)。另外一个极端情况是,复杂的应用可能会使用很多 进程类型 ,也就是零个或多个进程实例。也就是我们运行的进程在执行的过程不考虑执行的内容需不需要保留给下一次操作。将来的请求多半会由其他进程来服务。
应用的进程必须无状态且无共享。 任何需要持久化的数据都要存储在 后端服务内,比如数据库。需要共享的数据要存储在后端服务当中。 一些互联网系统依赖于 “粘性 session”, 这是指将用户 session 中的数据缓存至某进程的内存中,(缓存用户的浏览器中间) Session 中的数据应该保存在诸如 Memcached 或 Redis 这样的带有过期时间的缓存中。
7.通过端口绑定(Port binding)来提供服务
互联网应用 通过端口绑定来提供服务,该服务将监听所有发送到这个端口请求。
- 本地环境中,开发人员通过类似 http://localhost:5000/的地址来访问服务。在线上环境中,请求统一发送至公共域名而后路由至绑定了端口的网络进程。
- 端口绑定这种方式 一个服务可以为另外一个服务提供后端服务。调用方将使用服务方提供的 URL.
8.通过进程模型进行扩展
应用的进程所具备的无共享,水平分区的特性 意味着添加并发会变得简单而稳妥。这些进程的类型以及每个类型中进程的数量就被称作 进程构成 。
9.快速启动和优雅终止可最大化健壮性
进程 是 易处理(disposable)的,意思是说它们可以瞬间开启或停止。这有利于快速、弹性的伸缩应用,迅速部署变化的 代码 或 配置 ,稳健的部署应用。进程应当追求 最小启动时间 。 理想状态下,进程从敲下命令到真正启动并等待请求的时间应该只需很短的时间。更少的启动时间提供了更敏捷的 发布 以及扩展过程,此外还增加了健壮性,因为进程管理器可以在授权情形下容易的将进程搬到新的物理机器上。
对于普通进程来说 一旦接收 终止信号(SIGTERM) 就会优雅的终止 。网络进程的优雅终止是:某个服务与某个端口绑定,那么终止的时候,该服务将拒绝所有发送到该端口的请求,直到把已经接收到的请求全部处理完毕后终止。 对于 worker 进程来说,当前正在执行的所有任务全都回退到任务队列。隐含的要求是:“任务应该是可以重复执行的”。实现主要是通过“把结果包装进入事务”
10.尽可能的保持开发,预发布,线上环境相同
从以往经验来看,开发环境(即开发人员的本地 部署)和线上环境(外部用户访问的真实部署)之间存在着很多差异。这些差异表现在以下三个方面:
时间差异: 开发人员正在编写的代码可能需要几天,几周,甚至几个月才会上线。 人员差异: 开发人员编写代码,运维人员部署代码。 工具差异: 开发人员或许使用 Nginx,SQLite,OS X,而线上环境使用 Apache,MySQL 以及 Linux。
持续部署 就必须缩小本地与线上差异。 再回头看上面所描述的三个差异: 缩小时间差异:开发人员可以几小时,甚至几分钟就部署代码。 缩小人员差异:开发人员不只要编写代码,更应该密切参与部署过程以及代码在线上的表现。 缩小工具差异:尽量保证开发环境以及线上环境的一致性。像 Vagrant 这样轻量的虚拟环境就可以使得开发人员的本地环境与线上环境无限接近
开发人员有时会觉得在本地环境中使用轻量的后端服务具有很强的吸引力,而那些更重量级的健壮的后端服务应该使用在生产环境。例如,本地使用 SQLite 线上使用 PostgreSQL;又如本地缓存在进程内存中而线上存入 Memcached。
11.把日志当作事件流
不应该试图去写或者管理日志文件。相反,每一个运行的进程都会直接的标准输出(stdout)事件流。在预发布或线上部署中,每个进程的输出流由运行环境截获,并将其他输出流整理在一起,然后一并发送给一个或多个最终的处理程序,用于查看或是长期存档。这些存档路径对于应用来说不可见也不可配置,而是完全交给程序的运行环境管理。类似 Logplex 和 Fluentd 的开源工具可以达到这个目的。这些事件流可以输出至文件,或者在终端实时观察。最重要的,输出流可以发送到 Splunk 这样的日志索引及分析系统,或 Hadoop/Hive 这样的通用数据存储系统. 找出过去一段时间特殊的事件。 图形化一个大规模的趋势,比如每分钟的请求量。 根据用户定义的条件实时触发警报,比如每分钟的报错超过某个警戒线。
12.后台管理任务当作一次性进程运行
一次性管理进程应该和正常的 常驻进程 使用同样的环境。这些管理进程和任何其他的进程一样使用相同的 代码 和 配置 ,基于某个 发布版本 运行。后台管理代码应该随其他应用程序代码一起发布,从而避免同步问题。 所有进程类型应该使用同样的 依赖隔离 技术。