使用不同Docker网络模型搭建Zookeeper集群(ZT)

Zookeeper Cluster

ZooKeeper是一个流行的分布式开源框架,提供了协调分布式应用的基本服务。它提供了两种不同的部署方式:Standalone模式和Distributed模式。其中Standalone模式的部署非常简单,网上也有很多资料,我们今天会针对Distributed模式,利用Docker搭建Zookeeper集群来加速开发和测试,并来帮助大家熟悉利用Docker中不同的网络模式进行分布式应用部署。

ZooKeeper集群中所有的结点作为一个整体对分布式应用提供服务,集群中每个结点之间都互相连接。节点之间有两个的角色:Leader和Follower。在整个集群运行过程中,只有一个Leader,其他节点的都是Follower,如果ZK集群在运行过程中Leader出了问题,系统会采用选举算法重新在集群节点选出一个Leader。Zookeeper节点之间是利用点对点的方式互相联结在一起的,这样的点对点部署方式对利用Docker容器搭建ZK集群提出了挑战。

首先,当ZK部署时,每个节点都要了解集群中其他节点的端口信息。 我们知道Docker有一个容器链接(linking)机制允许将两个容器方便的连接在一起,共享访问信息。通过容器链接,目标容器可以通过环境变量的方式获取源容器的信息,也可以利用别名来访问源容器中服务。关于 Docker Linking的详细信息,请参见 https://docs.docker.com/userguide/dockerlinks/ 。Docker linking非常适于描述容器之间服务调用(比如基于Tomcat的JEE应用访问MySQL),主从模式(比如MySQL Master/Slave)等关系。但是Container linking只提供了容器之间的单向链接关系,而在源容器中无法获得目标容器的访问信息。所以我们无法利用Container linking来进行点对点服务的关联。

在Docker 1.7之前,Docker支持4种不同的网络模式,分别为bridge、host、container、none, 并可以在docker run命令中利用“--net”参数来指定。详解信息请参见https://docs.docker.com/reference/run/#network-settings。 Docker缺省的网络模式是bridge方式。Brdige桥接模式为每个Docker Container创建独立的网络栈,保证容器内的进程组使用独立的网络环境,实现容器间、容器与宿主机之间的网络栈隔离。另外,Docker通过宿主机上的网桥(docker0)来连通容器内部的网络栈与宿主机的网络栈,实现容器与宿主机乃至外界的网络通信。 bridge_networking

但是由于Zookeeper集群中每个节点需要在启动之前获得集群中所有节点的IP信息。 而对于Docker的bridge网络模式,当启动容器时Docker Daemon会为容器分配一个新的namespace,拥有一个新的IP地址。这样就形成了信息循环依赖。所以我们也不能利用Docker的bridge网络模式来启动ZK节点。

下面我们会利用其它不同的网络模型来实现对Zookeeper集群的部署

利用Host网络模式

如果启动容器的时候使用host模式,那么这个容器将不会获得一个独立的Network Namespace,而是和宿主机共用一个Network Namespace。容器将不会虚拟出自己的网卡,配置自己的IP等,而是使用使用宿主机的IP和端口,不用任何NAT转换,就如直接跑在宿主机中的进程一样。但是,容器的其他方面,如文件系统、进程列表等还是和宿主机隔离的。

我们可以利用host模式来搭建ZK集群。我们可以分别在三台的机器上用host模式启动一个ZK节点,监听2181, 2888, 3888等不同端口。比如三个主机的hostname分别为ZK1,ZK2和ZK3。我们要在不同主机上启动ZK节点。

首先我们需要获得一个ZK的Docker镜像。 本文会直接利用Docker Hub上的镜像https://registry.hub.docker.com/u/garland/zookeeper/ 你也可以参照GitHub上代码自己构造https://github.com/sekka1/mesosphere-docker/tree/master/zookeeper

