2017-07-16 14:05

(八)Docker 编配 - Compose、Consul、分布式、Swarm

编配(orchestration),这个概念大概描述了自动配置、协作和管理服务的过程。这里我将会使用到一些社区开发的工具:Compose、Consul、Swarm、Machine。这一节我们对这些工具会分别简单介绍,以及基本的操作与构建!在后面的分布式集群架构实战中,我们会综合这些工具,搭建一个更全面实用的应用场景。

  • Docker Compose:简单的容器编配,Python编写。从Docker1.13开始,Docker 内置了 Stack(应用栈),支持 v3版的 docker-compose.yml 部署管理。

  • Sonsul:分布式服务发现,Go语言开发,MPL2.0许可。提供分布式且高可用的服务发现功能。

  • Swarm:编配和集群,Go语言开发,Docker公司团队开发。关于 Swarm 后面会专门用一小节来讨论!

  • Docker Machine:配置和管理 Docker 主机的工具,在后面的分布式集群小节会有使用。

  • 除此之外,我们还有其他众多工具可以选择:

  • Fllet:CoreOS发布,集群管理工具;

  • etcd:CoreOS发布,高可用键值数据库,用于共享配置和服务发现;

  • Kubernetes:Google开源的容器集群管理工具,侧重分布扩展应用程序,弹性分布式微服务;

  • Apache Mesos:高可用集群管理工具;

  • Helios:Spotify发布,为在全流程中发布和管理容器而设计;

  • Centurion:NewRelic开源的部署工具。

Docker Compose

# 安装 Compose
$ sudo curl -L https://github.com/docker/compose/releases/download/1.17.0/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
$ sudo chmod +x /usr/local/bin/docker-compose
# Mac和Windows平台上,Docker Toolbox已经包含了Docker Compose;
# 使用PIP安装(Python-Pip):
$ sudo pip install -U docker-compose

这里我们使用Compose来构建一个 Python Flask 计数应用,将会用到如下2个容器:

  • 应用容器,运行示例程序;

  • Redis容器,运行数据库;

