master选举
1、使用场景及结构
现在很多时候我们的服务需要7*24小时工作,假如一台机器挂了,我们希望能有其它机器顶替它继续工作。此类问题现在多采用master-salve模式,也就是常说的主从模式,正常情况下主机提供服务,备机负责监听主机状态,当主机异常时,可以自动切换到备机继续提供服务(这里有点儿类似于数据库主库跟备库,备机正常情况下只监听,不工作),这个切换过程中选出下一个主机的过程就是master选举。
对于以上提到的场景,传统的解决方式是采用一个备用节点,这个备用节点定期给当前主节点发送ping包,主节点收到ping包后会向备用节点发送应答ack,当备用节点收到应答,就认为主节点还活着,让它继续提供服务,否则就认为主节点挂掉了,自己将开始行使主节点职责。如图1所示:
图1
但这种方式会存在一个隐患,就是网络故障问题。看一下图2:
图2
也就是说,我们的主节点并没有挂掉,只是在备用节点ping主节点,请求应答的时候发生网络故障,这样我们的备用节点同样收不到应答,就会认为主节点挂掉,然后备机会启动自己的master实例。这样就会导致系统中有两个主节点,也就是双master。出现双master以后,我们的从节点会将它做的事情一部分汇报给主节点,一部分汇报给备用节点,这样服务就乱套了。为了防止这种情况出现,我们可以考虑采用zookeeper,虽然它不能阻止网络故障的出现,但它能保证同一时刻系统中只存在一个主节点。我们来看zookeeper是怎么实现的:
在此处,抢主程序是包含在服务程序中,需要程序员来手动写抢主逻辑的,比如当当开源框架elastic-job中,就有关于选主的部分,参见:elastic-job-core/main/java/com/dangdang/ddframe/job/internal/election文件夹下的选主代码。
一点额外的话:zookeeper自己在集群环境下的抢主算法有三种,可以通过配置文件来设定,默认采用FastLeaderElection,不作赘述;此处主要讨论集群环境中,应用程序利用master的特点,自己选主的过程。程序自己选主,每个人都有自己的一套算法,有采用“最小编号”的,有采用类似“多数投票”的,各有优劣,本文的算法仅作演示理解使用:
结构图:
结构图解释:左侧树状结构为zookeeper集群,右侧为程序服务器。所有的服务器在启动的时候,都会订阅zookeeper中master节点的删除事件,以便在主服务器挂掉的时候进行抢主操作;所有服务器同时会在servers节点下注册一个临时节点(保存自己的基本信息),以便于应用程序读取当前可用的服务器列表。
选主原理介绍:zookeeper的节点有两种类型,持久节点跟临时节点。临时节点有个特性,就是如果注册这个节点的机器失去连接(通常是宕机),那么这个节点会被zookeeper删除。选主过程就是利用这个特性,在服务器启动的时候,去zookeeper特定的一个目录下注册一个临时节点(这个节点作为master,谁注册了这个节点谁就是master),注册的时候,如果发现该节点已经存在,则说明已经有别的服务器注册了(也就是有别的服务器已经抢主成功),那么当前服务器只能放弃抢主,作为从机存在。同时,抢主失败的当前服务器需要订阅该临时节点的删除事件,以便该节点删除时(也就是注册该节点的服务器宕机了或者网络断了之类的)进行再次抢主操作。从机具体需要去哪里注册服务器列表的临时节点,节点保存什么信息,根据具体的业务不同自行约定。选主的过程,其实就是简单的争抢在zookeeper注册临时节点的操作,谁注册了约定的临时节点,谁就是master。
ps:本文的例子中,并未用到结构图server节点下的数据。但换一种算法或者业务场景就会用到,算法比如提到的最小编号,主要逻辑是主节点挂掉后,从节点里边编号最小的成为主节点,此时会用到该节点内容。换一种业务场景:集群环境中,有很多任务要处理, 主节点负责接收任务,并根据一定算法将任务分配到不同的机器上执行;这种情况下,主节点跟从节点的职责也是不同的,主节点挂掉也会涉及到从节点进行master选举的问题。这种情况下,很显然,作为主节点需要知道当前有多少个从节点还活着,那么此时也会需要用到servers节点下的数据了。
2、编码实现
主要有两个类,WorkServer为主服务类,RunningData用于记录运行数据。因为是简单的demo,我们只做抢master节点的编码,对于从节点应该去哪里注册服务列表信息,不作编码。
public class WorkServer {
//客户端状态
private volatile boolean running = false;
private ZkClient zkClient;
//zk主节点路径
public static final String MASTER_PATH = "/masters";
//监听(用于监听主节点删除事件)
private IZkDataListener dataListener;
//服务器基本信息
private RunningData serverData;
//主节点基本信息
private RunningData masterData;
//调度器
private ScheduledExecutorService delayExector = Executors.newScheduledThreadPool(1);
//延迟时间5s
private int delayTime = 5;
public WorkServer(RunningData runningData){
this.serverData = runningData;
this.dataListener = new IZkDataListener() {
@Override
public void handleDataChange(String s, Object o) throws Exception {
}
@Override
public void handleDataDeleted(String s) throws Exception {
//takeMaster();
if(masterData != null && masterData.getName().equals(serverData.getName())){//若之前master为本机,则立即抢主,否则延迟5秒抢主(防止小故障引起的抢主可能导致的网络数据风暴)
takeMaster();
}else{
delayExector.schedule(new Runnable() {
@Override
public void run() {
takeMaster();
}
},delayTime, TimeUnit.SECONDS);
}
}
};
}
//启动
public void start() throws Exception{
if(running){
throw new Exception("server has startup....");
}
running = true;
zkClient.subscribeDataChanges(MASTER_PATH,dataListener);
takeMaster();
}
//停止
public void stop() throws Exception{
if(!running){
throw new Exception("server has stopped.....");
}
running = false;
delayExector.shutdown();
zkClient.unsubscribeDataChanges(MASTER_PATH,dataListener);
releaseMaster();
}
//抢注主节点
private void takeMaster(){
if(!running) return ;
try {
// System.out.println(serverData+"触发争抢");
zkClient.create(MASTER_PATH, serverData, CreateMode.EPHEMERAL);
masterData = serverData;
System.out.println(serverData.getName()+" is master");
delayExector.schedule(new Runnable() {//测试抢主用,每5s释放一次主节点
@Override
public void run() {
if(checkMaster()){
releaseMaster();
}
}
},5,TimeUnit.SECONDS);
}catch (ZkNodeExistsException e){//节点已存在
RunningData runningData = zkClient.readData(MASTER_PATH,true);
if(runningData == null){//读取主节点时,主节点被释放
takeMaster();
}else{
masterData = runningData;
}
} catch (Exception e) {
// ignore;
}
}
//释放主节点
private void releaseMaster(){
if(checkMaster()){
zkClient.delete(MASTER_PATH);
}
}
//检验自己是否是主节点
private boolean checkMaster(){
try {
RunningData runningData = zkClient.readData(MASTER_PATH);
masterData = runningData;
if (masterData.getName().equals(serverData.getName())) {
return true;
}
return false;
}catch (ZkNoNodeException e){//节点不存在
return false;
}catch (ZkInterruptedException e){//网络中断
return checkMaster();
}catch (Exception e){//其它
return false;
}
}
public void setZkClient(ZkClient zkClient) {
this.zkClient = zkClient;
}
public ZkClient getZkClient() {
return zkClient;
}
}
public class RunningData implements Serializable {
private static final long serialVersionUID = 4260577459043203630L;
//服务器id
private long cid;
//服务器名称
private String name;
public long getCid() {
return cid;
}
public void setCid(long cid) {
this.cid = cid;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public class LeaderSelectorZkClient {
//启动的服务个数
private static final int CLIENT_QTY = 10;
//zookeeper服务器的地址
private static final String ZOOKEEPER_SERVER = "localhost:2181,localhost:2182,localhost:2183";
public static void main(String[] args) throws Exception{
//保存所有zkClient的列表
List<ZkClient> clients = new ArrayList<ZkClient>();
//保存所有服务的列表
List<WorkServer> workServers = new ArrayList<WorkServer>();
try{
for ( int i = 0; i < CLIENT_QTY; ++i ){
//创建zkClient
ZkClient client = new ZkClient(ZOOKEEPER_SERVER, 5000, 5000, new SerializableSerializer());
clients.add(client);
//创建serverData
RunningData runningData = new RunningData();
runningData.setCid(Long.valueOf(i));
runningData.setName("Client #" + i);
//创建服务
WorkServer workServer = new WorkServer(runningData);
workServer.setZkClient(client);
workServers.add(workServer);
workServer.start();
}
System.out.println("敲回车键退出!\n");
new BufferedReader(new InputStreamReader(System.in)).readLine();
}finally{
System.out.println("Shutting down...");
for ( WorkServer workServer : workServers ){
try {
workServer.stop();
} catch (Exception e) {
e.printStackTrace();
}
}
for ( ZkClient client : clients ){
try {
client.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
至此,我们已经通过编码实现了简单的master选举。但是,不知你有没有发现,,,,这个选主过程的代码还真是麻烦啊!
我们只是做一个demo,其中并未考虑复杂的业务场景,但其中的 监听,异常 等代码的处理还是让我觉得有些头大,怎么办?Curator应运而生!