在ZK1上启动集群的第一个节点:利用环境变量SERVER_ID指明节点ID,并在/opt/zookeeper/conf/zoo.cfg中添加ZK集群节点配置信息,具体请详见Docker镜像的启动脚本https://github.com/sekka1/mesosphere-docker/blob/master/zookeeper/run.sh

docker run -d \
 --net=host \
 -e SERVER_ID=1 \
 -e ADDITIONAL_ZOOKEEPER_1=server.1=zk1:2888:3888 \
 -e ADDITIONAL_ZOOKEEPER_2=server.2=zk2:2888:3888 \
 -e ADDITIONAL_ZOOKEEPER_3=server.3=zk3:2888:3888 \
 garland/zookeeper

在ZK2上启动集群的第2个节点

docker run -d \
 --net=host \
 -e SERVER_ID=2 \
 -e ADDITIONAL_ZOOKEEPER_1=server.1=zk1:2888:3888 \
 -e ADDITIONAL_ZOOKEEPER_2=server.2=zk2:2888:3888 \
 -e ADDITIONAL_ZOOKEEPER_3=server.3=zk3:2888:3888 \
 garland/zookeeper 

在ZK3上启动集群的第3个节点

docker run -d \
 --net=host \
 -e SERVER_ID=3 \
 -e ADDITIONAL_ZOOKEEPER_1=server.1=zk1:2888:3888 \
 -e ADDITIONAL_ZOOKEEPER_2=server.2=zk2:2888:3888 \
 -e ADDITIONAL_ZOOKEEPER_3=server.3=zk3:2888:3888 \
 garland/zookeeper

采用host方式的好处是直接利用host网络,配置简单、网络性能和原生进程一样,对于关注性能和稳定性的生产环境,host方式是一个较好的选择。

但是如果在开发环境,我们需要将ZK集群部署在一台主机上进行开发测试的时候,我们必须采用手工的方法进行端口管理,来避免端口冲突。

比如,我们可以利用如下脚本配置ZK集群中不同容器

  • 第一个容器侦听localhost的2181,2888,3888端口
  • 第二个容器侦听localhost的2182,2889,3889端口
  • 第三个容器侦听localhost的2183,2890,3890端口
docker run -d \
 --name=zk1 \
 --net=host \
 -e SERVER_ID=1 \
 -e ADDITIONAL_ZOOKEEPER_1=server.1=localhost:2888:3888 \
 -e ADDITIONAL_ZOOKEEPER_2=server.2=localhost:2889:3889 \
 -e ADDITIONAL_ZOOKEEPER_3=server.3=localhost:2890:3890 \
 -e ADDITIONAL_ZOOKEEPER_4=clientPort=2181 \
 garland/zookeeper

docker run -d \
 --name=zk2 \
 --net=host \
 -e SERVER_ID=2 \
 -e ADDITIONAL_ZOOKEEPER_1=server.1=localhost:2888:3888 \
 -e ADDITIONAL_ZOOKEEPER_2=server.2=localhost:2889:3889 \
 -e ADDITIONAL_ZOOKEEPER_3=server.3=localhost:2890:3890 \
 -e ADDITIONAL_ZOOKEEPER_4=clientPort=2182 \
 garland/zookeeper

docker run -d \
 --name=zk3 \
 --net=host \
 -e SERVER_ID=3 \
 -e ADDITIONAL_ZOOKEEPER_1=server.1=localhost:2888:3888 \
 -e ADDITIONAL_ZOOKEEPER_2=server.2=localhost:2889:3889 \
 -e ADDITIONAL_ZOOKEEPER_3=server.3=localhost:2890:3890 \
 -e ADDITIONAL_ZOOKEEPER_4=clientPort=2183 \
 garland/zookeeper

之后我们可以利用docker ps来查看ZK集群容器

ubuntu@ubuntu:~/work/zookeeper$ docker ps | grep zk
cf6ef9081c68        garland/zookeeper:latest                   "/opt/run.sh"          3 minutes ago       Up 3 minutes                                                     zk3                                   
219c5a6080ed        garland/zookeeper:latest                   "/opt/run.sh"          3 minutes ago       Up 3 minutes                                                     zk2                                   
3b0b2647b664        garland/zookeeper:latest                   "/opt/run.sh"          3 minutes ago       Up 3 minutes                                                     zk1

