Hbase Split 解析

HBase Split是hbase根據一定的觸發條件和一定的分裂策略將HBase的一個region進行分裂成兩個子region並對父region進行清除處理的過程。Region是HBase中一個非常核心的組織單元,所有的region組成了整個HBase集羣,如下面的HBase的體系結構所示:

這裏寫圖片描述

在HBase中,split其實是進行sharding的一種技術手段,通過HBase的split條件和split策略,將region進行合理的split,再通過HBase的balance策略,將分裂的region負載均衡到各個regionserver上,最大化的發揮分佈式系統的優點。HBase這種自動的sharding技術比傳統的數據庫sharding要省事的多,減輕了維護的成本,但是這樣也會給HBase帶來額外的IO開銷,因此在很多系統中如果能很好的預計rowkey的分佈和數據增長情況,可以通過預先分區,事先將region分配好,再將HBase的自動分區禁掉。

Region split的大概流程如下:

1.region先更改ZK中該region的狀態爲SPLITING。
2.Master檢測到region狀態改變。
3.region會在存儲目錄下新建.split文件夾用於保存split後的daughter region信息。
4.Parent region關閉數據寫入並觸發flush操作,保證所有寫入Parent region的數據都能持久化。
5.在.split文件夾下新建兩個region,稱之爲daughter A、daughter B。
6.Daughter A、Daughter B拷貝到HBase根目錄下,形成兩個新的region。
7.Parent region通知修改.META.表後下線,不再提供服務。
8.Daughter A、Daughter B上線,開始向外提供服務。
9.如果開啓了balance_switch服務,split後的region將會被重新分佈。

上面1 ~ 9就是region split的整個過程,split過程非常快,速度基本會在秒級內,那麼在這麼快的時間內,region中的數據怎麼被重新組織的?其實,split只是簡單的把region從邏輯上劃分成兩個,並沒有涉及到底層數據的重組,split完成後,Parent region並沒有被銷燬,只是被做下線處理,不再對外部提供服務。而新產生的region Daughter A 和 Daughter B,內部的數據只是簡單的到Parent region數據的索引,Parent region數據的清理在Daughter A和Daughter B進行major compact以後,發現已經沒有到其內部數據的索引後,Parent region纔會被真正的清理。

HBase Split觸發條件

1 Pre-splitting
當一個table剛被創建的時候,Hbase默認的分配一個region給table。也就是說這個時候,所有的讀寫請求都會訪問到同一個regionServer的同一個region中,這個時候就達不到負載均衡的效果了,集羣中的其他regionServer就可能會處於比較空閒的狀態。解決這個問題可以用pre-splitting,在創建table的時候就配置好,生成多個region。

在table初始化的時候如果不配置的話,Hbase是不知道如何去split region的,因爲Hbase不知道應該那個row key可以作爲split的開始點。如果我們可以大概預測到row key的分佈,我們可以使用pre-spliting來幫助我們提前split region。不過如果我們預測得不準確的話,還是可能導致某個region過熱,被集中訪問,不過還好我們還有auto-split。最好的辦法就是首先預測split的切分點,做pre-splitting,然後後面讓auto-split來處理後面的負載均衡。

Hbase自帶了兩種pre-split的算法,分別是 HexStringSplit 和 UniformSplit 。如果我們的row key是十六進制的字符串作爲前綴的,就比較適合用HexStringSplit,作爲pre-split的算法。例如,我們使用HexHash(prefix)作爲row key的前綴,其中Hexhash爲最終得到十六進制字符串的hash算法。我們也可以用我們自己的split算法。

2 Auto splitting

1.當memstore flush操作後,HRegion寫入新的HFile,有可能產生較大的HFile,HBase就會調用CompactSplitThread.requestSplit判斷是否需要split操作。

2.HStore剛剛進行完compact操作後有可能產生較大的HFile,當滿足HBase的某一分裂策略後就會進行split操作。

