分布式系统中的协调者
在分布式系统中,协调者是一个很重要的角色,在我们淘系的系统中,大多数中间件系统的设计都引入了协调者这样一个概念。像我们工作中常用的
Tair的configServer:管理dataServer,维护/监控dataServer的使用状态
OceanBase中的RootServer:管理基线服务器ChunkServer
还有google的Hadoop里面的Zookpeer
主要工作是维护系统的元数据。和CS保持心跳,负责数据节点的租约管理等。虽然实现的细节上都大不相同,但概念上是一致的,做为一个中间角色统一管理数据资源,以降低数据资源的访问路径,同时还能起到负载均衡的作用。
协调者之间如何保证高可用性
Tair的configServer:使用的是主备的方式
OceanBase中的RootServer:使用Ha主备的方式
今天介绍的是Hadoop的Zookpeer中的互备方式。使用的是基于Paxos算法的实现
Paxos是分布式系统中的关键算法。在google的BigTable以及hadoop的hdfs底层为了实现服务器的高可用性,都使用了这个的实现。
首先了解一下Zookeeper,它是hadoop下最重要的一个子项目之一。
鸟览一下hadoop的架构组成
Hive:基于MapReduce之上的一个数据查询工具。可以将SQL转换为MR语句执行
MapReduce:MapReduce处理
Hbase:分布式数据库
Hdfs:分布式存储
Zookeeper:分布式一致问题
从hadoop的众多组件中。我们可以得知zookeeper是支撑起hadoop云计算大厦的地基。拥有它。Hadoop的服务器集群才有了无限扩容的可能。
在Zookeeper的实际使用中,它主要被用来做为集群管理来使用,用于监控hadoop的hdfs的服务器的使用状态
主要原理是zookeeper和hdfs服务持保持通信,保持服务器的状态,以确保hdfs服务器是存活的。但zookeeper服务器之间也需要做容灾。即如果某个zookeeper服务挂了,需要有新的zookeeper顶上,保持和hdfs服务器的通信,否则,hadoop的文件就不知道存储到哪些节点了。
Zookeeper系统布署图如下
Server:指专门用于管理HDFS服务器集群的服务器。HDFS文件服务器的存储状态,信息,都会保存在服务器上。
Client:是指HDFS服务器集群。用于存放数据块。
1. 服务器定义了三个角色。leader(领导者), follow(跟随者),Observer (跟随者)。
2. leader.一般会默认指定。会接受所有follow的请求后发起请求到所有的follow.,负责和所有的节点进行数据交换
3. follow,会参与leader发起的请求,并做出请求的回复。
4. 客户端不参与投票的请求,同会与leader做数据同步
5. 所有的server都会跟client通信,提供服务。
1. 选主实现主要类结构图
2. 选主实现交互时序图
服务端启动的入口为
org.apache.zookeeper.server.quorum.QuorumPeerMain
3. 选主流程
(描述流程的过程同时代码对应的在连接中,可以直接接ctrl+鼠标点击查看):
(图片来自互联网)
1. 接收投票消息。投票消息会包括id,zxid,epoch,state,这四种信息,分别代表
Id: 唯一标识一台机器,存储在myid文件中
Zxid: 标识了本机想要选举谁为leader,是本机目前所见到的最大的id值
Epoch: 逻辑时钟。用于判断选举是否过期
State: 本机的状态信息(包括looking,leading,following,observing)
见1. Notification数据结构
2. 判断PeerState状态,如果是looking状态,则继续.如果是leading,foolowing,observing则走别的流程
见2. PeerState枚举状态
3. 收到票后,会判断发送过来的逻辑时钟是否大于目前的逻辑时钟,如果是说明集群已经进入了新一轮的投票了。
4. 清空投票箱。因为这个之前的投票都是上一次投票期间维护的。
5. 如果等于目前的逻辑时钟,说明是当前的,则更新最大的leader id和提案id
判断是否需要更新当前自己的选举情况.在这里是根据选举leader id,保存的最大数据id来进行判断的,这两种数据之间对这个选举结果的影响的权重关系是:首先看数据id,数据id大者胜出;其次再判断leader id,leader id大者胜出
判读投票结果代码
6. 发送通知,通知其他的QuorumPeer更新leader信息.同时将更新后的leader信息放入投票箱
检查是否已经接收到了所有服务器的投票代码参考。如果是的,则设置自己的选择结果
如果没有接收到所有服务器的投票,那判读这个leadId是否得到了一半以后的服务器的投票代码参考,如果是则返回
以上流程描述的是在zookeeper中,参考使用的算法是FastLeaderElection
在zookeeper的的选主的流程,另外还提供了LeaderElection和AuthFastLeaderElection的实现
LeaderElection的实现比较简单。以(id,zxid)做为投票的依据.并且它的实现是同步的,需要等待所有服务器返回后再统计结果。
而相比FastLeaderElection是每次收到回复都会计算投票结果,效率上会比LeaderElection更好一些。
4. 代码细节
1.Notification数据结构
static public class Notification { /* * Proposed leader */ long leader; /* * zxid of the proposed leader */ long zxid; /* * Epoch */ long epoch; /* * current state of sender */ QuorumPeer.ServerState state; /* * Address of the sender */ InetSocketAddress addr; }
2. ServerState
public enumServerState { LOOKING, FOLLOWING, LEADING; } public enum LearnerType { PARTICIPANT, OBSERVER; }
3. 判断逻辑时钟逻辑
见FastLeaderElection类
//如果发送的逻辑时钟,大于服务器的逻辑时钟 if (n.epoch > logicalclock) { logicalclock = n.epoch; //清空投票箱 recvset.clear(); //判断是否需要更新leaderId if(totalOrderPredicate(n.leader, n.zxid, self.getId(), self.getLastLoggedZxid())) //如果发送的逻辑时钟大于本地,则发送发送的逻辑时钟以及相关信息 updateProposal(n.leader, n.zxid); else updateProposal(self.getId(), self.getLastLoggedZxid()); //发送 sendNotifications(); } else if (n.epoch < logicalclock) { //如果发送的逻辑时钟小于本地的。说明对方在一个相对较早的选举进程中 LOG.info("n.epoch < logicalclock"); break; } else if (totalOrderPredicate(n.leader, n.zxid, //如果逻辑时钟相同判断是否需要更新本机的数据。发送 proposedLeader, proposedZxid)) { LOG.info("Updating proposal"); updateProposal(n.leader, n.zxid); sendNotifications(); }
----------------------------------分割线--------------------------------
//判断对比的关键类。 private boolean totalOrderPredicate(long newId, long newZxid, long curId, long curZxid) { LOG.debug("id: " + newId + ", proposed id: " + curId + ", zxid: " + newZxid + ", proposed zxid: " + curZxid); //获得当前serverId的权重是否为0 if(self.getQuorumVerifier().getWeight(newId) == 0){ return false; } //1.如果新的选举ID大于当前保存的选主ID //2. 或者新的选举ID等当前保存的。判断机器ID是否相同,如果新的大于,则返//回true.如果返回true说明本机需要更新id if ((newZxid > curZxid) || ((newZxid == curZxid) && (newId > curId))) return true; else return false; }
4. 检查是否收到了大多数人投票
见FastLeaderElection类
if ((self.quorumPeers.size() == recvset.size()) && (self.getQuorumVerifier().getWeight(proposedLeader) != 0)){ self.setPeerState((proposedLeader == self.getId()) ? ServerState.LEADING: ServerState.FOLLOWING); leaveInstance(); return new Vote(proposedLeader, proposedZxid); } else if (termPredicate(recvset, new Vote(proposedLeader, proposedZxid, logicalclock))) { //Otherwise, wait for a fixed amount of time LOG.debug("Passed predicate"); // Verify if there is any change in the proposed leader while((n = recvqueue.poll(finalizeWait, TimeUnit.MILLISECONDS)) != null){ if(totalOrderPredicate(n.leader, n.zxid, proposedLeader, proposedZxid)){ recvqueue.put(n); break; } } if (n == null) { self.setPeerState((proposedLeader == self.getId()) ? ServerState.LEADING: ServerState.FOLLOWING); LOG.info("About to leave instance:" + proposedLeader + ", " + proposedZxid + ", " + self.getId() + ", " + self.getPeerState()); leaveInstance(); return new Vote(proposedLeader, proposedZxid); } }
5. 判断是否获得一半的票数
private boolean termPredicate( HashMap<Long, Vote> votes, Vote vote) { HashSet<Long> set = new HashSet<Long>(); /* * 迭代投票箱,判断是否和当前的leader是否是同一个,是则添加到set里面 */ for (Map.Entry<Long,Vote> entry : votes.entrySet()) { if (vote.equals(entry.getValue())){ set.add(entry.getKey()); } } //判读票数是否过半 if(self.getQuorumVerifier().containsQuorum(set)) return true; else return false; } self.getQuorumVerifier().containsQuorum(set)的实现很简单.如下 --------------------分割线-------------------- public boolean containsQuorum(HashSet<Long> set){ return (set.size() > half); }
5. 参考资料
Zookeeper源码地址:
http://zookeeper.apache.org/releases.html