容器服务Docker&Kubernetes 关注
手机版

学习Docker容器网络模型 - 搭建分布式Zookeeper集群

  1. 云栖社区>
  2. 容器服务Docker&Kubernetes>
  3. 博客>
  4. 正文

学习Docker容器网络模型 - 搭建分布式Zookeeper集群

易立 2016-04-16 12:19:57 浏览15102 评论2

摘要: 本文介绍了利用不同的Docker容器网络模型搭建Zookeeper分布式集群的方法。帮助大家学习理解容器网络模型中的新特性,来实现容器之间的互联互通和服务发现。

14607783002078

ZooKeeper是一个流行的分布式协调服务。它提供了两种不同的部署方式:单机模式和分布式模式。其中单机模式的部署非常简单,网上也有很多资料,我们今天会利用Docker搭建分布式Zookeeper集群,并来帮助大家熟悉Docker中容器网络模型的使用。

ZooKeeper集群中所有的节点作为一个整体对分布式应用提供服务。节点中有两个的角色:Leader和Follower。在整个集群运行过程中,只有一个Leader,其他节点的都是Follower,如果ZK集群在运行过程中Leader出了问题,系统会采用选举算法重新在集群节点选出一个Leader。

Zookeeper节点之间是利用点对点的方式互相联结在一起的,这样的点对点部署方式对利用Docker容器搭建ZK集群提出了挑战。这是因为Zookeeper集群中每个节点需要在启动之前获得集群中所有节点的IP信息, 而当使用Docker缺省bridge网络模式启动容器时,Docker Daemon会为容器分配一个新的的IP地址。这样就形成了信息循环依赖。我们需要一些技巧来确保Zookeeper集群配置正确。

利用Host网络模式

自从1.9.1之后,Docker容器支持5种不同的网络模式,分别为bridge、host、container、overlay,none。我们可以在docker run命令中利用“--net”参数来指定容器网络。详解信息请参见 https://docs.docker.com/reference/run/#network-settings

如果启动容器的时候使用--net host模式,那么这个容器将和宿主机共用一个Network Namespace。这时Docker Engine将不会为容器创建veth pair并配置IP等,而是直接使用宿主机的网络配置,就如直接跑在宿主机中的进程一样。注意,这时容器的其他资源,如文件系统、进程列表等还是和宿主机隔离的。

利用host网络,容器的IP地址和机器节点一致,这样我们在部署容器之前就能确定每个ZK节点的IP地址。我们可以分别在三台的机器上用host模式启动一个ZK容器并配置一个分布式集群。

首先我们需要获得一个ZK的Docker镜像。 本文会直接利用阿里云镜像服务上的镜像registry.aliyuncs.com/acs-sample/zookeeper:3.4.8,
你也可以参照GitHub上代码自己构造
https://github.com/AliyunContainerService/docker-zookeeper

我们假设三个节点的主机名:zookeeper1, zookeeper2, zookeeper3

我们分别在三台不同的主机上依次启动zookeeper容器:利用环境变量SERVER_ID指明节点ID,并在/opt/zookeeper/conf/zoo.cfg中添加ZK集群节点配置信息,具体请详见Docker镜像的启动脚本https://github.com/AliyunContainerService/docker-zookeeper/blob/master/run.sh

登录到zookeeper1上,并执行下列命令启动集群的第一个节点

docker run -d \
 --name=zk1 \
 --net=host \
 -e SERVER_ID=1 \
 -e ADDITIONAL_ZOOKEEPER_1=server.1=zookeeper1:2888:3888 \
 -e ADDITIONAL_ZOOKEEPER_2=server.2=zookeeper2:2888:3888 \
 -e ADDITIONAL_ZOOKEEPER_3=server.3=zookeeper3:2888:3888 \
 registry.aliyuncs.com/acs-sample/zookeeper:3.4.8

登录到zookeeper2上,并执行下列命令启动集群的第二个节点

docker run -d \
 --name=zk2 \
 --net=host \
 -e SERVER_ID=2 \
 -e ADDITIONAL_ZOOKEEPER_1=server.1=zookeeper1:2888:3888 \
 -e ADDITIONAL_ZOOKEEPER_2=server.2=zookeeper2:2888:3888 \
 -e ADDITIONAL_ZOOKEEPER_3=server.3=zookeeper3:2888:3888 \
 registry.aliyuncs.com/acs-sample/zookeeper:3.4.8 

