Docker Compose 实践

继续向 Kubernetes 进发,上一篇 Docker Swarm 集群模式实操 了解完 Swarm 后,有必要对 Docker Compose 了解一番。Docker Swarm 是把 Docker 宿主机组成集群,部署服务时只要告知 Manager 节点,它就会自动找到相应节点去运行相应的容器。Compose 完全是另一个概念,它把相关联的多个容器组织成一个整体来部署,如由负载容器,多个 Web 容器和一个 Redis 缓存容器构成一个整体。

由其名 Compose 正式有了容器编排这么一个概念,后来将要学的 Kubernetes 是更高级别的组织,运行,管理容器的工具。Compose  由于把容器组织起来了,所以能够一条命令启动多个相关联的容器,而无需单独启动一个一个的容器。

关于 Docker Compose  的安装,在 Mac OS X 下的  Docker Desktop 自带了 docker-compose; 由于 Docker Compose 是用 Python 编写的,我们可以用  pip install docker-compose 的安装,安装后使用它的命令就是 docker-compose

下方的 Docker Container, Swarm, Compose 三者之间的关系图很形像
Compose 由多个容器组成,它可以以一个整体部署到单个 Docker 宿主机或 Swarm 宿主机集群当中。

下面亲自来体验一下 Docker Compose 的功能,设计了一个服务含有如下容器

  1. 前端负载均衡服务器,由 HAProxy 来扮演,请求转发到以下两个  Web 容器
  2. 两个 Web 容器,用 Python Flask 来演示
  3. 一个 Redis 缓存容器,Web 将会访问该容器中的缓存数据

定义 Docker Compose 涉及到的文件有  Dockerfile 和 docker-compose.yml,需要为每一个自定义的 Docker 容器创建一个 Dockerfile,某些容器能直接使用 Docker 镜像仓库的就不用单独的 Dockerfile。也就是说一个 Compose 中可有一个或多个 Dockerfile,  如果是一个 Dockerfile  的情况时,目录结构可以是
myapp/
    - Dockerfile
    - docker-compose.yml
如果需要用到多个 Dockerfile 的情况时,Dockerfile 必须要放在不同的目录中,这时目录结构需要调整为如下(并加上后面将要用到的几个辅助文件)
myapp/
    - proxy/
        - Dockerfile
        - haproxy.cfg
    - web/
        - Dockerfile
        - app.py
        - requirements.txt
    - docker-compose.yml
接下来看每一个文件的内容

myapp/proxy/Dockerfile
1FROM haproxy:2.1.3
2COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg

myapp/proxy/haproxy.cfg
1frontend myweb
2    bind *:80
3    default_backend     realserver   
4backend realserver
5    balance     roundrobin
6    server      web1 web_a:5000 check
7    server      web2 web_b:5000 check

这是一个最简单的 haproxy.cfg 配置文件,只是把请求轮转的转发到 web_a 和 web_b, 将会在后面的 docker-compose.yml 文件中看到 web_a 和 web_b。

myapp/web/Dockerfile
1FROM python:3.7-alpine
2ADD app.py requirements.txt ./
3RUN pip install -r requirements.txt
4CMD ["python", "app.py"]

myapp/web/requirements.txt
1flask
2redis

myapp/web/app.py
 1import redis
 2import socket
 3from flask import Flask
 4
 5cache = redis.Redis('redis')
 6app = Flask(__name__)
 7
 8
 9def hit_count():
10    return cache.incr('hits')
11
12
13@app.route('/')
14def index():
15    count = hit_count()
16    return 'Served by <b>{}</b>, count: <b>{}</b>\n'.format(socket.gethostname(), count)
17
18
19if __name__ == '__main__':
20    app.run(host='0.0.0.0')

用简单的 Flask 来演示一个 Web 应用,访问计数保存在 Redis 中,并显示当前处理请处的机器名。

