集群单节点任务Failover

单节点任务执行方案
针对集群环境中单节点任务执行及失败转移的问题,本文提出了一种基于数据库悲观锁机制的解决方案,确保任务仅在一个节点上执行并能实现故障切换。

在我们的某个旧集群系统中,一直有个问题,一个希望单节点执行的任务在多节点都在运行,虽然由于该任务多次执行影响不大,只是CPU占用时间有点多,我们还是希望可以把这个问题解决。简单说下这个任务当前情况:在系统启动的时候进入waiting状态,每次监听到外部消息就执行。

*Task execution thread*
public class Task{
private Queue<Message> queue;
private long success;
private long failure;
public void start() throw InterruptedException{
     while(true){
        Message msg = queue.take();
        try{
              process(msg);
              success++;
              lastExecutedTime = System.currentTimeMillis();
              } catch (Exception e){
                LOG.error("fail to process message",e);
                failure++;
              }
        }
     }
  }
*Message provider class*
pubic class Provider{
private Queue<Message> queue;
private Connection con;
public Provider(){
  con.registerCallback(this);
}
private long success;
private long failure;
public void conMsgCallback(Callback data){
  Message msg = process(data);
  if(!queue.offer(msg)){
      success++;
      LOG.error("message discard!");
  } else {
      failure++;
  }
}
}

最开始我们希望把这个任务和消息监听单独出去做一个系统,采用Active-Standby模式,简单粗暴有效,可是没有资源扩展。只好内部解决。为了问题的简化,connection listen 打算继续在2个节点同时进行,只单节点化处理任务线程。Oracle有个cluster timer的paper和相关的实现,比较复杂。我们只好自己弄个简单方法来解决这个问题。
在我看来为了实现单节点执行任务以及失败转移,只需要多节点查询共享数据,通过节点信息和时间戳来判断当前由哪个节点执行任务。下面说说解决方案的具体设计思路。
在集群共享数据的存储上有2种想法,一种是利用Cluster Cache&Lock保证同步,另外一种是利用数据库来保证同步。旧系统的数据库远远没有达到性能瓶颈。所以选择使用数据库悲观锁机制来保证数据一致性。给出基于数据库的实现:

table syn_ActiveNode {
    long id;
    String task;
    String ip;
    long lastUpdatedTime; 
}
insert into syn_ActiveNode values (1,"task","192.168.1.1",0);

在集群同步实现考虑中,放弃了任务失败重启线程设计,改成上报告警人工干预,交于人工干预,任务执行保证交给失败转移实现。在最开始的设计中我们直接在任务线程收到外部消息的时候查询数据库,查看当前节点是否是活跃节点,是否需要执行任务和更新数据库共享数据。这个设计很快就被Pass了,原因是为了性能,每次查询运行节点和相关信息,设置更新节点信息需要时间。任务线程执行耗时太长。接下来就把任务线程中查询和判断执行节点的代码做成定时任务,在任务接收到外部消息时只查询定时任务中存储的当前是否可以运行,让检查和执行任务同时发生的几率减小。

public class Task{
  public Task(){
     start();
  }
  // blocking queue used to receive external message
  private Queue<Message> queue;
  private long lastTaskExecutedTime;
  public void start() throw InterruptedException{
     while(true){
        //no guarantee when the msg comes, to check the while loop lives, use timeout here
        Message msg = queue.poll(300, TimeUnit.SECONDS);
        if(null == msg || !isActiveNode()){      
           continue;
        }
        try{
          process(msg);
          } catch (Exception e){
            LOG.error("fail to process message",e);
          }
       }
       lastExecutedTime = System.currentTimeMillis();
  }

  private Lock lock = new ReentrantLock();
  private boolean active = false;
  public boolean isActiveNode(){
      try{
      lock.lock();
      if((System.currentTimeMillis() - lastUpdatedTime) > 360,000){ 
      //assume scheduled task is down
      active=false;
      }
      return active;
      } finally {
      lock.unlock();
      }
  }  

  private boolean validStatus(){
       return con.isOpen() && ((System.currentTimeMillis() - lastExecutedTime ) > 300,000 * 2);
  }