登录到在zookeeper3上,再执行下列命令启动集群的第三个节点

docker run -d \
 --name=zk3 \
 --net=host \
 -e SERVER_ID=3 \
 -e ADDITIONAL_ZOOKEEPER_1=server.1=zookeeper1:2888:3888 \
 -e ADDITIONAL_ZOOKEEPER_2=server.2=zookeeper2:2888:3888 \
 -e ADDITIONAL_ZOOKEEPER_3=server.3=zookeeper3:2888:3888 \
 registry.aliyuncs.com/acs-sample/zookeeper:3.4.8

你可以通过docker logs ...来查看容器日志,ZK集群是否搭建成功

采用host网络方式的优点是,配置简单、网络性能与原生进程一样,对于关注性能和稳定性的生产环境,host方式是一个较好的选择。但是需要登录到每台虚机、物理机上操作太过繁琐

如果利用阿里云容器服务,我们可以利用下面的docker-compose模板,一键在一组ESC实例上创建基于host网络方式的ZK集群。注意:下面模板部署要求集群上至少包括3个ECS实例。

zookeeper1:
    image: 'registry.aliyuncs.com/acs-sample/zookeeper:3.4.8'
    net: host
    environment:
        - SERVER_ID=1
        - ADDITIONAL_ZOOKEEPER_1=server.1=${ZOOKEEPER_1}:2888:3888
        - ADDITIONAL_ZOOKEEPER_2=server.2=${ZOOKEEPER_2}:2888:3888 
        - ADDITIONAL_ZOOKEEPER_3=server.3=${ZOOKEEPER_3}:2888:3888
        - constraint:aliyun.node_index==1
zookeeper2:
    image: 'registry.aliyuncs.com/acs-sample/zookeeper:3.4.8'
    net: host
    environment:
        - SERVER_ID=2
        - ADDITIONAL_ZOOKEEPER_1=server.1=${ZOOKEEPER_1}:2888:3888
        - ADDITIONAL_ZOOKEEPER_2=server.2=${ZOOKEEPER_2}:2888:3888 
        - ADDITIONAL_ZOOKEEPER_3=server.3=${ZOOKEEPER_3}:2888:3888
        - constraint:aliyun.node_index==2
zookeeper3:
    image: 'registry.aliyuncs.com/acs-sample/zookeeper:3.4.8'
    net: host
    environment:
        - SERVER_ID=3
        - ADDITIONAL_ZOOKEEPER_1=server.1=${ZOOKEEPER_1}:2888:3888
        - ADDITIONAL_ZOOKEEPER_2=server.2=${ZOOKEEPER_2}:2888:3888 
        - ADDITIONAL_ZOOKEEPER_3=server.3=${ZOOKEEPER_3}:2888:3888
        - constraint:aliyun.node_index==3

这里利用Docker Compose支持的变量名替换能力,比如 ${...} 可以在运行时让用户输入下列参数

  • ZOOKEEPER_1:第一个ECS实例的IP地址或域名
  • ZOOKEEPER_2:第二个ECS实例的IP地址或域名
  • ZOOKEEPER_3:第三个ECS实例的IP地址或域名

当部署应用时,会提示用户根据集群实际信息输入下面参数

14607232531860

然而容器服务是如何保证特定的zookeeper容器能够确保调度到指定ECS实例上呢?因为阿里云完全支持Docker Swarm的调度约束,比如对于zookeeper1容器,它会调度到满足下面约束的constraint:aliyun.node_index==1ECS实例上。对于容器服务集群中的每一个ECS实例,在加入集群时都会被自动添加一系列的标签(label),比如节点序号、地域(Region)、可用区(Avaliablity Zone)等信息。通过这些约束我们可以控制容器和ECS实例的亲和性,来控制调度过程。 关于Docker Swarm 调度容器服务Compose扩展的信息可以通过连接获得。

14607236421179

注:阿里云容器服务在Swarm基本调度策略上提供了很多有针对性的扩展和增强,比如:为节点动态编辑label,支持基于可用区的高可用调度,细粒度CPU share约束等等。我们未来将结合场景介绍。

体验Docker容器网络模型(Container Network Model)的自定义网络

