Spark无法Heartbeat长事务问题的解决方案

3 篇文章 0 订阅

我们对spark的源码进行了大量的修改,使得其能够支持事务表,能够对orc表进行update,delete等操作。上文我们提到spark事务o状态残留问题解决,但是该解决方案缺却引发了一个问题,即长事务的情况下会导致数据出错。

出现了该问题后,我们定位了很久,才找到原因。这是一个严重的生产环境问题,为了避免导致更严重的问题,我们回退了合并时自动timeout事务的bug修复,暂时还是改为手动修改残留事务状态(毕竟这种情况是少数,几天可能才出现一次)。此外,对于长事务问题,这里也有个workaround,那就是把hive的事务timeout时间设长一些,比如设置为5小时(默认为5分钟),但这样终究不是长久之计,问题的根源还是无法解决。

因此我开始着手解决这个问题,我大概想到了两个方案:

1. 在task执行阶段去定期heartbeat事务,但是heartbeat事务需要对应的TxnManager,而TxnManager是在driver端初始化的,且无法序列化,因此无法向task传递该参数。该方案不可行。

2. 在driver端定期去做统一的heartbeat 。

方案一否决后, 我研究了一下spark thriftserver相关的代码, 发现可以在SparkSQLOperationManager增加一个数据结构,记录session的句柄和其对应的TxnManager,这种数据当然是用HashMap是最合理的啦,同时,考虑到并发修改的问题,肯定还得是用ConcurrentHashMap。 同时,为了定期去heartbeat事务,我们还要启动一个定时的线程,因此我还增加了一个size为1的线程池。

val sessionToTxnManagers = new ConcurrentHashMap[SessionHandle, HiveTxnManager]()
lazy val heartbeatPool = Executors.newScheduledThreadPool(1)

同时,重写了SparkSQLOperationManager的start 和stop方法,将线程池初始化和调度, 关闭工作放在此处。这里还增加了一个配置,用户可以自行配置heartbeat的时间间隔。

 override def start(): Unit = {
    super.start()
    val heartbeatKey = "spark.txns.heartbeat.interval"
    val heartbeatInterval = getHiveConf.get(heartbeatKey,"60").toLong
    logInfo(s"HeartbeatInterval is $heartbeatInterval")
    heartbeatPool.scheduleAtFixedRate(new HeartbeatTxnThread(sessionToTxnManagers),
      0, heartbeatInterval, TimeUnit.SECONDS)
  }

  override def stop(): Unit = {
    super.stop()
    heartbeatPool.shutdown()
  }

当然,还需要在SparkSQLSessionManager调用opensession时将TxnManager放到map中, closesession时需要清理掉相应的TxnManager

// open session 
sparkSqlOperationManager.sessionToTxnManagers.put(sessionHandle,txnManager)

// close session
sparkSqlOperationManager.sessionToTxnManagers.remove(sessionHandle)

调试的过程中我发现线程在产生第一个session连接以后,就无法正常按时调度了, 经过排查是因为spark自定义的TxnManager的Heatbeat方法有问题,无法正常心跳,导致线程阻塞了。 

原始方法内容如下:

  override def heartbeat(): Unit = {
    txnIds.slice(0, txnIds.size - 1).foreach{ txnid =>
      try
        refClient.heartbeat(txnid, 0)
      catch {
        case e : Throwable=>
          logWarning(s"Error while heartbeat txn $txnid , exception : $e")
      }
    }
    super.heartbeat()
  }

这里由于每次生成的事务是在某个lock中,因此有对应的lockid,在heartbeat事务时如果总是将lockid置为0, 会出现不能成功heart的问题, 我仔细研究了父类的heartbeat方法,对heartbeat方法进行了修改,最终如下:


  override def heartbeat(): Unit = {
    val locks = getLockManager.getLocks(false,false)
    val lockIds = if(locks.size() == 0){
      Seq[Long](0)
    }else{
      JavaConverters.asScalaBufferConverter(locks).asScala.map(_.asInstanceOf[DbHiveLock].lockId)
    }
    lockIds.foreach(lockId =>{
      txnIds.foreach{ txnid =>
        try{
          logInfo(s"Trying to heartbeat txn:$txnid,lock id:$lockIds")
          refClient.heartbeat(txnid, lockId)
        } catch {
          case e : Throwable=>
            logError(s"Error while heartbeat txn $txnid , lockid:$lockIds exception : $e")
        }
      }
    })
  }

这里由于我们要支持merge语法,因此存在并发事务,所以这里可能会有超过一个以上的txnId,因此需要迭代每个事务id,对其进行heartbeat,必须要重写父类的方法。这样的我们就能够正常heartbeat事务和lock了。

最后就差一个heartbeat线程,在现场中调用下每个session的txnManager的heartbeat方法,去heartbeat一下所有的事务和锁。 代码如下:

package org.apache.spark.sql.hive.thriftserver

import java.util.concurrent.ConcurrentHashMap

import org.apache.hadoop.hive.ql.lockmgr.HiveTxnManager
import org.apache.hive.service.cli.SessionHandle
import org.apache.spark.internal.Logging

/**
  * created by XXXXX on 2020/03/17
  *
  * The thread to batch heartbeat txns and lock
  *
  * @param sessionToTxnManagers
  */
class HeartbeatTxnThread(sessionToTxnManagers: ConcurrentHashMap[SessionHandle, HiveTxnManager])
  extends Runnable with Logging {

  override def run(): Unit = {
    log.info("Start heartbeat thread to heartbeat txns and locks.")
    val iter = sessionToTxnManagers.values().iterator()
    while (iter.hasNext) {
      val txnManagers = iter.next()
      if (null != txnManagers){
        txnManagers.heartbeat()
      }
    }
  }
}

这样,我们在启动thriftserver后,就会有一个线程定期去做heartbeat工作,用户可以根据自己的需要配置heartbeat的时间间隔。也从根本上解决了事务heartbeat的问题,这样我们也可恢复之前compact时去timeout事务的逻辑,不会导致数据丢失,也不用手动去修改事务状态了。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值