1. ZooKeeper应用场景
ZooKeeper的应用场景主要包括:分布式协调,分布式锁,分布式元数据存储以及HA高可用。
分布式协调
通过ZooKeeper watch机制进行分布式服务间的通信。例如A系统发送请求到mq,然后B系统消息消费之后处理了。那 A 系统如何知道 B 系统的处理结果?使用Zookeeper就可以实现分布式系统之间的协调工作。A 系统发送请求之后可以在 ZooKeeper 上对某个节点的值注册个监听器,一旦 B 系统处理完了就修改 ZooKeeper那个节点的值,A 系统立马就可以收到通知,完美解决。
分布式锁
通过Zookeeper的原子性创建node或者临时序号节点的特性可以用于实现分布式锁。具体实现请参考文章第2小节:ZooKeeper分布式锁。
元数据存储
Zookeeper 可以用作分布式系统的配置信息的管理,比如 kafka、storm 等等很多分布式系统都会选用 Zookeeper来做一些元数据、配置信息的管理。
HA高可用
可以通过ZooKeeper临时节点watch功能来实现主备高可用。如下图所示:
HA高可用.PNG
2. ZooKeeper分布式锁
通过临时序号节点实现分布式锁:
public class ZooKeeperDistributedLock implements Watcher {
private ZooKeeper zk;
private String locksRoot = "/locks";
private String productId;
private String waitNode;
private String lockNode;
private CountDownLatch latch;
private CountDownLatch connectedLatch = new CountDownLatch(1);
private int sessionTimeout = 30000;
public ZooKeeperDistributedLock(String productId) {
this.productId = productId;
try {
String address = "192.168.31.187:2181,192.168.31.19:2181,192.168.31.227:2181";
zk = new ZooKeeper(address, sessionTimeout, this);
connectedLatch.await();
} catch (IOException e) {
throw new LockException(e);
} catch (KeeperException e) {
throw new LockException(e);
} catch (InterruptedException e) {
throw new LockException(e);
}
}
public void process(WatchedEvent event) {
if (event.getState() == KeeperState.SyncConnected) {
connectedLatch.countDown();
return;
}
if (this.latch != null) {
this.latch.countDown();
}
}
public void acquireDistributedLock() {
try {
if (this.tryLock()) {
return;
} else {
waitForLock(waitNode, sessionTimeout);
}
} catch (KeeperException e) {
throw new LockException(e);
} catch (InterruptedException e) {
throw new LockException(e);
}
}
public boolean tryLock() {
try {
// 传入进去的locksRoot + “/” + productId
// 假设productId代表了一个商品id,比如说1
// locksRoot = locks
// /locks/10000000000,/locks/10000000001,/locks/10000000002
lockNode = zk.create(locksRoot + "/" + productId, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
// 看看刚创建的节点是不是最小的节点
// locks:10000000000,10000000001,10000000002
List<String> locks = zk.getChildren(locksRoot, false);
Collections.sort(locks);
if(lockNode.equals(locksRoot+"/"+ locks.get(0))){
//如果是最小的节点,则表示取得锁
return true;
}
//如果不是最小的节点,找到比自己小1的节点
int previousLockIndex = -1;
for(int i = 0; i < locks.size(); i++) {
if(lockNode.equals(locksRoot + “/” + locks.get(i))) {
previousLockIndex = i - 1;
break;
}
}
this.waitNode = locks.get(previousLockIndex);
} catch (KeeperException e) {
throw new LockException(e);
} catch (InterruptedException e) {
throw new LockException(e);
}
return false;
}
private boolean waitForLock(String waitNode, long waitTime) throws InterruptedException, KeeperException {
Stat stat = zk.exists(locksRoot + "/" + waitNode, true);
if (stat != null) {
this.latch = new CountDownLatch(1);
this.latch.await(waitTime, TimeUnit.MILLISECONDS);
this.latch = null;
}
return true;
}
public void unlock() {
try {
// 删除/locks/10000000000节点
// 删除/locks/10000000001节点
System.out.println("unlock " + lockNode);
zk.delete(lockNode, -1);
lockNode = null;
zk.close();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (KeeperException e) {
e.printStackTrace();
}
}
public class LockException extends RuntimeException {
private static final long serialVersionUID = 1L;
public LockException(String e) {
super(e);
}
public LockException(Exception e) {
super(e);
}
}
}
redis 分布式锁和 zk 分布式锁的对比:
- redis 分布式锁,其实需要自己不断去尝试获取锁,比较消耗性能。另外,如果redis 获取锁的那个客户端挂了,那么只能等待超时时间之后才能释放锁。
- zk 分布式锁,获取不到锁,注册个监听器即可,不需要不断主动尝试获取锁,性能开销较小。而 zk 的话,由于创建的是临时znode,只要客户端挂了,znode 就没了,此时就自动释放锁。
3. ZooKeeper Watcher机制
ZooKeeper的Watcher机制主要包括客户端线程、客户端WatchManager和ZooKeeper服务器三部分。在具体工作流程上,简单地讲,客户端在向 ZooKeeper 服务器注册 Watcher 的同时,会将 Watcher 对象存储在客户端的 WatchManager 中。当 ZooKeeper 服务器端触发 Watcher 事件后,会向客户端发送通知,客户端线程从 WatchManager 中取出对应的 Watcher 对象来执行回调逻辑。
更多关于watch机制的原理,请参考:https://www.jianshu.com/p/4c071e963f18
4. etcd和zookeeper对比
etcd和zookeeper的功能非常相似,都提供了线性一致性的分布式存储,watch机制,原子性读写操作等操作。使用etcd或zookeeper,我们可以轻松的实现以下功能:
- 分布式锁
- 分布式元数据存储
- 服务发现
- 发布订阅
两者的相似点和区别:
- ZooKeeper会将所有znode的数据缓存进内存,每一个znode的大小限制在1M,因此适合用于分布式元数据存储,而非分布式文件存储系统。另外,ZooKeeper适合读多写少的场景(写入性能低,为保证一致性,每次需要n/2+1的写入完成才算完成)。
- etcd和ZooKeeper一样,都用于解决分布式系统的协调和元数据的存储,所以它们都不是一个存储组件,或者说都不是一个分布式数据库。ZooKeeper的存储结构是树状的,而etcd则是key/value类型的。
3. ZooKeeper采用ZAB协议进行一致性保证,而etcd采用更易实现的raft协议。ZAB选举用的是zxid和serverId来进行投票, 而raft则是根据term和index来进行投票。另外,ZAB协议一轮选举会进行多次投票,一轮选举一定能够产生一个Leader。而RAFT协议一轮选举则只进行一次投票,如果出现平票(没有Candidate收到大多数投票),则自动开始下一轮选举。
4. RAFT协议的心跳是从leader到follower,而zab协议则相反。ZAB协议中Leader如果在timeout时间内没有收到超过一半Follower发送的心跳,就会自动放弃Leader身份,并停止给Follower发送心跳。Follower在一段时间内没有收到Leader的心跳后,则会开始一轮新的Leader选举。
5. ZooKeeper的watch机制是一次性的,watch事件触发后就不会再触发第二次。另外,ZooKeeper的wach只能watch子节点的创建和删除,不能watch子节点值的变化,并且无法watch孙节点。而Etcd的watch channel是可以重复利用的,并且可以watch到所以子孙节点的变化。
更多ZooKeeper和etcd的对比,请参考:
网友评论