Compose原理:一探究竟

一般在需要解析原理时,都会先指出项目架构,然后对其中的精妙设计大书特书。遗憾的是,Compose不是这样一个项目,它甚至根本不给我们机会:docker-compose的调用过程扁平得像一张纸,仅用一张简单的模块图就足够解释明白,如图所示。

docker和kubernetes版本兼容关系 kubernetes docker-compose_Docker


以docker-compose up操作为例,docker-compose更像是docker client增强,它为docker client引入了“组”的概念:图右上角的docker-compose定义了一组“服务”来组成一个docker-compose的project,再通过service建立docker-compose.yml参数,从而与container建立关系,最后使用container来完成对docker-py(Docker Client的Python版)的调用,向Docker Daemon发起HTTP请求。为了更清楚地解释这个调用过程和其中一些有趣的细节,不妨来使用“代码走读”跟踪整个流程。

首先,用户执行的docker-compose up指令调用了命令行中的启动方法。功能很简单明了,一个docker-compose.yml定义了一个docker-compose的project,docker-compose up操作提供的命令行参数则作为这个project的启动参数交由project模块去处理。

然后,如果当前宿主机已经存在与该应用对应的容器,docker-compose将进行行为逻辑判断。如果用户指定可以重新启动已有服务,docker-compose就会执行service模块的容器重启方法,否则就将直接启动已有容器。这两种操作的区别在于前者会停止旧的容器,创建启动新的容器,并把旧容器移除掉。在这个过程中创建容器的各项自定义参数都是从docker-compose up指令和docker-compose.yml中传入的。

接下来,启动容器的方法也很简洁,这个方法中完成了一个Docker容器启动所需的主要参数的封装,并在container模块执行启动。该方法所支持的参数读者应该是有所了解的,不过可以预见,这样的参数列表同样也带来了诸多限制,后面的章节将会详细讨论。

最后,container模块会调用docker-py客户端来执行向Docker daemon发起创建容器的POST请求,再往后就是Docker处理的范畴了.

docker-compose 小神器”之名的由来

阅读至此,读者或许会问,如此简单的工具,我信手拈来便能写一个,如何称得上神器?原因是,为了方便读者快速了解docker-compose,前面的代码走读里故意忽略了一些细节,而这些细节才是docker-compose真正精髓所在。读者应该还记得在启动容器方法中有一个重建容器的逻辑,下面就从这个地方说起。

当使用Docker构建服务栈时,容器间的关系,尤其是link和volumes-from这两个参数是最为常用的。如果不使用docker-compose,就需要人工记录这些关系。为什么需要记录?因为一旦需要更新或者重启容器,就必须在容器启动参数里添加这两个参数,并且以正确的顺序来执行docker run,才能保证新的集群可以正常工作。在Compose中,这两个重要的关系参数是能够在docker-compose.yml中配置的,一旦在docker-compose up过程中发现了需要重新创建容器的情况(即用户指定当需要创建的容器已经存在时), Compose会依据容器间的关系来进行更新操作,保证更新后的容器依然是可以正常连接的。

先来谈谈link关系。在上例子中,就存在着web->redis这样的关系。正常情况下,当更新了redis容器后(比如更换了镜像版本),理应只需重新创建一个新的redis容器,可这时暗藏一个严重的问题:删除并创建一个同名的容器并不能更新原先的link信息。

回顾一下link实现原理,web容器里的hosts文件只有在redis容器重启的情况下才会被更新,而删除并重新创建一个同名redis容器,原web容器里hosts文件记录的还是一个旧IP地址。

幸运的是,在Compose的数据结构中,一个project里所有容器都是按照link关系排序的,对这些容器进行重新创建操作都会严格按照正确的顺序依次进行,即更新(比如重启)redis容器会连带更新link它的web容器,上述问题便迎刃而解。

再来谈谈volume以及volume-from参数。这个关系的处理与link略有不同。对于使用volume-from参数的两个容器,Compose需要处理的不仅仅是拓扑关系,更重要的是关系所对应的volume中的内容。更直白地说,容器重建操作事实上就是删除并重新创建一个同名容器,但如果这个容器包含volume,甚至该volume还被其他容器通过volume-from引用,这时,删除并新建的同名容器不但不具有旧容器volume内容,还会间接导致引用它的其他容器volume内容丢失——因为docker-compose会按照拓扑关系重新引用新容器的volume。为了解决这个问题,docker-compose中使用了中间容器(intermediate container)来暂时“记下”旧容器的volume,整个逻辑如图所示。

docker和kubernetes版本兼容关系 kubernetes docker-compose_运维_02

docker-compose的局限性有以下几点

首先,docker-compose是面向单宿主机部署的,这是一种部署能力的欠缺。在更多的场合下,管理员需要面对大量物理服务器(或者虚拟机),这时如果要实现基于docker-compose的容器自动化编排与部署,管理员就得借助成熟的自动化运维工具比如Puppet、Chef、SaltStack来负责管理多个目标机,将docker-compose所需的所有资源包括配置文件、用户代码等交给目标机,再在目标机上执行docker-compose指令。且不说这些工作本身需要一定的开发量,即使容器已经顺利运行,这些容器的状态监控,故障恢复等管理动作都没办法直接跟管理员产生交互,需要先通过Docker daemon,再通过docker-compose,最后经过运维工具,才能最终与管理员交互。近乎于托管的状态对于企业级运维场景来说,是难以忍受的。

其次,假设通过改造,新的运维工具已经能够很好地与docker-compose集成(事实上这项工作的难度不亚于自主开发了一个高级docker-compose),接下来的事情同样棘手,比如网络和存储。目前,Docker仍不能提供跨宿主机的网路,完全面向Docker daemon的docker-compose当然也不支持。这意味着管理员必须部署一套类似于Open vSwich的独立网络工具,而且管理员还需要完成集成工作。当好不容易把容器编排都安排妥当之后,又会发现容器还处在内网环境中,于是问题就又回到了之前讨论的“从小工到专家”的循环里:负载均衡、服务发现,一个又一个接踵而至的问题会很快消耗掉工程师所有的耐心。那么,是否有一种能够提供完善的面向服务器集群的Docker编排和部署方案呢?

Docker官方给出的答案是Compose同Machine和Swarm联动。接下来的章节中,将对Docker容器编排部署三驾马车中的另外两驾进行深入的剖析。