  private long lastUpdatedTime;
  @Scheduled(cron="0 5 * * * ?") 
  public void run() {
      try{
          lock.lock();
          ActiveNode info= persistService.loadActiveNodeForUpdate("task");
         if(info.getIP().equals(localIP)){
              //local node is active node
             if(validStatus()){
                 // task is running well, update status lastRunTime and release read lock
                 lastUpdatedTime= System.currentTimeMillis();
                 info.setLastUpdateTime(lastUpdatedTime);
       persistSerivce.saveActiveNodeAndReleaseLock(info);
                active = true;

             } else {
                // assume current node is in failure status, report alram
                Alarm.report(task.lastTaskExecutedTime, con.isOpen());
                active = false;
                lastUpdatedTime= System.currentTimeMillis();
             }
         } else {
            if(System.currentTimeMillis() - status.getLastUpdatedTime() > 3 * checkRate){
                //assume the active node is down, take the owner here
               if(validStatus()){
                info.setIP(localIP);
                lastUpdatedTime= System.currentTimeMillis();
                info.setLastUpdateTime(lastUpdatedTime);
                persistSerivce.saveActiveNodeAndReleaseLock(info);
                active = true;
               } else {
                  active = false;
                lastUpdatedTime= System.currentTimeMillis();
               }
            } else {
                // assume the active node is still live, nothing to do.
                active = false;
                lastUpdatedTime= System.currentTimeMillis();
            }
         }
      } finally {
          lock.unlock();
      }
  }
}

这种实现需要注意定时状态查询更新功能和业务功能的并发性,加入锁减少并发访问的可能性。同时业务功能查询定时功能提供的状态是要注意状态的更新时间防止定时任务已宕机的情况。定时判断状态也需要检查所有业务运行状态。这个设计的问题在于:自检任务和业务任务职责不清楚,所以把2个职责拆成2个任务:自检任务和业务任务。 自检任务:查看现在是哪个节点在运行任务(activeNode),任务运行是否能够运行正常(lastUpdatedTime & connection status)。为了防止自检任务自己宕机,查询功能被调用的时候要同时检查自检任务自己的最后运行时间,超过自检线程运行周期2倍时间就无视自检线程的当前状态并且上报异常。业务任务则在每次运行的时候查询自检任务的状态,并把业务执行完毕后把自己运行状态上报给自检任务。显然这里有个双向依赖。为了解决这个问题。业务任务依赖自检接口,自检任务提供统一接口函数接收业务任务运行的前置状态和业务任务本身运行状态。一旦其中一个状态出现异常,则发生失败转移。

自检任务接口:

public boolean isCurrentActive();
public TaskStatus registerTask(String taskName, long checkRate);
public boolean reportStatus(String taskName, boolean active);

自检任务:

public class Check{

private class TaskStatus {
  public boolean isActive;
  public long lastCheckTime;
  public long checkRate;
}
private static ConcurrentHashMap<String, TaskStatus> map = new ConcurrentHashMap<String,TaskStatus>(); 
private lastRunTime = 0;
private boolean isCurrentActive = false;
public boolean isCurrentActive(){
    if(0 != lastRunTime && ((System.currentTimeMillis() - lastUpdatedTime) > 360,000)){ 
      //assume scheduled task is down, it should executed every 5 mins, but no update time for 6 min
      isCurrentActive = false;
      return false;
      }
      return isCurrentActive;
}

private boolean isTasksActive(){
    /* check related tasks' last report time */
    for(TaskStatus status : map.entryset()){
        if (System.currentTimeMillis() - status.lastCheckTime > status.checkRate){
        return false;
        }
        if(!status.isActive){
           return false;
        }
    }
    return true;
}
public TaskStatus registerTask(String taskName, long checkRate){
    TaskStatus status = new TaskStatus(false, 0, checkRate);
    map.put(taskName, status);
    return status;
}
public void reportStatus(String taskName, boolean active){
    TaskStatus status = map.get(taskName);
    status.setLastCheckTime(System.currentTimeMillis());
    status.setActive(active);
}


    /** node active check task */
    private String localIP = InetAddress.getLocalHost().getHostAddress();
    @Scheduled(cron="0 5 * * * ?") 
    public void run() {
         ActiveNode status = persistService.loadActiveNodeForUpdate("task");
         if(null == status.getIP() || status.getIP().equals(localIP)){
              //local node is active node
             if(isTasksActive()){
                 // task is running well, update status lastRunTime and release read lock
                 status.setIP(localIP);
                 lastRunTime = System.currentTimeMillis();
                 status.setLastUpdateTime(lastRunTime);
                persistSerivce.saveActiveNodeAndReleaseLock(status);
                isCurrentActive = true;
                reported = false;
             } else {
                // assume current node is owner but in failure status, report alram and wait for other nodes to take the ownership
                if(!reported){Alarm.report(displayTasks());}
                isCurrentActive = false;
                reported = true;
                lastRunTime = System.currentTimeMillis();
             }
         } else {
            if(System.currentTimeMillis() -  status.getLastUpdateTime() > 900,000){
                //assume the active node is down, system is not updated in 15 mins (max 10 mins gap for task scheduled 5 mins), take the ownership here if task status is okay
               if(isTasksActive()){
                status.setIP(localIP);
                lastRunTime = System.currentTimeMillis();
                status.setLastUpdateTime(lastRunTime);
               persistSerivce.saveActiveNodeAndReleaseLock(status);
               isCurrentActive = true;
               reported = false;
                } else {
                   // try to take ownership but system is not in good ship
                   if(!reported){Alarm.report(displayTasks());}
                   reported = true;
                   isCurrentActive = false;
                   lastRunTime = System.currentTimeMillis();
                }
            } else {
                // assume the active node is still live, nothing to do.
                isCurrentActive = false;
                lastRunTime = System.currentTimeMillis();
            }
         }

    }     
    }  

