今天准备重新回到简书写文章,在这之前我的技术文章都放在github上,这次一律都搬过来,后面的文章技术的两边都发,其他类型的发简书,毕竟简书上写还有人看,稍微有些乐趣(本文原链接点我)。
初识
定义
ZooKeeper是一个开源的分布式协调服务,其设计目标是将原本复杂容易出错的分布式一致性服务封装,构成一个高效可靠简单易用的接口供用户使用。
特点
- 顺序一致性:同一个客户端发起的事物请求会被顺序的在ZooKeeper中执行,每个客户端的事物请求,ZooKooper都会分配一个全局唯一的递增序号来区分事物的先后顺序
- 原子性:所有的事物请求在整个集群中的所有机器上的应用情况是一致的。
- 单一视图:无论客户端连接的是ZooKeeper的哪个节点机器,其看到的服务端数据模型都是一致的
- 实时性:保证在一定时间段内,客户端最终一定能从服务端上读取到最新的数据状态
- 数据模型简单:树形的结构,类型文件系统
- 可构建集群:多台ZooKeeper节点组成集群,角色可分为Leader,Follower,Observer,Leader可读可写,Follower和Observer可读,Follower可被选举为Leader;集群的每台机器存储的数据是一致的
- 高可靠性:一旦服务端成功地执行了一个事物并响应给客户端,那么其状态变更就会被一直保留下来,除非下一次再进行变更。数据也会被增量和全量地持久化到磁盘中
- 高性能:全量数据位于内存中
- 提供事件监听机制:客户端可以对节点注册监听事件,当事件发生时会通知客户端
- 会话状态:客户端与服务端建立连接后保持一个会话状态,客户端可以创建临时节点,在会话失效后临时节点将被清理
背后的算法原理
ZAB协议:ZooKeeper保证数据一致性的核心算法,类似二阶提交的过程,每个事物由全局唯一的Leader来将该请求转换成一个提议,并将该提议分发给所有的Follower,超过半数的Follower进行正确反馈后,Leader才会再次向所有的Follower服务器发送确认提交,Follower服务器要么响应,要么丢弃,正常情况下Leader接收到超半数反馈,如果没有超半数反馈则ZooKeeper进入崩溃恢复状态,重新选举Leader,重新选举出来的Leader具有事物序号最大的特点,这也说明这台Leader的数据最齐全,当选举完成后,全局递增的事物序号会发生改变,该序号由64位数字表示,低32位是递增序号,高32位每次重新选举会增1,这样新选举出来的递增序号高于之前Leader的序号,所以就算之前的Leader恢复正常了再发送之前的事物序号,集群发现序号太小不会执行,这样就保证了强一致性
如何选举Leader:目的是选举出唯一的一台服务器作为Leader,并让所有Follower知道谁被选举为Leader,且该Leader具有全局递增序号最大的特点(这样的数据才最新)
假设有三台Follower进行选举:三台机器ID分别为1,2,3;3台机器的递增序号(ZID)都为0(初始选举),每台机器将[myID, ZID]投票给其他机器,每台机器接收到投票后,将投票处理,先检查ZID,以ZID较大的再作为选票,如果ZID相同就将myID大的服务器作为选票,将新选票再投出去;每次投票后机器就统计是否有超过半数的机器收到相同的投票,如果有就他是Leader了;这里第一轮投票大家各自将自己投出去,机器1收到[2,0],[3,0]后将投票变更为[3,0],机器2收到[1,0],[3,0]将投票变更为[3,0],机器3收到[1,0],[2,0]不做变更,第二轮投票后再统计3机器就超半数当选Leader;如果3台机器首轮的选票为[1,5],[2,4],[3,4],机器1收到[2,4][3,4]后选票不变,机器2收到[1,5][3,4]后将选票改为[1,5],机器3收到[1,5][2,4]后将选票改为[1,5],第二轮选出机器1,所以最后ZID最大的最有可能成为Leader。
ZooKeeper的应用
- 数据发布/订阅,配置管理,如qconf
- 命名服务,如RPC服务框架
- 分布式协调/通知:如心跳检测,任务调度,Master选举,分布式锁等
实践一下
环境搭建
- 下载稳定的releases包,并解压到工作目录中
- 创建配置文件:conf 目录下 cp zoo_sample.cfg zoo.cfg,设置dataDir到一个存在的目录(由于机器太差就没有搭集群,集群的配置网上找找总是有的)
- 启动服务端: bin/zkServer.sh start
- 验证服务是否正常,用客户端连上去随便测试几个命令 bin/zkCli.sh -server 127.0.0.1:2181
- JAVA客户端选择:原生的Api,ZkClient,Curator,这里我们选择Curator,它是一个Apache开源项目,相对原生的Api更简单易用
实现一个分布式锁
实现一个分布式锁,场景为ES build全量build索引时只能由单一进程来build,在进程build索引前先拿到锁才可以开始build,build完了得释放锁,在锁住的情况下如果程序挂了或者发布代码将进程kill了也得释放掉锁,否则后续的进程将再也无法拿到锁。之前实现过一个本地文件的版本,缺点在于无法应用到分布式场景下,且如果kill程序时暴力地使用了kill -9,进程退出无法清理锁文件将导致后续进程再也无法处理;利用ZooKeeper很好地解决了分布式和锁无法清理的问题,用ZooKeeper的临时节点,获取锁的时候试图创建节点,如果说节点已经存在则说明锁被别人拿着,如果创建成功则成功获取锁,程序结束删除节点,如果恰好进程异常终止甚至被无情的kill -9杀掉,那等一会在服务端无法和客户端取得重连的情况下,临时节点也会被清理掉,这样后续的进程又可以正常的运行了。代码如下,留了个接口,也可以自己实现别的锁逻辑。
接口
package BizLock;
public interface BizLock {
public boolean getLock(String lockName) throws Exception;
public void releaseLock(String lockName) throws Exception;
}
ZooKeeper实现的分布式锁
package BizLock;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.RetryNTimes;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
public class ZkLock implements BizLock {
/**
* 这些变量可以写入配置中
*/
private static final String nameSpace = "zkLock";
private static final String zkUrl = "127.0.0.1:2181";
private static ZkLock zkLock = null;
private static CuratorFramework client = null;
private ZkLock() {
client = CuratorFrameworkFactory.newClient(zkUrl, new RetryNTimes(5, 5000));
client.start();
client.usingNamespace(nameSpace);
}
public static ZkLock getInstance() {
if (zkLock == null) {
zkLock = new ZkLock();
}
return zkLock;
}
public boolean getLock(String lockName) throws Exception{
try {
client.create().withMode(CreateMode.EPHEMERAL).forPath(getLockNode(lockName));
} catch (KeeperException.NodeExistsException e) {
return false;
}
return true;
}
public void releaseLock(String lockName) throws Exception{
client.delete().forPath(lockName);
}
private String getLockNode(String loockName) {
return "/"+loockName;
}
}
参考:
《从Paxos到ZooKeeper》
http://zookeeper.apache.org/
http://curator.apache.org/