这次的主角终于轮到了大鲸鱼Docker。不晓得有多少人是因为Docker认识了CoreOS的,至少它在社区的知名度事实上高于CoreOS项目本身。这篇文章里不会对Docker做很深入的讲解,而重点放在开始使用Docker所需的基本知识以及在CoreOS中使用Docker托管服务的推荐实践方法。
雷教主说,“站在风口上,猪也能飞起来”。Docker正是借着云计算的风飞上了天。伴随着Docker和应用容器的兴起,拉动了一批PaaS产品的发展,而CoreOS也借了这股劲儿赚足了人气,进行得风生水起。同时CoreOS的成熟也在回馈Docker社区,为社区带来了例如Etcd、Deis(私有PaaS云平台,目前是基于CoreOS构建的)等许多新的活力。
说起CoreOS与Docker的渊源,确有一段历史了。故事大致是这样开始的,2013年2月,美国的dotCloud公司发布了一款新型的Linux容器软件Docker,并建立了一个网站发布它的首个演示版本(见Docker第一篇官方博客)。而几乎同时,2013年3月,美国加州,年轻的帅小伙Alex Polvi正在自己的车库开始他的 第二次创业。此前,他的首个创业公司Cloudkick卖给了云计算巨头Rackspcace(就是OpenStack的东家)。
有了第一桶金的Alex这次准备干一票大的,他计划开发一个足以颠覆传统的服务器系统的Linux发行版。为了提供能够从任意操作系统版本稳定无缝地升级到最新版系统的能力,Alex急需解决应用程序与操作系统之间的耦合问题。因此,当时还名不见经传的Docker容器引起了他的注意,凭着敏锐直觉,Alex预见了这个项目的价值,当仁不让地将Docker做为了这个系统支持的第一套应用程序隔离方案。不久以后,他们成立了以自己的系统发行版命名的组织:CoreOS。事实证明,采用Docker这个决定,后来很大程度上成就了CoreOS的生态系统。
现在看来,CoreOS已经不是唯一预装了Docker的操作系统了,但它是第一个,也是目前做得最成功的一个。RedHat和Canonical(Ubuntu的母公司)随其后也分别推出了自己的预装Docker的系统发行版,但知悉者寥寥,并没有做成气候。其项目发起时间见下图(出自成都ThoughtWorks技术雷达分享活动),Atomic和Ubuntu Core Snappy分别是RedHat和Canonical公司推出的预装Docker的操作系统,目标也都是直指服务器集群和容器化部署。
“应用容器”现在对许多人已经并不陌生了。但它在服务器的系统上还不是那么普及,至少与你手上的智能手机系统相比。至今在服务器系统上流行的安装软件方式依然是编译源代码、手工的安装包或各种包管理工具,虽然包管理工具的出现解决了应用软件安装、卸载以及自身依赖等诸多问题,却无法很好的解决软件之间的依赖冲突。而早在Docker诞生以前,“沙盒”的概念已经被普遍使用在Android、iOS等主流的手机系统中了。通过沙盒的隔离,应用软件将自己所有的依赖与应用本身打包在一起,并通过SDK API提供的可控的方式访问操作系统,软件与系统的耦合度大大降低。这样带来的直接好处是,软件之间的依赖冲突得到了很好的解决,移除一个应用软件一般只需要很短的几秒钟并且彻底无痕,软件访问系统的安全性也更加可控。
事实上,Android实现沙盒同样的基于Linux内核的cgroup和namespace机制用于限制和隔离资源的使用,所使用的技术与Docker如出一辙。这些早在Linux 2.6.x版本就已经加入了的新特性,已经通过了较长时间的检验,被证实是可行并且可靠的。这篇文章里不会专门介绍Docker的使用,而是关注在具体的场景下,如何在CoreOS中恰当的管理Docker容器。了解过Docker在CoreOS生态系统中的角色后,下面通过在两个容器中分别运行NodeJS和MongoDB的例子说明如何在CoreOS中通过Systemd管理服务,并在此基础上快速浏览一些基本的Docker命令。
一、拉取基础镜像
每一个具体的容器实际上是运行在虚拟出来的独立空间里面的,它被设计成只能够访问到存在于同一个虚拟空间下面的其他文件。因此为了使应用能够使用基本的运行时依赖,还需要将一些Linux的命令和配置文件也打包放到虚拟空间里,这种打包好的依赖文件集合就是镜像。
操作 docker 的方式与 systemctl、etcdctl 类似,需要由一个二级命令共同组成一个完整的命令。通过 docker pull 命令可以指定的网络地址拉取镜像到本地(如果指定的是名称而不是网络地址,则会在docker官方的镜像仓库里面搜索,比如下面的两个例子)。$ docker pull node:latest
...
Status: Downloaded newer image for node:latest
$ docker pull mongo:latest
...
Status: Downloaded newer image for mongo:latest
镜像是按照“地址/镜像名:版本标签”格式命名的,其中镜像名是必须的,如果地址部分为空则默认为官方仓库地址。如果版本标签部分为空,对于较新的Docker版本(大约1.3.x以后),会仅仅下载标签为latest的版本,而较早版本的Docker则会下载指定镜像的所有版本,常常会因此意外下载许多不需要的镜像版本。
在一大段输出以后,若一切顺利(事实是,在国内可能不会太顺利),本地的Docker已经可以直接使用这两个预装了NodeJS和MongoDB的镜像了。可以通过 docker images 命令验证。
$ docker images
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
node latest 61afc26cd88e 3 days ago 696.2 MB
mongo latest 59b3d123f9b8 6 days ago 392.4 MB
...
在国内的一些地区,拉取官方镜像仓库的镜像可能会失败(或许是大名鼎鼎的某防火墙的功劳)。此时可以采用国内的第三方开源镜像仓库,比如DockerPool或Docker.cn提供的镜像文件。前者需要配置本地的SSL证书,否则会遇到“Error: Invalid registry endpoint”错误,略微麻烦。后者可以直接使用:
docker pull docker.cn/docker/node:latest
docker pull docker.cn/docker/mongo:latest
二、制作定制镜像
MongoDB可以直接使用官方的Docker镜像。而NodeJs的容器还需要些许定制,将应由部署到容器中然后生成新的镜像。再次说明,制作镜像的最佳途径是写一个Dockerfile,实现基础设施可视化。以下通过修改现有镜像的方法一般只用于演示目的。
接下来我们要分别启动MongoDB和NodeJs的容器实例,并将MongoDB的端口暴露到NodeJs的容器中。
首先启动一个MongoDB容器实例,命名为mongo-ins。启动容器的命令是 docker run,除了运行配置参数如 --name、--port 等,这个命令的最后两个参数分别是实例使用的镜像名字,和实例本身需要运行的命令。有的容器已经配置好了默认的运行程序,此时后面的一个参数可以省略,比如下面的例子。
参数 -d 表示运行后直接进入后台,屏幕上回显的一串输出是新启动容器实例的ID。
然后启动一个NodeJs容器实例,使用官方的node镜像作为基础镜像,并将它与 mongo-ins 实例建立“连接”。这个容器实例命名为node-app。
$ docker run --name node-app -p 3000 --link mongo-ins:mongo -it node /bin/bash
root@e73e7d7836a6:/# <— 已经进入容器中的Bash>
-it 实际上是 -i -t 的简便写法,表示启用交互式模式和启用显示终端,这样我们可以进入容器中做一些手工操作。而参数 --link 用来将两个容器进行关联,关于Docker Link的用法可以参考Docker的相关文档。简单来说,Link的参数 mongo-ins:mongo 表示将容器 mongo-ins 引入到正在建立的容器镜像中,并将其称为 mongo。这样做的结果是,在新建的 node-app 容器实例中,能够访问到两个全局环境变量: $MONGO_PORT_27017_TCP_ADDR 和 $MONGO_PORT_27017_TCP_PORT,分别是用来访问 MongoDB 的 IP 地址和端口。
作为演示,我们将在容器中部署一个从Github获取的简单示例。
$ git clone https://github.com/ijason/NodeJS-Sample-App.git
$ cd /NodeJS-Sample-App/EmployeeDB
$ sed -i -e "s/27017/process.env.MONGO_PORT_27017_TCP_PORT/" -e "s/'localhost'/process.env.MONGO_PORT_27017_TCP_ADDR/" app.js
$ exit
上面的第三条命令将原本容器中指定的 MongoDB 位置改成了从另一个容器中暴露的IP地址和端口。至此这个node-app容器已经部署好了一个名为 Employees 的示例应用,接下来将它生成镜像并放到集群的每个节点上。
三、生成并提交镜像
为了在集群里对容器中的服务提供横向扩展能力,需要将定制好的容器在集群的所有节点共享。
首先需要一个存放共享镜像的地方,在企业环境可以使用私有的镜像仓库,但为了演示简便起见,我们直接使用Docker的公共仓库。首先需要在Docker Hub注册一个用户,然后使用 docker login 命令登陆到仓库服务器。
$ docker login
Username: linfan
Password:
Email: linfan@******.com
Login Succeeded
然后我们需要将本地修改过的容器使用 docker commit 命令生成一个本地的镜像。注意,由于之后需要将镜像提交至Docker Hub,这里镜像的名字必须以自己的Docker Hub用户名作为前缀,否则在后面的 push 时候会遇到 403 “Access Denied: Not allowed to create Repo at given location” 错误。例如名为 linfan/employees。
$ docker commit node-app linfan/employees
a4281aa8baf9aee1173509b30b26b17fd1bb2de62d4d90fa31b86779dd15109b
$ docker images
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
linfan/employees latest a4281aa8baf9 14 seconds ago 696.2 MB
最后,使用 docker push 命令将这个准备好的镜像提交到Docker Hub仓库中。
$ docker push linfan/employees
The push refers to a repository [linfan/employees] (len: 1)
Sending image list
...
Pushing tag for rev [5577d6743652] on {https://cdn-registry-1.docker.io/v1/repositories/linfan/employees/tags/latest}
提交完成后,在其他节点就可以使用 docker pull 命令获取到这个镜像了。
注意:严格来说,将数据库服务容器通过Docker Link暴露给应用服务容器的方法并不符合分布式应用的12条准则,因为通过Docker Link连接的两个容器必须运行在同一个物理主机上,数据与应用不能在集群中分别独立的部署或横向扩展。
一、编写 Unit 文件
有了相应的服务容器后,在CoreOS中正确启动服务的方法应该是通过Fleet来管理。通过合理使用 Unit 的 X-Fleet 配置,能够很好的解决容器直接相互依赖的问题。
用 vagrant ssh 进入一个 CoreOS 的 Shell 中,创建以下两个服务 Unit 文件。
首先是mongo.service
[Unit]
Description=General MongoDB Service
After=docker.service
[Service]
TimeoutStartSec=0
ExecStart=/opt/bin/docker-run.sh --name mongo-ins -d mongo
ExecStop=/usr/bin/docker stop mongo-ins
然后是employees.service,请注意它的 Unit 和 X-Fleet 段的内容。在Unit段指定了这个服务启动前必须首先启动 mongo.service 服务,而在 X-Fleet 段指定了自己需要运行在与 mongo.service 相同的服务节点上。
[Unit]
Description=Employee Information Management Service
After=docker.service
After=mongo.service
[Service]
TimeoutStartSec=0
ExecStart=/opt/bin/docker-run.sh -p 3000:3000 --link mongo-ins:mongo -d --name node-app node-app node /NodeJS-Sample-App/EmployeeDB/app.js
ExecStop=/usr/bin/docker stop mongo-ins
[X-Fleet]
X-ConditionMachineOf=mongo.service
上面的两个 Unit 文件都使用到了一个 /opt/bin/docker-run.sh 脚本,用于替代 docker run 命令。这个脚本需要额外创建并放置到 /opt/bin 目录下面,其作用是检测是否已经有一个同名的容器在运行了,如果没有则执行相应的 docker run 命令,否则直接使用 docker start 命令启动已经存在的容器。其内容如下:
#!/bin/bash
PARA="${*}"
NAME=$(echo "${PARA}" | grep '\-\-name' | sed 's/.*--name \([^ ]*\).*/\1/g')
if [ "${NAME}" == "" ]; then
echo "[ERROR] Must specify a name to the container!";
exit -1;
fi
EXIST=$(sudo docker ps -a | grep "${NAME}[ ]*$")
if [ "${EXIST}" == "" ]; then
sudo docker run ${PARA}
else
sudo docker start ${NAME}
fi
二、启动服务
通过 fleetctl 命令启动服务,具体的用法在系列前面的内容里面已经介绍过了。
fleetctl start ./mongo.service
fleetctl start ./employees.service
这里为了简便直接用了 fleetctl start 命令,更推荐的启动服务方法请参考系列中关于Fleet的一篇。
到这一步,这个部署在容器中的服务已经可以使用了。从外部访问服务器的 3000 端口即可打开下面这个页面,并向MongoDB服务中的数据库中添加员工信息了。最后,再来看一些用于检测容器运行状态和日常管理的Docker命令。
一、查看运行日志
容器通过-d参数进入后台运行之后,其中服务输出的日志内容可以通过 docker logs 命令查看到。
$ docker logs mongo-ins
MongoDB starting : pid=1 port=27017 dbpath=/data/db 64-bit host=d9bba1bfc8be
...
二、容器实例列表
命令 docker ps 能够列出所有当前正在运行的容器的基本信息。
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
d9bba1bfc8be mongo:2 "/entrypoint.sh" 4 minutes ago Up 4 minutes 27017/tcp mongo-ins
22de21d77174 node:0 "/bin/bash" 3 minutes ago Up 5 minutes node-app
...
三、容器实例详情
使用 docker inspect 命令能够查看到指定一个容器的详细运行信息。
$ docker inspect mongo-ins
{ ... }
四、备份和还原容器
简单的提一下,用来将现有的本地镜像打包备份和还原的命令是 docker save 和 docker load。也可以直接将容器实例打包,相关命令是 docker export 和 docker import,注意 import 之后会将备份的数据恢复成一个新的本地镜像,而不是容器实例。
这两个命令的使用可以参考文档。只额外说明一个问题,既然两种还原都会将备份的内容还原为容器,为什么需要两种还原命令呢?原因在于使用 save 和 export 生成的打包效果是不太一样的,简单说就是 export 生成的备份会丢弃所有的镜像分层结构,而 save 生成的备份不会。镜像分层结构有利于减少相似镜像本地存储所需的空间,细节可参考这篇文章。
以上介绍的这些命令仅仅是Docker强大功能的冰山一角,网络上已经有许多十分优秀的Docker使用教程,作为学习Docker和应用容器都是极好的途径。这里推荐一个Dockerone翻译的Docker系列文章。
事实上,随着CoreOS的独立容器项目 Rocket 的发起,Docker 在未来将不再是 CoreOS 和其他Linux操作系统设计容器方案的唯一选择。但作为 CoreOS 乃至整个 Linux 生态圈的应用容器服务佼佼者,Docker的王者地位还会持续很长的时间,而CoreOS始终会保持对Docker容器的一流支持(见CoreOS关于Rocket博客中的F&Q)。
正值提笔写这篇文章的那天,Bing的首页内容是泰国的曼谷港,这幅画面与Docker的Logo颇有几分神似。如此的巧合,使人不由的联想,这艘万吨货轮底下是否也正藏着一只蓄势待发的蓝鲸呢。在这一篇内容中,将重点放在了使用Docker容器管理服务的介绍,正如文章中已经指出的,例子中的有些实践(使用docker commit创建镜像,以及fleet start直接启动服务等)并不适合在实际的项目中使用。从下下篇的文章起,我们将讲解几个完整的,符合产品应用的例子。在进入正式的综合实例前,在下一篇中,会对 Systemd 和 Fleet 使用的 Unit 文件做一个更深入的探索。