首先构建应用容器,分别准备下面几个文件然后构建:app.py、requirements.txt、Dockerfile (wget:https://github.com/oooline/dockerbook-code/tree/master/code/7/composeapp)

$ sudo docker build -t leon/composeapp . 

构建好的镜像已经准备好了应用和依赖关系包;接下来我们准备 Compose 的配置文件:

# docker-compose.yml : https://github.com/oooline/dockerbook-code/tree/master/code/7/composeapp
version: '3'
services:
  web:
    image: leon/composeapp
    command: python app.py
    ports:
     - "5000:5000"
    volumes:
     - .:/composeapp
  redis:
    image: registry.docker-cn.com/library/redis
    
# 这个脚本指定了2个镜像并启动 app.py 应用。web的定义效果等同:
# $ docker run -d -p 5000:5000 -v .:/composeapp --link redis:redis --name leon/composeapp python app.py
# 也可以在web定义中直接构建镜像,如添加:build: /home/~/composeapp (指定Dockerfile目录)
# 这里的 redis 我们不用构建,直接使用远程源的 redis 镜像。
启动Compose

$ sudo docker-compose up   注:-d 以守护进程方式运行

至此,我们已经可以在浏览器里访问 5000 端口了,可以用2个浏览器测试 Redis 的计数是否工作。

管理Compose

$ docker-compose ps  # 可以查看由compose启动的容器;

$ sudo docker-compose logs # 查看服务的日志事件;

$ sudo docker-compose stop # 停止正在运行的服务;

$ sudo docker-compose kill    # 强制杀死服务;

$ sudo docker-compose start # 启动服务;

$ sudo docker-compose rm    # 删除服务;

服务发现 Consul

这一节我们要创建一个 Consule 服务的 Docker 镜像,然后构建3台运行Docker的宿主机,并在每台上运行一个 Consul。这3台宿主提供一个分布式环境,来展现 Consul 如何处理弹性和失效工作的。

# Consul Dockerfile
FROM registry.docker-cn.com/library/ubuntu:16.04
MAINTAINER Leon <test@weippt.com>
ENV REFRESHED_AT 2017-07-15

RUN apt-get -qq update
RUN apt-get -qq install curl unzip

ADD https://releases.hashicorp.com/consul/1.0.1/consul_1.0.1_linux_amd64.zip /tmp/consul.zip
RUN cd /usr/sbin && unzip /tmp/consul.zip && chmod +x /usr/sbin/consul && rm /tmp/consul.zip
ADD consul.json /config/

EXPOSE 8300 8301 8301/udp 8302 8302/udp 8400 8500 53/udp

VOLUME ["/data"]

ENTRYPOINT [ "/usr/sbin/consul", "agent", "-config-dir=/config" ]
CMD []
#  端口功能
# 53/udp DNS服务器
# 8300   服务器RPC
# 8301+udp  Serf的LAN端口
# 8302+udp  Serf的WAN端口
# 8400   RPC接入点
# 8500   HTTP API
# consul.json
{
  "data_dir": "/data",
  "client_addr": "0.0.0.0",
  "ports": {
    "dns": 53
  },
  "recursors": ["223.5.5.5", "8.8.8.8"]
}
# 指定Consul的DNS服务运行在53端口,recursor用于解析Consul无法解析的DNS请求;

构建 Consul 镜像并启动容器:

$ sudo docker build -t leon/consul .

$ sudo docker run -p 8500:8500 -p 53:53/udp -h node1 leon/consul -server -bootstrap -ui

现在通过浏览器访问 8500 端口,可以看到 consul 服务和一个 node1 节点状态。我们也可用直接使用Docker Hub上的官方 Consul。另外 Consul 参数详见:https://www.consul.io/docs/agent/options.html

Consul 集群

我们先准备3台宿主机:HostA、HostB、HostC,并安装好Docker,然后分别构建 consul 镜像。有以下几种方法我们可以在多台主机部署相同的镜像:

  • 复制 Dockerfile 到每一台主机构建;

  • 使用 save 导出镜像,复制到其他主机,再使用 load 导入;

  • Push 到 Registry (DockerHub或自建);

  • 对于 consul 镜像,你也可以直接使用 Docker Hub 中的官方版 consul。

构建好之后,我们分别获取 3 台机器的IP地址,并设置环境变量:

$ Host(A/B/C)$ PUBLIC_IP="$(ifconfig ens33 | awk -F ' *|:' '/inet addr/{print $4}')" && echo $PUBLIC_IP
HostA: 10.0.0.130
HostB: 10.0.0.128
HostC: 10.0.0.129
$ HOST(B/C)$ JOIN_IP=10.0.0.130 # 指定一台宿主为自启动主机

接下来,修改每台宿主上的 Docker 守护进程的网络配置,将其DNS查找设置为:

  • 本地Docker的IP,以便使用Consul解析DNS;

  • 公共DNS用于解析其他请求;

  • 为Consul查询指定搜索域;

$ ip addr show docker0 #首先查看docker0的ip地址:172.17.0.1;
$ vi /etc/default/docker #然后修改docker的DNS配置;
# 将 #DOCKER_OPTS=... 修改为:
DOCKER_OPTS='--dns 172.17.0.1 --dns 8.8.8.8 --dns-search service .consul'

$ sudo service docker restart # 修改完重启每台宿主的Docker守护进程:

现在我们启动 HostA 的容器节点,由于要和其他容器通信,我们需要将每个端口都映射到宿主:

HostA$ sudo docker run -d -h $HOSTNAME -p 8301:8301 -p 8301:8301/udp -p 8302:8302 -p 8302:8302/udp -p 8300:8300 -p 8400:8400 -p 8500:8500 -p 53:53/udp --name hosta_agent leon/consul -server -advertise $PUBLIC_IP -bootstrap-expect 3 -ui

  • -server 以服务器模式运行;

  • -bootstrap 可自选为集群领导者;

  • -bootstrap-expect 告诉Consul集群有多少代理,同时指定本节点具有自启动功能;

  • -advertise 通过指定的 IP 广播自己;

现在通过日志可以看到:No Cluster leader,由于其他节点尚未加入集群,当前并未触发选举操作。现在我们启动其他节点:

HostB$ sudo docker run -d -h $HOSTNAME -p 8301:8301 -p 8301:8301/udp -p 8302:8302 -p 8302:8302/udp -p 8300:8300 -p 8400:8400 -p 8500:8500 -p 53:53/udp --name hostb_agent leon/consul -server -advertise $PUBLIC_IP -join $JOIN_IP

查看日志可以看到 HostB 已经连接到了HostA;接着我们在 HostC 上启动最后一个代理:

HostC$ sudo docker run -d -h $HOSTNAME -p 8301:8301 -p 8301:8301/udp -p 8302:8302 -p 8302:8302/udp -p 8300:8300 -p 8400:8400 -p 8500:8500 -p 53:53/udp --name hostc_agent leon/consul -server -advertise $PUBLIC_IP -join $JOIN_IP

现在所有的集群都已经加入,通过浏览器可以查看当前的状态:http://hosta:8500  (所有的节点都可以启动-ui,且都能查看节点状态)。

最后通过 dig 确认 DNS 记录:

$ dig @172.17.0.1 consul.service.consul  #可以看到三台主机的对应 A 记录;

$ dig @172.17.0.1 webservice.service.consul #可以看到webservice服务的 A 记录;

ACL 访问控制
# consul.json
{
  "data_dir": "/data",
  "client_addr": "0.0.0.0",
  "ports": {
    "dns": 53
  },
  "recursors": ["223.5.5.5", "8.8.8.8"]
   "acl_datacenter": "dc1",
  "acl_master_token": "xogp39c3dt",
  "acl_default_policy": "deny",
  "acl_down_policy": "extend-cache"

在使用上面的配置重构的 Consul,即可使用 ACL。开启容器之后,使用下面的命令生成一个ID(集群情况下:必须所有的节点都启动,才可以生成ID):

$ curl -H "X-Consul-Token: secret" -X PUT -d '{"Name":"dc1", "Type": "management"}' http://127.0.0.1:8500/v1/acl/create?token=xogp39c3dt

生成的ID可配置于 ui 界面的 Settings中,即可访问 ACL 页面(这里仅用于测试,不对 ACL 过多讨论。这里还有一个小问题,启用ACL后,路由没有刷新172DNS的A记录)。

Compose 构建 Consul

最后我们介绍一个使用 docker-compose.yml 构建 Consul 的便捷方法:

version: '2'

services:
  consul:
    container_name: consul
    image: registry.docker-cn.com/library/consul
    ports:
      - 8500:8500
      - 8301:8301
      - 8300:8300
    command: -server -bootstrap

  consul-server:
    container_name: consul
    image: registry.docker-cn.com/library/consul
    network_mode: host
    environment:
      - 'CONSUL_LOCAL_CONFIG={"skip_leave_on_interrupt": true}'
    command: agent -server -bind=$DOCKER_IP -bootstrap-expect=1 -client=$DOCKER_IP -ui

  consul-agent:
    container_name: consul
    image: registry.docker-cn.com/library/consul
    ports:
      - 8500:8500
      - 8301:8301
      - 8300:8300
    network_mode: host
    environment:
      - 'CONSUL_LOCAL_CONFIG={"leave_on_terminate": true}'
    command: agent -server -advertise $DOCKER_IP -join $CONSUL_SERVER_IP -ui

# 部署方法:
# eval $(docker-machine env HostA)
# export DOCKER_IP=$(docker-machine ip HostA)
# docker-compose -f docker-compose.yml up -d consul-server
# export CONSUL_SERVER_IP=$(docker-machine ip HostA)
# for i in {B..C}; do eval $(docker-machine env Host$i);  \
#  export DOCKER_IP=$(docker-machine ip Host$i);  \
#  docker-compose -f docker-compose.yml up -d consul-agent; \
# done

测试:

HostA:$ curl "http://$(docker-machine ip HostA):8500/v1/catalog/service/web"   # 正常应该会返回 []

HostA:$ curl -X PUT -d 'test msg' "http://$(docker-machine ip HostA):8500/v1/kv/msg1"  # 在 Consul's KV Store 中创建一个值;

HostA:$ curl "http://$(docker-machine ip HostA):8500/v1/kv/msg1?raw"  # 获取刚刚的 msg1 的 value;

HostB:$ curl -X PUT -d 'test another' "http://$(docker-machine ip HostB):8500/v1/kv/msg2"  # 在HostB上创建一个kv;

HostB:$ curl "http://$(docker-machine ip HostA):8500/v1/kv/msg2?raw"  # get msg value

HostC:$ curl "http://$(docker-machine ip HostC):8500/v1/kv/msg2?raw"  # get msg value in HostC

分布式应用

上面的集群看上去很酷,但没什么实际用处,现在我们将基于 uWSGI(web server) 创建一个分布式应用,主要包含以下两部分:

  • 一个Web应用:distributed_app,会在2个节点上运行(HostA、HostB),她会启动相关 Web 进程,并将其作为服务注册到 Consul;

  • 一个应用客户端:distributed_client,会在 HostC 上运行,它从Consul读取与distributed_app相关的信息,并报告当前应用状态和配置;

我们先创建 distributed_app 的 Dockerfile:

# distributed_app Dockerfile
FROM registry.docker-cn.com/library/ubuntu:16.04
MAINTAINER Leon <test@weippt.com>
ENV REFRESHED_AT 2017-07-15

RUN apt-get -yqq update
RUN apt-get -yqq install ruby-dev git libcurl4-openssl-dev curl build-essential python
RUN gem install --no-ri --no-rdoc uwsgi sinatra

RUN mkdir -p /opt/distributed_app
WORKDIR /opt/distributed_app
RUN uwsgi --build-plugin https://github.com/unbit/uwsgi-consul

ADD uwsgi-consul.ini /opt/distributed_app/
ADD config.ru /opt/distributed_app/
ENTRYPOINT [ "uwsgi", "--ini", "uwsgi-consul.ini", "--ini", "uwsgi-consul.ini:server1", "--ini", "uwsgi-consul.ini:server2" ]
CMD []
# uwsgi-consul 可以让uWSGI写入Consul的插件;

# uwsgi-consul.ini 和 config.ru 可以在 https://github.com/oooline/dockerbook-code/tree/master/code/7/consul找到;

$ sudo sed -i "1inameserver 172.17.0.1" /etc/resolv.conf # 最好配置在网卡接口上

Host(A/B)$ sudo docker build -t="leon/distributed_app" .

在构建之前先在宿主 resolv.conf 首行添加DNS:172.17.0.1,以确保容器中 uwsgi 能路由到指定主机。接着我们再 2 台宿主上分别构建这个镜像。构建之后,我们再创建一个 distributed_client 的 Dockerfile:

# distributed_client Dockerfile
FROM registry.docker-cn.com/library/ubuntu:16.04
MAINTAINER Leon <test@weippt.com>
ENV REFRESHED_AT 2017-07-15

RUN apt-get -yqq update
RUN apt-get -yqq install ruby ruby-dev build-essential
RUN gem install --no-ri --no-rdoc json

RUN mkdir -p /opt/distributed_client
ADD client.rb /opt/distributed_client/
WORKDIR /opt/distributed_client

ENTRYPOINT [ "ruby", "/opt/distributed_client/client.rb" ]
CMD []

# client.rb 可以在https://github.com/oooline/dockerbook-code/tree/master/code/7/consul找到;

这个脚本首先会检查 Consul HTTP API 和 ConsulDNS,判断是是否存在名叫 distributed_app 的服务。如果找到服务,则:解析从API返回的JSON,并将有用的信息输出到控制台;对这个服务执行DNS查找,并将返回所有的A记录输出到控制台。

现在我们构建这个镜像:

HostC$ sudo docker build -t="leon/distributed_client" .

然后我们在 HostA 上启动 distributed_app:

HostA$ sudo docker run -h $HOSTNAME -d --name hosta_distributed leon/distributed_app

查看日志可以看到 uWSGI 启动了Web应用,并将其作为服务注册到了Consul。Consul插件为每个distributed_app工作进程构造了一个服务项,并将其注册到Consul里,现在查看网页界面,应该可以看到新注册的服务。

在HostB上也启动 distibuted_app:

HostB$ sudo docker run -h $HOSTNAME -d --name hostb_distributed leon/distributed_app

现在我们启动应用客户端:

HostC$ sudo docker run -h $HOSTNAME -d --name hostc_distributed leon/distributed_client

查看日志可以看到 distributed_client 查询了API,并且找到了 distributed_app及其 server1 和 server2 工作进程的服务项。

在真实的分布式应用程序里,客户端和工作进程可以通过这些信息在分布式应用的节点进行配置、连接、分派消息。这提供了一种简单、方便且具有弹性的方法来构建分离的 Docker 容器和宿主机里运行的分布式应用程序。

Docker Swarm

我们将会在下一小节单独介绍 Docker Swarm !