myapp/docker-compose.yml
 1version: '3.7'
 2services:
 3  web_a:
 4    build: ./web
 5    ports:
 6      - 5000
 7  web_b:
 8    build: ./web
 9    ports:
10      - 5000
11  proxy:
12    build: ./proxy
13    ports:
14      - "80:80"
15  redis:
16    image: "redis:alpine"

自已构建两个 Docker  镜像,Redis 的镜像直接用  redis:alpine 的,在这里定义了端口及映射,所以在 Dockerfile 中不用 EXPOSE 来定义端口号。我们将启动两个 Web 服务,分别为 web_a, web_b, 在前方的 haproxy.cfg 转发请求就是到这两个容器中去的。

现在我们可以来到命令行下的 myapp 目录处,执行命令
$ docker-compose up -d

此时,如果本地没有相应的镜像文件将会构建 myapp_proxy, myapp_web_a, myapp_web_b 镜像,然后启动它们。如果本地已有那些镜像则直接启动那些容器。启动完成后,用 docker ps 查看
1$ docker ps
2CONTAINER ID        IMAGE               COMMAND                  CREATED              STATUS              PORTS                     NAMES
3e5d918e0ae2a        myapp_proxy         "/docker-entrypoint.…"   About a minute ago   Up About a minute   0.0.0.0:80-&gt;80/tcp        myapp_proxy_1
456819534cfa6        myapp_web_a         "python app.py"          About a minute ago   Up About a minute   0.0.0.0:32777-&gt;5000/tcp   myapp_web_a_1
5a2125855e55a        myapp_web_b         "python app.py"          About a minute ago   Up About a minute   0.0.0.0:32778-&gt;5000/tcp   myapp_web_b_1
6f216a7b5f95b        redis:alpine        "docker-entrypoint.s…"   About a minute ago   Up About a minute   6379/tcp                  myapp_redis_1

启动了四个容器,一个 haproxy, 两个 web, 一个  redis,正是我们所需要的。再来验证一下服务,是否能共同完成请求:
1$ curl http://localhost
2Served by <b>a2125855e55a</b>, count: <b>1</b>
3$ curl http://localhost
4Served by <b>56819534cfa6</b>, count: <b>2</b>
5$ curl http://localhost
6Served by <b>a2125855e55a</b>, count: <b>3</b>

负载均衡,Web 服务,Redis 正在协同无误的工作

如果此时用 docker kill 56 杀掉任何一个容器,该容器不会自动恢复起来,但再次运行 docker-compose run -d 后,只是刚刚被杀的容器又起来了,其他的容器没变化。
1$ docker-compose up -d
2Starting myapp_web_a_1 ...
3myapp_redis_1 is up-to-date
4Starting myapp_web_a_1 ... done

请用 docker-compose -h 查看更丰富的命令参数,不少与 docker 命令参数是一样的,只是针对 docker-compose而已,例如下面常用的
  1. docker-compose images: 显示当前 compose 所用到的 docker 镜像
  2. docker-compose logs: 显示当前 compose  的运行日志
  3. docker-compose down: 停掉 compose 涉及到的所有容器
  4. docker-compose kill: 杀掉  compose 涉及的所有容器,而无须用 docker kill 逐个杀
  5. docker-compose restart: 重启整个  compose  服务(所有容器)

还有更简单的支持 HAProxy + 多个动态的 Web 容器的方式

修改前面的 docker-compose.yml 文件内容如下:
 1version: '3.7'
 2services:
 3  web:
 4    build: ./web
 5    ports:
 6      - 5000
 7  proxy:
 8    image: dockercloud/haproxy
 9    links:
10      - web
11    ports:
12      - "80:80"
13    volumes:
14      - /var/run/docker.sock:/var/run/docker.sock
15  redis:
16    image: "redis:alpine"

proxy 选用了 dockercloud/haproxy 镜像,自己构建用的 myapp/proxy 目录及其中的 Dockerfilehaproxy.cfg 文件也不用了。