HBase默認有三種自動split的策略,ConstantSizeRegionSplitPolicy,IncreasingToUpperBoundRegionSplitPolicy還有 KeyPrefixRegionSplitPolicy。在0.94版本之前ConstantSizeRegionSplitPolicy 是默認和唯一的split策略。當某個store(對應一個column family)的大小大於配置值 ‘hbase.hregion.max.filesize’的時候(默認10G)region就會自動分裂。而0.94版本中,IncreasingToUpperBoundRegionSplitPolicy 是默認的split策略。這個策略中,最小的分裂大小和table的某個region server的region 個數有關,當store file的大小大於如下公式得出的值的時候就會split,公式如下

 Min (R^2 * “hbase.hregion.memstore.flush.size”, “hbase.hregion.max.filesize”)  R爲同一個table中在同一個region server中region的個數。

例如:

hbase.hregion.memstore.flush.size 默認值 128MB。

hbase.hregion.max.filesize默認值爲10GB 。

  如果初始時R=1,那麼Min(128MB,10GB)=128MB,也就是說在第一個flush的時候就會觸發分裂操作。

  當R=2的時候Min(2*2*128MB,10GB)=512MB ,當某個store file大小達到512MB的時候,就會觸發分裂。

  如此類推,當R=9的時候,store file 達到10GB的時候就會分裂,也就是說當R>=9的時候,store file 達到10GB的時候就會分裂。

split 點都位於region中row key的中間點。KeyPrefixRegionSplitPolicy可以保證相同的前綴的row保存在同一個region中。指定rowkey前綴位數劃分region,通過讀取 KeyPrefixRegionSplitPolicy.prefix_length 屬性,該屬性爲數字類型,表示前綴長度,在進行split時,按此長度對splitPoint進行截取。此種策略比較適合固定前綴的rowkey。當table中沒有設置該屬性,指定此策略效果等同與使用IncreasingToUpperBoundRegionSplitPolicy。我們可以通過配置 hbase.regionserver.region.split.policy 來指定split策略,我們也可以寫我們自己的split策略。

3 Forced Splits
當HBaseAdmin手動發起split時,也會觸發split操作。 、

split策略

  1. 採用ConstantSizeRegionSplitPolicy策略,即storefile固定大小策略:

在0.94版本之前ConstantSizeRegionSplitPolicy 是默認和唯一的split策略。當某個storefile(對應一個columnfamily)的大小大於配置值 ‘hbase.hregion.max.filesize’的時候(默認DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024 *1024L=10G)region就會自動分裂:

對應的源代碼如下:

@Override
  protected void configureForRegion(HRegion region) {
    super.configureForRegion(region);
    Configuration conf = getConf();
    HTableDescriptor desc = region.getTableDesc();
    if (desc != null) {
      this.desiredMaxFileSize = desc.getMaxFileSize();
    }
    if (this.desiredMaxFileSize <= 0) {
      this.desiredMaxFileSize = conf.getLong(HConstants.HREGION_MAX_FILESIZE,
        HConstants.DEFAULT_MAX_FILE_SIZE);
    }
    double jitter = conf.getDouble("hbase.hregion.max.filesize.jitter", 0.25D);
    this.jitterRate = (RANDOM.nextFloat() - 0.5D) * jitter;
    long jitterValue = (long) (this.desiredMaxFileSize * this.jitterRate);
    // make sure the long value won't overflow with jitter
    if (this.jitterRate > 0 && jitterValue > (Long.MAX_VALUE - this.desiredMaxFileSize)) {
      this.desiredMaxFileSize = Long.MAX_VALUE;
    } else {
      this.desiredMaxFileSize += jitterValue;
    }
  }

  @Override
  protected boolean shouldSplit() {
    boolean force = region.shouldForceSplit();
    boolean foundABigStore = false;

    for (Store store : region.getStores()) {
      // If any of the stores are unable to split (eg they contain reference files)
      // then don't split
      if ((!store.canSplit())) {
        return false;
      }

      // Mark if any store is big enough
      if (store.getSize() > desiredMaxFileSize) {
        foundABigStore = true;
      }
    }

    return foundABigStore || force;
  }
  1. 採用IncreasingToUpperBoundRegionSplitPolicy策略,即根據region數來決定:

