云原生时代下的12-factor应用与实践
在云的时代,应用会更多地迁移到云端,基于云的架构设计和开发模式需要一套全新的理念去承载,于是云原生思想应运而生,而针对云原生应用开发的最佳实践原则,12-Factor脱颖而出,同时也带来了新的解读。本次分享将结合 Docker等技术,介绍在 Cloud Native时代下,如何一一实践 12-Factor原则。
云原生
云原生(Cloud Native)是由 Pivotal 的 Matt Stine在 2013年提出的一个概念,是他多年的架构和咨询总结出来的一个思想的集合。
那怎么去理解云原生应用?我觉得可以从三个角度来说明,这和云计算平台的三个层次不谋而合,如下图:
IaaS看做基础云设施,用来提供各种基础资源 (Infrastructure)
PaaS作为开发平台,用来提供各种平台服务 (Platform)
SaaS交付应用或服务,直面用户,提供应用价值 (Application)
云原生应用,正好契合了云、平台和服务,一层层建构,所以我通常就把它理解为面向云(平台)来设计我们的应用。网易三拾众筹的架构师陈晓辉还为它起了一个小清新的名字——向云而生,我觉得非常贴切,再通俗一点讲,也可以叫做云平台应用。
12-Factor
12-Factor,是由 Heroku创始人 Adam Wiggins首次提出并开源,并由众多经验丰富的开发者共同完善,这综合了他们关于 SaaS应用几乎所有的经验和智慧,是开发此类应用的理想实践标准。
12-Factor 全称叫 The Twelve-Factor App,它定义了一个优雅的互联网应用在设计过程中,需要遵循的一些基本原则,和 Cloud-Native 有异曲同工之处。其中文翻译不少,我觉得“十二要素”或“十二原则”比较贴切。
那具体有哪十二原则了,见下图:
在接下来的应用和实践当中,我们会一一实践每条原则。
注:虽然 12-Factor 的原文书籍都是发布在其官网上,但因为网络问题和格式问题,不是很方便阅读,我将其转化为了 GitBook 格式,并架设在网易云基础服务(蜂巢)平台上,同时开源在 GitHub 上,方便大家阅读和下载:
在线阅读地址:
http://12.bingohuang.com/zh_cn/index.html
GitHub 开源地址:
https://github.com/bingohuang/12factor-gitbook
pdf/epub下载地址:
https://github.com/bingohuang/12factor-gitbook/download
GitBook 地址:
https://www.gitbook.com/book/bingohuang/12factor/details
应用与实践
既然 12-factor作为 SaaS开发的最佳实践原则,当然脱离不了实践,接下来我们就来设计一款云原生应用,并依照 12-factor,一步步验证和升级我们的应用。从中,我们将讲解每个 Factor的要点,以及如何在我们的应用中实践 Factor。
应用准备
这是一个面向云平台设计的简单 Web应用,它将暴露一个 HTTP REST风格的接口,可以实现对 user 的增删改查功能,将用到以下技术栈:
1. 基于 Node.js,用 Node.js 写 Web应用非常方便,而且是当今最火的编程平台之一。
下载安装 Node.js (包含 npm):https://nodejs.org/zh-cn/download/
注:Node 版本只要 v4.4 以上就够用,当前最新的稳定版是 v6.9.5, 我本地的版本是 v5.12.0
2. 基于 Sails,类似 Rails 框架,用于快速开发 Node.js 应用:http://sailsjs.com/
安装 Sails 框架最新版:
npm install sails -g
3. 基于 mongo 3.2 :https://docs.mongodb.org/manual/installation/
4. 基于 Docker,非常契合 12-Factor理念,作为我们打包、发布、运行的工具。
安装 Docker:https://docs.docker.com/engine/installation/
5. 以上环境安装好之后,就开始初始化我们的应用并运行,应用的名称就叫:12factor-app。
$ sails new 12factor-app
info: Created a new Sails app `12factor-app`!
$ cd 12factor-app
$ sails generate api user
info: Created a new api!
$ npm start
注:本文源代码放在 GitHub上,请参考文后参考链接
仅需 4条命令就搞定了应用的框架代码,并自动生成了基于 user的 CRUD接口,我们已经将应用启动起来,可以通过以下方式本地调试接口:
控制台输出正常,在浏览器中访问下面链接,即可看到 Sails应用的首页:http://localhost:1337
接着,就可以通过本地 curl命令或者 http工具来做接口调试,这里以常规的增删改查为例:
1. 增加一个新用户
$ curl -XPOST http://localhost:1337/user?name=bingo
{
"name": "bingo",
"createdAt": "2017-02-13T06:13:53.791Z",
"updatedAt": "2017-02-13T06:13:53.791Z",
"id": 58a41d952f53291200b9e065
}
2. 获取用户列表
$ curl http://localhost:1337/user
[
{
"name": "bingo",
"createdAt": "2017-02-13T06:13:53.791Z",
"updatedAt": "2017-02-13T06:13:53.791Z",
"id": 58a41d952f53291200b9e065
}
]
3. 修改一个用户
curl -XPUT http://localhost:1337/user/58a41d952f53291200b9e065?name=bingohuang
{
"name": "bingohuang",
"createdAt": "2017-02-13T06:13:53.791Z",
"updatedAt": "2017-02-13T06:14:13.460Z",
"id": 58a41d952f53291200b9e065
}
4. 删除一个用户
curl -XDELETE http://localhost:1337/user/58a41d952f53291200b9e065
我已经将该应用部署到了网易云基础服务(蜂巢)在线平台,如果您对这个应用感兴趣,直接将 localhost替换为 59.111.110.95,一样可以体验 CRUD操作,如下所示,只要把 yourname换成您的名字即可(建议在 PC端操作):
# 注册你自己
curl -XPOST http://59.111.110.95:1337/user?name=yourname
# 查看所有注册过的用户
curl http://59.111.110.95:1337/user
# 或者 PC浏览器直接访问 http://59.111.110.95:1337/user
接下来开始就让我们开始一一实践 12-Factor中的每条原则吧,每个原则中我们将分为 Factor解说和 Factor实践两块。
1
基准代码
Factor解说:
12-Factor应用只有一份基准代码(Codebase),可以多份部署(deploy)。
意思就是说一个应用只有一份用来跟踪所有修订版本的代码仓库,基准代码和应用之间总是保持一一对应的关系,因为:
○
一旦有多个基准代码,就不能称为一个应用,而是一个分布式系统。分布式系统中的每一个组件都是一个应用,每一个应用可以分别使用 12-Factor 进行开发。
○
多个应用共享一份基准代码是有悖于 12-Factor 原则的。解决方案是将共享的代码拆分为独立的类库,然后使用依赖管理(第二个原则) 策略去加载它们。
○
多份部署相当于是运行了该应用的多个实例,比如开发环境一个实例,测试环境、生产环境都有一个实例。
○
一个代码仓库,确保了单一的信任源,从而保证了更少的配置错误和更强的容错和复原能力。
Factor实践:
使用 Git作为应用的版本管理系统,使用 GitHub我们的在线仓库。
在刚刚创建好的应用目录下执行:
$ echo "# 12factor-app" >> README.md
$ git init
$ git add .
$ git commit -m "first commit"
$ git remote add origin git@github.com:bingohuang/12factor-app.git
$ git push -u origin master
2
依赖
Factor解说:
12-Factor规则下的应用程序不会隐式依赖系统级的类库。
意思就是说:通过依赖清单声明所有依赖项,通过依赖隔离工具确保程序不会调用系统中存在但清单中未声明的依赖项。并统一应用到生产和开发环境。
云平台根据这些声明管理依赖,确保云应用所需的库和服务。
Factor实践:
package.json 就是我们的依赖清单,所有应用程序的依赖都声明在此。
{
"name": "12factor-app",
"private": true,
"version": "0.0.0",
"description": "a Sails application",
"keywords": [],
"dependencies": {
"ejs": "2.3.4",
"grunt": "1.0.1",
"grunt-contrib-clean": "1.0.0",
"grunt-contrib-coffee": "1.0.0",
"grunt-contrib-concat": "1.0.1",
"grunt-contrib-copy": "1.0.0",
"grunt-contrib-cssmin": "1.0.1",
"grunt-contrib-jst": "1.0.0",
"grunt-contrib-less": "1.3.0",
"grunt-contrib-uglify": "1.0.1",
"grunt-contrib-watch": "1.0.0",
"grunt-sails-linker": "~0.10.1",
"grunt-sync": "0.5.2",
"include-all": "^1.0.0",
"rc": "1.0.1",
"sails": "~0.12.11",
"sails-disk": "~0.10.9"
},
"scripts": {
"debug": "node debug app.js",
"start": "node app.js"
},
"main": "app.js",
"repository": {
"type": "git",
"url": "git://github.com/bingo/12factor-app.git"
},
"author": "bingo",
"license": ""
}
# 接下来我们加入 mongodb的库依赖(后续会用到),只需要执行:
npm install sails-mongo --save
# 同时 package.json 中会有相应的变更
{
...
"dependencies": {
...
"sails-mongo": "^0.12.2" //最新加入的依赖
}
...
}
应用程序需要用到的依赖库都安装在 node_modules文件夹下,该文件夹就是作为应用的依赖隔离,并且和系统的库是隔离的。
3
配置
Factor解说:
12-Factor推荐将应用的配置存储于环境变量中,保证配置排除在代码之外,有如下好处:
环境变量是一种清楚、容易理解和标准化的配置方法;
环境变量可以非常方便地在不同的部署间做修改,却不动一行代码;
与配置文件不同,不小心把它们签入代码库的概率微乎其微;
与一些传统的解决配置问题的机制(比如 Java 的属性配置文件)相比,环境变量与语言和系统无关;
存储在环境变量中的另一个好处是,方便和 Docker配合使用。
一个技巧:判断一个应用是否正确地将配置排除在代码之外,一个简单的方法是看该应用可以立刻开源,而不用担心会暴露任何敏感的信息。
Factor实践:
在应用程序的 config/connections.js 文件中,我们使用 MONGO_URL 这个环境变量来定义 mongo 的连接方式。
module.exports.connections = {
mongo: {
adapter: 'sails-mongo',
url: process.env.MONGO_URL
}
};
在文件中,指定 module所使用的连接。
module.exports.models = {
connection: mongo,
migrate: 'safe'
};
如果你在本地起了一个 mongodb测试服务,就可以用这个命令验证应用是否正常配置。
MONGO_URL=mongodb://localhost:27017/12factor-app npm start
4
后端服务
Factor解说:
12-Factor 应用不会区别对待本地或第三方服务,统一把后端服务 (backing services)当作附加资源或者说是远程的资源。
所谓后端服务是指程序运行所需要的通过网络调用的各种服务,如数据库(MySQL),消息/队列系统(RabbitMQ),SMTP 邮件发送服务( Postfix),以及缓存系统(Memcached)等。
除了本地服务之外,应用程序有可能使用了第三方发布和管理的服务,如 SMTP(例如 Postmark),数据收集服务,数据存储服务(如 Amazon S3),以及使用 API 访问的服务(例如 Twitter)等。
对应用程序而言,本地或第三方服务都是附加资源,通过一个 url 或是其他存储在 配置中的设置来获取数据,仅需修改配置中的资源地址即可。
应用也因此具有容错和复原能力,因为它一方面要求编码时就要考虑资源不可用的情况,另外一方面也增强微服务方法的好处。
Factor实践:
对我们的应用程序来说,用到的后端服务就是 MongoDB 数据库。我们正是通过 MONGO_URL 来传递 MongoDB 的资源地址,从而实现了后端服务和应用程序的解耦。
如果当前这个 MongoDB 实例出问题了,我们可以通过设置 MONGO_URL 这个环境变量,很方便的切换一个新的实例。
5
构建,发布,运行
Factor解说:
12-Factor 应用严格区分构建,发布,运行这三个步骤。
Cloud Native应用的构建流程把大部分发布配置挪到开发阶段,包括实际的代码构建和运行应用所需的生产环境配置。
举例来说,直接修改处于运行状态的代码是非常不可取的做法,因为这些修改很难再同步回构建步骤。
发布的版本就像一本只能追加的账本,一旦发布就不可修改,任何的变动都应该产生一个新的发布版本。
Factor实践:
针对这条原则,强烈推荐使用 Docker及其组件(Compose),它的核心理念正是:Build, Ship and Run,将适合在整个构建、发布和运行流程,我们也将从这三个方面进行讲解。
① 构建:
书写构建脚本:Dockerfile
FROM hub.c.163.com/library/node:5.12.0
MAINTAINER bingohuang <me@bingohuang.com>
# 拷贝依赖清单
COPY package.json /tmp/package.json
# 安装依赖包
RUN cd /tmp && npm install --registry=https://registry.npm.taobao.org
# 将依赖包拷贝到应用程序目录下
RUN mkdir /app && cp -a /tmp/node_modules /app/
# 更改工作目录
WORKDIR /app
# 拷贝应用程序代码
COPY . /app
# 设置应用启动端口
ENV PORT 1337
# 暴露应用程序端口
EXPOSE 1337
# 启动应用
CMD ["npm","start"]
Docker 构建
$ docker build -t 12factor-app:v1.0 .
Docker 镜像推送:可以将其 push 到指定的镜像仓库,比如网易云基础服务(蜂巢)的镜像仓库中。
$ docker push hub.c.163.com/bingohuang/12factor-app:1.0
② 发布:
书写发布脚本:docker-compose.yml
version: '2'
services:
mongo:
image: hub.c.163.com/library/mongo:3.2
volumes:
- mongo-data:/data/db
ports:
- "27017:27017"
app:
image: hub.c.163.com/bingohuang/12factor-app:1.0
ports:
- "1337:1337"
links:
- mongo
depends_on:
- mongo
environment:
- MONGO_URL=mongodb://mongo/12factor-app
volumes:
mongo-data:
以上在构建好的镜像基础上,定义了一个发布过程,并将配置(MONGO_URL)通过环境变量注入进去。
MONGO_URL=mongodb://mongo/12factor-app
③ 运行:
可以通过 Docker Compose在本地运行,也可以通过云平台来在线编排(网易云基础服务即将支持服务编排功能)。
docker-compose up -d
继而查看日志
docker-compose logs -f
注:为了方便不熟悉 docker和 docker-compose命令的人快速运行程序和本地调试,我在源代码中还提供了 docker.sh 脚本,方便构建、发布和运行应用(源码请看后续资料链接)。
6
进程
Factor解说:
12-Factor 应用的进程必须无状态且无共享。
任何需要持久化的数据都要存储在后端服务内,比如数据库。Session 中的数据应该保存在诸如 Memcached 或 Redis 这样的带有过期时间的缓存中。
运行环境中,应用程序通常是以一个和多个进程 运行的。
最简单的场景中,代码是一个独立的脚本,运行环境是开发人员自己的笔记本电脑,进程由一条命令行(例如 python my_script.py)。另外一个极端情况是,复杂的应用可能会使用很多 进程类型 ,也就是零个或多个进程实例。
这么做是为了保证 Cloud Native基础设施的速度和效率。
Factor实践:
虽然这是一个简单的 demo应用,但查看 docker容器中的运行进程,发现也有4个进程在运行,其中 npm也就是我们的启动进程,`node app.js` 是实际运行应用的进程。
$ docker exec 12-factor ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.2 2.0 1076204 42024 ? Ssl 18:22 0:00 npm
root 17 0.0 0.0 4340 724 ? S 18:22 0:00 sh -c node app.js
root 18 0.9 4.5 1253808 93808 ? Sl 18:22 0:01 node app.js
root 27 1.1 3.7 962884 77076 ? Sl 18:22 0:01 grunt
在这里,我们的应用进程是无状态的,持久化的数据都存储在了后端服务 MongoDB 当中。
7
端口绑定
Factor解说:
12-Factor 应用通过自我加载而不依赖于任何网络服务器就可以创建一个面向网络的服务。
意思就是说:Web应用通过端口绑定 (Port binding)来提供服务 ,并监听发送至该端口的请求。Cloud Native应用的服务接口优先选择 HTTP API 作为通用的集成框架。
还要指出的是,端口绑定这种方式也意味着一个应用可以成为另外一个应用的 后端服务 ,调用方将服务方提供的相应 URL 当作资源存入 配置 以备将来调用。
Factor实践:
docker-compose文件为我们很好的定义了端口绑定。
ports: "1337:1337" // 应用容器暴露 1337端口在容器中,宿主机将其映射到 1337端口。
需要注意的是,如果在一个宿主机中部署多个应用实例,就不能将一个宿主机端口映射到多个容器端口(端口冲突),解决方法是在这之上加一个负载均衡,负载宿主机的不同端口服务所对应的不同容器。
8
并发
Factor解说:
12-factor 应用通过进程模型进行扩展,把进程看作是一等公民,并且具备具备的无共享,水平分区的特性。
这意味着依赖底层平台就能实现横向扩展,不需要技术难度高的多线程编码。
举例来说,HTTP 请求可以交给 web 进程来处理,而常驻的后台工作则交由 worker 进程负责,定时任务交由 clock 来处理,这样扩展每一类的进程就非常方便,如下图所示:
Factor实践:
如第六个原则所描述,我们的应用拥有多个进程,最主要的是 Node.js 的http server进程,进程都是无状态并无共享,所以我们可以非常容易的水平扩展应用。
9
易处理
Factor解说:
12-Factor 应用的进程是易处理(disposable)的,意思是说任何进程都可以快速启动和优雅终止,这样做的好处是:
这有利于快速、弹性的伸缩应用,迅速部署变化的代码或配置,提高健壮性;
进程应当追求最小启动时间,可以提供了更敏捷的发布以及扩展过程;
进程一旦接收终止信号(SIGTERM) 就会优雅的终止。
如下图所示,就是一个优雅的应用启动和终止流程。
Factor实践:
Docker 先天的轻量级和隔离性,就非常适合来做快速启动和优雅终止,Docker非常适合实践这条原则,在我们的应用中,就加入了 Docker和Compose实践。
针对线上环境,推荐构建在容器云平台之上(比如网易云基础服务平台),可以更优雅的处理进程的启动和停止。
10
环境等价
Factor解说:
12-Factor 应用想要做到持续部署就必须缩小本地与线上差异,包括以下三种差异:
缩小时间差异:开发人员可以几小时,甚至几分钟就部署代码;
缩小人员差异:开发人员不只要编写代码,更应该密切参与部署过程以及代码在线上的表现;
缩小工具差异:尽量保证开发环境以及线上环境的一致性。
12-Factor 应用的开发人员应该反对在不同环境间使用不同的后端服务。
这是因为,不同的后端服务意味着会突然出现的不兼容,从而导致测试、预发布都正常的代码在线上出现问题。
Factor实践:
我们的应用程序中,使用了 docker-compose作为我们的发布脚本,它使得应用既可以在本地运行,也可以在任何支持 Docker 的云平台上运行,应用无需变化,只需修改配置文件,很好的解除了不同环境的差异化。
从以往经验来看,传统应用和 12-Factor应用会存在如下差异:
11
日志
Factor解说:
12-factor应用本身从不考虑存储自己的输出流。相反,每一个运行的进程都会直接的标准输出(stdout)事件流。
当日志是由云平台而不是应用包含的库处理时,日志处理机制必须保持简单。
Factor实践:
许多服务都能提供日志集中管理,比如 ELK、Splunk、Logentries,而且大多数都能方便的和 Docker集成在一起。
这里以 Logentries 为例来为应用集成日志服务,需要在 docker-compose 文件中加入 log 服务,如下:
log:
command: '-t a80277ea-4233-7785203ae328'
image: 'logentries/docker-logentries’
restart: always
tags:
- development
volumes:
- '/var/run/docker.sock:/var/run/docker.sock'
一个典型的 Logentries 面板界面如下:
12
管理进程
Factor解说:
开发人员经常希望执行一些管理或维护应用的一次性任务,例如:
运行数据移植
运行一个控制台也被称为 REPL shell,来执行一些代码或是针对线上数据库做一些检查。
运行一些提交到代码仓库的一次性脚本。
12-Factor应用中,一次性管理进程应该和正常的常驻进程(应用进程)使用同样的环境,并且使用相同的代码和配置,基于某个发布版本运行,随着其他的应用程序一起发布。
在 Cloud Native中,管理任务也是一个进程,而不是特别的工具;同样重要的是,管理任务的进程不应使用秘密的 API 或者内部机制。
Factor实践:
我们可以在 docker-compose 文件中定义管理服务,和程序一起执行。
我们可以通过通过docker exec命令执行一些管理任务,比如:
docker exec -ti ADMIN_CONTAINER_ID bash
如果多个容器处在相同的网络下,可以通过一个容器来管理其它容器。
总结
至此,12-Factor一一实践完毕,从中可以看出,12-Factor并非相互独立,而是一个整体,有的涉及代码和框架(Node和Rails),有的涉及工具(Docker和Compose)有的涉及架构和平台。在云原生时代,12-Factor仍然具有强大的生命力,每一条原则都是应用开发的珠玑,而且每一个原则也不是一成不变的,随着新的理念出现,原有的Factor会得到延伸和发展,也会出现新的原则,有兴趣的同学,不妨读一读《Beyond the 12 Factor App》这本书,还会有更大的收获。最后,希望此次分享对你理解云原生应用、实践 12-Factor有所帮助。
参考链接
源代码:
https://github.com/bingohuang/12factor-app
文章地址:
http://talks.bingohuang.com/2017/cloud-native-12factor.article
12-Factor 在线书籍:
http://12.bingohuang.com/zh_cn/index.html
12-Factor 书籍开源:
https://github.com/bingohuang/12factor-gitbook