学习Docker容器网络模型 - 搭建分布式Zookeeper集群
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地址或域名
当部署应用时,会提示用户根据集群实际信息输入下面参数
然而容器服务是如何保证特定的zookeeper容器能够确保调度到指定ECS实例上呢?因为阿里云完全支持Docker Swarm的调度约束,比如对于zookeeper1容器,它会调度到满足下面约束的constraint:aliyun.node_index==1
ECS实例上。对于容器服务集群中的每一个ECS实例,在加入集群时都会被自动添加一系列的标签(label),比如节点序号、地域(Region)、可用区(Avaliablity Zone)等信息。通过这些约束我们可以控制容器和ECS实例的亲和性,来控制调度过程。 关于Docker Swarm 调度和容器服务Compose扩展的信息可以通过连接获得。
注:阿里云容器服务在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版本以来网络模型的进展。也参考了朋友车漾的相关项目。