利用docker ps zk1等命令查看容器日志,并可登入容器内部来利用ZK CLI来做简单的验证

ubuntu@ubuntu:~/work/zookeeper$ docker exec -ti zk1 bash
root@ubuntu:/opt/zookeeper# bin/zkCli.sh  -server 127.0.0.1:2181
Connecting to 127.0.0.1:2181
...

WATCHER::

WatchedEvent state:SyncConnected type:None path:null
[zk: 127.0.0.1:2181(CONNECTED) 1] create /zk_test my_data
Created /zk_test
[zk: 127.0.0.1:2181(CONNECTED) 2] ls /
[zookeeper, zk_test]

测试完毕,我们可以将容器从系统中删除。

docker rm -f zk1
docker rm -f zk2
docker rm -f zk3

由于利用Host模式会引入有端口管理的复杂性,那么在开发测试环境中是否可以有其他方式来解决呢?

利用Container网络模式

Docker在提供了host模式之外还提供了Container模式。这个模式指定新创建的容器和已经存在的一个容器共享一个Network Namespace,而不是和宿主机共享。新创建的容器不会创建自己的网卡,配置自己的IP,而是和指定的容器共享IP、端口范围等。同样,两个容器除了网络方面,其他的如文件系统、进程列表等还是隔离的。

利用Container模式,我们可以预先创建一组网络容器zkhost1,zkhost2和zkhost3, 并获取相应的IP地址列表;然后利用--net container:的方式让ZK容器关联到指定到特定的网络容器上,并利用之前IP地址信息完成集群部署。这样每个ZK容器也都有自己独立的网络名空间和IP地址,也无需担心端口冲突了。

CID1=$(docker run --name zkhost1 -d busybox /bin/sh -c "while true; do sleep 1; done")
HOST1=$(docker inspect --format '{{ .NetworkSettings.IPAddress }}' ${CID1})
CID2=$(docker run --name zkhost2 -d busybox /bin/sh -c "while true; do sleep 1; done")
HOST2=$(docker inspect --format '{{ .NetworkSettings.IPAddress }}' ${CID2})
CID3=$(docker run --name zkhost3 -d busybox /bin/sh -c "while true; do sleep 1; done")
HOST3=$(docker inspect --format '{{ .NetworkSettings.IPAddress }}' ${CID3})

docker run -d \
 --name=zk1 \
 --net container:${CID1} \
 -e SERVER_ID=1 \
 -e ADDITIONAL_ZOOKEEPER_1=server.1=${HOST1}:2888:3888 \
 -e ADDITIONAL_ZOOKEEPER_2=server.2=${HOST2}:2888:3888 \
 -e ADDITIONAL_ZOOKEEPER_3=server.3=${HOST3}:2888:3888 \
 garland/zookeeper

docker run -d \
 --name=zk2 \
 --net container:${CID2} \
 -e SERVER_ID=2 \
 -e ADDITIONAL_ZOOKEEPER_1=server.1=${HOST1}:2888:3888 \
 -e ADDITIONAL_ZOOKEEPER_2=server.2=${HOST2}:2888:3888 \
 -e ADDITIONAL_ZOOKEEPER_3=server.3=${HOST3}:2888:3888 \
 garland/zookeeper

docker run -d \
 --name=zk3 \
 --net container:${CID3} \
 -e SERVER_ID=3 \
 -e ADDITIONAL_ZOOKEEPER_1=server.1=${HOST1}:2888:3888 \
 -e ADDITIONAL_ZOOKEEPER_2=server.2=${HOST2}:2888:3888 \
 -e ADDITIONAL_ZOOKEEPER_3=server.3=${HOST3}:2888:3888 \
 garland/zookeeper

并利用如下命令来查看ZK相关容器

