spark讀取Hbase的優化

        因爲要對HBase中的鏈路數據進行分析,考慮到直接掃描HBase表對HBase集羣壓力較大,因此通過掃描HFile文件來完成。

        HBase的中數據表是按照小時來存儲的,在掃描某一個小時的數據表時,首先建立該表的快照(Snapshot),再基於HBase提供的TableSnapshotInputFormat類來完成HFile的讀取,核心的代碼如下:

val inputs = spark.sparkContext.newAPIHadoopRDD(
      job.getConfiguration,
      classOf[TableSnapshotInputFormat],
      classOf[org.apache.hadoop.hbase.io.ImmutableBytesWritable],
      classOf[org.apache.hadoop.hbase.client.Result])

         運行了一段時間過後,由於業務數據量不斷增加,導致程序處理一個小時表的時間越來越長,發現程序運行最耗時的地方在於讀取HFile進行反序列化的過程,如圖所示:

         第一個stage耗時23min,該stage主要是讀取HFile,將中間結果進行shuffle write,其並行度爲48,正好等於HBase表的region個數,由於第一個階段的序列化比較耗時,因此提高程序速度的方式在於加大第一個階段的並行度,也即讀取HFile的初始並行度。我們讀取HFile使用的format是org.apache.hadoop.hbase.mapreduce.TableSnapshotInputFormat類,查看其split方法:

public static List<TableSnapshotInputFormatImpl.InputSplit> getSplits(Configuration conf) throws IOException {
    String snapshotName = getSnapshotName(conf);
    Path rootDir = FSUtils.getRootDir(conf);
    FileSystem fs = rootDir.getFileSystem(conf);
    SnapshotManifest manifest = getSnapshotManifest(conf, snapshotName, rootDir, fs);
    List<HRegionInfo> regionInfos = getRegionInfosFromManifest(manifest);
    Scan scan = extractScanFromConf(conf);
    Path restoreDir = new Path(conf.get("hbase.TableSnapshotInputFormat.restore.dir"));
    SplitAlgorithm splitAlgo = getSplitAlgo(conf);
    int numSplits = conf.getInt("hbase.mapreduce.splits.per.region", 1);
    return getSplits(scan, manifest, regionInfos, restoreDir, conf, splitAlgo, numSplits);
}

        其中的regionInfos是HBase表的region列表,其中有個參數hbase.mapreduce.splits.per.region,默認爲1,查看getSplits方法中的實現:

         當該參數大於1時,會對每個region進行分割讀取,同時需要實現org.apache.hadoop.hbase.util.SplitAlgorithm的分割算法.

         首先我們實現對HBase region讀取時的切分策略,由於HBase表在寫入的過程中採用的是HexStringSplit分割方式,故實現的切分邏輯如下:

class TraceSplit extends HexStringSplit {

    override def split(start: Array[Byte], end: Array[Byte], numSplits: Int, inclusive: Boolean): Array[Array[Byte]] = {
        var keyStart = start
        var keyEnd = end
        if (StringUtils.isEmpty(Bytes.toStringBinary(start))) {
            keyStart = "00000000".getBytes
        }
        if (StringUtils.isEmpty(Bytes.toStringBinary(end))) {
            keyEnd = "FFFFFFFF".getBytes
        }
        super.split(keyStart, keyEnd, numSplits, inclusive)
    }
}

         由於第一個Region的startKey和最後一個Region的endKey爲空,爲了保持切分的連續性,將第一個Region的startKey設置爲00000000,最後一個Region的endKey設置爲FFFFFFFF,採用的切分邏輯即HexStringSplit的split方法。

         設置切分的方法爲:

TableSnapshotInputFormat.setInput(job, snapshot, path, new TraceSplit(), splitPerRegion)

          將splitPerRegion設爲2,即每個region切分爲2個,則讀取並行度爲96。

          (注: 該圖與上一個執行圖執行的數據並不是同一個小時的,當前小時的數據未切分前運行時間需要1個小時)

          將hbase.mapreduce.splits.per.region設置爲大於1之後,出現了以下問題,也即第一個stage中出現了task failed情況:

