Pack 0.4.0新特性解读 |Cluster集群实现

点击蓝字关注我呗~

640?wx_fmt=gif

alpha可以通过扩展部署多个实例实现高可用部署,并且通过在节点上设置启动参数↓

 alpha.event.scanner.enabled=false 

关闭一些节点后台定时执行的事务扫描,避免多个节点同时扫描事物表可能导致的性能问题。但是当启动事务扫描的进程宕机后会导致没有进程进行事务扫描,从而无法进行超时事务补偿。在0.4.0版本中,我们通过一种基于数据库表的抢占锁机制,实现alpha集群中主节点的动态选举,并让事务扫描方法只在主节点上执行。当主节点宕机后其他节点通过抢占的方式选出一个新的主节点,本文将介绍在0.4.0版本相关的代码实现。


640?wx_fmt=gif 快速体验


在0.4.0版本中开启动态主节点集群模式支持只需要增加启动参数 alpha.cluster.master.enabled=true

  • 启动两个节点

 
 

 
 
java -jar alpha-server-0.4.0-snapshot-exec.jar \  --server.port=8090 \  --alpha.server.port=8080 \  --spring.datasource.url="jdbc:postgresql://127.0.0.1:5432/saga?usessl=false" \  --spring.datasource.username=saga-user \  --spring.datasource.password=saga-password \  --alpha.cluster.master.enabled=true
  --server.port=8090 \
  --alpha.server.port=8080 \
  --spring.datasource.url="jdbc:postgresql://127.0.0.1:5432/saga?usessl=false" \
  --spring.datasource.username=saga-user \
  --spring.datasource.password=saga-password \
  --alpha.cluster.master.enabled=true


 
 
java -jar alpha-server-0.4.0-snapshot-exec.jar \  --server.port=8091 \  --alpha.server.port=8081 \  --spring.datasource.url="jdbc:postgresql://127.0.0.1:5432/saga?usessl=false" \  --spring.datasource.username=saga-user \  --spring.datasource.password=saga-password \  --alpha.cluster.master.enabled=true
  --server.port=8091 \
  --alpha.server.port=8081 \
  --spring.datasource.url="jdbc:postgresql://127.0.0.1:5432/saga?usessl=false" \
  --spring.datasource.username=saga-user \
  --spring.datasource.password=saga-password \
  --alpha.cluster.master.enabled=true
  • 节点类型信息查看


在日志中看到 master node 则表示这个进程是主节点

 
 
01:31:07.032 [pool-3-thread-1] info  org.apache.servicecomb.pack.alpha.server.cluster.master.clusterlockservice - master node 31:07.032 [pool-3-thread-1] info  org.apache.servicecomb.pack.alpha.server.cluster.master.clusterlockservice - master node 


日志中看到 slave node 则表示这个进程是从节点

 
 
01:31:31.059 [pool-3-thread-1] info  org.apache.servicecomb.pack.alpha.server.cluster.master.clusterlockservice - slave node31:31.059 [pool-3-thread-1] info  org.apache.servicecomb.pack.alpha.server.cluster.master.clusterlockservice - slave node


  • 节点切换

当主节点进程宕后,其他从节点会采用抢占的产生一个新的主节点


640?wx_fmt=gif 让事务扫描运行在主节点


事务扫描是通过 eventscanner.java 实现的,并且在 alphaconfig.java 中进行初始化,可以看到在 new eventscanner 代码执行前进行了eventscannerenabled判断,这个参数就是通过alpha.event.scanner.enabled 指定的(默认是true),然后传入了nodestatus 对象,这个对象就记录着这个节点的状态(主节点或者从节点),后边会讲解 nodestatus 是如何构造的。

 
 

