本篇是我学习 docker 多镜像编排的第一篇博客,首先通过一个实际的例子使用 docekr 原生命令来对应用栈进行编排连接,再通过 docker-compose 工具进行自动化编排。并从中介绍一些原生命令的使用以及相关的底层知识。

AUFS & Volume

参考:

Docker 本身的设计理念就与传统虚拟机不同,Docker 更倾向于进行资源的隔离。而对于文件系统,Docker 使用了 AUFS(Advanced union filesystem) 来进行文件的隔离(这里我觉得更准确的说是写保护)。那我们肯定要先从系统的层面了解 AUFS

其实在多个 Linux 系统发行版中都是有 AUFS 对应的实现方式: mount -t aufs **。其初衷是想将一个不想被修改的文件 A 与另一个空闲空间 B 联合,那么所有对 A 进行的修改都会保存在 B 中,不会改坏原来的东西。当然在这个初衷的刺激下就产生了功能更强大一些的 AUFS 命令,可以将多个文件/文件夹 union 到一个文件/文件夹上,并且可以为这多个文件/文件夹设置权限。

1
$ sudo mount -t aufs -o dirs=./a=rw:./b=ro none ./c
  • 该命令就是将 a 文件夹和 b 文件夹 unionc 文件夹,a 相对于 c 的权限是读写权限,意思就是 ac 各自的改变都会在对方身上显现。而 b 只是可读权限,意思就是 b 修改后,c 能够观察到,但是 c 修改 b 下属的文件不会在 b 中有任何作用。

  • ab 中有同名文件的时候,c 中的该文件依照先后顺序决定,越往前的优先级越高。

  • 当多个 rw 的文件被 union 在一起的,当我们创建文件的时候,会被轮流写到各个文件夹中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    $ sudo mount -t aufs  -o dirs=./1=rw:./2=rw:./3=rw -o create=rr none ./mnt
    $ touch ./mnt/a ./mnt/b ./mnt/c
    $ tree
    .
    ├── 1
    │ └── a
    ├── 2
    │ └── c
    └── 3
    └── b

    当然可以设置一些轮询的策略,比如 create=mfs | most-free-space 选一个可用空间最大的分支; create=mfsrr:low 选一个空间大于 lowbranch

而这个 Docker 本身的文件策略也有一些不足的地方,就是当你删除 Docker 容器并通过该镜像重启的时候,之前的更改将会丢失。所以为了持久化保存数据并且共享容器间的数据,Docker 提出了 Volume 的概念,它可以绕过默认的 AUFS 而已正常的文件或者目录的形式存在于 hostcontainer 当中。

1
$ docker run -it --name debian-test -v ~/Projects/DebianTest/App1:/usr/src/app:rw debian /bin/bash

该命令就在 image 运行的时候初始化了 Volume,将 host~/Projects/DebianTest 文件夹与 container/usr/src/app 文件夹 union 了起来,权限是 rw

之后可以新开一个 terminal ,检查一下是否成功。

1
2
$ docker inspect --format "{{ .Volumes }}" debian-test
// docker inspect 命令可以查看镜像和容器的详细信息,默认列出全部信息,可以通过--format参数来指定输出的模板格式,以便输出特定的信息

但是我发现在我的 Mac 上这个命令会出现一些问题,可以尝试打出全部内容,手动过滤。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ docker inspect | grep Mount -A 10
// -A 限制输出的行数

