高可用设计方案之插槽机制(stand by),m工位(插槽)n节点(工人),没工位的工人就看着,有空工位就顶上

场景

假设现在我系统内有很多的机器人需要运行在节点上,如何去保证高可用?

解决思路

插槽机制 stand by模式
假定我有5台机器,我在数据库中设置3个插槽(用ip,port,time标识,占有插槽的节点需要定时更新time),让机器人对应自己由哪个插槽的节点来执行,机器人和插槽建立映射,而插槽上干活的具体是谁,这个我们是可以变化的,采用抢占的思路,一个节点上线后,开始抢占插槽(抢之前先查有没有已经有效占领的插槽,抢占时冲突小采用哈希分散+乐观锁提高抢占效率),抢占到插槽后就把所有和这个插槽建立映射的机器人在本节点上运行,而之前占领了这个插槽的节点如果还存活,仅因为网络等原因让自己失去了对该插槽的控制权后需要释放手里所有机器人,重新去抢插槽,如果插槽全部被抢走了没有插槽了,那么就定时的去询问是不是有空闲的插槽呀,有就上去。
在这里插入图片描述
在这个方案下,机器人只需要绑定到某个插槽即可,无需关心自己是在哪个节点被启动的,插槽其实就相当于工位,而另一堆的节点就不断的去尝试占领一个插槽(工位),占领到了就在那个工位上处理那个工位的事,也就是去启动属于自己的所有机器人,而其他没有抢到工位的节点就定时的轮询,相当于在旁边站着看,看哪里有空位出来了自己就过去尝试占领,这样的话一旦某个节点挂了,自然就会有空闲节点来接替他的工作。
注意:每个机器人运行的时候要随时可能挂掉之后再启动时数据不会丢失,那么需要做好数据持久化,避免数据丢失,如果运算比较复杂有状态和相关数据的机器人建议采用上下文机制,给每个机器人存好上下文,一旦死机,占领插槽后的节点也能从上下文恢复状态,继续运行这个机器人。
数据库设计思路示意:
机器人表和插槽表ID绑定关联起来

create table t_robot(
...
slotId (插槽ID)
....
);
create table t_slot(
id
ip
port
time(最后存活时间,占领后的节点要定时更新)
);

插槽占领的思路

  • 节点启动占领插槽
    • 占领成功
      • 启动绑定到该插槽的所有机器人
      • 定时更新自己的最后存活时间
    • 占领失败
      • 定时查询是否有没有节点正占领的插槽(原节点超时未更新最后存活时间(2倍定时更新时间),就失去了占领权,需要释放当前运行中的所有机器人,重新尝试占领)

相关函数

  • findLessLoadNode() 找出一个低负载的节点(机器人创建的时候分配插槽时使用)
  • tryOccupySlot(ip,port) 尝试占领一个插槽(如果已经占领了就直接更新这个插槽最后存活时间并返回该插槽)

Demo

Typescript + MongoDB + Mongoose(orm框架)

  • 插槽Bean(_id查出来就会有,mongodb必定有_id)
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';

@Schema({ collection: 't_strategy_node_slot' })
export class StrategyNodeSlot extends Document {
  @Prop()
  ip: string;
  @Prop()
  port: number;
  @Prop()
  time: number;
}

export const StrategyNodeSlotSchema = SchemaFactory.createForClass(StrategyNodeSlot);

  • 策略节点插槽服务(对外提供上面两个函数)
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { StrategyNodeSlot } from '../schema/strategy.node.slot.schema';
import { SubscribeStrategy } from '../schema/subscribe.strategy.schema';

@Injectable()
export class StrategyNodeSlotService {
  constructor(
    @InjectModel('StrategyNodeSlot') private readonly strategyNodeSlotModel: Model<StrategyNodeSlot>,
    @InjectModel('SubscribeStrategy') private readonly subscribeStrategyModel: Model<SubscribeStrategy>
  ) {
  }

  /**
   * 找一个最小负载的插槽
   */
  async findLessNodeSlot():Promise<StrategyNodeSlot>{
    let validTime = this.getValidTime()
    let subscribeStrategies = await this.subscribeStrategyModel.find({})
    let slots = await this.strategyNodeSlotModel.find({time:{$gt:validTime}})
    let slotCountMap = new Map<string,number>()
    if (!slots || slots.length == 0){
      return null
    }
    //初始化计数表
    for (let slot of slots){
      slotCountMap.set(slot['_id'].toString(),0)
    }
    for (let s of subscribeStrategies){
      slotCountMap.set(s.nodeId,slotCountMap.get(s.nodeId)+1)
    }
    let minCountSlot = slots[0]
    let minCount = slotCountMap.get(minCountSlot['_id'].toString())
    for (let slot of slots){
      let id = slot['_id'].toString()
      let count = slotCountMap.get(id)
      if (count < minCount){
        minCount = count
        minCountSlot = slot
      }
    }
    return minCountSlot
  }
  private getValidTime():number{
    return new Date().getTime() - 1000*5
  }

  /**
   * 查询自己IP和端口占领的插槽
   * @param ip
   * @param port
   */
  private async getSlot(ip:string,port:number):Promise<StrategyNodeSlot>{
    let validTime = this.getValidTime()
    let filter = {
      ip: ip,
      port: port,
      time:{
        $gt: validTime
      }
    }
    let slot = await this.strategyNodeSlotModel.findOne(filter)
    return slot
  }



  /**
   * 尝试占领一个插槽
   */
  async tryOccupySlot(ip:string,port:number):Promise<StrategyNodeSlot>{
    let validTime = this.getValidTime()
    let slot = await this.getSlot(ip,port)
    //已经占领了一个插槽了,直接返回他占领的这个
    if (slot){
      //更新时间
      slot.time = new Date().getTime()
      await this.strategyNodeSlotModel.findByIdAndUpdate(slot['_id'],slot)
      return slot
    }
    let filter = {
      time:{
        $lt: validTime
      }
    }
    let slots = await this.strategyNodeSlotModel.find(filter)
    if (slots && slots.length > 0) {
      let index = +(ip.replace(/\./g,'')+port) % slots.length
      let slot = slots[index]
      let ret = await this.tryOccupyThisSlot(ip,port,slot)
      if (ret){
        return await this.getSlot(ip,port)
      }
    }
    return null
  }
  private async tryOccupyThisSlot(ip:string,port:number,slot:StrategyNodeSlot):Promise<boolean>{
    let filter = {
      _id: slot['_id'],
      time: slot.time
    }
    let newSlot = {
      _id: filter._id,
      ip: ip,
      port: port,
      time: new Date().getTime()
    }
    let ret = await this.strategyNodeSlotModel.updateOne(filter,newSlot)
    if (ret && ret['nModified'] == 1){
      return true
    }
    return false
  }
}

在这里插入图片描述

  • 该节点占领插槽后,不断的定时在更新自己的最后存活时间time以保证对该插槽的占领状态,因为超过两倍定时时间就会被认为你已经失去所有权了。
posted @ 2021-07-21 18:24  HumorChen99  阅读(0)  评论(0编辑  收藏  举报  来源