0.94.0(包含)之後,默認採用此策略,即當同一table在同一regionserver上的region數量在[0,100)之間時按照如下的計算公式算,否則按照上一策略計算:

Min (R^2* “hbase.hregion.memstore.flush.size”*2, “hbase.hregion.max.filesize”)R爲同一個table中在同一個regionserver中region的個數,hbase.hregion.memstore.flush.size默認爲128M,hbase.hregion.max.filesize默認爲10G。

 @Override
  protected void configureForRegion(HRegion region) {
    super.configureForRegion(region);
    Configuration conf = getConf();
    initialSize = conf.getLong("hbase.increasing.policy.initial.size", -1);
    if (initialSize > 0) {
      return;
    }
    HTableDescriptor desc = region.getTableDesc();
    if (desc != null) {
      initialSize = 2 * desc.getMemStoreFlushSize();
    }
    if (initialSize <= 0) {
      initialSize = 2 * conf.getLong(HConstants.HREGION_MEMSTORE_FLUSH_SIZE,
                                     HTableDescriptor.DEFAULT_MEMSTORE_FLUSH_SIZE);
    }
  }

  @Override
  protected boolean shouldSplit() {
    boolean force = region.shouldForceSplit();
    boolean foundABigStore = false;
    // Get count of regions that have the same common table as this.region
    int tableRegionsCount = getCountOfCommonTableRegions();
    // Get size to check
    long sizeToCheck = getSizeToCheck(tableRegionsCount);

    for (Store store : region.getStores()) {
      // If any of the stores is unable to split (eg they contain reference files)
      // then don't split
      if (!store.canSplit()) {
        return false;
      }

      // Mark if any store is big enough
      long size = store.getSize();
      if (size > sizeToCheck) {
        LOG.debug("ShouldSplit because " + store.getColumnFamilyName() + " size=" + size
                  + ", sizeToCheck=" + sizeToCheck + ", regionsWithCommonTable="
                  + tableRegionsCount);
        foundABigStore = true;
      }
    }

    return foundABigStore | force;
  }

  /**
   * @return Count of regions on this server that share the table this.region
   * belongs to
   */
  private int getCountOfCommonTableRegions() {
    RegionServerServices rss = region.getRegionServerServices();
    // Can be null in tests
    if (rss == null) {
      return 0;
    }
    TableName tablename = region.getTableDesc().getTableName();
    int tableRegionsCount = 0;
    try {
      List<Region> hri = rss.getOnlineRegions(tablename);
      tableRegionsCount = hri == null || hri.isEmpty() ? 0 : hri.size();
    } catch (IOException e) {
      LOG.debug("Failed getOnlineRegions " + tablename, e);
    }
    return tableRegionsCount;
  }

兩種用戶自定義策略:KeyPrefixRegionSplitPolicy和DelimitedKeyPrefixRegionSplitPolicy,這兩種策略是IncreasingToUpperBoundRegionSplitPolicy策略的具體實現。

KeyPrefixRegionSplitPolicy

KeyPrefixRegionSplitPolicy策略,即根據rowkey指定長度的前綴來劃分region:

即保證相同的前綴的row保存在同一個region中。指定rowkey前綴位數劃分region,通過讀取 KeyPrefixRegionSplitPolicy.prefix_length 屬性,該屬性爲數字類型,表示前綴長度,在進行split時,按此長度對splitPoint進行截取。此種策略比較適合固定前綴的rowkey。當table中沒有設置該屬性,指定此策略效果等同與使用IncreasingToUpperBoundRegionSplitPolicy。

