1

本文分享如何在docker环境中搭建redis cluster集群,并在搭建过程中分享一些docker的常用知识。

准备镜像

关于bin/ubuntu:16.04镜像的构建请参考docker基础环境搭建

构建一个bin/redis:5.0.7镜像,该镜像基于bin/ubuntu:16.04,使用源码安装redis-5.0.7(请先下载redis-5.0.7.tar.gz)。
Dockerfile如下:

FROM bin/ubuntu:16.04

RUN apt-get update &&  apt install -yqq   make gcc 

WORKDIR /var/lib

COPY redis-5.0.7.tar.gz .
RUN tar -xzvf redis-5.0.7.tar.gz && rm redis-5.0.7.tar.gz

WORKDIR /var/lib/redis-5.0.7
RUN make install

构建一个bin/redis-server镜像,Dockerfile如下

FROM bin/redis:5.0.7

COPY docker-entrypoint.sh /usr/local/bin

RUN groupadd -r redis && useradd -r -g redis redis \
&& chmod 777  /usr/local/bin/docker-entrypoint.sh

VOLUME /usr/local/redis/data/
ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["--port 6379"]

CMD 和 ENTRYPOINT 指令都是用来指定容器启动时运行的命令。而使用RUN命令启动容器时也可以指定容器启动时运行的命令。他们区别如下

  1. 不存在ENTRYPOINT指令时,CMD指令可以指定默认命令。如果使用RUN启动容器时指定了命令,会覆盖CMD指令,而如果没有指定,则执行CMD中的默认命令。
  2. 存在ENTRYPOINT指令,ENTRYPOINT指定的命令不可以被RUN覆盖(除非使用--entrypoint参数),但CMD/RUN可以指定参数(作为ENTRYPOINT指定命令的参数)。如果RUN中指定参数,覆盖CMD中的参数。如果没有指定,则使用CMD中的默认参数。
  3. CMD和ENTRYPOINT指令可以exec模式,如ENTRYPOINT ["docker-entrypoint.sh","--port 6379"],也可以使用shell模式,如ENTRYPOINT docker-entrypoint.sh --port 6379,但这时docker会在指定命令前加/bin/sh -c,这样可能会导致一些错误,推荐使用exec模式。

上面Dockerfile配置了容器启动命令为docker-entrypoint.sh,默认参数为--port 6379,也可以在RUN命令中指定参数。
(这里说的RUN命令是启动容器的命令,不是Dockerfile中的RUN指令)

docker-entrypoint.sh负责修改配置,启动redis

#!/bin/bash

mkdir -p /etc/redis/ /var/log/redis/ /usr/local/redis/data/
chown -R redis:redis /var/log/redis/ /usr/local/redis/data/

cat>>/etc/redis/redis.conf<<EOF
protected-mode no
appendonly yes
logfile /var/log/redis/redis.log
dir /usr/local/redis/data/
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
EOF

exec gosu redis redis-server  /etc/redis/redis.conf  $@
exec是 bash 的内置命令,exec用被执行的命令替换掉当前的shell进程,且exec命令后的其他命令将不再执行。使用exec和gosu启动redis,可以让redis成为PID等于1的进程,保证SIGTERM等信号正常工作。
CMD/ENTRYPOINT指令的exce模式也有这个作用。

启动容器

启动所有的redis容器

for i in `seq 1 6`; do 
     sudo docker run -d --name redis-$i bin/redis-server
done

注意,这里run命令没有linux命令参数,则容器启动后执行ENTRYPOINT指定命令和CMD默认参数 docker-entrypoint.sh '--port 6379'

查看进程

$ sudo docker top redis-1

可以通过logs目录查看日志

$ sudo docker logs -f  redis-1

(正常使用下redis可能没有日志输出到前台)

docker容器运行时应该尽量不进行写数据操作(否则删除容器后数据也被删除了),对于数据库这类需要保存动态数据的应用,其数据文件应该保存于卷(volume)中。
VOLUME指令就是用于挂载卷的。
简单来说,就是将docker目录(/usr/local/redis/data/)挂载到宿主机的目录(docker会在宿主机/var/lib/docker/volumes下创建一个子目录),这样docker输出到该docker目录的文件实际保存到宿主机目录。
如果我们修改宿主目录的文件,docker也会马上知道到,这样我们将代码文件放在宿主机上(方便我们修改代码),然后让docker容器通过卷来读取文件。
卷还可以实现数据共享, 通过在run命令中使用--volumes-from选项。

Dockerfile中使用了VOLUME指令挂载了一个卷保存redis的AOF文件,这样容器被删除后,文件还保留在宿主机内。
通过docker inspect 可以查看Volume卷的挂载信息

"Mounts": [
    {
        "Type": "volume",
        "Name": "025f5fff7a31e61f8a068b7b6de38731d16b5efac7e96ac0c01892a4139e8d83",
        "Source": "/var/lib/docker/volumes/025f5fff7a31e61f8a068b7b6de38731d16b5efac7e96ac0c01892a4139e8d83/_data",
        "Destination": "/usr/local/redis/data",
        "Driver": "local",
        "Mode": "",
        "RW": true,
        "Propagation": ""
    }
],

run命令可以通过-v参数挂载卷,并且可以指定宿主机目录,VOLUME指令无法做到这点。

sudo docker run -d  -v /home/binecy/redis/data/:/usr/local/redis/data/ bin/redis-server

