作者简介:黄庆兵,网易蜂巢首席技术布道师,浙大硕士毕业,从事云计算、Docker、Go等相关开发及技术布道工作;喜欢开源,乐于分享,勤于布道,折腾过开源小工具,制作过Docker课程,分享过 Gopher Meetup。欢迎一起来探讨技术!个人主页:http://bingohuang.com/
以下为正文:
1. 简介
在云的时代,应用会更多的迁移到云端,基于云的架构设计和开发模式需要一套全新的理念去承载,于是云原生思想应运而生,而针对云原生应用开发的最佳实践原则,12-Factor脱颖而出,同时也带来了新的解读。本文将介绍在Cloud Native时代下,结合Docker等技术,如何一一实践12-Factor原则。
2. 云原生
云原生(Cloud Native)是由 Pivotal 的Matt Stine在2013年提出的一个概念,是他多年的架构和咨询总结出来的一个思想的集合。
那怎么去理解云原生应用?我觉得可以从三个角度来说明,这和云计算平台的三个层次不谋而合,如下图:
- IaaS看做基础 云 设施,用来提供各种基础资源(Infrastructure)
- PaaS作为开发 平台,用来提供各种平台服务(Platform)
- SaaS交付 应用 或 服务,直面用户,提供应用价值(Application)
云原生应用,正好契合了云、平台和服务,一层层构建,所以我通常就把它理解为面向云(平台)来设计我们的应用。[网易三拾众筹][3]的架构师陈晓辉,还为它起了一个小清新的名字——向云而生,我觉得非常贴切,再通俗一点讲,也可以叫做云平台应用。
3. 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。
0. 应用准备
这是一个面向云平台设计的简单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 框架最新版:
<code>npm install sails -g</code>
3. 基于 mongo 3.2 :https://docs.mongodb.org/manual/installation/
4. 基于 Docker,非常契合12-Factor理念,作为我们打包、发布、运行的工具
安装 Docker:https://docs.docker.com/engine/installation/
5. 以上环境安装好之后,就开始初始化我们的应用并运行,应用的名称就叫:12factor-app
<code>
$ 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
</code>
仅需4条命令就搞定了应用的框架代码,并自动生成了基于user的CRUD接口,我们已经将应用启动起来,可以通过以下方式本地调试接口:
控制台输出正常,在浏览器中访问下面链接,即可看到Sails应用的首页
接着,就可以通过本地curl命令或者http工具来做接口调试,这里以常规的增删改查为例:
1.增加一个新用户
<code>
$ 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
}
</code>
2.获取用户列表
<code>
$ curl http://localhost:1337/user
[
{
"name": "bingo",
"createdAt": "2017-02-13T06:13:53.791Z",
"updatedAt": "2017-02-13T06:13:53.791Z",
"id": 58a41d952f53291200b9e065
}
]
</code>
3.修改一个用户
<code>
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
}
</code>
4.删除一个用户
<code>
curl -XDELETE http://localhost:1337/user/58a41d952f53291200b9e065
</code>
我已经将该应用部署到了网易蜂巢在线平台,如果您对这个应用感兴趣,直接将localhost替换为59.111.110.95,一样可以体验CRUD操作,如下所示,只要把yourname换成您的名字即可(建议在PC端操作):
<code>
注册你自己
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
</code>
接下来开始就让我们开始一一实践12-Factor中的每条原则吧,每个原则中我们将分为Factor解说和Factor实践两块。
I. 基准代码
Factor解说:
12-Factor应用只有一份基准代码(Codebase),可以多份部署(deploy)。意思就是说一个应用只有一份用来跟踪所有修订版本的代码仓库,基准代码和应用之间总是保持一一对应的关系,因为:
- 一旦有多个基准代码,就不能称为一个应用,而是一个分布式系统。分布式系统中的每一个组件都是一个应用,每一个应用可以分别使用 12-Factor 进行开发。
- 多个应用共享一份基准代码是有悖于 12-Factor 原则的。解决方案是将共享的代码拆分为独立的类库,然后使用 依赖管理(第二个原则) 策略去加载它们。
- 多份部署相当于是运行了该应用的多个实例,比如开发环境一个实例,测试环境、生产环境都有一个实例
- 一个代码仓库,确保了单一的信任源,从而保证了更少的配置错误和更强的容错和复原能力
Factor实践:
使用Git作为应用的版本管理系统,使用GitHub我们的在线仓库
在刚刚创建好的应用目录下执行:
<code>
$ 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
</code>
II. 依赖
Factor解说:
12-Factor规则下的应用程序不会隐式依赖系统级的类库。意思就是说:通过 依赖清单 声明所有依赖项,通过 依赖隔离 工具确保程序不会调用系统中存在但清单中未声明的依赖项。并统统一应用到生产和开发环境。
云平台根据这些声明管理依赖,确保云应用所需的库和服务。
Factor实践:
package.json 就是我们的 依赖清单,所有应用程序的依赖都声明在此。
<code>
{
"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": ""
}
</code>
接下来我们加入 mongodb 的库依赖(后续会用到),只需要执行:
<code>
npm install sails-mongo --save
</code>
同时 package.json 中会有相应的变更
<code>
{
...
"dependencies": {
...
"sails-mongo": "^0.12.2" //最新加入的依赖
}
...
}
</code>
应用程序需要用到的依赖库都安装在node_modules文件夹下,该文件夹就是作为应用的 依赖隔离,并且和系统的库是隔离的
III. 配置
Factor解说:
12-Factor推荐将应用的配置存储于 环境变量 中,保证配置排除在代码之外,有如下好处:
- 环境变量是一种清楚、容易理解和标准化的配置方法
- 环境变量可以非常方便地在不同的部署间做修改,却不动一行代码
- 与配置文件不同,不小心把它们签入代码库的概率微乎其微
- 与一些传统的解决配置问题的机制(比如 Java 的属性配置文件)相比,环境变量与语言和系统无关
- 存储在环境变量中的另一个好处是,方便和Docker配合使用
一个技巧:判断一个应用是否正确地将配置排除在代码之外,一个简单的方法是看该应用可以立刻开源,而不用担心会暴露任何敏感的信息:)
Factor实践:
在应用程序的 config/connections.js 文件中,我们使用 MONGO_URL 这个环境变量来定义 mongo 的连接方式
<code>
module.exports.connections = {
mongo: {
adapter: 'sails-mongo',
url: process.env.MONGO_URL
}
};
</code>
在 文件中,指定module所使用的连接
<code>
module.exports.models = {
connection: mongo,
migrate: 'safe'
};
</code>
如果你在本地起了一个mongodb测试服务,就可以用这个命令验证应用是否正常配置
<code>
MONGO_URL=mongodb://localhost:27017/12factor-app npm start
</code>
IV. 后端服务
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 这个环境变量,很方便的切换一个新的实例
V. 构建,发布,运行
Factor解说:
12-facfor 应用严格区分构建,发布,运行这三个步骤:
- Cloud Native应用的构建流程把大部分发布配置挪到开发阶段,包括实际的代码构建和运行应用所需的生产环境配置
- 举例来说,直接修改处于运行状态的代码是非常不可取的做法,因为这些修改很难再同步回构建步骤
- 发布的版本就像一本只能追加的账本,一旦发布就不可修改,任何的变动都应该产生一个新的发布版本
Factor实践:
针对这条原则,强烈推荐使用Docker及其组件(Compose),它的核心理念正是:Build, Ship and Run,将适合在整个构建、发布和运行流程,我们也将从这三个方面进行讲解。
构建:
书写构建脚本:Dockerfile
<code>
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"]
</code>
Docker 构建
<code>$ docker build -t message-app:v1.0 .
</code>
Docker 镜像 推送:可以将其 push 到指定的镜像仓库,比如网易蜂巢的镜像中仓库
<code>docker push hub.c.163.com/bingohuang/12factor-app:1.0
</code>
发布:
书写发布脚本:docker-compose.yml
<code>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:
</code>
以上在构建好的镜像基础上,定义了一个发布过程,并将配置(MONGO_URL)通过环境变量注入进去
<code>
MONGO_URL=mongodb://mongo/12factor-app
</code>
运行:
可以通过Docker Compose在本地运行,也可以通过云平台来在线编排(网易蜂巢即将支持服务编排功能)
<code>
docker-compose up -d
</code>
继而查看日志
<code>
docker-compose logs -f
</code>
注:为了方便不熟悉docker和docker-compose命令的人快速运行程序和本地调试,我在源代码中还提供了 docker.sh 脚本,方便构建、发布和运行应用(源码请看后续资料链接)。
VI. 进程
Factor解说:
- 12-Factor 应用的进程必须无状态且无共享。
- 任何需要持久化的数据都要存储在 后端服务内,比如数据库。
- Session 中的数据应该保存在诸如 Memcached 或 Redis 这样的带有过期时间的缓存中。
- 运行环境中,应用程序通常是以一个和多个 进程 运行的。
- 最简单的场景中,代码是一个独立的脚本,运行环境是开发人员自己的笔记本电脑,进程由一条命令行(例如python my_script.py)。另外一个极端情况是,复杂的应用可能会使用很多 进程类型 ,也就是零个或多个进程实例。
- 这么做是为了保证Cloud Native基础设施的速度和效率。
Factor实践:
虽然这是一个简单的demo应用,但查看docker容器中的运行进程,发现也有4个进程在运行,其中 npm 也就是我们的启动进程,node app.js
是实际运行应用的进程
<code>
$ docker exec eaaa922abf08 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
</code>
在这里,我们的应用进程是无状态的,持久化的数据都存储在了后端服务 MongoDB 当中
到此,我们已经分享完了6个Factor,并且都成功实践在了我们的应用上,Yes!
接下来,我们继续分享和实践后续的6个Factor。
VII. 端口绑定
Factor解说:
12-Factor 应用通过自我加载而不依赖于任何网络服务器就可以创建一个面向网络的服务。
- 意思就是说:Web应用通过端口绑定(Port binding)来提供服务 ,并监听发送至该端口的请求。
- Cloud Native应用的服务接口优先选择 HTTP API 作为通用的集成框架。
还要指出的是,端口绑定这种方式也意味着一个应用可以成为另外一个应用的 后端服务 ,调用方将服务方提供的相应 URL 当作资源存入 配置 以备将来调用。
Factor实践:
docker-compose文件为我们很好的定义了端口绑定
<code>
ports:
- "1337:1337" // 应用容器暴露1337端口在容器中,宿主机将其映射到1337端口
</code>
需要注意的是,如果在一个宿主机中部署多个应用实例,就不能将一个宿主机端口映射到多个容器端口(端口冲突),解决方法是在这之上加一个负载均衡,负载宿主机的不同端口服务所对应的不同容器。
VIII. 并发
Factor解说:
12-factor 应用通过进程模型进行扩展,把进程看作是一等公民,并且具备具备的无共享,水平分区的特性
- 这意味着依赖底层平台就能实现横向扩展,不需要技术难度高的多线程编码。
举例来说,HTTP 请求可以交给 web 进程来处理,而常驻的后台工作则交由 worker 进程负责,定时任务交由 clock 来处理,这样扩展每一类的进程就非常方便,如下图所示:
Factor实践:
如第六个原则所描述,我们的应用拥有多个进程,最主要的是 Node.js 的http server进程,进程都是无状态并无共享,所以我们可以非常容易的水平扩展应用。
IX. 易处理
Factor解说:
12-Factor 应用的进程是易处理(disposable)的,意思是说任何进程都可以快速启动和优雅终止,这样做的好处是:
- 这有利于快速、弹性的伸缩应用,迅速部署变化的代码或配置,提高健壮性
- 进程应当追求最小启动时间,可以提供了更敏捷的发布以及扩展过程
- 进程一旦接收终止信号(SIGTERM) 就会优雅的终止
如下图所示,就是一个优雅的应用启动和终止流程
Factor实践:
Docker 先天的轻量级和隔离性,就非常适合来做快速启动和优雅终止,Docker非常适合实践这条原则,在我们的应用中,就加入了 Docker和Compose实践
针对线上环境,推荐构建在容器云平台之上(比如网易蜂巢平台),可以更优雅的处理进程的启动和停止。
X. 环境等价
Factor解说:
12-Factor 应用想要做到持续部署就必须缩小本地与线上差异,包括以下三种差异:
- 缩小时间差异:开发人员可以几小时,甚至几分钟就部署代码
- 缩小人员差异:开发人员不只要编写代码,更应该密切参与部署过程以及代码在线上的表现
- 缩小工具差异:尽量保证开发环境以及线上环境的一致性
12-Factor 应用的开发人员应该反对在不同环境间使用不同的后端服务
这是因为,不同的后端服务意味着会突然出现的不兼容,从而导致测试、预发布都正常的代码在线上出现问题。
Factor实践:
我们的应用程序中,使用了docker-compose作为我们的发布脚本,它使得应用既可以在本地运行,也可以在任何支持 Docker 的云平台上运行,应用无需变化,只需修改配置文件,很好的解除了不同环境的差异化
从以往经验来看,传统应用和12-Factor应用会存在如下差异:
XI. 日志
Factor解说:
- 12-factor应用本身从不考虑存储自己的输出流。相反,每一个运行的进程都会直接的标准输出(stdout)事件流。
- 当日志是由云平台而不是应用包含的库处理时,日志处理机制必须保持简单。
Factor实践:
- 许多服务都能提供日志集中管理,比如ELK、Splunk、Logentries,而且大多数都能方便的和Docker集成在一起
- 这里以 Logentries 为例来为应用集成日志服务,需要在 docker-compose 文件中加入 log 服务,如下:
<code>
log:
command: '-t a80277ea-4233-7785203ae328'
image: 'logentries/docker-logentries’
restart: always
tags:- development
volumes: - '/var/run/docker.sock:/var/run/docker.sock'
</code>
一个典型的 Logentries 面板界面如下:
- development
XII. 管理进程
Factor解说:
开发人员经常希望执行一些管理或维护应用的一次性任务,例如:
运行数据移植
运行一个控制台也被称为 REPL shell),来执行一些代码或是针对线上数据库做一些检查。
运行一些提交到代码仓库的一次性脚本。
12-Factor应用中,一次性管理进程应该和正常的常驻进程(应用进程)使用同样的环境,并且使用相同的代码和配置,基于某个发布版本运行,随着其他的应用程序一起发布
在Cloud Native中,管理任务也是一个进程,而不是特别的工具;同样重要的是,管理任务的进程不应使用秘密的 API 或者内部机制。
Factor实践:
- 我们可以在 docker-compose 文件中定义管理服务,和程序一起执行
- 我们可以通过通过docker exec命令执行一些管理任务,比如:
<code>docker exec -ti ADMIN_CONTAINER_ID bash
</code> - 如果多个容器处在相同的网络下,可以通过一个容器来管理其它容器
5. 总结
至此,12-Factor一一实践完毕,从中可以看出,12-Factor并非相互独立,而是一个整体,有的涉及代码和框架(Node和Rails),有的涉及工具(Docker和Compose)有的涉及架构和平台。在云原生时代,12-Factor仍然具有强大的生命力,每一条原则都是应用开发的珠玑,而且每一个原则也不是一成不变的,随着新的理念出现,原有的Factor会得到延伸和发展,也会出现新的原则,有兴趣的同学,不妨读一读《Beyond the 12 Factor App》这本书,还会有更大的收获。最后,希望此次分享对你理解云原生应用、实践12-Factor有所帮助。
6. 参考链接:
- 12-Factor 在线书籍:http://12.bingohuang.com/zh_cn/index.html
- 12-Factor 开源地址:https://github.com/bingohuang/12factor-gitbook
- 文章分享链接:http://talks.bingohuang.com/2017/cloud-native-12factor.article
- Demo开源地址:https://github.com/bingohuang/12factor-app