@Override
  protected void configureForRegion(HRegion region) {
    super.configureForRegion(region);
    prefixLength = 0;

    // read the prefix length from the table descriptor
    String prefixLengthString = region.getTableDesc().getValue(
        PREFIX_LENGTH_KEY);
    if (prefixLengthString == null) {
      //read the deprecated value
      prefixLengthString = region.getTableDesc().getValue(PREFIX_LENGTH_KEY_DEPRECATED);
      if (prefixLengthString == null) {
        LOG.error(PREFIX_LENGTH_KEY + " not specified for table "
            + region.getTableDesc().getTableName()
            + ". Using default RegionSplitPolicy");
        return;
      }
    }
    try {
      prefixLength = Integer.parseInt(prefixLengthString);
    } catch (NumberFormatException nfe) {
      /* Differentiate NumberFormatException from an invalid value range reported below. */
      LOG.error("Number format exception when parsing " + PREFIX_LENGTH_KEY + " for table "
          + region.getTableDesc().getTableName() + ":"
          + prefixLengthString + ". " + nfe);
      return;
    }
    if (prefixLength <= 0) {
      LOG.error("Invalid value for " + PREFIX_LENGTH_KEY + " for table "
          + region.getTableDesc().getTableName() + ":"
          + prefixLengthString + ". Using default RegionSplitPolicy");
    }
  }

DelimitedKeyPrefixRegionSplitPolicy策略

保證以分隔符前面的前綴爲splitPoint,保證相同RowKey前綴的數據在一個Region中。

 @Override
  protected void configureForRegion(HRegion region) {
    super.configureForRegion(region);
    // read the prefix length from the table descriptor
    String delimiterString = region.getTableDesc().getValue(DELIMITER_KEY);
    if (delimiterString == null || delimiterString.length() == 0) {
      LOG.error(DELIMITER_KEY + " not specified for table " + region.getTableDesc().getTableName() +
        ". Using default RegionSplitPolicy");
      return;
    }
    delimiter = Bytes.toBytes(delimiterString);
  }

  @Override
  protected byte[] getSplitPoint() {
    byte[] splitPoint = super.getSplitPoint();
    if (splitPoint != null && delimiter != null) {

      //find the first occurrence of delimiter in split point
      int index = com.google.common.primitives.Bytes.indexOf(splitPoint, delimiter);
      if (index < 0) {
        LOG.warn("Delimiter " + Bytes.toString(delimiter) + "  not found for split key "
            + Bytes.toString(splitPoint));
        return splitPoint;
      }

      // group split keys by a prefix
      return Arrays.copyOf(splitPoint, Math.min(index, splitPoint.length));
    } else {
      return splitPoint;
    }
  }

HBase split的流程

這裏寫圖片描述

1.RegionServer觸發在本地進行split,並準備split。第一步就是在zk的/hbase/region-in-transition/region-name的節點下創建一個znode節點,並置爲SPLITTING狀態;

2.因爲Master一直watch着zk的znode,發現parentregion需要split。

3.region server 在hdfs的parent region的目錄下創建一個名爲“.splits”的子目錄。

4.region server關閉parent region。強制flush緩存,並且在本地數據結構中標記region爲下線狀態。如果這個時候客戶端剛好請求到parent region,會拋出NotServingRegionException。這時客戶端會進行重試。

5.region server在.split目錄下分別爲兩個daughter region(A,B)創建目錄和必要的數據結構。然後創建兩個引用文件指向parent regions的文件。

6.region server在HDFS中,創建真正的region目錄,並且把引用文件移到對應的目錄下。

7.region server發送一個put的請求到.META.表中,並且在.META.表中設置parent region爲下線狀態,並添加關於daughter regions的信息。但是這個時候在.META.表中daughter region 還不是獨立的row,客戶端在此時scan .META.表時會發現parent region在split,但是還不能獲得daughter region的信息,直到她們獨立的出現.META.表中。如果此時這個往.META.表中的put操作成功,parent region會高效的split,如果此時rs在RPC請求成功前失敗,Master和下一個regionserver會重新打開這個parent region並將之前產生的split的髒數據清掉,.META.表成功更新後,HBase繼續進行下面split的流程。

8.region server並行打開兩個daughter region接受寫操作。