在开发环测试境,为了节省资源,我们经常需要将ZK集群部署在一台主机。这时候我们必须采用手工的方法调整每个Docker容器暴露的服务端口来避免端口冲突,然而这样会导致端口管理的复杂性。那么是否可以有其他方式来解决呢?我们是否可以让每个ZK容器有自己独立的IP,它们之间可以互相发现对方呢?

在Docker 1.9以前,Docker的网络模型有很多限制,比如:不支持跨节点的容器网络,服务发现能力较弱等等。而且不同应用对网络的需求不同,不同的底层网络技术也各有特点。所以Docker在2015年中发布了一个可扩展的容器网络管理项目libnetwork,并引入了新的容器网络模型(Container Network Model CNM)。

CNM在Docker 1.9版本中第一次正式发布,并持续增强。从此network成为了Docker的第一类资源。用户可以创建容器网络,并将容器关联到网络之上。在相同network上的任何容器都可以利用容器名称来解析服务的访问地址。这样不但解决了容器之间网络互联的问题,还简化了容器之间的服务端点发现。

我们下面利用CNM的自定义特性来部署ZK集群

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

$ docker network ls
NETWORK ID          NAME                TYPE
c642305f9430        none                null               
7cbaa2884a5e        host                host               
22078b0862fc        bridge              bridge             

$ docker network create foo
9b2d3faa64bdcecc1fdd2eb649e57719a9f1b7e4ac5d93b9c11430703908dd2f

$ docker network ls
NETWORK ID          NAME                TYPE
c642305f9430        none                null               
7cbaa2884a5e        host                host               
22078b0862fc        bridge              bridge             
9b2d3faa64bd        foo                 bridge    

然后,我们会创建三个ZK节点容器并分别在“foo”的网络将它们命名为"zk1","zk2",和"zk3"。利用CNM的特性,我们可以使用容器名称来访问网络中其他容器。执行命令如下

docker run -d \
 --name zk1 \
 --net foo \
 -e SERVER_ID=1 \
 -e ADDITIONAL_ZOOKEEPER_1=server.1=0.0.0.0:2888:3888 \
 -e ADDITIONAL_ZOOKEEPER_2=server.2=zk2:2888:3888 \
 -e ADDITIONAL_ZOOKEEPER_3=server.3=zk3:2888:3888 \
 registry.aliyuncs.com/acs-sample/zookeeper:3.4.8 

docker run -d \
 --name zk2 \
 --net foo \
 -e SERVER_ID=2 \
 -e ADDITIONAL_ZOOKEEPER_1=server.1=zk1:2888:3888 \
 -e ADDITIONAL_ZOOKEEPER_2=server.2=0.0.0.0:2888:3888 \
 -e ADDITIONAL_ZOOKEEPER_3=server.3=zk3:2888:3888 \
 registry.aliyuncs.com/acs-sample/zookeeper:3.4.8

docker run -d \
 --name zk3 \
 --net 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=0.0.0.0:2888:3888 \
 registry.aliyuncs.com/acs-sample/zookeeper:3.4.8
 

在这里我们利用容器名作为DNS中容器的域名来配置ZK节点。由于3个ZK容器都被挂载到相同的"foo"网络,他们之间可以通过容器名相互访问。这就优雅地解决了ZK配置和动态容器IP地址之间的循环依赖问题

当然,我们还可以在本地使用下面的docker-compose.yml文件,执行docker-compose up -d来一键部署一个ZK集群。这将大大简化搭建测试环境的工作。

zookeeper1:
    image: 'registry.aliyuncs.com/acs-sample/zookeeper:3.4.8'
    container_name: zookeeper1
    net: foo
    environment:
        - SERVER_ID=1
        - ADDITIONAL_ZOOKEEPER_1=server.1=0.0.0.0:2888:3888
        - ADDITIONAL_ZOOKEEPER_2=server.2=zookeeper2:2888:3888 
        - ADDITIONAL_ZOOKEEPER_3=server.3=zookeeper3:2888:3888
zookeeper2:
    image: 'registry.aliyuncs.com/acs-sample/zookeeper:3.4.8'
    container_name: zookeeper2
    net: foo
    environment:
        - SERVER_ID=2
        - ADDITIONAL_ZOOKEEPER_1=server.1=zookeeper1:2888:3888
        - ADDITIONAL_ZOOKEEPER_2=server.2=0.0.0.0:2888:3888 
        - ADDITIONAL_ZOOKEEPER_3=server.3=zookeeper3:2888:3888
