• 进入"运维那点事"后,希望您第一件事就是阅读“关于”栏目,仔细阅读“关于Ctrl+c问题”,不希望误会!

Docker:Compose架构设计与实现

Docker 彭东稳 8年前 (2016-12-27) 32099次浏览 已收录 0个评论

一、引言

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配置文件。

此配置文件定义了两个服务,名称分别为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的架构,如下图:

Docker: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

设置容器名称

  • hostname

设置主机名称

  • environment

环境变量

  • mem_limit

资源限制

  • extra_hosts

添加hosts文件

此参数的意义就相当于添加一个hosts文件,在程序中直接调用域名,然后通过hosts文件解析。

  • image

指定要从中启动容器的映像,可以是存储库/标记或image ID。

如果image不存在,Compose会尝试拉取它,除非你也指定了build,在这种情况下,它使用指定的选项构建它,并用指定的标签标记它。

等等,更多可以参考 Docker:Compose file v2 reference

五、安装Compose

docker-compose是Python写的,所以可以直接使用pip去安装docker-compose。官方:Compose file reference

到这里docker-compse就完成了,可以执行docker-compose命令看一下当前的版本。

六、使用Compose

前面介绍完Compose语法后,下面我们来测试docker-compose,先提供一个配置文件。

使用docker-compose up执行这个配置文件(配置文件在当前目录)。

本地没有镜像时,会自动从远程仓库拉取。docker-compose up表示这个容器都是在前台运行的,我们可以指定-d命令以daemon的方式启动容器。

你可以再次执行这个命令,会返回如下信息:

由于没有任何改变,它不会做任何操作,你也可以使用--force-recreate强制执行以下up命令。

我们改变一下配置文件,增加一个端口配置。

再次执行docker-compose,会重新创建一个容器。

root_redis_1是默认的容器名称(root表示当前目录,可使用-p指定)。在重建的时候,只是会替换原有的容器,就算你把配置文件的端口改掉,也只是替换原有的容器。

但是如果你使用-p指定了一个项目名称的话,再次执行docker-compose up命令时会报启动错误,因为端口冲突,但是此容器会创建成功的,使用docker ps -a可以看见。如果你指定了一个项目名称,又修改了端口,那么一个新的容器将会产生且启动。

七、Compose语法

Docker-compose的选项包括:

--verbose:输出详细信息。

-f:制定一个非docker-compose.yml命名的yaml文件。

-p:设置一个项目名称,默认是当前目录的名称。

Docker-compose的动作包括:

build:构建yml中某个服务的镜像,本地需存在Dockerfile文件,才使用docker-compose build来构建服务的镜像。

config:验证和查看yml文件。

create:创建一个服务,但并不会启动容器,使用docker ps -a可以看见此容器。

down:移除某个容器,包括网络、镜像和卷;跟rm命令不同,down命令不管是运行的还是不运行的都可以移除。

events:接收容器的事件。

exec:对一个正在运行的容器执行命令。

kill -s SIGINT:给服务发送特定的信号。

logs:输出日志。

pause:暂停某个服务,在服务暂停期间,关闭、删除都无法操作。

unpause:解锁某个暂停的服务。

port:输出绑定的端口。

ps:输出运行的容器。

pull:pull一个服务镜像。

push:push一个服务镜像。

rm:删除已经停止的容器,正在运行的删除不了。

start:启动服务。

stop:提供服务。

restart:重启一个服务。

up:创建和启动一个容器。

run:运行某个服务。

scale:设置服务运行的容器数量.

八、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/


如果您觉得本站对你有帮助,那么可以支付宝扫码捐助以帮助本站更好地发展,在此谢过。
喜欢 (1)
[资助本站您就扫码 谢谢]
分享 (0)

您必须 登录 才能发表评论!