Spark無法Heartbeat長事務問題的解決方案

我們對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事務的邏輯,不會導致數據丟失,也不用手動去修改事務狀態了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章