前言: 在上一篇 《Docker集群管理之Docker Machine》 中,我们通过源码分析了解了Docker Machine的工作原理,使用者可以通过Docker Machine的一条命令在任意支持的平台创建一个Docker主机,并能集中管理这些主机。Docker主机创建好之后,接下来就该考虑Docker容器部署的问题了。本篇中我们将通过分析Docker Compose的源码,了解Docker Compose的工作原理。
与容器技术同样受到关注的微服务架构也在潜移默化的改变着应用的部署方式,其提倡将应用分割成一系列细小的服务,每个服务专注于单一业务功能,服务之间采用轻量级通信机制相互沟通。同时数据库解决方案也在发生在变化,多种持久化混合方案(Polyglot Persistence)提倡将数据存放在最适合的数据库解决方案中,而传统的数据库解决方案将数据存在在同一个数据库服务中。服务数量的增加也就意味着容器数量的增多,逐渐增加的容器数量为容器部署,运行及管理带来了挑战。Docker Compose的出现解决多个容器部署的问题并提高了多个容器解决方案的可移植性。
Docker Compose的前身是Fig,它是一个定义及运行多个Docker容器的工具。使用Docker Compose你只需要在一个配置文件中定义多个Docker容器,然后使用一条命令将多个容器启动,Docker Compose会通过解析容器件的依赖关系(link, 网络容器 –net-from或数据容器 –volume-from)按先后顺序启动所定义的容器。
可以通过下载二进制可执行文件的方式安装Docker Compose
Docker Compose将所管理的容器分为三层,工程(project),服务(service)以及容器(contaienr)。Docker Compose运行的目录下的所有文件(docker-compose.yml, extends文件或环境变量文件等)组成一个工程,若无特殊指定工程名即为当前目录名。一个工程当中可包含多个服务,每个服务中定义了容器运行的镜像,参数,依赖。一个服务当中可包括多个容器实例,Docker Compose并没有解决负载均衡的问题,因此需要借助其他工具实现服务发现及负载均衡。
Docker Compose的工程配置文件默认为docker-compose.yml,可通过环境变量COMPOSE_FILE或-f参数自定义配置文件,其定义了多个有依赖关系的服务及每个服务运行的容器。以下是一个简单的配置文件:
其定义了两个服务web和redis。web服务的镜像需要在当前目录实时构建,其容器运行时需要在宿主机开放端口5000并映射到容器端口5000,并且挂载存储卷/code以及关联服务redis。redis服务通过镜像redis启动。
Docker Compose是由python语言实现的,它通过调用docker-py库(可参考https://github.com/docker/docker-py)与docker engine通信实现构建docker镜像,启动停止docker容器等。Docker-py库调用docker remote API(可参考https://docs.docker.com/reference/api/docker_remote_api/)与Docker Daemon通信,可通过DOCKER_HOST配置本地或远程Docker Daemon的地址。
下面我们通过分析docker-compose最复杂的命令up的源码来了解一下docker compose的工作原理。
通过命令docker-compose up -d启动以上docker-compose.yml定义的服务。
我们可以看到redis服务先于web服务创建,这是因为docker-compose解析到服务间的依赖关系,web服务依赖于redis服务,所以进行了排序。我们来看一下docker compose up的工作流程。
1. 工程初始化 – 解析配置文件(包括docker-compose.yml,外部配置文件extends files,环境变量配置文件env_file),并将每个服务的配置转换成python字典,初始化docker-py客户端用于与本地或远端的docker engine通信。
2. 根据docker-compose的命令参数将命令分发给相应的处理函数,此处为up
3. 调用project类的up函数,得到当前工程中的所有服务,并根据服务的依赖性(links,external links – 本工程或docker-compose之外的容器,多用于多项目共用容器,网络容器net-from以及数据容器volume-from)进行排序并去掉重复出现服务(此情况可因某服务被其他多个服务依赖所造成)
4. Docker Compose使用labels标记启动的容器
使用docker inspect可以看到一个通过docker compose启动的容器被添加了一些compose使用的标记
5. Docker Compose通过compose工程名以及服务名从docker engine获取当前所有含有此标记的容器以检查当前工程所包含的服务状态,根据当前状态为每个服务制定接下来的动作。
a. 若容器不存在,则服务动作设置为创建(create)
b. 若容器存在但设置不允许重建,则服务动作设置为启动(start)
c. 若容器配置发生变化(config-hash)或者设置强制重建标志,则服务动作设置为重建(recreate)
d. 若容器状态为停止,则服务动作设置为启动(start)
e. 若容器状态为运行但其依赖容器需要重建,则服务状态设置为重建(recreate)
f. 若容器状态为运行其无配置改变则不作操作
6. 根据每个服务不同的动作执行不同的操作
服务动作为创建
a. 检查镜像是否存在(调用docker-py client inspect函数从本地或远程的docker engine获取镜像信息)。若镜像不存在,则检查配置文件中关于镜像的定义。如果在配置文件中设置为build则调用docker-py build函数与docker engine通信完成docker build的功能。如果在配置文件中设置为image则通过docker-py pull函数与docker engine通信完成docker pull的功能。b. 获取当前服务中容器的配置信息,如端口,存储卷,主机名,使用镜像环境变量等配置的信息。若在配置中指定本服务必须与某个服务在同一台主机(previous_container,用于集群)则在环境变量中设置affinity:container=<container name>。通过docker-py与docker engine通信创建并启动容器。
服务动作为重建
a. 停止当前的容器
b. 将现有的容器重命名,这样数据卷在原容器被删除前就可以拷贝到新创建的容器中了
c. 创建并启动新容器,previsous_container设置为原容器确保其运行在同一台主机(存储卷挂载)
d. 删除旧容器
服务动作为启动则启动停止的容器
7. 若docker-compose up没有指明-d即前台运行,则将所有各个容器的输出汇聚打印
8. 注册信号SIGINT处理函数,若收到信号SIGINT则并行杀掉所有容器
到此为止docker-compose up的命令执行完毕,docker-compose.yml文件中定义的服务 全部启动。
Docker Compose的命令主要分为以下几类
管理服务:up/start/scale/stop/restart/rm/kill
管理镜像:build/pull
查看服务运行状态:ps/port
打印运行中的服务log:logs
在一个服务中执行一个一次性命令:run
以上这些命令都是通过label标记来过滤当前工程里的容器,然后调用docker-py库调用Docker remote API与docker engine通信控制容器。
Docker Compose是一个部署多个容器的简单但是非常必要的工具,它使你使用一条简单的命令部署多个容器。Docker Compose在实际工作中非常有价值,其大大简化了多容器的部署过程,避免了在不同环境运行多个重复步骤所带来的错误可能,使多容器移植变得简单可控。从其Roadmap可以看出,Docker Compose的目标是做一个生产环境可用的工具,包括服务回滚,多环境支持(dev/test/staging/prod),支持在线服务部署升级,防止服务中断并且监控服务使其始终运行在正确的状态。Roadmap中的另一个目标是更好的与Docker Swarm集成,目前版本存在的主要问题是无法保证处于多个主机的容器间正常通信因为目前不支持跨主机间容器通信,我们相信新的Docker网络实现将会解决这一问题。另一个问题是在Docker Compose中定义构建的镜像只存在在一台Docker Swarm主机上,无法做到多主机共享,因此目前需要手动构建镜像并上传到一个镜像仓库使多个Docker Swarm主机可以访问并下载镜像。相信随着Docker Compose的完善,其必将取代docker run成为开发者启动docker容器的首选。
作者简介: 杜航,Websense云基础架构组开发经理,专注于Openstack和Docker。灵雀云资深用户。
预告:9月1日晚上8点整,杜航将通过在线培训的方式详细探讨《Docker集群管理三剑客》,报名地址请点击这里