9.region server在.META.表中增加daughters A和 B region的相關信息,在這以後,client就能發現這兩個新的regions並且能發送請求到這兩個新的region了。client本地具體有.META.表的緩存,當他們訪問到parent region的時候,發現parent region下線了,就會重新訪問.META.表獲取最新的信息,並且更新本地緩存。

10.region server 更新znode 的狀態爲SPLIT。master就能知道狀態更新了,master的平衡機制會判斷是否需要把daughter regions 分配到其他region server中。

11.在split之後,meta和HDFS依然會有引用指向parentregion.當compact操作發生在daughter regions中,會重寫數據file,這個時候引用就會被逐漸的去掉。GC任務會定時檢測daughter regions是否還有引用指向parent files,如果沒有引用指向parent files的話,parent region 就會被刪除。

HBase split的過程分析

這裏寫圖片描述

解析:

分析Hbase的split流程,先從RegionServer中的相應線程作爲突破口,依次分析split的觸發條件和split的執行實現。

split的觸發條件:

在hbase的regionserver中維護着一個CompactSplitThread類型的變量,所有的compact/split請求都會交給該變量處理,以下是CompactSplitThread中定義的幾個與split相關的變量。

private final ThreadPoolExecutor longCompactions;     //long合併線程池  
private final ThreadPoolExecutor shortCompactions;    //short合併線程池  
private final ThreadPoolExecutor splits;              //split線程池  
private final ThreadPoolExecutor mergePool;           //merge線程池  
private int regionSplitLimit;  

需要注意的是最後一個整型變量regionSplitLimit,它定義了一個regionserver所持有的最大region總數,如果region的數量超過了它的限制,則split不會再發生。該變量的值由hbase.regionserver.regionSplitLimit配置,默認是1000;splits是該regionserver用於參與split的線程池,線程池中的線程數量由“hbase.regionserver.thread.split”配置,默認是1。

region發生split時,調用方會調用CompactSplitThread中的requestSplit方法,該方案將split請求包裝成一個Runnable的SplitRequest對象放入前文所說的線程池split中去執行。

    this.splits.execute(new SplitRequest(r, midKey, this.server));  

分析requestSplit方法的調用時機就可以理出region發生split行爲的時間:

第一種方式是region發生compact之後,此時會形成比較大的文件,split會在這個時候被調用;

第二種方式是region發生flush的時候,這一部分的代碼如下:

    lock.readLock().lock();                 
    try {  
      notifyFlushRequest(region, emergencyFlush);  
      FlushResult flushResult = region.flush(forceFlushAllStores);  
      boolean shouldCompact = flushResult.isCompactionNeeded();  
      // We just want to check the size  
      boolean shouldSplit = ((HRegion)region).checkSplit() != null;  
      if (shouldSplit) {  
        this.server.compactSplitThread.requestSplit(region);          
      } else if (shouldCompact) {  
        server.compactSplitThread.requestSystemCompaction(  
            region, Thread.currentThread().getName());               
      }  
      if (flushResult.isFlushSucceeded()) {  
        long endTime = EnvironmentEdgeManager.currentTime();  
        server.metricsRegionServer.updateFlushTime(endTime - startTime);      //將本次flush的時間記錄下來  
      }  
    } catch (DroppedSnapshotException ex) {  
      ..........  
    } catch (IOException ex) {  
      ..........  
    } finally {  
      lock.readLock().unlock();  
      wakeUpIfBlocking();           //喚醒所有等待的線程  
    }  

需要注意,region上的memstore發生flush的時候會獲取readLock。readLock是讀鎖,讀寫鎖是一種多線程同步的方案,所謂讀鎖其實就是共享鎖,所謂寫鎖就是排他鎖,當一個線程獲取讀鎖時,所有其他以讀模式訪問的線程都可以獲得訪問權,而已寫模式對它進行加鎖的線程都會被阻塞。而當一個線程獲取寫鎖的時候,所有其他試圖獲取鎖的線程都會被阻塞。這裏獲取了readLock,所以flush時對region的更新都會被阻塞。