现在用同样的命令启动 docker-compose up -d
1$ docker ps
2CONTAINER ID        IMAGE                 COMMAND                  CREATED             STATUS              PORTS                                   NAMES
33ce8fcd986ac        dockercloud/haproxy   "/sbin/tini -- docke…"   6 minutes ago       Up 4 minutes        443/tcp, 0.0.0.0:80-&gt;80/tcp, 1936/tcp   myapp_proxy_1
4c73be3d3f795        myapp_web             "python app.py"          6 minutes ago       Up 4 minutes        0.0.0.0:32793-&gt;5000/tcp                 myapp_web_1
50fabf6c6e81f        redis:alpine          "docker-entrypoint.s…"   16 minutes ago      Up 4 minutes        6379/tcp                                myapp_redis_1

只有一个 web 容器,现在还能动态扩容,用 docker-compose up --scale web=d -d
 1$ docker-compose up --scale web=3 -d
 2Starting myapp_web_1 ...
 3Starting myapp_web_1 ... done
 4Creating myapp_web_2 ... done
 5Creating myapp_web_3 ... done
 6myapp_proxy_1 is up-to-date
 7$ docker ps
 8CONTAINER ID        IMAGE                 COMMAND                  CREATED             STATUS              PORTS                                   NAMES
 9e3b4db8db92d        myapp_web             "python app.py"          7 seconds ago       Up 5 seconds        0.0.0.0:32796-&gt;5000/tcp                 myapp_web_2
10f1ba850865e0        myapp_web             "python app.py"          7 seconds ago       Up 5 seconds        0.0.0.0:32795-&gt;5000/tcp                 myapp_web_3
113ce8fcd986ac        dockercloud/haproxy   "/sbin/tini -- docke…"   8 minutes ago       Up 6 minutes        443/tcp, 0.0.0.0:80-&gt;80/tcp, 1936/tcp   myapp_proxy_1
12c73be3d3f795        myapp_web             "python app.py"          8 minutes ago       Up 6 minutes        0.0.0.0:32793-&gt;5000/tcp                 myapp_web_1
130fabf6c6e81f        redis:alpine          "docker-entrypoint.s…"   17 minutes ago      Up 6 minutes        6379/tcp                                myapp_redis_1

来测试一下他们是否能正常工作
1$ curl http://localhost
2Served by <b>c73be3d3f795</b>, <b>count: 20</b>
3$ curl http://localhost
4Served by <b>e3b4db8db92d</b>, count: <b>21</b>
5$ curl http://localhost
6Served by <b>f1ba850865e0</b>, count: <b>22</b>

三次请求分别被三个不同的 Web 容器处理。

注:

  1. docker-compose scale web=2 命令不推荐使用,而应用 docker-compose up --scale web=2 -d
  2. docker-compose.yml 中的 deploy 配置只能工作于 Swarm 模式,而 deploy 下的 replicas 是工作于 docker stack deploy 命令的,在 docker-compose updocker-compose run 中被忽略

关于在 Swarm  集群之上运行 Compose

看起来有点麻烦,见官方的 Use Compose with Swarm. 需要用到 docker stack 来部署 Compose。自己就不作实际操作了,也不知道产品中这种结合情景多不多,关键了解一下部署后容器是如何在 Swarm  节点中分配的,是把  Compose 当作一个整体呢?还是以 Compose 中的容器为粒度来部署。

Deploy Docker Compose (v3) to Swarm (mode) Cluster 中的一张图片来看

Swarm 还是能把 Compose 中定义的容器拆分出来,单独的部署到 Swarm 集群中的节点上,而不是把 Compose 当作一个整体来看待,表现的不错。

链接:

  1. How to use Docker Compose to run complex multi container apps on your Raspberry Pi
  2. Docker Compose 多容器部署 (五)
  3. Docker-Compose简介安装使用
永久链接 https://yanbin.blog/docker-compose-in-action/, 来自 隔叶黄莺 Yanbin's Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。