@bean
txconsistentservice txconsistentservice(
  @value("${alpha.event.pollinginterval:500}"int eventpollinginterval,
  @value("${alpha.event.scanner.enabled:true}") boolean eventscannerenabled,
  scheduledexecutorservice scheduler,
  txeventrepository eventrepository,
  commandrepository commandrepository,
  txtimeoutrepository timeoutrepository,
  omegacallback omegacallback) 
{
    if (eventscannerenabled) {
      new eventscanner(scheduler,
          eventrepository, commandrepository, timeoutrepository,
          omegacallback, eventpollinginterval, nodestatus).run();
      log.info("starting the eventscanner.");
      }
    txconsistentservice consistentservice = new txconsistentservice(eventrepository);
    return consistentservice;
}


eventscanner.java 的 pollevents 方法进行定时事务扫描,并使用 nodestatus.ismaster() 判断自己是否是主节点,只有主节点才允许执行。看到这里大家应该知道 nodestatus.ismaster() 是我们判断主节点的关键对象,那么 nodestatus.java 是如何被创建并初始化的呢?

 
 
private void pollEvents() {    scheduler.scheduleWithFixedDelay(        () -> {          // only pull the events when working in the master mode          if(nodeStatus.isMaster()){            updateTimeoutStatus();            findTimeoutEvents();            abortTimeoutEvents();            saveUncompensatedEventsToCommands();            compensate();            updateCompensatedCommands();            deleteDuplicateSagaEndedEvents();            updateTransactionStatus();          }        },        0,        eventPollingInterval,        MILLISECONDS);}
    scheduler.scheduleWithFixedDelay(
        () -> {
          // only pull the events when working in the master mode
          if(nodeStatus.isMaster()){
            updateTimeoutStatus();
            findTimeoutEvents();
            abortTimeoutEvents();
            saveUncompensatedEventsToCommands();
            compensate();
            updateCompensatedCommands();
            deleteDuplicateSagaEndedEvents();
            updateTransactionStatus();
          }
        },
        0,
        eventPollingInterval,
        MILLISECONDS);
}


我们在 alphaconfig.java 中通过以下方式创建实例,以确保无论您是否指定了 alpha.cluster.master.enabled 参数事务扫描都可以正常工作,在这里可以看到当我们开启了集群模式后节点刚启动的时候状态是slave,下面会说明状态是如何切换到master的。

 
 

@bean
nodestatus nodestatus (){
if(masterenabled){
  return new nodestatus(nodestatus.typeenum.slave);
}else{
  return new nodestatus(nodestatus.typeenum.master);
}
}

@autowired
nodestatus nodestatus;

控制节点状态切换的是 clusterlockservice.java ,这个服务会定时执行锁抢占,抢占成功后设置本节点为master,否则为slave

 
 
@AutowiredLockProvider lockProvider;......@Scheduled(cron = "0/1 * * * * ?")public void masterCheck() {if (applicationReady) {  this.locker = lockProvider.lock(this.getMasterLock());  if (this.locker.isPresent()) {    if (!this.locked) {      this.locked = true;      nodeStatus.setTypeEnum(NodeStatus.TypeEnum.MASTER);      LOG.info("Master Node");    }    //Keep locked  } else {    if (this.locked || !lockExecuted) {      locked = false;      nodeStatus.setTypeEnum(NodeStatus.TypeEnum.SLAVE);      LOG.info("Slave Node");    }  }  lockExecuted = true;}}
LockProvider lockProvider;
...
...
@Scheduled(cron = "0/1 * * * * ?")
public void masterCheck() {
if (applicationReady) {
  this.locker = lockProvider.lock(this.getMasterLock());
  if (this.locker.isPresent()) {
    if (!this.locked) {
      this.locked = true;
      nodeStatus.setTypeEnum(NodeStatus.TypeEnum.MASTER);
      LOG.info("Master Node");
    }
    //Keep locked
  } else {
    if (this.locked || !lockExecuted) {
      locked = false;
      nodeStatus.setTypeEnum(NodeStatus.TypeEnum.SLAVE);
      LOG.info("Slave Node");
    }
  }
  lockExecuted = true;
}
}



640?wx_fmt=gif 加锁服务基础类


在前边的说明中可以看到,在 clusterlockservice.java 的 mastercheck 方法中通过 this.locker = lockprovider.lock(this.getmasterlock()); 获取一个锁并判断是否锁成功。

lockprovider.java 是一个接口,目前我们提供了基于 jdbc 的实现,包结构以及类依赖关系如下:

640?wx_fmt=png


依赖关系如下↓

640?wx_fmt=png

  • lockprovider.java

    接口定义了锁方法 lock

  • lockproviderpersistence.java

    接口定义了以下三个方法,作为持久化锁的接口

    • initlock 创建锁,尝试进行锁定并返回锁定是否成功

    • updatelock 更新锁,进行再次锁定并返回是否成功(更新锁的接口设计的目的是为了非长连接锁设计,例如对于按照固定周期进行加锁的实现)

    • unlock 解锁,取消锁定

  • abstractlockprovider.java

    抽象类实现了 lockprovider.java 接口的lock方法,并调用内部的 lockproviderpersistence.java 接口进行锁操作


640?wx_fmt=gif 加锁服务jdbc实现


我喜欢和你们一起跑步,一致的步伐,一样的心跳,这感觉真好,当你掉队了,我带着大家跑

  • jdbc 类关系图

640?wx_fmt=png

  • jdbclockpersistence.java

    lockproviderpersistence.java 接口实现,用来实现对数据库表操作

  • jdbclockprovider.java

    继承抽象类 abstractlockprovider.java ,在构造函数中传入 lockproviderpersistence.java 的接口实现 jdbclockpersistence`

  • lockproviderjdbcconfiguration.java

    锁的jdbc实例构造类

  • jpa

    masterlockentityrepository.java、springmasterlockrepository.java、org.apache.servicecomb.pack.alpha.server.cluster.master.provider.jdbc.jpa.*

  • 锁定表结构设计

 
 

create table if not exists master_lock (
  servicename varchar(36) not null,
  expiretime datetime not null default current_timestamp,
  lockedtime datetime not null default current_timestamp,
  instanceid  varchar(255) not null,
  primary key (servicename)
) engine
=innodb default charset=utf8;


  • servicename 服务名,这个字段取值 ${spring.application.name}

  • expiretime 锁定过期时间,这个字段取值 lockedtime+5s

  • lockedtime 最近一次锁定时间

  • instanceid 集群实例id,这个字段取值 ${alpha.server.host}]:${alpha.server.port}


640?wx_fmt=gif 加锁过程


加锁/更新锁的过程是一个周期性重复执行的动作,步骤如下:

  • clusterlockservice.java 服务中会每秒调用一次 lockprovider.java 接口的lock方法

  • abstractlockprovider.java 抽象类中的 lock方法会尝试调用 jdbclockpersistence.java 的 inilock方法进行加锁,加锁的sql实现定义在 masterlockentityrepository.java 中

    • 如果表为空,那么插入一条记录并返回加锁成功

    • 如果表中存在servicename字段相同的记录,则捕获异常加锁失败

 
 

 
 
@transactional   @modifying   @query(value = "insert into master_lock "       + "(servicename, expiretime, lockedtime, instanceid) "       + "values "       + "(?1, ?2, ?3, ?4)", nativequery = true)   int initlock(       @param("servicename") string servicename,       @param("expiretime") date expiretime,       @param("lockedtime") date lockedtime,       @param("instanceid") string instanceid);
   @modifying
   @query(value = "insert into master_lock "
       + "(servicename, expiretime, lockedtime, instanceid) "
       + "values "
       + "(?1, ?2, ?3, ?4)", nativequery = true)
   int initlock(
       @param("servicename")
 string servicename,
       @param("expiretime") date expiretime,
       @param("lockedtime") date lockedtime,
       @param("instanceid") string instanceid)
;

果 initlock加锁失败,则尝试调用
  masterlockentityrepository.java  中的 updatelock 方法尝试更新锁
  • 表中存在的本服务记录 instanceid 与本实例 instanceid 相同,则更新成功并返回加锁成功(这个表明上一个更新周期也是本服务更新的)

  • 表中存在的本服务记录 expiretime 小于当前锁定时间 ,则更新成功并返回加锁成功(表示上一个锁定周期并没有实例进行锁定更新操作)

 
 
@transactional   @modifying(clearautomatically = true)   @query("update org.apache.servicecomb.pack.alpha.server.cluster.master.provider.jdbc.jpa.masterlock t "       + "set t.expiretime = :expiretime"       + ",t.lockedtime = :lockedtime "       + ",t.instanceid = :instanceid "       + "where t.servicename = :servicename and (t.expiretime <= :lockedtime or t.instanceid = :instanceid)")   int updatelock(       @param("servicename") string servicename,       @param("lockedtime") date lockedtime,       @param("expiretime") date expiretime,       @param("instanceid") string instanceid);
   @modifying(clearautomatically = true)
   @query("update org.apache.servicecomb.pack.alpha.server.cluster.master.provider.jdbc.jpa.masterlock t "
       + "set t.expiretime = :expiretime"
       + ",t.lockedtime = :lockedtime "
       + ",t.instanceid = :instanceid "
       + "where t.servicename = :servicename and (t.expiretime <= :lockedtime or t.instanceid = :instanceid)")
   int updatelock(
       @param("servicename")
 string servicename,
       @param("lockedtime") date lockedtime,
       @param("expiretime") date expiretime,
       @param("instanceid") string instanceid)
;


  • 释放锁 unlock

    保留接口,用于扩展其他锁定方式


640?wx_fmt=gif 锁的其他实现


可以通过lockprovider.java 和

 lockproviderpersistence.java 接口实现其他方式的锁,例如zookeeper,redis等


注意

  • 基于数据库表的方式需要集群中多个节点的服务器时钟同步

  • 基于 mysql 数据库时需要配置正确的时区,例如:servertimezone=gmt%2b8


640?wx_fmt=gif

未来,新功能还会不断加入,欢迎大家一起参与完善。共同打造优秀的微服务生态圈。


640?wx_fmt=gif

Pack 0.4.0 的重要更新


*提供Alpha HA实现

*支持使用eureka或consul进行Alpha的服务发现

*修复事件扫描线程退出的问题

640?wx_fmt=gif


640?wx_fmt=png前期阅读


ServiceComb1.2.0发布|新版本特性解读

Javachaiss1.2.0新特性解读 |使用inspector功能...

Pack 0.4.0新特性解读 |集成Consul

Pack 0.4.0新特性解读 |集成Spring Cloud Eureka


项目地址:

https://github.com/apache/servicecomb-pack

更多信息请浏览官网:

http://servicecomb.apache.org/cn


640?wx_fmt=gif



长按关注 >>>盘它 640?wx_fmt=png


640?wx_fmt=png 640?wx_fmt=png 了解更多新特性请点在看 640?wx_fmt=gif 640?wx_fmt=png


“阅读原文”给Pack点个“⭐”吧

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值