第三種調用是在RSRpcServices的splitRegion函數中,表示的是用戶對region發出的split請求。

split的執行實現:

前面說過spilt請求會包裝成SplitRequest類型的對象交由splits線程處理。所以具體的split實現是在SplitRequest的run方法中,下面我們篩選出run方法的重點流程以分析split的實現:

    public void run() {  
        .......  
        SplitTransactionImpl st = new SplitTransactionImpl(parent, midKey);  
        try {  
           tableLock = server.getTableLockManager().readLock(.........);  
           try {  
            tableLock.acquire();  
           } catch (IOException ex) {  
              .......  
           }  
           if (!st.prepare()) return;  
           try {  
              st.execute(this.server, this.server);  
           } catch (Exception e) {  
              .......  
           }  
        } catch (Exception e) {  
           server.checkFileSystem();  
        } finally {  
           //處理post coprocessor  
           releaseTableLock();  
           //更新metrics  
        }  
    }  

除了一些異常處理和回滾,run方法的主要邏輯已經梳理出來了,可以看出split前首先獲取了table的共享鎖,這樣做的目的是防止其它併發的table修改行爲修改當前table的狀態或者schema等。然後初始化一個SplitTransactionImpl類型的變量,依次調用它的prepare和execute方法完成region切割。
perpare用以完成split的前期準備,包括構造兩個子HRegionInfo,分別是hri_a和hri_b,其中hri_a的startKey胃parent的startKey,endKey爲midKey;hri_b的startKey爲midKey,endKey爲parent的endKey。

接下來在execute方法中,主要代碼包括以下三步:

    PairOfSameType<Region> regions = createDaughters(server, services);  
    if (this.parent.getCoprocessorHost() != null) {  
       this.parent.getCoprocessorHost().preSplitAfterPONR();  
    }  
    regions = stepsAfterPONR(server, services, regions);  
    transition(SplitTransactionPhase.COMPLETED);  

從createDaughters方法進去,我們一點點分析split的實現,其中比較關鍵的步驟是createDaughters,我們把該方法的主要邏輯列出在下面:

    PairOfSameType<Region> daughterRegions = stepsBeforePONR(server, services, testing);  
    List<Mutation> metaEntries = new ArrayList<>;  
    if (!testing && useZKForAssignment) {  
        if (metaEntries == null || metaEntries.isEmpty()) {  
           MetaTableAccessor.splitRegion(server.getConnection(),  
             parent.getRegionInfo(), daughterRegions.getFirst().getRegionInfo(),  
             daughterRegions.getSecond().getRegionInfo(), server.getServerName(),  
             parent.getTableDesc().getRegionReplication());  
        } else {  
           offlineParentInMetaAndputMetaEntries(server.getConnection(),  
             parent.getRegionInfo(), daughterRegions.getFirst().getRegionInfo(), daughterRegions  
                  .getSecond().getRegionInfo(), server.getServerName(), metaEntries,  
                  parent.getTableDesc().getRegionReplication());  
        }  
    } else if (services != null && !useZKForAssignment) {  
        ..........  
    }  

createDaughters返回的regions實際上是由stepsBeforePONR返回的,下面我們列出stepsBeforePONR中的主要邏輯:

    this.parent.getRegionFileSystem().createSplitsDir();  
    try {  
       hstoreFilesToSplit = this.parent.close(false);  
    } catch (Exception e) {  
       .........  
    }  
    Pari<Integer,Integer> expectedReferences = splitStoreFiles(hstoreFilesToSplit);  
    Region a = this.parent.createDaughterRegionFromSplits(this.hri_a);  
    Region b = this.parent.createDaughterRegionFromSplits(this.hri_b);  
    return new PairOfSametype<Region>(a,b);  

上面我們列出了幾個主要步驟,穿插在這些主要步驟之間的是將當前split的狀態更新到zookeeper的節點上,hbase的region在發生split的同時,會在zookeeper的region-in-transition目錄下創建一個節點,供master同步監聽新region的上線和老region的下線這些信息,此外如果split中間發生錯誤,也需要zookeeper上的狀態信息同步以協調region之間的變化。