ubuntu@ubuntu:~/work/zookeeper$ docker ps | grep zk
a72947ebf5fb        garland/zookeeper:latest                   "/opt/run.sh"          4 seconds ago        Up 3 seconds                                                      zk2                                    
c4c43138c7b4        garland/zookeeper:latest                   "/opt/run.sh"          About a minute ago   Up About a minute                                                 zk3                                    
c5e5ab62ea50        garland/zookeeper:latest                   "/opt/run.sh"          About a minute ago   Up About a minute                                                 zk1                                    
4714c5858a77        busybox:latest                             "/bin/sh -c 'while t   3 minutes ago        Up 3 minutes                                                      zkhost3                                
7ed3b335e52f        busybox:latest                             "/bin/sh -c 'while t   3 minutes ago        Up 3 minutes                                                      zkhost2                                
0716efa582e1        busybox:latest                             "/bin/sh -c 'while t   3 minutes ago        Up 3 minutes                                                      zkhost1                                

测试完毕可以利用如下代码清除

docker rm -f zk1
docker rm -f zk2
docker rm -f zk3
docker rm -f zkhost1
docker rm -f zkhost2
docker rm -f zkhost3

采用Container网络模式我们可以使一组容器共享相同的网络名空间/IP地址,也可以灵活的控制容器获取IP的时序依赖关系。在Google的容器集群方案Kubernetes中就采用了类似的技术来实现Container Pod。但是这个方案也有很大的局限性,当网络容器被杀死或重启之后,由于原有namespace被删,就会导致应用容器无法访问。而且为应用容器引入网络容器也增加了容器管理的复杂性。另外利用IP地址作为集群的配置,当容器IP变化之后也对变更集群配置带来了复杂性。

体验新Container Network Model支持

在Docker 1.7和之前版本的Docker实现的Docker容器模型还有很多限制,比如缺省容器网络不够灵活,不支持多节点的Docker网络等等。而不同用户对网络的需求各有不同,而才用的底层网络技术也各有特点。所以Docker发布了自家的容器网络管理项目libnetwork,并引入了容器网络模型(Container Network Model CNM)。

由于时间所限,本文不会对容器的网络模型做过多的介绍和讨论。详细信息可以参阅https://github.com/docker/libnetwork/blob/master/docs/design.md

CNM

但是我们可以利用开发中的Docker网络插件来体验一下新的网络模型如何可以方便的简化容器网络部署的。

首先需要安装Docker的experimental binary。Docker官方提供了实验性质的版本,允许用户及早体验Docker的新功能,并且给Docker的维护者提供反馈,从而让大家在实践以及反馈中构建这些重要功能。目前Docker的网络插件以及volume插件是最先问世的试验性功能插件。关于Docker的experimental binary详细安装步骤请参见https://github.com/docker/docker/tree/master/experimental

安装之后可以利用docker version命令来验证安装

vagrant@vagrant-ubuntu-trusty-64:~$ docker version
Client version: 1.8.0-dev
Client API version: 1.20
Go version (client): go1.4.2
Git commit (client): f39b9a0
OS/Arch (client): linux/amd64
Experimental (client): true
Server version: 1.8.0-dev
Server API version: 1.20
Go version (server): go1.4.2
Git commit (server): f39b9a0
OS/Arch (server): linux/amd64
Experimental (server): true

在最新的网络插件中,network和service成为了Docker的第一类资源。用户可以创建容器网络,发布服务,并将容器关联到服务之上。Docker提供了简单的服务发现能力,相同network上的任何容器都可以利用DNS来解析服务的访问地址。这样不但解决了容器之间网络互联的问题,还简化了容器之间的服务端点发现。https://github.com/docker/docker/blob/master/experimental/networking.md

下面我们首先创建一个名为“foo”的网络

