现代软件通常以Web服务的方式交付,称为软件即服务(SaaS),十二因子指导原则即是构建SaaS应用的一套方法论。其不仅是构建SaaS应用的指导原则,也是微服务,云原生应用开发须遵循的指导原则。
十二因子指导原则或者最佳实践以期达到:
使用统一的规范,可以使新进开发人员节省时间成本,按照最佳实践走即可;
应用应与底层操作系统解耦,以在各种运行环境之间提供最大的可移植性;
应用应适于部署在现代化云平台上,摒弃对服务器和系统管理的依赖;
开发环境与生产环境之间不应有很多差异,可以以最大化的敏捷性进行持续部署;
可以在不对工具,架构,开发实践进行重大改变的情况下进行自由扩展。
十二因子指导原则对应用的开发语言及后端服务的类型均没有限制,是一套统一的方法论。
I 代码库
一个应用使用一个代码库,一个代码库支持多个部署环境。
十二因子应用总是使用版本控制系统(Git,SVN等)来作代码跟踪的。一个应用应有一个代码库,一个代码库存储一套代码,可以有多个版本。不同的部署环境可以使用一个代码库上的不同版本。
实践中也是这样做的,如我们使用Git来托管代码,一个应用使用一个仓库,不同的应用不应使用一个仓库,而是将依赖关系拆出来,然后分成不同的仓库。每到一个版本开发完毕,我们在仓库上打Tag或者新建Release分支,然后逐步升级开发,测试,类生产,生产环境。
II 依赖
显式的声明并抽出依赖。
绝大多数工具都提供打包功能,依赖可以全局安装或者安装在应用的指定文件夹,如nodejs的site-packages,Golang的vendoring等。
十二因子指导原则建议应用绝不要隐式依赖系统的全局包。要将应用的所有依赖通过依赖配置文件显式的,完整的,准确的声明出来。且要使用依赖辅助工具将所有依赖抽出来,使用统一的完整的显式的依赖贯穿开发及生产。
使用显式的依赖声明也简化了新人开发应用的准备步骤。开发者只需将代码检出,并安装必须的语言运行时及依赖管理软件即可。如我们使用Maven构建Java项目,依赖都声明在工程的pom.xml文件了,开发只要将代码拿下来,mvn package即可打包。
十二因子指导原则建议不要隐式的依赖任何系统工具。如我们应用需要curl,即便大多数系统都自带这个工具,但也保不齐版本或者兼容性不一致,若应用对这个工具强依赖,就该考虑将其打进应用中,或者使用Docker方式构建。
III 配置
将配置存在环境变量中。
一个应用的配置在不同环境之间是不同的。如:
连接数据库,缓存或其它后端服务的配置信息;
连接诸如AWS s3等外部服务的密钥信息;
部署时需要的特定配置信息,如域名等。
应用有时将这些配置信息作为常量保存在代码中,这是违背十二因子指导原则的。应将配置与代码严格的分离,配置随不同的部署环境发生变化,而代码却只需一套。
一个检测应用是否将所有配置信息从代码中抽出来的方法即是你的代码仓库是否可以随时开源,而无需担心有密钥信息暴露出来。
当然这里配置的定义并不包含应用内部的配置,如Spring的bean配置信息,这类配置并不随部署环境变化,理应放在代码中。
另一个方法是使用配置文件,但不将其纳入版本控制。这相比在代码中使用常量已有很大进步,但仍有诸多弊端:
易于错将配置文件提到仓库;
易将配置文件以不同的格式放置在不同的地方,不便于统一管理;
配置文件格式可能与语言或框架相关。
根据十二因子指导原则,应将应用配置存在环境变量中。环境变量在不改变代码的情况下可以根据不同部署环境而改变。也不会误将其提到代码仓库,并且其与常规配置文件不同的是其不受语言或操作系统限制。
另一个办法是将配置文件分组,如建立开发,测试,生产目录,将不同配置放在不同的环境目录下。这样也不好,随着后续环境的增加,管理起来也挺麻烦。
综上,环境变量是一个粒度恰当的控制办法,其随每次部署独立管理。当环境增多时,可以做到平滑的扩展。
IV 后端服务
将后端服务看作附加资源。
后端服务可以是被应用通过网络来消费的任意服务,这是其常规操作的一部分。诸如MySQL数据库,RabbitMQ队列,Memcached缓存等都是后端服务。
诸如数据库等的后端服务通常同样由部署应用运行时的系统管理员所管理。除了本地管理的服务以外,也可能有三方组织所提供及管理的服务。诸如指标信息收集服务New Relic,二进制资产服务AWS s3,甚至通过API访问的服务Twitter等。
十二因子指导原则有一条准则是应用对本地或者三方服务不应有任何区别,都应看作是可以通过URL或者密钥访问的附加资源。应用能够将本地MySQL数据库换成诸如AWS RDS等三方数据库而无需任何代码变更,而仅需改一下配置即可。
每一个后端服务即是一个资源。两个MySQL实例即是两个资源,其与部署环境是解耦的。资源是可以随部署意愿进行附加或移除的。如生产环境应用使用的一个数据库实例坏掉了,那管理员可以基于其最近一次备份新建一个新的实例顶上去,而无需变更代码。
V 构建,发布及运行
严格将构建与运行阶段分离。
代码库通过如下三步来进行部署:
部署过程从代码的某次提交点拉一个版本出来,构建即是获取该版本的依赖并且将其编译为二进制资产。
发布阶段组合可以在运行环境立即执行的包与配置。
运行应用对应版本的进程。
十二因子指导原则严格将构建,发布,运行阶段分离。诸如,我们无法在运行阶段修改代码,因我们无法将这些变更传回到构建阶段。
一些典型的提供发布管理的工具,最显著的能力即是支持回滚到上一个版本。如,Capistrano部署工具将发布版本存储在releases子目录下,当前版本是当前发布文件夹的一个链接,其回滚命令即很容易使其回到上一个版本。
每次发布应有一个唯一的发布ID,诸如一个发布时间戳(2020-03-20-20:32:17
)或一个增长的数值(v100)。发布版本只可叠加且一旦创建即不可修改,任何变更必须新建一个发布版本。
VI 进程
以一个或多个无状态进程运行应用。
先说一个简单的运行场景:代码为一个独立的脚本,运行环境是开发者本机且已安装对应语言的运行时,这样我们即可通过一条命令(如:python start.py
)来启动应用进程。其它极端情况下,一个复杂应用的生产部署可能会使用多种进程类型:实例化为0个或多个进程。
十二因子建议应用为无状态的且不要共享任何资源。任何需要持久化的数据存到有状态的后端服务(通常为数据库)就好了。
十二因子不会假想任何在内存或硬盘上的缓存数据在后续的请求被使用。因应用运行为多个进程,后续的请求不一定打到哪个进程上,即便只有一个运行进程,也保不齐一次重启即会丢掉所有数据。
在运行环境使用文件系统的缓存来加速编译也是不建议的。十二因子建议将打包放在构建阶段,这样诸如maven package
等工具即可在该阶段将包打好,运行阶段用就好了。
此外,一些Web系统依赖“粘性Session”,即将用户Session数据缓存到应用进程的内存中。这个与上面一样,多应用进程无法保证下一次请求就正好打到这个节点上。还是建议将Session状态数据存到诸如支持时间过期的Memcached,Redis等数据存储服务上。
VII 端口绑定
通过端口绑定暴露服务。
Web应用有时在Web容器内运行,诸如Java应用在Tomcat中运行等。
十二因子建议应用完全自包含。不要依赖运行时注入以创建Web接口服务。即Web应用通过绑定端口来暴露为HTTP服务,以监听打到该端口的请求。
如在本地环境,开发通过访问http://localhost:5000/
来访问应用服务。在生产,通过公共域名来访问端口绑定的Web服务进程。
当然,不仅HTTP服务可以通过端口绑定来暴露服务以被访问。其它4层服务也可以通过端口绑定来接收请求(如:Redis等)。
此外,一个应用还可以通过端口绑定成为另一个应用的后端服务,如通过提供URL被其它应用作为资源服务使用。
VIII 并发
通过进程模型进行横向扩展。
任何计算机程序,运行都是以一个或多个进程来表示的。Web应用有多种进程运行方式。如,PHP进程以Apache的子进程方式运行,随请求容量按需启动。而Java进程则相反,JVM启动时保留一块大的系统资源(CPU和内存)以提供一个大的进程,而内部使用线程来进行并发。此两种情况,应用开发者所见的最小单位都仅是进程。
在十二因子应用中,进程是一等公民。吸纳了Unix守护进程模型的思想。采用该模型,开发者通过对不同类型的工作分配不同的进程类型即可以使应用处理不同的工作载荷。如,Web进程处理HTTP请求,后台进程处理长任务。
这并不与独立进程进行内部多路复用(运行时虚拟机内部进行线程方式并发,或诸如Node.js的异步事件模型等)相悖,但虚拟机仅能纵向扩展,所以,应用必须同样能够横向扩展,以支持将多个进程运行在多个物理机上。
进程模型会在横向扩展时大放异彩。对于不共享任何资源还支持水平分区的十二因子应用来说,支持并发是简单可靠的操作。
十二因子应用不应作为守护进程也不要写PID文件。相反,应该交给操作系统进程管理器(诸如分布式进程管理器systemd)来管理输出流,以处理进程崩溃以及用户发起的重启与停机。
IX 可便性
使用快速启动及优雅停止来最大化健壮性。
十二因子应用的进程是非常可便的,其可在某时按需启停。这样即可支持快速弹性扩展,代码及配置变更后快速部署。
进程应做到尽量缩短启动时间。理想情况下,从执行命令到进程可用以便接收请求或处理任务只需花费几秒种。这样即可对进程部署及扩容提供更好的敏捷性,而必要时将进程快速移至新的物理机即提供了更强的健壮性。
当遇到终止信号时进程应优雅的终止。对于Web应用来说,优雅的终止是在接收到终止命令时,当将当前请求处理完毕再退出,然后停止监听服务端口的流量。一般来说HTTP请求极短,一般不超过几秒,而对于长轮询场景,当连接断开后,客户端当尝试重连以实现对用户无感知。
对于工作进程来说,优雅的停止是通过将当前任务返回到工作队列来实现的。如在Beanstalkd,当一个工作进程断开时,将任务自动返回到队列中。
进程还当对突然死掉的情形(如硬件故障)作应对以达到更好的健壮性。如使用Beanstalkd后端队列,其可在客户端断开或超时后将任务返回到队列。
X 开发环境与生产环境的相似性
开发环境,测试环境及生产环境越相似越好。
由于历史原因,开发环境与测试环境是有鸿沟的。诸如:
时间鸿沟,开发的代码可能很久才上线生产;
个人鸿沟,开发写代码,运维部署代码;
工具鸿沟,开发环境与生产环境使用的技术栈有差别(开发环境使用Nginx,MySQL,OS X;生产环境使用Apache,SQLite,Linux)。
十二因子应用建议设计时当考虑持续部署,将开发与生产的差别保持的越小越好。再看上面的3个鸿沟:
解决时间鸿沟,开发者开发了代码,几分钟即部署到生产;
解决个人鸿沟,DevOps打通,写代码的人要关注部署;
解决工具鸿沟,开发环境与生产环境越接近越好。
同时,十二因子应避免开发在开发环境及生产环境使用不同的后端服务。
XI 日志
将日志看作事件流。
日志提供了对一个运行中应用行为的可见性。在基于服务器的环境中日志通常会写到诸如logfile的磁盘文件,但这仅是一种输出方式。
收集所有运行进程及后端服务的输出流,然后将其按时间序组合起来即为日志流。日志原始即是一行一个事件的文本格式(出现异常堆栈时可能会有多行),其没有固定开头及结尾,但只要应用有操作就会有连续的日志。
十二因子应用建议不要自己路由或存储输出流。即不要尝试自己写日志文件,而应让每个运行进程将日志写到stdout。在本地开发中,开发者可以在自己的终端来查看日志以观察应用的行为。
在测试及开发环境,每个进程的输出会被运行环境捕获,然后存档到某些位置以备查看。存档位置不应由应用来配置,而应交给运行环境。开源的日志路由(诸如Fluent)即是做这些事情的。
将日志发到诸如Splunk的检索分析系统有如下好处:
检索之前的特定事件;
绘制流量趋势图;
按用户定义规则来进行告警(如每分钟错误数超过某阈值即告警)。
XII 管理类进程
将管理类任务作为一次性进程运行。
除了运行常规任务(处理Web请求)的进程之外,开发者经常有对应用运行管理及维护的意愿,诸如:
运行数据库迁移任务;
运行控制台任务以执行代码或对线上数据库作检查;
运行一次性脚本(如php scripts/fix_bad_records.php
)。
一次性管理进程当与常规常驻进程使用一样的环境。即管理进程与其它进程使用相同的代码和配置,且管理代码随应用程序一起发布,从而规避不同步问题。
所有进程类型应使用相同的依赖隔离技术。如,使用bundle exec thin start
运行Ruby Web进程,使用bundle exec rake db:migrate
执行数据迁移。
参考资料
[1] https://en.wikipedia.org/wiki/Twelve-Factor_App_methodology