首先createSplitsDir在parent region的目錄下創建了一個.split目錄,接着在.split目錄下創建daughter region A和region B兩個子目錄,然後調用close將parent關掉,調用是傳入的參數false表示在關掉parent region前先強制執行一次flush,將region memstore中的數據刷寫到磁盤。關閉region後region server將region標記爲offline狀態。

region的關閉在實現上是提交了該region擁有的store到一個線程池中,然後每個store的close方法進行關閉的,store的關閉結果異步獲取。hbase在實現這一步中使用了CompletionService,返回結果通過CompletionService的take方法獲取,使用這種方式的優勢就是當多線程啓動了多個Callable之後,每個callable都會返回一個future,CompletionService自己維護了一個線程安全的list,保證先完成的future一定先返回。

現在回來繼續講解我們的split流程,進入splitStoreFiles方法,該方法實際上是爲parent中的每個storeFile創建了兩個索引文件,核心代碼在下面的片段中:

    ThreadFactoryBuilder builder = new ThreadFactoryBuilder();  
    builder.setNameFormat("StoreFileSplitter-%1$d");  
    ThreadFactory factory = builder.build();  
    ThreadPoolExecutor threadPool =  
       (ThreadPoolExecutor) Executors.newFixedThreadPool(maxThreads, factory);  
    List<Future<Pair<Path,Path>>> futures = new ArrayList<Future<Pair<Path,Path>>> (nbFiles);  

    // Split each store file.  
    for (Map.Entry<byte[], List<StoreFile>> entry: hstoreFilesToSplit.entrySet()) {  
       for (StoreFile sf: entry.getValue()) {  
          StoreFileSplitter sfs = new StoreFileSplitter(entry.getKey(), sf);  
          futures.add(threadPool.submit(sfs));         //storefile被提交執行split了  
       }  
    }  

StoreFileSpliitter最終調用HRegionFileSystem中的下面一句代碼完成索引文件的創建:

    Reference r =  
          top ? Reference.createTopReference(splitRow): Reference.createBottomReference(splitRow);  

回到stepsBeforePONR方法,最後兩步根據子region的元信息創建了HRegion A和B,實際上就是創建了A/B的實際存儲目錄。完成這些後stepsBeforePONR方法返回,然後調用如下的代碼修改meta表中parent region和分裂出的region A和region B的信息。

MetaTableAccessor.splitRegion  

splitRegion是一個原子方法,它將父region的信息置爲offline,並寫入子region的信息,但是此時的子region A和B還不能對外提供服務。需要等待regionServer打開該子region纔可以,帶着這個疑問,我們進入split流程的最後一個方法——stepsAfterPORN,在該方法中調用openDaughters將子RegionA和RegionB打開以接受寫請求,regionsrver打開A和B之後會補充上述兩個子region在.META.表中的信息,此時客戶端才能夠發現兩個子region並向該兩個region發送請求。

負責open region的線程是繼承了HasThread接口的DaughterOpener類,主要包括了下面兩個步驟:

1> 調用openHRegion函數的initilize,主要步驟如下:

a、向hdfs上寫入.regionInfo文件以便meta掛掉時恢復;

b、初始化其下的HStore,主要是調LoadStoreFiles函數;

2> 將子Region添加到rs的online region列表上,並添加到meta表中;
最後更新zk節點上/hbase/region-in-transition/region-name節點的狀態爲COMPLETED,指示split結束。

Split過程結束後,HDFS和META中還會保留着指向parent region索引文件的信息,這些索引文件會在子region執行major compact重寫的時候被刪除掉。master的Garbage Collection任務會週期性地檢查子region中是否還包含指向parents Region的索引文件,如果不再包含,此時master會將parent Region刪除掉。

發佈了87 篇原創文章 · 獲贊 50 · 訪問量 23萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章