vagrant@vagrant-ubuntu-trusty-64:~$ docker network ls
NETWORK ID          NAME                TYPE
c642305f9430        none                null               
7cbaa2884a5e        host                host               
22078b0862fc        bridge              bridge             
vagrant@vagrant-ubuntu-trusty-64:~$ docker network create foo
9b2d3faa64bdcecc1fdd2eb649e57719a9f1b7e4ac5d93b9c11430703908dd2f
vagrant@vagrant-ubuntu-trusty-64:~$ docker network ls
NETWORK ID          NAME                TYPE
c642305f9430        none                null               
7cbaa2884a5e        host                host               
22078b0862fc        bridge              bridge             
9b2d3faa64bd        foo                 bridge    

之后,我们会创建三个ZK节点容器并分别在“foo”的网络将它们发布成为服务zk1.foo,zk2.foo,和zk3.foo,然后利用CNM的特性,使用服务名称来访问集群中其他容器。

docker run -d \
 --name zknode1 \
 --publish-service zk1.foo \
 -e SERVER_ID=1 \
 -e ADDITIONAL_ZOOKEEPER_1=server.1=zk1:2888:3888 \
 -e ADDITIONAL_ZOOKEEPER_2=server.2=zk2:2888:3888 \
 -e ADDITIONAL_ZOOKEEPER_3=server.3=zk3:2888:3888 \
 garland/zookeeper 

docker run -d \
 --name zknode2 \
 --publish-service zk2.foo \
 -e SERVER_ID=2 \
 -e ADDITIONAL_ZOOKEEPER_1=server.1=zk1:2888:3888 \
 -e ADDITIONAL_ZOOKEEPER_2=server.2=zk2:2888:3888 \
 -e ADDITIONAL_ZOOKEEPER_3=server.3=zk3:2888:3888 \
 garland/zookeeper 

docker run -d \
 --name zknode3 \
 --publish-service zk3.foo \
 -e SERVER_ID=3 \
 -e ADDITIONAL_ZOOKEEPER_1=server.1=zk1:2888:3888 \
 -e ADDITIONAL_ZOOKEEPER_2=server.2=zk2:2888:3888 \
 -e ADDITIONAL_ZOOKEEPER_3=server.3=zk3:2888:3888 \
 garland/zookeeper

为了了解CNM如何实现服务发现,我们可以登入ZK节点容器,显示其/etc/hosts文件内容

vagrant@vagrant-ubuntu-trusty-64:~$ docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS                          NAMES
1ccce1f7437d        garland/zookeeper   "/opt/run.sh"       2 minutes ago       Up 2 minutes        2181/tcp, 2888/tcp, 3888/tcp   zknode3            
706a5d760ce3        garland/zookeeper   "/opt/run.sh"       2 minutes ago       Up 2 minutes        2181/tcp, 2888/tcp, 3888/tcp   zknode2            
047d4b4cf71a        garland/zookeeper   "/opt/run.sh"       2 minutes ago       Up 2 minutes        2181/tcp, 2888/tcp, 3888/tcp   zknode1

vagrant@vagrant-ubuntu-trusty-64:~$ docker exec -ti zknode1 bash
root@047d4b4cf71a:/opt/zookeeper# cat /etc/hosts
172.18.42.2 047d4b4cf71a
127.0.0.1   localhost
::1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
172.18.42.2 zk1
172.18.42.2 zk1.foo
172.18.42.3 zk2
172.18.42.3 zk2.foo
172.18.42.4 zk3
172.18.42.4 zk3.foo

我们可以看到在容器内部Docker已经将“foo”的网络上发布的服务注册在容器的/etc/hosts文件中,这样就可以通过服务名称来对其他容器进行访问。而且当容器变化时,Docker Daemon会动态修改主机解析内容。

总结

Docker为容器通信提供了不同的网络模型,而且在快速发展中。根据应用需要,善用容器网络模型可以解决很多不同类型的问题。本文还主要是介绍单节点环境的容器网络,有机会我也会和大家介绍跨节点的容器网络模型。

 

From: https://github.com/denverdino/aliyungo/wiki/Zookeeper-cluster-with-Docker

发表评论

电子邮件地址不会被公开。 必填项已用*标注