创建集群

获取所有的docker容器ip

$ for i in `seq 1 6`; do 
>    echo `sudo docker inspect -f '{{ .NetworkSettings.IPAddress}}'  redis-$i`
> done
172.17.0.2
172.17.0.3
172.17.0.4
172.17.0.5
172.17.0.6
172.17.0.7

连接到redis-1容器,执行以下命令,创建redis cluster

$ sudo docker exec -it redis-1 /bin/bash
root$ redis-cli --cluster create  172.17.0.2:6379 172.17.0.3:6379 172.17.0.4:6379 172.17.0.5:6379 172.17.0.6:6379 172.17.0.7:6379  --cluster-replicas 1

docker网络

下面重点来看看docker网络

bridge模式

上面例子我使用的是docker默认的网络模式,即bridge模式

Docker使用了Linux的Namespaces技术来进行资源隔离,如PID Namespace隔离进程,Mount Namespace隔离文件系统,Network Namespace隔离网络等。

当Docker 进程启动时,会自动在主机上创建一个 docker0 虚拟网桥,默认分配网段172.17.0.0/16,实际上就是一个 Linux bridge 网桥,可以理解为一个软件交换机,附加在其上的任何网卡之间都能自动转发数据包。

在bridge模式下,docker会为每一个容器创建一个Network Namespace。创建一个容器时,容器从docker0中的子网分配一个IP地址,并创建一对veth虚拟网络设备,其中一个设备在容器中作为容器的网卡,另一个设备桥接在宿主机docker0上,可通过命令brctl show 查看(名称为vethXXX),通过这样的桥接方法宿主机上的所有容器都处于同一个二层网络中,这样使得容器与容器以及容器与宿主机之间能够互相通信。(但不能跨宿主机通信)。

veth 是 Virtual ETHernet 的缩写,是一种虚拟网络设备。当总是以两张虚拟网卡(Veth peer)形式被创建,并且在一个网卡上的数据包可直接转发给另一个网卡上,即使这两个网卡不在同一个namespace中。

我们也可以自定义了一个网桥

docker network create redis-net

自定义网桥和默认的docker0网桥最大区别是自定义网桥可以通过--network-alias指定容器的网络别名,容器间可以通过网络别名通信。

for i in `seq 1 6`; do 
     sudo docker run -d --name  redis-$i --network redis-net --network-alias redis-$i bin/redis-server
done

这样,就可以在redis-1容器中,可以ping redis-2 ping ping通redis-2容器,也可以通过 redis-cli-h redis-2 连接到redis-2容器的redis。

Mysql MGR,Zookeeper等分布式系统,需要在应用启动前将集群内其他成员的ip信息写入配置文件,这时很适合使用网络别名。
(redis-cli --cluster create命令无法使用网络别名)

使用自定义网络后,可以通过以下命令获取docker容器ip

sudo docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'  redis-1
docker inspect -f可以支持go template语法,这里使用range循环遍历所有的.NetworkSettings.Networks,并取其下的IPAddress变量。

不过这样创建的redis cluster只有宿主机可以访问,外部机器(非宿主机)无法访问,因为docker中的redis端口没有映射到宿主机上。
我们也可以在run命令上使用-p参数映射端口(-P可以映射Dockerfile中EXPOSE指定的端口):

for i in `seq 1 6`; do 
     sudo docker run -d --name redis-$i -p 6379 -p 16379 bin/redis-server
done

但即使这样外部机器还是无法访问redis cluster,因为redis cluster内部通信用的docker容器ip(就是redis-cli --cluster create命令中的ip),外部机器访问redis cluster时,redis cluster会将这些ip返回给外部机器,并让外部机器通过它们来访问redis cluster,但外部机器无法访问docker容器ip,所以这种方式只能在宿主机上访问redis cluster。

run命令中的-p选项映射端口是通过NAT协议实现的,可以使用iptables命令查看。

host模式

下面说一下host模式
host模式下容器共享宿主机的Network Namespace,容器内启动的端口直接是宿主机的端口,并且容器不会创建网卡和IP,直接使用宿主机的网卡和IP

​下面在host模式下搭建redis cluster集群。
启动redis 应用

for port in `seq 7000 7005`; do 
    sudo docker run -d  --name redis-${port} --net=host  bin/redis-server "--port  ${port}" 
done

这里RUN命令指定了参数,容器启动后执行ENTRYPOINT指定命令和RUN参数docker-entrypoint.sh '--port ${port}'

注意,这里容器启动的端口就是宿主机的端口,要保证宿主机端口不会冲突。

通过宿主机IP192.168.0.102启动redis cluster

$ sudo docker exec -it redis-7000 /bin/bash
root$ redis-cli --cluster create  192.168.0.102:7000 192.168.0.102:7002 192.168.0.102:7002 192.168.0.102:7003 192.168.0.102:7004 192.168.0.102:7005  --cluster-replicas 1

这样外部机器就可以通过宿主机IP192.168.0.102访问redis cluster。

关于docker网络,这有一篇很好的文章 -- docker网络
Dockerfile优化 -- 如何编写最佳的Dockerfile

本文说了docker CMD/ENTRYPOINT,VOLUME,inspect,网络等知识点,基本满足我们日常使用docker了。

如果您觉得本文不错,欢迎关注我的微信公众号,您的关注是我坚持的动力!


binecy
49 声望18 粉丝