本文分享如何在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命令启动容器时也可以指定容器启动时运行的命令。他们区别如下
- 不存在ENTRYPOINT指令时,CMD指令可以指定默认命令。如果使用RUN启动容器时指定了命令,会覆盖CMD指令,而如果没有指定,则执行CMD中的默认命令。
- 存在ENTRYPOINT指令,ENTRYPOINT指定的命令不可以被RUN覆盖(除非使用--entrypoint参数),但CMD/RUN可以指定参数(作为ENTRYPOINT指定命令的参数)。如果RUN中指定参数,覆盖CMD中的参数。如果没有指定,则使用CMD中的默认参数。
- 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<
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了。
如果您觉得本文不错,欢迎关注我的微信公众号,您的关注是我坚持的动力!