zookeeper3:
    image: 'registry.aliyuncs.com/acs-sample/zookeeper:3.4.8'
    container_name: zookeeper3
    net: foo
    environment:
        - SERVER_ID=3
        - ADDITIONAL_ZOOKEEPER_1=server.1=zookeeper1:2888:3888
        - ADDITIONAL_ZOOKEEPER_2=server.2=zookeeper2:2888:3888 
        - ADDITIONAL_ZOOKEEPER_3=server.3=0.0.0.0:2888:3888

在云端,阿里容器服务为集群缺省创建了一个全局跨节点的“multi-host-network”网络,这样在集群内部容器之间的网络是相互连通的。无需指明网络模式,容器之间就可以直接通过容器名进行访问,相应的docker-compose模板被简化如下

zookeeper1:
    image: 'registry.aliyuncs.com/acs-sample/zookeeper:3.4.8'
    container_name: zookeeper1
    environment:
        - SERVER_ID=1
        - ADDITIONAL_ZOOKEEPER_1=server.1=0.0.0.0:2888:3888
        - ADDITIONAL_ZOOKEEPER_2=server.2=zookeeper2:2888:3888 
        - ADDITIONAL_ZOOKEEPER_3=server.3=zookeeper3:2888:3888
zookeeper2:
    image: 'registry.aliyuncs.com/acs-sample/zookeeper:3.4.8'
    container_name: zookeeper2
    environment:
        - SERVER_ID=2
        - ADDITIONAL_ZOOKEEPER_1=server.1=zookeeper1:2888:3888
        - ADDITIONAL_ZOOKEEPER_2=server.2=0.0.0.0:2888:3888 
        - ADDITIONAL_ZOOKEEPER_3=server.3=zookeeper3:2888:3888
zookeeper3:
    image: 'registry.aliyuncs.com/acs-sample/zookeeper:3.4.8'
    container_name: zookeeper3
    environment:
        - SERVER_ID=3
        - ADDITIONAL_ZOOKEEPER_1=server.1=zookeeper1:2888:3888
        - ADDITIONAL_ZOOKEEPER_2=server.2=zookeeper2:2888:3888 
        - ADDITIONAL_ZOOKEEPER_3=server.3=0.0.0.0:2888:3888

利用容器网络模型提供的容器连接

除了上面的方法,我们还可以在自定义网络中利用容器连接。

自从Docker 1.10版本之后,Docker内嵌了DNS服务,还支持在用户定义的网络上使用容器链接别名来访问引用的容器。 比如我们在容器zk1中,定义了连接 --link zk2:zknode2 那么意味着,在容器中可以利用zknode2作为容器zk2的别名来访问,而Docker容器内部的DNS将其动态解析正确的IP地址。注意与经典的容器链接不同,在自定义网络中容器链接并不需要被连接的容器已经启动,所以我们可以方便地描述双向连接或者P2P方式部署的应用。

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

docker run -d \
 --name zk1 \
 --link zk2:zknode2 \
 --link zk3:zknode3 \
 --net foo \
 -e SERVER_ID=1 \
 -e ADDITIONAL_ZOOKEEPER_1=server.1=0.0.0.0:2888:3888 \
 -e ADDITIONAL_ZOOKEEPER_2=server.2=zknode2:2888:3888 \
 -e ADDITIONAL_ZOOKEEPER_3=server.3=zknode3:2888:3888 \
 registry.aliyuncs.com/acs-sample/zookeeper:3.4.8 

docker run -d \
 --name zk2 \
 --link zk1:zknode1 \
 --link zk3:zknode3 \
 --net foo \
 -e SERVER_ID=2 \
 -e ADDITIONAL_ZOOKEEPER_1=server.1=zknode1:2888:3888 \
 -e ADDITIONAL_ZOOKEEPER_2=server.2=0.0.0.0:2888:3888 \
 -e ADDITIONAL_ZOOKEEPER_3=server.3=zknode3:2888:3888 \
 registry.aliyuncs.com/acs-sample/zookeeper:3.4.8

