1.5 十二要素应用
十二要素应用程序方法是Heroku的开发者起草的。十二要素中提到的特征并不特定于云提供者、平台或语言。这些要素代表了针对云环境中蓬勃发展的可移植弹性应用程序(特别是“软件即服务”应用程序)的一组准则或最佳实践。下面列出了这十二要素。
(1)一份基准代码(Codebase),多份部署(Deploy)
企业一般会采用代码版本控制系统来跟踪管理所有修订版本的代码库,这样就只需要一份代码,却可以同时存在多份部署,如图1-6所示。每份部署相当于运行了一个应用的实例。例如作者所在学校的Portal应用,学校有一个生产环境的部署,也有一个用于测试的预发布环境部署。学校将同一套代码先在预发布服务器上部署,进行初步评估,然后再发布到正式生产服务器上。

图1-6 一份基准代码多份部署
(2)显式声明依赖关系(Dependency)
应用程序不会隐式依赖系统级类库。它一定通过依赖清单确切地声明所有依赖项。此外,应用程序在运行过程中通过依赖隔离工具来确保程序不会调用系统中存在但清单中未声明的依赖项。这一做法在生产和开发环境中都是统一的。例如Node.js应用程序通过package.json来声明依赖项,Maven项目使用pom.xml来声明依赖项。遵循该要素的应用程序也不会隐式依赖系统级工具,例如ImageMagick和curl等工具,即使这些工具在Linux不同版本中都存在,但无法保证所有未来的系统都部署了该工具,例如Docker中的Linux就不一定存在这两个工具。如果要使用这些工具,则必须包括在应用程序中。
例如Drupal中的命令工具drush就包含在bin目录下。
(3)在环境中存储配置
十二要素应用推荐将应用的配置存储于环境变量中,如图1-7所示。这允许应用程序非常方便地在不同的部署间修改,而不需要改动一行代码。例如wget用的环境变量HTTP_PROXY。还有一种做法是使用配置文件但把该配置文件从版本控制中排除。例如Drupal站点的settings.php文件,里面含有数据库连接信息,为了保证安全,必须将该文件排除在版本控制外。但是有时仍然难免将该文件加入版本控制,从而造成了数据库连接信息泄露。

图1-7 在环境中存储配置
在Kubernetes中,系统会自动将服务根据名称创建多个不同的细粒度环境变量。
(4)把后端服务(backing services)当作附加资源
后端服务是指程序运行所需要的通过网络调用的各种服务,如数据库(MySQL、CouchDB),消息/队列系统(RabbitMQ、Beanstalkd),以及缓存系统(Memcached)。符合规则的应用程序应该可以在不进行任何代码改动的情况下,将本地数据库切换至异地或者云上的数据库服务。
(5)严格分离构建、发布和运行
基准代码通过构建、发布和运行三个阶段转化成一份部署。构建阶段是指将代码进行编译、打包等操作,生成可执行文件。而发布则是将构建的结果和相关配置发布到运行环境中投入使用。运行阶段则是在执行环境中启动一系列应用程序。
例如Node.js应用程序,其构建步骤较为简单,只需要复制相关文件即可。而发布到运行环境时,则通过npm install安装相关依赖项;在运行阶段可以通过Node.js进程管理工具pm2安装或者重启服务。
(6)以一个或多个无状态进程运行应用
符合十二要素的应用程序的进程必须是无状态且无共享的。任何需要持久化的数据都需要存储在后端服务内。例如Apereo CAS,所有认证的Ticket均保存在后端数据库中,如memcached集群。而需要处理的是session状态,这个也是需要通过后端memcached或者redis进行统一存储,或者通过前端负载均衡粘性路由到同一个应用进程中。
(7)通过端口绑定(Port binding)来提供服务
符合十二要素的应用程序可以自我加载而不依赖于任何网络服务器。例如Java代码可以直接使用JVM的Jetty,而不依赖于Tomcat。这一点就像Node.js不需要Apache一样。还是以Apereo CAS为例,最新版本的CAS可以自我加载运行,而不依赖Tomcat。如果查看常用软件的Dockerfile,会发现这些Docker的执行命令不再是以后端程序的方式运行,而是直接以前端运行。例如PHP的Docker镜像,命令为apache2-foreground。

(8)通过进程模型进行扩展
在十二要素应用中的进程主要借鉴了UNIX守护进程模型,不同的工作分配给不同类型的进程处理。尤其是无共享、水平分区的特性让并发处理更加简单。十二要素应用的进程不需要守护进程,也不需要写入PID文件,而是借助操作系统的进程管理器(如systemd)进行输出流控制、进程崩溃响应,以及进程的重启和关闭的请求。
(9)快速启动和优雅终止可最大化健壮性
十二要素应用的进程是可分解的(disposable),它可以瞬间开启或者停止。这有利于快速、弹性伸缩应用,迅速部署变化的代码或配置。进程应当追求最短的启动时间,一旦接收到终止信号则会优雅地终止。进程还应当合理处理意外终止,例如可以在客户端断开或者超时连接后自动退回任务。
(10)尽可能地保持开发、预发布和线上环境相同
开发人员可能使用Macintosh开发,也可能使用Windows开发,这就造成各种环境的不一致,尤其是开发环境和生产环境。即便同样的操作系统,也有可能随着时间变化、工具差异、人员差异等出现不一致。十二要素应用要求必须缩小本地和生产环境的差异,企业可以使用Docker来进行环境的统一,无论是开发环境还是生产环境,都使用Docker来进行测试和正式运行。
十二要素应用要求不同环境下的后端服务也要一致,例如开发环境使用MariaDB,则生产环境亦应使用MariaDB。
(11)把日志当作事件流
日志使得应用程序运行的动作变得透明。在基于服务器的环境中,日志通常被写在硬盘的一个文件里,但这只是一种输出格式。在十二要素应用中则不应该考虑存储到自己的输出流,不应该试图去写或者管理日志文件。相反,每一个运行的进程都会直接将日志存储到标准输出(stdout)事件流。开发环境中,开发人员可以通过这些数据流,实时在终端看到应用的活动。
这点在Docker环境下尤其如此。每个Docker中的应用不应该自己进行日志的管理,而应该直接提交给标准输出和标准错误事件流。下面是PHP镜像的Dockerfile代码:

这段代码改造了Apache2的日志,默认情况下,Apache2将访问日志写入access.log文件,错误日志写入error.log,而通过ln软链接命令,实现了将这些日志流直接重定向为标准事件流。这样,这些应用的日志流将被Docker或者Kubernetes捕获。
(12)后台管理任务当作一次性进程运行
进程构成(process formation)是指用来处理应用的常规业务(如处理Web请求)的一组进程。与此不同,开发人员经常希望执行一些管理或维护应用的一次性任务,例如开源软件OwnCloud。升级OwnCloud可以在Web界面下操作,但更建议通过occ upgrade命令进行升级。

在这个命令中,使用了Apache2进程的用户www-data,保证了命令行和Web方式的环境一致。这也是十二要素的要求:一次性管理进程应该和正常的常驻进程使用同样的环境。这些管理进程和任何其他进程一样使用相同的代码和配置,基于某个发布版本运行。后台管理代码应该随其他应用程序代码一起发布,从而避免同步问题。
要实现高质量的微服务环境,可以不用严格遵循这些要素。但是,通过牢记这些要素,用户可以在持续交付环境中构建和维护可移植应用程序或服务。这是非常重要的,读者一定要充分理解这十二要素。在本书后续的描述和案例中,读者可以看到十二要素的实战。