Zookeeper:分布式程序的基石
一、目录
1、zookeeper是什么?
2、安装、配置、启动、监控
3、javaApi基础用法
4、应用场景
5、CAP理论/paxos算法
二、zookeeper简介
官方版:zookeeper是 一个分布式的,开放源码的分布式应用程序协调服务,它包含一个简单的原语集,分布式应用程序可以基于它实现同步服务,配置维护和命名服务等。
概括版:zookeeper是“一致、 有头、数据树”。
一致:数据一致性(核心)。例如,有一个1000台机器的集群,我想修改1000台机器上相同的配置文件,那么我们是一台一台去改吗?显然这种解决办法并不可靠,所以把配置信息注册到zookeeper,集群机器就成为了观察者,当配置信息改变的时候,通知集群所有的机器改变。从而保证了集群的配置文件一致性。
有头:有头领。为什么要有头领?例如,有1000太机器的集群,客户端发送请求修改配置文件,那么谁去处理?有人可能说,我想指定那台机器,我就指定它ip呗?这样的话,那怎么实现负载均衡,手动控制吗?所以,选择一个头领,当客户端请求来的时候,由leader(头领)去控制,把任务分配给follower(跟班)。
数据树:绑定数据的树状结构。简单理解,它也是一个数据库,只不过它是存储于内存的树状结构的数据库。
三、安装-配置-启动-监控
1、物理架构
搭建zookeeper集群,zookeeper规定是总共集群的机器是奇数,而集群正常运行的条件是:存活的机器大于集群总数的二分之一。
2、安装配置(注意:集群的所有机器都需要设置)
-
下载zookeeper-3.4.10.tar.gz
-
解压:tar xvf zookeeper-3.4.10.tar.gz
-
配置
- 切换到/zookeeper/conf目录:/usr/local/zookeeper-3.4.10/conf (我的路径)
- 拷贝:cp zoo_sample.cfg zoo.cfg
- 修改zoo.cfg:vim zoo.cfg
- dataDir=/tmp/zookeeper (数据存储位置,生产环境需要修改,这个是linux的临时目录,可能会被删除)
- 在配置文件底部添加一下内容:我这个配置了域名,如果没有配置域名就用ip。
- server.1=master:2888:3888
- server.2=slave2:2888:3888
- server.3=slave3:2888:3888
- 修改数据文件
- 切换/tmp目录:cd /tmp
- 创建zookeeper目录:mkdir zookeeper
- 切换至zookeeper目录:cd /tmp/zookeeper
- 创建myid文件:vim myid
- master上,输入1保存;slave2,输入2保存;slave3,输入3保存。
-
启动、观测
- 切换至/zookeeper/bin目录:/usr/local/zookeeper-3.4.10/bin
- 服务端
- 启动:./zkServer.sh start
- 查看:./zkServer.sh status
- 停止:./zkServer.sh stop
- jps(查看状态)
- 2289 QuorumPeerMain
- 2302 Jps
- 客户端 .
- ./zkCli.sh -server master:2181
-
create /tank tankservers
-
create /tank/server1 server1info
-
create /tank/server2 server2info
-
create /tank/server3 server3info
-
ls /tank
-
get /tank
-
set /tank tankserversinfo
-
get /tank
-
delete /tank/server3
四、javaApi基础用法
/**
* .测试zookeeper的基本操作
* Creator:邱勇Aaron
* DateTime:2017/6/25 14:51
*/
public class ZookeeperBasicOperator {
public static String connectString="192.168.0.100,192.168.0.102,192.168.0.103:2181";
private ZooKeeper zk;
</span><span style="color: #0000ff;">public</span><span style="color: #000000;"> ZookeeperBasicOperator(){
</span><span style="color: #0000ff;">this</span>(connectString,2000,<span style="color: #0000ff;">null</span><span style="color: #000000;">);
}
</span><span style="color: #0000ff;">public</span> ZookeeperBasicOperator(String connectString,<span style="color: #0000ff;">int</span><span style="color: #000000;"> sessionTimeout,Watcher watcher){
</span><span style="color: #0000ff;">try</span><span style="color: #000000;"> {
zk</span>=<span style="color: #0000ff;">new</span><span style="color: #000000;"> ZooKeeper(connectString,sessionTimeout,watcher);
}</span><span style="color: #0000ff;">catch</span><span style="color: #000000;"> (IOException e){
e.printStackTrace();
}
}
</span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">static</span> <span style="color: #0000ff;">void</span> main(String [] args) <span style="color: #0000ff;">throws</span><span style="color: #000000;"> Exception {
connectString</span>="192.168.0.100,192.168.0.102,192.168.0.103:2181"<span style="color: #000000;">;
ZookeeperBasicOperator zkOperator</span>=<span style="color: #0000ff;">new</span> ZookeeperBasicOperator(connectString,2000,<span style="color: #0000ff;">new</span><span style="color: #000000;"> MyWatcher());
</span><span style="color: #008000;">//</span><span style="color: #008000;">创建目录节点</span>
zkOperator.create("/testRootPath","testRootData"<span style="color: #000000;">,CreateMode.PERSISTENT);
</span><span style="color: #008000;">//</span><span style="color: #008000;">查看目录节点</span>
zkOperator.getData("/testRootPath"<span style="color: #000000;">);
</span><span style="color: #008000;">//</span><span style="color: #008000;">创建目录子节点</span>
zkOperator.create("/testRootPath/testRootChild","testRootChild"<span style="color: #000000;">,CreateMode.PERSISTENT);
</span><span style="color: #008000;">//</span><span style="color: #008000;">查看目录节点的子节点目录;</span>
zkOperator.getChildern("/testRootPath"<span style="color: #000000;">);
</span><span style="color: #008000;">//</span><span style="color: #008000;">重新设置目录节点的数据</span>
zkOperator.setData("/testRootPath","helloTestRootData"<span style="color: #000000;">);
</span><span style="color: #008000;">//</span><span style="color: #008000;">查看目录节点的数据</span>
zkOperator.getData("/testRootPath"<span style="color: #000000;">);
</span><span style="color: #008000;">//</span><span style="color: #008000;">删除目录节点的子数据节点</span>
zkOperator.delete("/testRootPath/testRootChild",-1<span style="color: #000000;">);
</span><span style="color: #008000;">//</span><span style="color: #008000;">删除目录节点</span>
zkOperator.delete("/testRootPath",-1<span style="color: #000000;">);
</span><span style="color: #008000;">//</span><span style="color: #008000;">关闭zookeeper</span>
zkOperator.closeZookeeper();
}
</span><span style="color: #008000;">//</span><span style="color: #008000;">创建目录节点、孩子节点</span>
<span style="color: #0000ff;">public</span> <span style="color: #0000ff;">void</span> create(String path,String bindingData,CreateMode mode) <span style="color: #0000ff;">throws</span><span style="color: #000000;"> KeeperException, InterruptedException {
zk.create(path,bindingData.getBytes(),ZooDefs.Ids.OPEN_ACL_UNSAFE,mode);
System.out.println();
}
</span><span style="color: #008000;">//</span><span style="color: #008000;">修改目录节点的数据</span>
<span style="color: #0000ff;">public</span> <span style="color: #0000ff;">void</span> setData(String path,String bindingData,<span style="color: #0000ff;">int</span> version) <span style="color: #0000ff;">throws</span><span style="color: #000000;"> KeeperException, InterruptedException {
zk.setData(path,bindingData.getBytes(),version);
}
</span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">void</span> setData(String path,String bindingData) <span style="color: #0000ff;">throws</span><span style="color: #000000;"> KeeperException, InterruptedException {
</span><span style="color: #0000ff;">this</span>.setData(path,bindingData,-1<span style="color: #000000;">);
}
</span><span style="color: #008000;">//</span><span style="color: #008000;">删除节点</span>
<span style="color: #0000ff;">public</span> <span style="color: #0000ff;">void</span> delete(String path,<span style="color: #0000ff;">int</span> version) <span style="color: #0000ff;">throws</span><span style="color: #000000;"> KeeperException, InterruptedException {
zk.delete(path,version);
}
</span><span style="color: #008000;">//</span><span style="color: #008000;">获得目录节点数据</span>
<span style="color: #0000ff;">public</span> <span style="color: #0000ff;">void</span> getData(String path) <span style="color: #0000ff;">throws</span><span style="color: #000000;"> KeeperException, InterruptedException {
System.out.println(</span><span style="color: #0000ff;">new</span> String(zk.getData(path,<span style="color: #0000ff;">false</span>,<span style="color: #0000ff;">null</span><span style="color: #000000;">)));
}
</span><span style="color: #008000;">//</span><span style="color: #008000;">获得孩子节点数据</span>
<span style="color: #0000ff;">public</span> <span style="color: #0000ff;">void</span> getChildern(String path) <span style="color: #0000ff;">throws</span><span style="color: #000000;"> KeeperException, InterruptedException {
System.out.println(zk.getChildren(path,</span><span style="color: #0000ff;">true</span><span style="color: #000000;">));
}
</span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">void</span><span style="color: #000000;"> closeZookeeper(){
</span><span style="color: #0000ff;">try</span><span style="color: #000000;"> {
zk.close();
} </span><span style="color: #0000ff;">catch</span><span style="color: #000000;"> (InterruptedException e) {
e.printStackTrace();
}
}
}
class MyWatcher implements Watcher{
@Override
public void process(WatchedEvent watchedEvent) {
System.out.println("已经触发了:"+watchedEvent.getType()+"事件!");
}
}
五、ZooKeeper 典型的应用场景(转自:http://www.cnblogs.com/ggjucheng/p/3370359.html)
Zookeeper 从设计模式角度来看,是一个基于观察者模式设计的分布式服务管理框架,它负责存储和管理大家都关心的数据,然后接受观察者的注册,一旦这些数据的状态发生变化,Zookeeper 就将负责通知已经在 Zookeeper 上注册的那些观察者做出相应的反应,从而实现集群中类似 Master/Slave 管理模式,关于 Zookeeper 的详细架构等内部细节可以阅读 Zookeeper 的源码
统一命名服务(Name Service)
分布式应用中,通常需要有一套完整的命名规则,既能够产生唯一的名称又便于人识别和记住,通常情况下用树形的名称结构是一个理想的选择,树形的名称结构是一个有层次的目录结构,既对人友好又不会重复。说到这里你可能想到了 JNDI,没错 Zookeeper 的 Name Service 与 JNDI 能够完成的功能是差不多的,它们都是将有层次的目录结构关联到一定资源上,但是 Zookeeper 的 Name Service 更加是广泛意义上的关联,也许你并不需要将名称关联到特定资源上,你可能只需要一个不会重复名称,就像数据库中产生一个唯一的数字主键一样。
配置管理(Configuration Management)
配置的管理在分布式应用环境中很常见,例如同一个应用系统需要多台 PC Server 运行,但是它们运行的应用系统的某些配置项是相同的,如果要修改这些相同的配置项,那么就必须同时修改每台运行这个应用系统的 PC Server,这样非常麻烦而且容易出错。
像这样的配置信息完全可以交给 Zookeeper 来管理,将配置信息保存在 Zookeeper 的某个目录节点中,然后将所有需要修改的应用机器监控配置信息的状态,一旦配置信息发生变化,每台应用机器就会收到 Zookeeper 的通知,然后从 Zookeeper 获取新的配置信息应用到系统中。
图 2. 配置管理结构图
集群管理(Group Membership)
Zookeeper 能够很容易的实现集群管理的功能,如有多台 Server 组成一个服务集群,那么必须要一个“总管”知道当前集群中每台机器的服务状态,一旦有机器不能提供服务,集群中其它集群必须知道,从而做出调整重新分配服务策略。同样当增加集群的服务能力时,就会增加一台或多台 Server,同样也必须让“总管”知道。
Zookeeper 不仅能够帮你维护当前的集群中机器的服务状态,而且能够帮你选出一个“总管”,让这个总管来管理集群,这就是 Zookeeper 的另一个功能 Leader Election。
它们的实现方式都是在 Zookeeper 上创建一个 EPHEMERAL 类型的目录节点,然后每个 Server 在它们创建目录节点的父目录节点上调用 getChildren(String path, boolean watch) 方法并设置 watch 为 true,由于是 EPHEMERAL 目录节点,当创建它的 Server 死去,这个目录节点也随之被删除,所以 Children 将会变化,这时 getChildren上的 Watch 将会被调用,所以其它 Server 就知道已经有某台 Server 死去了。新增 Server 也是同样的原理。
Zookeeper 如何实现 Leader Election,也就是选出一个 Master Server。和前面的一样每台 Server 创建一个 EPHEMERAL 目录节点,不同的是它还是一个 SEQUENTIAL 目录节点,所以它是个 EPHEMERAL_SEQUENTIAL 目录节点。之所以它是 EPHEMERAL_SEQUENTIAL 目录节点,是因为我们可以给每台 Server 编号,我们可以选择当前是最小编号的 Server 为 Master,假如这个最小编号的 Server 死去,由于是 EPHEMERAL 节点,死去的 Server 对应的节点也被删除,所以当前的节点列表中又出现一个最小编号的节点,我们就选择这个节点为当前 Master。这样就实现了动态选择 Master,避免了传统意义上单 Master 容易出现单点故障的问题。
图 3. 集群管理结构图
void findLeader() throws InterruptedException {
byte[] leader = null;
try {
leader = zk.getData(root + "/leader", true, null);
} catch (Exception e) {
logger.error(e);
}
if (leader != null) {
following();
} else {
String newLeader = null;
try {
byte[] localhost = InetAddress.getLocalHost().getAddress();
newLeader = zk.create(root + "/leader", localhost,
ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
} catch (Exception e) {
logger.error(e);
}
if (newLeader != null) {
leading();
} else {
mutex.wait();
}
}
}
共享锁(Locks)
共享锁在同一个进程中很容易实现,但是在跨进程或者在不同 Server 之间就不好实现了。Zookeeper 却很容易实现这个功能,实现方式也是需要获得锁的 Server 创建一个 EPHEMERAL_SEQUENTIAL 目录节点,然后调用 getChildren方法获取当前的目录节点列表中最小的目录节点是不是就是自己创建的目录节点,如果正是自己创建的,那么它就获得了这个锁,如果不是那么它就调用exists(String path, boolean watch) 方法并监控 Zookeeper 上目录节点列表的变化,一直到自己创建的节点是列表中最小编号的目录节点,从而获得锁,释放锁很简单,只要删除前面它自己所创建的目录节点就行了。
图 4. Zookeeper 实现 Locks 的流程图
同步锁思路:
加锁:
ZooKeeper 将按照如下方式实现加锁的操作:
1 ) ZooKeeper 调用 create ()方法来创建一个路径格式为“ _locknode_/lock- ”的节点,此节点类型为sequence (连续)和 ephemeral (临时)。也就是说,创建的节点为临时节点,并且所有的节点连续编号,即“ lock-i ”的格式。
2 )在创建的锁节点上调用 getChildren ()方法,来获取锁目录下的最小编号节点,并且不设置 watch 。
3 )步骤 2 中获取的节点恰好是步骤 1 中客户端创建的节点,那么此客户端获得此种类型的锁,然后退出操作。
4 )客户端在锁目录上调用 exists ()方法,并且设置 watch 来监视锁目录下比自己小一个的连续临时节点的状态。
5 )如果监视节点状态发生变化,则跳转到第 2 步,继续进行后续的操作,直到退出锁竞争。
void getLock() throws KeeperException, InterruptedException{
List<String> list = zk.getChildren(root, false);
String[] nodes = list.toArray(new String[list.size()]);
Arrays.sort(nodes);
if(myZnode.equals(root+"/"+nodes[0])){
doAction();
}
else{
waitForLock(nodes[0]);
}
}
void waitForLock(String lower) throws InterruptedException, KeeperException {
Stat stat = zk.exists(root + "/" + lower,true);
if(stat != null){
mutex.wait();
}
else{
getLock();
}
}
队列管理
Zookeeper 可以处理两种类型的队列:
- 当一个队列的成员都聚齐时,这个队列才可用,否则一直等待所有成员到达,这种是同步队列。
- 队列按照 FIFO 方式进行入队和出队操作,例如实现生产者和消费者模型。
同步队列用 Zookeeper 实现的实现思路如下:
创建一个父目录 /synchronizing,每个成员都监控标志(Set Watch)位目录 /synchronizing/start 是否存在,然后每个成员都加入这个队列,加入队列的方式就是创建 /synchronizing/member_i 的临时目录节点,然后每个成员获取 / synchronizing 目录的所有目录节点,也就是 member_i。判断 i 的值是否已经是成员的个数,如果小于成员个数等待 /synchronizing/start 的出现,如果已经相等就创建 /synchronizing/start。
用下面的流程图更容易理解:
图 5. 同步队列流程图
同步队列
void addQueue() throws KeeperException, InterruptedException{
zk.exists(root + "/start",true);
zk.create(root + "/" + name, new byte[0], Ids.OPEN_ACL_UNSAFE,
CreateMode.EPHEMERAL_SEQUENTIAL);
synchronized (mutex) {
List<String> list = zk.getChildren(root, false);
if (list.size() < size) {
mutex.wait();
} else {
zk.create(root + "/start", new byte[0], Ids.OPEN_ACL_UNSAFE,
CreateMode.PERSISTENT);
}
}
}
当队列没满是进入wait(),然后会一直等待Watch的通知,Watch的代码如下:
public void process(WatchedEvent event) {
if(event.getPath().equals(root + "/start") &&
event.getType() == Event.EventType.NodeCreated){
System.out.println("得到通知");
super.process(event);
doAction();
}
}
FIFO 队列用 Zookeeper 实现思路如下:
实现的思路也非常简单,就是在特定的目录下创建 SEQUENTIAL 类型的子目录 /queue_i,这样就能保证所有成员加入队列时都是有编号的,出队列时通过 getChildren( ) 方法可以返回当前所有的队列中的元素,然后消费其中最小的一个,这样就能保证 FIFO。
生产者代码
boolean produce(int i) throws KeeperException, InterruptedException{
ByteBuffer b = ByteBuffer.allocate(4);
byte[] value;
b.putInt(i);
value = b.array();
zk.create(root + "/element", value, ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.PERSISTENT_SEQUENTIAL);
return true;
}
int consume() throws KeeperException, InterruptedException{
int retvalue = -1;
Stat stat = null;
while (true) {
synchronized (mutex) {
List<String> list = zk.getChildren(root, true);
if (list.size() == 0) {
mutex.wait();
} else {
Integer min = new Integer(list.get(0).substring(7));
for(String s : list){
Integer tempValue = new Integer(s.substring(7));
if(tempValue < min) min = tempValue;
}
byte[] b = zk.getData(root + "/element" + min,false, stat);
zk.delete(root + "/element" + min, 0);
ByteBuffer buffer = ByteBuffer.wrap(b);
retvalue = buffer.getInt();
return retvalue;
}
}
}
}
CAP理论:
Paxos算法(选举算法):详情参考http://www.cnblogs.com/yuyijq/p/4116365.html
了解了这个过程,我们来看看另外一个问题:
一个集群有3台机器,挂了一台后的影响是什么?挂了两台呢?
挂了一台:挂了一台后就是收不到其中一台的投票,但是有两台可以参与投票,按照上面的逻辑,它们开始都投给自己,后来按照选举的原则,两个人都投票给其中一个,那么就有一个节点获得的票等于2,2 > (3/2)=1 的,超过了半数,这个时候是能选出leader的。
七、版权声明
作者:邱勇Aaron
出处:http://www.cnblogs.com/qiuyong/
您的支持是对博主深入思考总结的最大鼓励。
本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,尊重作者的劳动成果。
参考:马士兵zookeeper、zookeeper官方文档