一、引言
Docker随着不断地发展与完善,其API接口变得越来越多,尤其在容器参数的配置方面,功能的完善势必造成参数列表的增长。若在Docker的范畴内管理容器,则唯一的途径是使用Docker client。而Docker client最原生的使用方式是:利用docker二进制文件发送命令行命令来完成容器的管理,这显然不是长久之计,很长一段时间内,全球的Docker爱好者都在探索以及寻找方便容器部署的途径。
Docker诞生于2013年3月,同年12月,基于Docker容器的部署工具Fig隆重登场。在Docker生态圈中,经过了两年多的洗礼,Fig项目得到飞速发展的同时,背后的东家也发生了很大的变化。作为Docker届容器自动化部署工具的翘楚,Fig原本是英国伦敦一家创业型公司的产品。随着产品的发展,Fig的巨大潜力受到工业界的普遍认可,在不到一年的时间内就受到Docker公司的密切关注。很快就在2014年7月双发爆出新闻:Docker收购Fig,收购完成之后,Fig改名为Compose,命令改为docker-compose。
Docker Compose的前身是Fig,它是一个定义及运行多个Docker容器的工具。使用Docker Compose你只需要在一个配置文件中定义多个Docker容器,然后使用一条命令将多个容器启动,Docker Compose会通过解析容器件的依赖关系(link, 网络容器 –net-from或数据容器 –volume-from)按先后顺序启动所定义的容器。
二、Compose介绍
探听Fig与Compose的前世今生之后,让我们回到Compose本身,认识一样新事物,从新事物的作用入手,往往不会出太大差错。而Compose最大的作用就是帮助用户缓解甚至解决容器部署的复杂性。最原始的情况下,通过Docker Client发送容器管理请求,尤其是docker run命令,一旦参数数量剧增,通过命令行终端来配置容器较为耗时,同时容错性较差。Compose则将所有容器参数通过精简的配置文件来存储,用户最终通过剪短有效的docker-compose命令管理该配置文件,完成Docker容器的部署。
编辑配置文件与编辑命令行命令的难易程度高下立判,同时配置文件数据的结构化程度越高,可读性也会越强。传统情况下,如docker run等命令的参数数量很多时,由于flag参数的书写格式各异,很容易造成用户费解的情况;而配置文件中一行内容就是一类具体的参数值,可读性大大增强。
在生产环境中,docker client还有一方面经常被Docker爱好者所诟病,那就是难以进行多容器的管理,每次管理的容器对象最多只能有1个。容器虽然运行时相对非常独立,但是很多情况下,容器之间会存在逻辑关系,如容器A使用容器B的data volume,如容器C需要对容器D执行link操作等。对于有逻辑关联的容器,如果能将其作为一个整体,被工具统一化管理,那将大大减少用户的认为参与,提高部署效率。
Compose软件的开发绝大部分是通过Python语言完成。由于Docker社区大部分项目是Go编写的,Compose使用python不利于项目间代码共享。 所幸的是,Compose社区目前已经开始着手此事,并以lib方式提供。
三、Compose架构
Docker生态圈中,Compose扮演的是部署工具的角色。用户使用Compose时,首先需要将部署意图通过配置文件的形式交给Compose。这样的需求包括:容器的服务名、容器镜像的build路径、容器运行环境的配置等。以下是一个较为简单的Compose配置文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
version: '2' services: web: build: . ports: - "5000:5000" volumes: - /data/log:/var/log links: - redis redis: image: redis container_name: redis-01 hostname: live-app-redis-01 restart: always mem_limit: 1024M #volumes: #networks: |
此配置文件定义了两个服务,名称分别为web以及redis。服务web的镜像可以通过docker build来构建,Dockerfile所处目录为该配置文件当前目录;服务web需要对redis服务进行links操作;最终服务web将宿主机上的5000端口映射到内部5000端口,并且挂载卷。服务redis通过镜像来创建。
配置文件是Compose体系中不可或缺,Fig时代支持的配置文件名为fig.yml以及fig.yaml;为了兼容遗留的Fig化配置,目前Compose支持的配置文件类型非常丰富,主要有以下5种:fig.yaml、docker-compose.yml、docker-compose.yaml以及用户指定的配置文件路径。可通过环境变量COMPOSE_FILE或-f参数自定义配置文件。
配置文件的存在未Compose提供了容器服务的配置信息,在此基础上,Compose通过不同的命令类型,将用户的docker-compose命令请求分发到不同的处理方法进行相应的处理。用户docker-compose的命令类型有很多,如命令请求docker-compose up….的类型为up请求,Compose将up请求分发至隶属于up的处理方法来处理;命令请求docker-compose run…..的类型为run,Compose将run请求分发至隶属于run的处理方法来处理。对于不同的docker-compose请求,Compose将调用不同的处理方法来处理。由于最终处理必须落实到Docker Daemon对容器的部署与管理上,故Compose最终必须与Docker Daemon建立连接,并在该连接之上完成Docker的API请求。事实上,Compose借助docker-py来完成此任务。docker-py是一个使用Python开发并调用docker daemon API的docker client包。需要说明的是:毕竟docker-py作为docker官方的一个Python软件包,和docker并不隶属于同一个项目,因此docker-py在很多方面的发展均会滞后于Docker。
清楚了Compose的配置文件,处理方法以及docker-py概念之后,接下来可以看一下Compose的架构,如下图:
Compose将所管理的容器分为三层,工程(project),服务(service)以及容器(contaienr)。这三个概念均为Compose抽象的数据类型,其中project会包含service以及container。首先介绍这三者的意义。
project代表用户需要完成的一个项目,何为项目?Compose的一个配置文件可以解析为一个项目,即Compose通过分析指定配置文件,得出配置文件所需完成的所有容器管理与部署操作。例如:用户在当前目录下执行docker-compose up -d,配置文件为当前目录下的配置文件docker-compose.yml,命令请求类型为up,-d为命令参数,对于配置文件中的内容,compose会将其解析为一个project。一个project拥有特定的名称,并且包含多个或一个service,同时还带有一个Docker Client。
service,代表配置文件中的每一项服务,何为服务?即以容器为粒度,用户需要Compose所完成的任务,比如前面的配置文件中包含了两个service,第一个为web,第二个为redis。一个service包含的内容,无非是用户对服务的定义。定义一个服务,可以为服务容器指定镜像,设定构建的Dockerfile,可以为其指定link的其他容器,还可以为其指定端口的映射等。
从配置文件到service,实现了用户语义到Compose语义的转换。虽然一个service尽可能详细地描述了一个容器的具体信息,但是Compose并一定必须在service之上管理容器,如果用户使用docker-compose pull redis命令,则仅仅完成redis服务中指定镜像的下载。除此之外,Compose的service还可以映射到多个容器,如果用户使用docker-compose scale web=3命令,则可以将web服务横向扩展到3个容器。
读到这里,再来说一说容器怎么解决service之间关系的依赖的。如果Compose一味按照配置文件中的书写顺序来完成service的指定任务,显然会出现一些不可避免的问题,假设多个service所描述的容器之间存在依赖关系,一旦配置文件中的顺序与实际的正常启动顺序不一致,必将导致容器启动失败。一般而言,容器依赖关系会存在以下三种情况:
- 存在links参数,容器的启动需要链接到另一个容器,就是一个容器需要通过本地访问另一个容器的服务。
- 存在volumes_from参数,容器的启动需要挂载另一个容器的data volume。
- 存在net参数,容器的启动过程中网络模式采用other container模式,使用另一个容器的网络栈。
为了解决这些问题,对于用户的某些请求,如docker-compose up等,Compose在解析出所有service之后,需要根据各个service的定义情况,梳理出所有的依赖关系,并最终以一个没有冲突的顺序启动所有的service容器。当然,这种情况无法应对环式依赖。
说说links参数?
如上dockerfile,web服务links了redis服务,什么意思呢?
容器之间的链接实际做了什么?一个链接允许一个源容器提供信息访问给一个接收容器。在本例中,web容器作为一个接收者,允许访问源容器redis的相关服务信息。Docker创建了一个安全隧道而不需要对外公开任何端口给外部容器,因此不需要在创建容器的时候添加-p或-P指定对外公开的端口,这也是链接容器的最大好处,也就是说redis无法对外提供服务,只能由web容器来调用。
上面也说了Docker compose不会根据服务的先后顺序来启动容器。而是经过compose自身重新编排,梳理出所有的依赖关系,并最终以一个没有冲突的顺序启动所有的service容器。如web依赖redis,那么compose就会先启动redis容器,后启动web容器。
解决依赖,先后启动。看似很美好的一个过程,虽然compose解决了依赖,先启动redis后再启动web。如果redis服务自身启动时间比web要长,比如redis启动5,而web启动只要3秒。这个时候用户访问一样会报错,也就是说compose无法判断被依赖的容器是否可以正常提供服务,如果正常后才启动依赖者。但是我猜想后面compose一定会解决这个问题的,而现在在GitHub上有人就为这个问题写了一个小工具。
四、docker-compose.yml参考
每个docker-compose.yml必须定义image或者build中的一个,其它的是可选的。
- container_name
设置容器名称
1 |
container_name: name-01 |
- hostname
设置主机名称
1 |
hostname: name-01 |
- environment
环境变量
1 2 3 |
environment: - test=false - product=true |
- mem_limit
资源限制
1 |
mem_limit: 1024M |
- extra_hosts
添加hosts文件
1 2 |
extra_hosts: - "www.ywnds.com:10.106.136.7" |
此参数的意义就相当于添加一个hosts文件,在程序中直接调用域名,然后通过hosts文件解析。
- image
指定要从中启动容器的映像,可以是存储库/标记或image ID。
1 2 3 4 5 |
image: redis image: ubuntu:14.04 image: tutum/influxdb image: example-registry.com:4000/postgresql image: a4bc65fd |
如果image不存在,Compose会尝试拉取它,除非你也指定了build,在这种情况下,它使用指定的选项构建它,并用指定的标签标记它。
等等,更多可以参考 Docker:Compose file v2 reference
五、安装Compose
docker-compose是Python写的,所以可以直接使用pip去安装docker-compose。官方:Compose file reference
1 2 3 |
$ yum -y install epel-release $ yum -y install python-pip $ pip install -U docker-compose |
到这里docker-compse就完成了,可以执行docker-compose命令看一下当前的版本。
1 2 |
$ docker-compose --version docker-compose version 1.9.0, build 2585387 |
六、使用Compose
前面介绍完Compose语法后,下面我们来测试docker-compose,先提供一个配置文件。
1 2 3 4 5 6 |
$ cat docker-compose.yml version: '2' services: redis: image: redis restart: always |
使用docker-compose up执行这个配置文件(配置文件在当前目录)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
$ docker-compose up -d Creating network "root_default" with the default driver Pulling redis (redis:latest)... latest: Pulling from library/redis 75a822cd7888: Pull complete e40c2fafe648: Pull complete ce384d4aea4f: Pull complete 5e29dd684b84: Pull complete 29a3c975c335: Pull complete a405554540f9: Pull complete 4b2454731fda: Pull complete Digest: sha256:eed4da4937cb562e9005f3c66eb8c3abc14bb95ad497c03dc89d66bcd172fc7f Status: Downloaded newer image for redis:latest Creating root_redis_1 Attaching to root_redis_1 |
1 2 3 |
$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 9ddfcb38c9b5 redis "docker-entrypoint.sh" 30 seconds ago Up 6 seconds 6379/tcp root_redis_1 |
本地没有镜像时,会自动从远程仓库拉取。docker-compose up表示这个容器都是在前台运行的,我们可以指定-d命令以daemon的方式启动容器。
你可以再次执行这个命令,会返回如下信息:
1 2 |
$ docker-compose up -d root_redis_1 is up-to-date |
由于没有任何改变,它不会做任何操作,你也可以使用--force-recreate
强制执行以下up命令。
我们改变一下配置文件,增加一个端口配置。
1 2 3 4 5 6 7 |
version: '2' services: redis: ports: - "6379:6379" image: redis restart: always |
再次执行docker-compose,会重新创建一个容器。
1 2 |
$ docker-compose up -d Recreating root_redis_1 |
root_redis_1是默认的容器名称(root表示当前目录,可使用-p指定)。在重建的时候,只是会替换原有的容器,就算你把配置文件的端口改掉,也只是替换原有的容器。
但是如果你使用-p指定了一个项目名称的话,再次执行docker-compose up命令时会报启动错误,因为端口冲突,但是此容器会创建成功的,使用docker ps -a可以看见。如果你指定了一个项目名称,又修改了端口,那么一个新的容器将会产生且启动。
1 2 3 |
$ docker-compose -p test up -d Removing test_redis_1 Recreating c1f0dba2bdc4_test_redis_1 |
1 2 3 4 |
$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 4c486384557e redis "docker-entrypoint.sh" 6 seconds ago Up 6 seconds 0.0.0.0:6370->6379/tcp test_redis_1 f98499e1acfe redis "docker-entrypoint.sh" 5 minutes ago Up 5 minutes 0.0.0.0:6379->6379/tcp root_redis_1 |
七、Compose语法
1 |
docker-compose [-f <arg>...] [options] [COMMAND] [ARGS...] |
Docker-compose的选项包括:
--verbose
:输出详细信息。
-f
:制定一个非docker-compose.yml命名的yaml文件。
-p
:设置一个项目名称,默认是当前目录的名称。
Docker-compose的动作包括:
build:构建yml中某个服务的镜像,本地需存在Dockerfile文件,才使用docker-compose build来构建服务的镜像。
config:验证和查看yml文件。
1 2 3 4 5 6 7 8 9 10 |
$ docker-compose config networks: {} services: redis: image: redis ports: - 6379:6379 restart: always version: '2.0' volumes: {} |
1 2 |
$ docker-compose config --services redis |
create:创建一个服务,但并不会启动容器,使用docker ps -a可以看见此容器。
1 2 |
$ docker-compose -p test create Creating test_redis_1 |
down:移除某个容器,包括网络、镜像和卷;跟rm命令不同,down命令不管是运行的还是不运行的都可以移除。
1 2 3 4 |
$ docker-compose down Stopping root_redis_2 ... done Removing root_redis_2 ... done Removing root_redis_1 ... done |
events:接收容器的事件。
1 |
$ docker-compose events |
exec:对一个正在运行的容器执行命令。
1 2 3 |
$ docker-compose exec redis ss -nplt | grep 6379 LISTEN 0 128 *:6379 *:* LISTEN 0 128 :::6379 :::* |
kill -s SIGINT:给服务发送特定的信号。
1 2 |
$ docker-compose kill Killing root_redis_1 ... done |
logs:输出日志。
1 |
$ docker-compose logs redis |
pause:暂停某个服务,在服务暂停期间,关闭、删除都无法操作。
1 2 |
$ docker-compose pause redis Pausing root_redis_1 ... done |
unpause:解锁某个暂停的服务。
1 2 |
$ docker-compose unpause redis Unpausing root_redis_1 ... done |
port:输出绑定的端口。
1 2 |
docker-compose port --protocol=tcp redis 6379 0.0.0.0:6379 |
ps:输出运行的容器。
1 2 3 4 |
docker-compose ps Name Command State Ports ------------------------------------------------------------------------------ root_redis_1 docker-entrypoint.sh redis ... Up 0.0.0.0:6379->6379/tcp |
pull:pull一个服务镜像。
1 |
$ docker-compose pull redis |
push:push一个服务镜像。
1 |
$ docker-compose push redis |
rm:删除已经停止的容器,正在运行的删除不了。
1 |
$ docker-compose rm |
start:启动服务。
1 |
$ docker-compose start |
stop:提供服务。
1 |
$ docker-compose stop |
restart:重启一个服务。
1 |
$ docker-compose restart |
up:创建和启动一个容器。
1 |
$ docker-compose up -d |
run:运行某个服务。
1 |
$ docker-compose run redis |
scale:设置服务运行的容器数量.
1 |
$ docker-compose scale redis=2 |
八、Compose不足
虽然Compose的存在非常好地缓解了用户部署与管理容器的痛点,但还是有很多不足之处:
- 没有Daemon
没有Deamon,也就没有高可用、HA之说。 但是同时没有Deamon,所用动作需要用户自己触发。AutoScaling、self healing等也就没有办法提供。
- 模型简单
模型相对简单,只有service。 缺乏诸如网络、存储之类的资源抽象和管理。也缺乏诸如kubernetes中Pod、RC、service proxy之类的抽象,由于servie本身粒度太细,操作管理起来相对麻烦。
- Python编写
虽然我很喜欢Python语言,但是由于Docker社区大部分项目是Go编写的,Compose使用python不利于项目间代码共享。 所幸的是,Compose社区目前已经开始着手此事,并以lib方式提供。
- 跨节点能力
Docker容器跨节点主机部署的需求逐步增大,稍令人失望的是,Compose目前在这方面的功能依旧不令人满意。实际应用场景下,Docker用户往往希望将不同类型的容器部署在不同的Docker节点上,满足负载、安全、资源利用等多方面的考虑。虽然Compose目前不具备这样的能力,但并不以为着Docker会放弃这方面的市场,再等等……..
九、Compose与Swarm
Docker容器跨节点部署方案的发展,用”需求决定方向”来形容再准确不过。目前,Docker正在酝酿着Compose与Swarm的深度结合,目标是:使用户在一个Swarm集群上运行Compose来部署容器,效果和在单机上使用Compose完全一致。
先分析跨节点容器没有依赖的情况,容器之间一旦没有依赖,容器对自身所处的节点位置也就没有太多需求。这种情况下,理论上,Compose完全可以通过Swarm的label环境变量,将容器与满足条件的Docker Node联系在一起;同时也可以通过环境变量affinity,使几个容器部署在同一个Docker Node上或者避免在同一个Docker Node上。
再研究跨节点容器存在依赖的情况,跨节点容器有依赖,第一个需要解决的问题是跨节点容器的通信能力。而在Docker的范畴内,如果不借助其他工具,跨节点容器的通信目前还没有很好的支持。因此,如links、volumes_from、net:container等容器依赖的情况,目前还会默认将相应的容器部署在同一台机器上运行。
<参考>
https://docs.docker.com/compose/
https://docs.docker.com/compose/compose-file/