分布式系统中的协调者
在分布式系统中,协调者是一个很重要的角色,在我们淘系的系统中,大多数中间件系统的设计都引入了协调者这样一个概念。像我们工作中常用的
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 votes,
Vote vote) {
HashSet set = new HashSet();
/*
* 迭代投票箱,判断是否和当前的leader是否是同一个,是则添加到set里面
*/
for (Map.Entry 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 set){
return (set.size() > half);
}
5. 参考资料
Zookeeper源码地址:
http://zookeeper.apache.org/releases.html