docker run -d \
 --name zk3 \
 --link zk1:zknode1 \
 --link zk2:zknode2 \
 --net foo \
 -e SERVER_ID=3 \
 -e ADDITIONAL_ZOOKEEPER_1=server.1=zknode1:2888:3888 \
 -e ADDITIONAL_ZOOKEEPER_2=server.2=zknode2:2888:3888 \
 -e ADDITIONAL_ZOOKEEPER_3=server.3=0.0.0.0:2888:3888 \
 registry.aliyuncs.com/acs-sample/zookeeper:3.4.8
 

注:在Docker 1.11版本中对容器链接方式进一步增强,支持一对多这种方式的容器链接。并支持DNS轮询来实现简单的负载均衡。

静态分配容器IP

自从Docker 1.10开始,已经允许用户直接指明容器的IP地址。我们也可以利用这种方法来配置ZK集群。

我们首先创建一个网络“bar”,并设置它的子网为“172.19.0.0/16”动态IP分配区域为“172.19.0.0/17”

docker network create --subnet 172.19.0.0/16 --gateway 172.19.0.1 --ip-range 172.19.0.0/17 bar

这样,我们就保留了172.19.128.0/17这个子网可以用于静态IP地址分配了,下面我们可以自己选择几个不冲突的IP地址来配置ZK集群。比如:节点1至3的IP地址为,172.19.200.1,172.19.200.2和172.19.200.3。

docker run -d \
 --name zk1 \
 --net bar \
 --ip 172.19.200.1 \
 -e SERVER_ID=1 \
 -e ADDITIONAL_ZOOKEEPER_1=server.1=172.19.200.1:2888:3888 \
 -e ADDITIONAL_ZOOKEEPER_2=server.2=172.19.200.2:2888:3888 \
 -e ADDITIONAL_ZOOKEEPER_3=server.3=172.19.200.3:2888:3888 \
 registry.aliyuncs.com/acs-sample/zookeeper:3.4.8 

docker run -d \
 --name zk2 \
 --net bar \
 --ip 172.19.200.2 \
 -e SERVER_ID=2 \
 -e ADDITIONAL_ZOOKEEPER_1=server.1=172.19.200.1:2888:3888 \
 -e ADDITIONAL_ZOOKEEPER_2=server.2=172.19.200.2:2888:3888 \
 -e ADDITIONAL_ZOOKEEPER_3=server.3=172.19.200.3:2888:3888 \
 registry.aliyuncs.com/acs-sample/zookeeper:3.4.8

docker run -d \
 --name zk3 \
 --net bar \
 --ip 172.19.200.3 \
 -e SERVER_ID=3 \
 -e ADDITIONAL_ZOOKEEPER_1=server.1=172.19.200.1:2888:3888 \
 -e ADDITIONAL_ZOOKEEPER_2=server.2=172.19.200.2:2888:3888 \
 -e ADDITIONAL_ZOOKEEPER_3=server.3=172.19.200.3:2888:3888 \
 registry.aliyuncs.com/acs-sample/zookeeper:3.4.8

总结

Docker的容器网络模型的出现大大推动了容器网络技术的发展。善用容器网络模型可以解决不同应用的互联互通问题。

阿里云容器服务完全支持Docker的容器网络模型,并简化和优化了跨节点容器网络的配置。结合Docker Swarm, Docker Compose的能力可以方便地部署各种类型的应用。

本文部分改写自我去年介绍单机环境搭建Zookeeper的文章,补充了很多Docker 1.9版本以来网络模型的进展。也参考了朋友车漾的相关项目

【云栖快讯】阿里云栖开发者沙龙(Java技术专场)火热来袭!快来报名参与吧!  详情请点击

网友评论

1F
biubiubiubiubiu

请问,这三台zk怎么对外暴露IP?

易立

如果利用host模式可以直接使用host IP
如果利用内嵌了DNS服务,在相同的网络上应用可以通过Container Name或容器的host name来访问

biubiubiubiubiu

@易立 我是用的阿里云容器服务容器网络模型那个示例搭建的zookeeper,最终是通过建立宿主机和容器的端口映射暴露IP的

评论
2F
1422499838793414

我想要的是在一个虚拟机下的一个docker主机下 分别起多个容器做集群咋整……

timestatic

可以用swarm, 不一样的体验

评论