"Mounts": [
{
"Type": "bind",
"Source": "/Users/CoderSong/Projects/DebianTest/App1",
"Destination": "/usr/src/app",
"Mode": "",
"RW": true,
"Propagation": "rprivate"
}
],
"Config": {

然后我们在 host:/Users/CoderSong/Projects/DebianTest/App1 中创建文件就能在 container 中看见。这里我们指定了 container 中的文件夹路径,但是同样的功能如果是使用 dockerfile 文件实现的话,是不能指定的。

1
2
FROM debian
VOLUME ~/Projects/Django/App1

当然更方便的是可以使用别的容器的 Volume

1
$ docker run -it --name debian-re-test --volumes-from debian-test /bin/bash

无论 debian-test 是否在运行,它都能起作用。只要有容器连接 Volume,它就不会被删除。

1
2
$ docker run -it --name redis-test redis /bin/bash
$ docker run -it --name node-test --link redis-test:redis node /bin/bash

以上的命令,我们先启动了一个 redis 镜像,然后启动了一个 node 镜像,并将它连接到了 redis 镜像。这里 node 容器叫做接收容器,或者父容器;redis 容器叫做子容器或者源容器。一个接收容器可以连接多个源容器,多个源容器可以连接多个接收容器。

--link 指令主要做了三件事情:

  • 设置接收容器的环境变量
  • 更新接收容器的 /etc/hosts 文件
  • 建立 iptables 规则进行通信

设置环境变量

  • 每有一个源容器,接收容器就会设置一个名为 <alias>_NAME 环境变量。

  • 预先在源容器中设置的部分环境变量同样会设置在接受容器的环境变量中,这些环境变量包括 Dockerfile 中使用 ENV 命令设置的,以及 docker run 命令中使用 -e, --env=[] 参数设置的。

  • 接收容器同样会为源容器中暴露的端口设置环境变量。如 redis 容器的IP为 172.17.0.2, 且暴露了8000的 tcp 端口,则在web容器中会看到如下环境变量。

    1
    2
    3
    4
    5
    REDIS_PORT_8080_TCP_ADDR=172.17.0.2
    REDIS_PORT_8080_TCP_PORT=8080
    REDIS_PORT_8080_PROTO=tcp
    REDIS_PORT_8080_TCP=tcp://172.17.0.82:8080
    REDIS_PORT=tcp://172.17.0.82:8080

更新容器的 /etc/hosts 文件

Docker 容器的IP地址是不固定的,容器重启后IP地址可能和之前不同。所以 link 操作会在 /etc/hosts 中添加一项–源容器的IP和别名,以用来解析源容器的IP地址。并且当源容器重启以后,会自动更新接收容器的 /etc/hosts 文件。这样就不用担心IP的变化对连接的影响。

这个整个过程是在容器启动的时候完成的:

  • 先找到接收容器(将要启动的容器)的所有源容器,然后将源容器的别名和IP地址添加到接收容器的 /etc/hosts
  • 更新所有父sandbox的 hosts 文件

这样当一个容器重启以后,自身的 hosts 文件和以自身为源容器的接受容器的 hosts 文件更新。

建立 iptabls 规则

Docker 为了安全起见,默认会将 Docker daemon-icc 参数设置为 false,容器间的通信就被禁止了。当 redis 容器想要向外界提供服务时,必定暴露一定的端口,假如暴露了 tcp/5432 端口。这样仅需要 node 容器和 redis 容器的 tcp/5432 端口进行通信就可以了。假如 node IP地址为 172.17.0.2/16 ,db容器的IP为 172.17.0.1/16,则需建立如下 iptables 规则。

1
2
-A DOCKER -s 172.17.0.2/32 -d 172.17.0.1/32 -i docker0 -o docker0 -p tcp -m tcp --dport 5432 -j ACCEPT
-A DOCKER -s 172.17.0.1/32 -d 172.17.0.2/32 -i docker0 -o docker0 -p tcp -m tcp --sport 5432 -j ACCEPT

这样就能确保通信的流量不会被丢弃掉。

1
2
$ docker inspect --format='{{ .NetworkSettings.IPAddress }}' [name]
// 该命令可以查看容器的IP

Example

construct-image

拉取3个需要的 image

1
2
3
$ docker pull redis
$ docker pull node
$ docker pull haproxy

运行6个 container ,注意启动顺序和数据卷的挂载

1
2
3
4
5
6
$ docker run -it --name redis-master -v ~/Projects/redis/master:/data redis /bin/bash
$ docker run -it --name redis-slave1 --link redis-master:master -v ~/Projects/redis/slave1 redis:/data /bin/bash
$ docker run -it --name redis-slave2 --link redis-master:master -v ~/Projects/redis/slave2 redis:/data /bin/bash
$ docker run -it --name APP1 --link redis-master:db -v ~/Projects/Node/App1:/usr/src/app node /bin/bash
$ docker run -it --name APP2 --link redis-master:db -v ~/Projects/Node/App2:/usr/src/app node /bin/bash
$ docker run -it --name HAProxy --link APP1:APP1 --link APP2:APP2 -p 6301:6301 -v ~/Projects/HAProxy:/tmp haproxy /bin/bash

检查一下启动状态

1
2
3
4
5
6
7
8
9
$ docker ps

CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
4db8c514f5d2 haproxy "/docker-entrypoin..." 6 seconds ago Up 5 seconds 0.0.0.0:6301->6301/tcp HAProxy
f970b888ef3c node "/bin/bash" 9 minutes ago Up 9 minutes APP2
469506f852a9 node "/bin/bash" 20 minutes ago Up 19 minutes APP1
e0afd181685a redis "docker-entrypoint..." About an hour ago Up About an hour 6379/tcp redis-slave2
272b43e402cc redis "docker-entrypoint..." About an hour ago Up About an hour 6379/tcp redis-slave1
ea63586ce28c redis "docker-entrypoint..." About an hour ago Up About an hour 6379/tcp redis-master

现在将 redis 配置复制到 host-dir

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// redis配置

daemonize yes
pidfile /var/run/redis.pid
port 6379
timeout 300
loglevel debug
logfile /usr/local/bin/log-redis.log
databases 8
save 900 1
save 300 10
save 60 10000
rdbcompression yes
dbfilename dump.rdb
dir /usr/local/bin/db/
appendonly no
appendfsync everysec

// slave 节点需要填上这一行配置,master不用
slaveof master 6379

然后进入 redis container 中修改配置并启动 redis(下面以 redis-master 为例)

1
2
3
4
5
6
7
8
9
// 连入正在运行的容器
$ docker attach redis-master
$ cp redis.conf /usr/local/bin/redis.conf
// 新建本地数据库的位置(这是在配置中写的地址)
$ mkdir db
// 用配置文件启动服务
$ redis-server redis.conf
// 用客户端检查一下服务是否启动
$ redis-cli

接下来测试一下3个 redis 节点是否连通

  • 先到 redis-master 节点放入值

    1
    2
    3
    4
    5
    $ docker attach redis-master
    $ redis-cli
    $ 127.0.0.1:6379> set master testtest
    $ 127.0.0.1:6379> get master
    $ 127.0.0.1:6379> testtest
  • 分别到两个 slave 节点检查

    1
    2
    3
    4
    $ docker attach redis-slave1
    $ redis-cli
    $ 127.0.0.1:6379> get master
    $ 127.0.0.1:6379> testtest

初始化 App 节点(下面以 App1 为例)

1
2
3
4
5
6
7
$ docker attach APP1
$ npm i -g koa-generator pm2
$ cd /usr/src/app
$ koa2 APP1
$ cd APP1
$ npm i
$ npm i -s ioredis

然后用下面的文件覆盖 /router/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const router = require('koa-router')()
const redis = require('ioredis')
const redisClient = new redis(6379, 'db')

router.get('/', async (ctx, next) => {
// 利用我们刚放入 redis 里的值
const result = await redisClient.get('master')
await ctx.render('index', {
title: `Hello Koa 2! -- ${result}`
})
})

router.get('/string', async (ctx, next) => {
ctx.body = 'koa2 string'
})

router.get('/json', async (ctx, next) => {
ctx.body = {
title: 'koa2 json'
}
})

module.exports = router

最后用 pm2 守护进程

1
2
3
$ pm2 start bin/www
// 测试
curl localhost:3000

在配置 APP2 的时候注意更换一个端口,然后配置 HAProxy 节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
global
log 127.0.0.1 local0
maxconn 4096
chroot /usr/local/sbin
daemon
nbproc 4
pidfile /usr/local/sbin/haproxy.pid

defaults
log 127.0.0.1 local3
mode http
option dontlognull
option redispatch
retries 2
maxconn 2000
balance roundrobin
timeout connect 5000ms
timeout client 50000ms
timeout server 50000ms

listen redis_proxy
bind 0.0.0.0:6301
stats enable
stats uri /haproxy-stats
server APP1 APP1:3000 check inter 2000 rise 2 fall 5
server APP2 APP2:4000 check inter 2000 rise 2 fall 5

然后进入 HAProxy 节点启动服务

1
2
3
4
5
$ docker attach HAProxy
$ cd /tmp
$ cp haproxy.conf /usr/local/sbin/
$ cd /usr/local/sbin/
$ haproxy -f haproxy.conf

然后就能在本地访问 http://[harpoxy-ip]:6301

Comments

⬆︎TOP