001/**
002 *
003 * Licensed to the Apache Software Foundation (ASF) under one
004 * or more contributor license agreements.  See the NOTICE file
005 * distributed with this work for additional information
006 * regarding copyright ownership.  The ASF licenses this file
007 * to you under the Apache License, Version 2.0 (the
008 * "License"); you may not use this file except in compliance
009 * with the License.  You may obtain a copy of the License at
010 *
011 *     http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 */
019package org.apache.hadoop.hbase.master.normalizer;
020
021import java.io.IOException;
022import java.util.ArrayList;
023import java.util.Collections;
024import java.util.Comparator;
025import java.util.List;
026import org.apache.hadoop.hbase.HBaseConfiguration;
027import org.apache.hadoop.hbase.HBaseIOException;
028import org.apache.hadoop.hbase.RegionMetrics;
029import org.apache.hadoop.hbase.ServerName;
030import org.apache.hadoop.hbase.Size;
031import org.apache.hadoop.hbase.TableName;
032import org.apache.hadoop.hbase.client.MasterSwitchType;
033import org.apache.hadoop.hbase.client.RegionInfo;
034import org.apache.hadoop.hbase.client.TableDescriptor;
035import org.apache.hadoop.hbase.master.MasterRpcServices;
036import org.apache.hadoop.hbase.master.MasterServices;
037import org.apache.hadoop.hbase.master.normalizer.NormalizationPlan.PlanType;
038import org.apache.yetus.audience.InterfaceAudience;
039import org.slf4j.Logger;
040import org.slf4j.LoggerFactory;
041
042import org.apache.hadoop.hbase.shaded.protobuf.RequestConverter;
043
044/**
045 * Simple implementation of region normalizer.
046 *
047 * Logic in use:
048 *
049 *  <ol>
050 *  <li> Get all regions of a given table
051 *  <li> Get avg size S of each region (by total size of store files reported in RegionMetrics)
052 *  <li> Seek every single region one by one. If a region R0 is bigger than S * 2, it is
053 *  kindly requested to split. Thereon evaluate the next region R1
054 *  <li> Otherwise, if R0 + R1 is smaller than S, R0 and R1 are kindly requested to merge.
055 *  Thereon evaluate the next region R2
056 *  <li> Otherwise, R1 is evaluated
057 * </ol>
058 * <p>
059 * Region sizes are coarse and approximate on the order of megabytes. Additionally,
060 * "empty" regions (less than 1MB, with the previous note) are not merged away. This
061 * is by design to prevent normalization from undoing the pre-splitting of a table.
062 */
063@InterfaceAudience.Private
064public class SimpleRegionNormalizer implements RegionNormalizer {
065
066  private static final Logger LOG = LoggerFactory.getLogger(SimpleRegionNormalizer.class);
067  private int minRegionCount;
068  private MasterServices masterServices;
069  private MasterRpcServices masterRpcServices;
070  private static long[] skippedCount = new long[NormalizationPlan.PlanType.values().length];
071
072  public SimpleRegionNormalizer() {
073    minRegionCount = HBaseConfiguration.create().getInt("hbase.normalizer.min.region.count", 3);
074  }
075  /**
076   * Set the master service.
077   * @param masterServices inject instance of MasterServices
078   */
079  @Override
080  public void setMasterServices(MasterServices masterServices) {
081    this.masterServices = masterServices;
082  }
083
084  @Override
085  public void setMasterRpcServices(MasterRpcServices masterRpcServices) {
086    this.masterRpcServices = masterRpcServices;
087  }
088
089  @Override
090  public void planSkipped(RegionInfo hri, PlanType type) {
091    skippedCount[type.ordinal()]++;
092  }
093
094  @Override
095  public long getSkippedCount(NormalizationPlan.PlanType type) {
096    return skippedCount[type.ordinal()];
097  }
098
099  /**
100   * Comparator class that gives higher priority to region Split plan.
101   */
102  static class PlanComparator implements Comparator<NormalizationPlan> {
103    @Override
104    public int compare(NormalizationPlan plan1, NormalizationPlan plan2) {
105      boolean plan1IsSplit = plan1 instanceof SplitNormalizationPlan;
106      boolean plan2IsSplit = plan2 instanceof SplitNormalizationPlan;
107      if (plan1IsSplit && plan2IsSplit) {
108        return 0;
109      } else if (plan1IsSplit) {
110        return -1;
111      } else if (plan2IsSplit) {
112        return 1;
113      } else {
114        return 0;
115      }
116    }
117  }
118
119  private Comparator<NormalizationPlan> planComparator = new PlanComparator();
120
121  /**
122   * Computes next most "urgent" normalization action on the table.
123   * Action may be either a split, or a merge, or no action.
124   *
125   * @param table table to normalize
126   * @return normalization plan to execute
127   */
128  @Override
129  public List<NormalizationPlan> computePlanForTable(TableName table) throws HBaseIOException {
130    if (table == null || table.isSystemTable()) {
131      LOG.debug("Normalization of system table " + table + " isn't allowed");
132      return null;
133    }
134
135    List<NormalizationPlan> plans = new ArrayList<>();
136    List<RegionInfo> tableRegions = masterServices.getAssignmentManager().getRegionStates().
137      getRegionsOfTable(table);
138
139    //TODO: should we make min number of regions a config param?
140    if (tableRegions == null || tableRegions.size() < minRegionCount) {
141      int nrRegions = tableRegions == null ? 0 : tableRegions.size();
142      LOG.debug("Table " + table + " has " + nrRegions + " regions, required min number"
143        + " of regions for normalizer to run is " + minRegionCount + ", not running normalizer");
144      return null;
145    }
146
147    LOG.debug("Computing normalization plan for table: " + table +
148      ", number of regions: " + tableRegions.size());
149
150    long totalSizeMb = 0;
151    int acutalRegionCnt = 0;
152
153    for (int i = 0; i < tableRegions.size(); i++) {
154      RegionInfo hri = tableRegions.get(i);
155      long regionSize = getRegionSize(hri);
156      if (regionSize > 0) {
157        acutalRegionCnt++;
158        totalSizeMb += regionSize;
159      }
160    }
161    int targetRegionCount = -1;
162    long targetRegionSize = -1;
163    try {
164      TableDescriptor tableDescriptor = masterServices.getTableDescriptors().get(table);
165      if(tableDescriptor != null) {
166        targetRegionCount =
167            tableDescriptor.getNormalizerTargetRegionCount();
168        targetRegionSize =
169            tableDescriptor.getNormalizerTargetRegionSize();
170        LOG.debug("Table {}:  target region count is {}, target region size is {}", table,
171            targetRegionCount, targetRegionSize);
172      }
173    } catch (IOException e) {
174      LOG.warn(
175        "cannot get the target number and target size of table {}, they will be default value -1.",
176        table);
177    }
178
179    double avgRegionSize;
180    if (targetRegionSize > 0) {
181      avgRegionSize = targetRegionSize;
182    } else if (targetRegionCount > 0) {
183      avgRegionSize = totalSizeMb / (double) targetRegionCount;
184    } else {
185      avgRegionSize = acutalRegionCnt == 0 ? 0 : totalSizeMb / (double) acutalRegionCnt;
186    }
187
188    LOG.debug("Table " + table + ", total aggregated regions size: " + totalSizeMb);
189    LOG.debug("Table " + table + ", average region size: " + avgRegionSize);
190
191    int candidateIdx = 0;
192    boolean splitEnabled = true, mergeEnabled = true;
193    try {
194      splitEnabled = masterRpcServices.isSplitOrMergeEnabled(null,
195        RequestConverter.buildIsSplitOrMergeEnabledRequest(MasterSwitchType.SPLIT)).getEnabled();
196    } catch (org.apache.hbase.thirdparty.com.google.protobuf.ServiceException e) {
197      LOG.debug("Unable to determine whether split is enabled", e);
198    }
199    try {
200      mergeEnabled = masterRpcServices.isSplitOrMergeEnabled(null,
201        RequestConverter.buildIsSplitOrMergeEnabledRequest(MasterSwitchType.MERGE)).getEnabled();
202    } catch (org.apache.hbase.thirdparty.com.google.protobuf.ServiceException e) {
203      LOG.debug("Unable to determine whether split is enabled", e);
204    }
205    while (candidateIdx < tableRegions.size()) {
206      RegionInfo hri = tableRegions.get(candidateIdx);
207      long regionSize = getRegionSize(hri);
208      // if the region is > 2 times larger than average, we split it, split
209      // is more high priority normalization action than merge.
210      if (regionSize > 2 * avgRegionSize) {
211        if (splitEnabled) {
212          LOG.info("Table " + table + ", large region " + hri.getRegionNameAsString() + " has size "
213              + regionSize + ", more than twice avg size, splitting");
214          plans.add(new SplitNormalizationPlan(hri, null));
215        }
216      } else {
217        if (candidateIdx == tableRegions.size()-1) {
218          break;
219        }
220        if (mergeEnabled) {
221          RegionInfo hri2 = tableRegions.get(candidateIdx+1);
222          long regionSize2 = getRegionSize(hri2);
223          if (regionSize >= 0 && regionSize2 >= 0 && regionSize + regionSize2 < avgRegionSize) {
224            LOG.info("Table " + table + ", small region size: " + regionSize
225              + " plus its neighbor size: " + regionSize2
226              + ", less than the avg size " + avgRegionSize + ", merging them");
227            plans.add(new MergeNormalizationPlan(hri, hri2));
228            candidateIdx++;
229          }
230        }
231      }
232      candidateIdx++;
233    }
234    if (plans.isEmpty()) {
235      LOG.debug("No normalization needed, regions look good for table: " + table);
236      return null;
237    }
238    Collections.sort(plans, planComparator);
239    return plans;
240  }
241
242  private long getRegionSize(RegionInfo hri) {
243    ServerName sn = masterServices.getAssignmentManager().getRegionStates().
244      getRegionServerOfRegion(hri);
245    RegionMetrics regionLoad = masterServices.getServerManager().getLoad(sn).
246      getRegionMetrics().get(hri.getRegionName());
247    if (regionLoad == null) {
248      LOG.debug(hri.getRegionNameAsString() + " was not found in RegionsLoad");
249      return -1;
250    }
251    return (long) regionLoad.getStoreFileSize().get(Size.Unit.MEGABYTE);
252  }
253}