19/03/01 11:23:27 ERROR executor.Executor: Exception in task 16.0 in stage 0.0 (TID 18)
org.apache.hadoop.ipc.RemoteException(org.apache.hadoop.hdfs.protocol.AlreadyBeingCreatedException): Failed to CREATE_FILE /user/gwjiang/reflux/benchmark/output/v2/restore/bfe4e1ff-20c5-41ec-b6db-5b48f808344c/data/default/leyden-2019-02-28-21/313225b6e5b440c96f161f8cb54670f0/recovered.edits/3780577.seqid for DFSClient_NONMAPREDUCE_623145177_155 on 10.101.5.10 because this file lease is currently owned by DFSClient_NONMAPREDUCE_-1558587010_154 on 10.101.5.10
    at org.apache.hadoop.hdfs.server.namenode.FSNamesystem.recoverLeaseInternal(FSNamesystem.java:2455)
    at org.apache.hadoop.hdfs.server.namenode.FSDirWriteFileOp.startFile(FSDirWriteFileOp.java:357)
    at org.apache.hadoop.hdfs.server.namenode.FSNamesystem.startFileInt(FSNamesystem.java:2303)
    at org.apache.hadoop.hdfs.server.namenode.FSNamesystem.startFile(FSNamesystem.java:2223)
    at org.apache.hadoop.hdfs.server.namenode.NameNodeRpcServer.create(NameNodeRpcServer.java:728)
    at org.apache.hadoop.hdfs.protocolPB.ClientNamenodeProtocolServerSideTranslatorPB.create(ClientNamenodeProtocolServerSideTranslatorPB.java:413)
    at org.apache.hadoop.hdfs.protocol.proto.ClientNamenodeProtocolProtos$ClientNamenodeProtocol$2.callBlockingMethod(ClientNamenodeProtocolProtos.java)
    at org.apache.hadoop.ipc.ProtobufRpcEngine$Server$ProtoBufRpcInvoker.call(ProtobufRpcEngine.java:447)
    at org.apache.hadoop.ipc.RPC$Server.call(RPC.java:989)
    at org.apache.hadoop.ipc.Server$RpcCall.run(Server.java:850)
    at org.apache.hadoop.ipc.Server$RpcCall.run(Server.java:793)
    at java.security.AccessController.doPrivileged(Native Method)
    at javax.security.auth.Subject.doAs(Subject.java:422)
    at org.apache.hadoop.security.UserGroupInformation.doAs(UserGroupInformation.java:1840)
    at org.apache.hadoop.ipc.Server$Handler.run(Server.java:2489)
 
    at org.apache.hadoop.ipc.Client.getRpcResponse(Client.java:1489)
    at org.apache.hadoop.ipc.Client.call(Client.java:1435)
    at org.apache.hadoop.ipc.Client.call(Client.java:1345)
    at org.apache.hadoop.ipc.ProtobufRpcEngine$Invoker.invoke(ProtobufRpcEngine.java:227)
    at org.apache.hadoop.ipc.ProtobufRpcEngine$Invoker.invoke(ProtobufRpcEngine.java:116)
    at com.sun.proxy.$Proxy14.create(Unknown Source)
    at org.apache.hadoop.hdfs.protocolPB.ClientNamenodeProtocolTranslatorPB.create(ClientNamenodeProtocolTranslatorPB.java:297)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java)

          從報錯信息來看,是因爲兩個進程同時創建一個文件造成的互斥錯誤。HBase在創建snapshot時需要指定一個restore目錄,並且將region的元信息複製到該目錄中,由於region是48個,因此restore下的目錄也是48個,並且與region一一對應。通過代碼棧跟蹤信息,發現shuffleMap的每個task需要從restore中獲取region信息,雖然第一個stage有96個task,但每兩個task都是基於同一個region進行key分割得到,因此這個兩個task需要讀取同一個region信息,

          定位到HBase源碼中:

// The openSeqNum will always be increase even for read only region, as we rely on it to
// determine whether a region has been successfully reopend, so here we always need to update
// the max sequence id file.
if (RegionReplicaUtil.isDefaultReplica(getRegionInfo())) {
  LOG.debug("writing seq id for {}", this.getRegionInfo().getEncodedName());
  WALSplitter.writeRegionSequenceIdFile(fs.getFileSystem(), fs.getRegionDir(), nextSeqId - 1);
}
 
LOG.info("Opened {}; next sequenceid={}", this.getRegionInfo().getShortNameToLog(), nextSeqId);

          該段代碼會在restore對應的region目錄中創建seqid文件,從代碼註釋可知,該文件是作爲當前region是否打開的標誌位,由於兩個task同屬於一個region,因此同時創建該標誌位會報錯。task報錯後,會重新執行(spark中會重試4次),第二次執行時,由於該標誌位已經創建,因此執行正常。因此,當hbase.mapreduce.splits.per.region設置大於1之後,在第一個stage中會出現部分task failed的情況。

 

          爲了規避這個錯誤,對org.apache.hadoop.hbase.mapreduce.TableSnapshotInputFormatImpl.RecordReader進行了重新實現,在其中初始化scanner中進行了重試:

def initialize(split: InputSplit, conf: Configuration): Unit ={
    this.scan = TableMapReduceUtil.convertStringToScan(split.getScan)
    this.split = split
    val htd = split.getHtd
    val hri = this.split.getRegionInfo
    val fs = CommonFSUtils.getCurrentFileSystem(conf)
 
    scan.setIsolationLevel(IsolationLevel.READ_UNCOMMITTED)
    scan.setCacheBlocks(false)
    scan.setScanMetricsEnabled(true)
 
    try {
        this.scanner = new ClientSideRegionScanner(conf, fs, new Path(split.getRestoreDir), htd, hri, scan, null)
    } catch {
        case e: IOException =>
            logger.error(s"create ClientSideRegionScanner error", e)
            e match {
                case e: RemoteException =>
                    if (e.getClassName.contains("AlreadyBeingCreatedException")) {
                        logger.info("try to create ClientSideRegionScanner again")
                        Thread.sleep(1000)
                        this.scanner = new ClientSideRegionScanner(conf, fs, new Path(split.getRestoreDir), htd, hri, scan, null)
                    }
                case _ =>
            }
    }
}

         在初始化scanner時,如果遇到AlreadyBeingCreatedException異常,則重新初始化scanner,由於這時標誌位已經創建好,重新初始化時不會報錯。 

         重寫了org.apache.hadoop.hbase.mapreduce.TableSnapshotInputFormat的ceateRecordReader方法:

class TraceTableSnapshotInputFormat extends TableSnapshotInputFormat{
 
    override def createRecordReader(split: InputSplit, context: TaskAttemptContext): mapreduce.RecordReader[ImmutableBytesWritable, Result] = {
        new TableSnapshotRegionRecordReader
    }
}

         修改之後,當hbase.mapreduce.splits.per.region大於1時,沒有出現task運行失敗的情況。

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