业务任务:

check.registerTask("task",360,000);

 while(true){
        Message msg = queue.poll(300, TimeUnit.SECONDS);
        if(null == msg || !check.isCurrentActive()){
            continue;
        }
        try{
              process(msg);
              success++;
              } catch (Exception e){
                LOG.error("fail to process message",e);
                failure++;
              }
        }
        check.reportStatus("task",true);
     }

这里却掉了更新isCurrentActive 和lastRunTime 的lock,原因是因为我们的系统对failover发生过程中重复一两次不敏感,对数据库访问占时长敏感。

<think>好的,我现在需要帮助用户了解如何从Redis集群中移除节点。首先,我得回忆一下Redis集群的相关知识,特别是节点移除的步骤和注意事项。根据用户提供的引用内容,有几个关键点需要注意: 1. 用户提到了两种删除节点的方法,一种是使用`redis-cli --cluster del-node`命令,另一种是使用`redis-trib.rb del-node`脚本。这可能涉及不同版本的Redis工具,需要区分清楚。 2. 引用[3]指出,如果节点不为空,或者已经包含数据,删除时会报错。这意味着在删除节点前需要确保节点是空的,或者数据已经迁移走。 3. 引用[4]提到了failover的处理,这可能和删除主节点前需要先进行故障转移有关,特别是当要删除的是主节点时,需要先将其数据迁移到其他节点。 接下来,我需要整理步骤。首先,确定要删除的节点是主节点还是从节点。如果是主节点,必须确保它没有负责的哈希槽,否则需要重新分片。而删除从节点相对简单,直接执行命令即可。但需要注意,如果主节点有从节点,删除从节点可能会影响高可用性。 然后,检查节点是否为空。如果节点包含数据,需要使用`redis-cli --cluster reshard`或`redis-cli --cluster rebalance`来迁移数据。这可能涉及到哈希槽的重新分配,需要用户确认迁移的数据量。 此外,还要注意集群的健康状态。在删除节点前,确保集群处于正常状态,没有故障节点,否则操作可能会失败。使用`CLUSTER NODES`命令查看节点信息,确认要删除的节点的ID和角色。 可能遇到的错误包括节点不为空([ERR] Node ... is not empty),这时候需要先清空节点数据或者迁移数据。另外,删除主节点前如果没有迁移哈希槽,也会导致失败。需要逐步引导用户完成这些步骤,避免遗漏。 最后,生成相关问题时要涵盖用户可能进一步想了解的内容,比如数据迁移、集群检查、故障处理等,确保全面覆盖移除节点相关的知识点。</think>从Redis集群中移除节点的步骤如下,需根据节点类型(主/从)选择不同操作: ### 一、移除从节点(Slave) 1. **查看节点信息** 执行`redis-cli --cluster check <任意节点IP>:<端口>`确认要删除的从节点ID和状态[^2]。 2. **执行删除命令** ```bash redis-cli --cluster del-node <从节点IP>:<端口> <从节点ID> ``` 或使用旧版工具: ```bash redis-trib.rb del-node <从节点IP>:<端口> <从节点ID> ``` ### 二、移除主节点(Master) 1. **迁移哈希槽** 主节点需先释放所有哈希槽: ```bash redis-cli --cluster reshard <主节点IP>:<端口> ``` 按提示将哈希槽迁移到其他主节点[^3]。 2. **确认数据清空** 检查节点是否已无数据: ```bash redis-cli -h <主节点IP> -p <端口> DBSIZE ``` 若返回值不为0,需重新分片或等待迁移完成[^3]。 3. **删除节点** ```bash redis-cli --cluster del-node <主节点IP>:<端口> <主节点ID> ``` ### 三、验证操作 执行集群检查确认节点已移除: ```bash redis-cli --cluster check <存活节点IP>:<端口> ``` ### 常见错误处理 - **节点不为空错误**:若出现`[ERR] Node ... is not empty`,需先完成数据迁移 - **节点仍存连接**:使用`CLUSTER FORGET <节点ID>`手动断开残留连接[^1]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值