001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.apache.hadoop.hbase.master.balancer;
019
020import static org.apache.hadoop.hbase.master.balancer.CandidateGeneratorTestUtil.createMockBalancerClusterState;
021import static org.junit.Assert.assertEquals;
022import static org.junit.Assert.assertTrue;
023import static org.mockito.Mockito.when;
024
025import java.util.ArrayDeque;
026import java.util.Arrays;
027import java.util.Deque;
028import java.util.HashMap;
029import java.util.List;
030import java.util.Map;
031import java.util.Random;
032import org.apache.hadoop.conf.Configuration;
033import org.apache.hadoop.hbase.HBaseClassTestRule;
034import org.apache.hadoop.hbase.RegionMetrics;
035import org.apache.hadoop.hbase.ServerName;
036import org.apache.hadoop.hbase.Size;
037import org.apache.hadoop.hbase.TableName;
038import org.apache.hadoop.hbase.client.RegionInfo;
039import org.apache.hadoop.hbase.client.RegionInfoBuilder;
040import org.apache.hadoop.hbase.testclassification.MasterTests;
041import org.apache.hadoop.hbase.testclassification.SmallTests;
042import org.junit.ClassRule;
043import org.junit.Test;
044import org.junit.experimental.categories.Category;
045import org.mockito.Mockito;
046
047@Category({ MasterTests.class, SmallTests.class })
048public class TestStoreFileTableSkewCostFunction {
049
050  @ClassRule
051  public static final HBaseClassTestRule CLASS_RULE =
052    HBaseClassTestRule.forClass(TestStoreFileTableSkewCostFunction.class);
053
054  private static final TableName DEFAULT_TABLE = TableName.valueOf("testTable");
055  private static final Map<Long, Integer> REGION_TO_STORE_FILE_SIZE_MB = new HashMap<>();
056
057  /**
058   * Tests that a uniform store file distribution (single table) across servers results in zero
059   * cost.
060   */
061  @Test
062  public void testUniformDistribution() {
063    ServerName server1 = ServerName.valueOf("server1.example.org", 1234, 1L);
064    ServerName server2 = ServerName.valueOf("server2.example.org", 1234, 1L);
065    ServerName server3 = ServerName.valueOf("server3.example.org", 1234, 1L);
066    ServerName server4 = ServerName.valueOf("server4.example.org", 1234, 1L);
067
068    Map<ServerName, List<RegionInfo>> serverToRegions = new HashMap<>();
069    serverToRegions.put(server1, Arrays.asList(createMockRegionInfo(10), createMockRegionInfo(10)));
070    serverToRegions.put(server2, Arrays.asList(createMockRegionInfo(10), createMockRegionInfo(10)));
071    serverToRegions.put(server3, Arrays.asList(createMockRegionInfo(10), createMockRegionInfo(10)));
072    serverToRegions.put(server4, Arrays.asList(createMockRegionInfo(10), createMockRegionInfo(10)));
073
074    BalancerClusterState clusterState = createMockBalancerClusterState(serverToRegions);
075    DummyBalancerClusterState state = new DummyBalancerClusterState(clusterState);
076
077    StoreFileTableSkewCostFunction costFunction =
078      new StoreFileTableSkewCostFunction(new Configuration());
079    costFunction.prepare(state);
080    double cost = costFunction.cost();
081
082    // Expect zero cost since all regions (from the same table) are balanced.
083    assertEquals("Uniform distribution should yield zero cost", 0.0, cost, 1e-6);
084  }
085
086  /**
087   * Tests that a skewed store file distribution (single table) results in a positive cost.
088   */
089  @Test
090  public void testSkewedDistribution() {
091    ServerName server1 = ServerName.valueOf("server1.example.org", 1234, 1L);
092    ServerName server2 = ServerName.valueOf("server2.example.org", 1234, 1L);
093    ServerName server3 = ServerName.valueOf("server3.example.org", 1234, 1L);
094    ServerName server4 = ServerName.valueOf("server4.example.org", 1234, 1L);
095
096    Map<ServerName, List<RegionInfo>> serverToRegions = new HashMap<>();
097    // Three servers get regions with 10 store files each,
098    // while one server gets regions with 30 store files each.
099    serverToRegions.put(server1, Arrays.asList(createMockRegionInfo(10), createMockRegionInfo(10)));
100    serverToRegions.put(server2, Arrays.asList(createMockRegionInfo(10), createMockRegionInfo(10)));
101    serverToRegions.put(server3, Arrays.asList(createMockRegionInfo(10), createMockRegionInfo(10)));
102    serverToRegions.put(server4, Arrays.asList(createMockRegionInfo(30), createMockRegionInfo(30)));
103
104    BalancerClusterState clusterState = createMockBalancerClusterState(serverToRegions);
105    DummyBalancerClusterState state = new DummyBalancerClusterState(clusterState);
106
107    StoreFileTableSkewCostFunction costFunction =
108      new StoreFileTableSkewCostFunction(new Configuration());
109    costFunction.prepare(state);
110    double cost = costFunction.cost();
111
112    // Expect a positive cost because the distribution is skewed.
113    assertTrue("Skewed distribution should yield a positive cost", cost > 0.0);
114  }
115
116  /**
117   * Tests that an empty cluster (no servers/regions) is handled gracefully.
118   */
119  @Test
120  public void testEmptyDistribution() {
121    Map<ServerName, List<RegionInfo>> serverToRegions = new HashMap<>();
122
123    BalancerClusterState clusterState = createMockBalancerClusterState(serverToRegions);
124    DummyBalancerClusterState state = new DummyBalancerClusterState(clusterState);
125
126    StoreFileTableSkewCostFunction costFunction =
127      new StoreFileTableSkewCostFunction(new Configuration());
128    costFunction.prepare(state);
129    double cost = costFunction.cost();
130
131    // Expect zero cost when there is no load.
132    assertEquals("Empty distribution should yield zero cost", 0.0, cost, 1e-6);
133  }
134
135  /**
136   * Tests that having multiple tables results in a positive cost when each table's regions are not
137   * balanced across servers – even if the overall load per server is balanced.
138   */
139  @Test
140  public void testMultipleTablesDistribution() {
141    // Two servers.
142    ServerName server1 = ServerName.valueOf("server1.example.org", 1234, 1L);
143    ServerName server2 = ServerName.valueOf("server2.example.org", 1234, 1L);
144
145    // Define two tables.
146    TableName table1 = TableName.valueOf("testTable1");
147    TableName table2 = TableName.valueOf("testTable2");
148
149    // For table1, all regions are on server1.
150    // For table2, all regions are on server2.
151    Map<ServerName, List<RegionInfo>> serverToRegions = new HashMap<>();
152    serverToRegions.put(server1,
153      Arrays.asList(createMockRegionInfo(table1, 10), createMockRegionInfo(table1, 10)));
154    serverToRegions.put(server2,
155      Arrays.asList(createMockRegionInfo(table2, 10), createMockRegionInfo(table2, 10)));
156
157    // Although each server gets 20 MB overall, table1 and table2 are not balanced across servers.
158    BalancerClusterState clusterState = createMockBalancerClusterState(serverToRegions);
159    DummyBalancerClusterState state = new DummyBalancerClusterState(clusterState);
160
161    StoreFileTableSkewCostFunction costFunction =
162      new StoreFileTableSkewCostFunction(new Configuration());
163    costFunction.prepare(state);
164    double cost = costFunction.cost();
165
166    // Expect a positive cost because the skew is computed per table.
167    assertTrue("Multiple table distribution should yield a positive cost", cost > 0.0);
168  }
169
170  /**
171   * Helper method to create a RegionInfo for the default table with the given store file size.
172   */
173  private static RegionInfo createMockRegionInfo(int storeFileSizeMb) {
174    return createMockRegionInfo(DEFAULT_TABLE, storeFileSizeMb);
175  }
176
177  /**
178   * Helper method to create a RegionInfo for a specified table with the given store file size.
179   */
180  private static RegionInfo createMockRegionInfo(TableName table, int storeFileSizeMb) {
181    long regionId = new Random().nextLong();
182    REGION_TO_STORE_FILE_SIZE_MB.put(regionId, storeFileSizeMb);
183    return RegionInfoBuilder.newBuilder(table).setStartKey(generateRandomByteArray(4))
184      .setEndKey(generateRandomByteArray(4)).setReplicaId(0).setRegionId(regionId).build();
185  }
186
187  private static byte[] generateRandomByteArray(int n) {
188    byte[] byteArray = new byte[n];
189    new Random().nextBytes(byteArray);
190    return byteArray;
191  }
192
193  /**
194   * A simplified BalancerClusterState which ensures we provide the intended test RegionMetrics data
195   * when balancing this cluster
196   */
197  private static class DummyBalancerClusterState extends BalancerClusterState {
198    private final RegionInfo[] testRegions;
199
200    DummyBalancerClusterState(BalancerClusterState bcs) {
201      super(bcs.clusterState, null, null, null, null);
202      this.testRegions = bcs.regions;
203    }
204
205    @Override
206    Deque<BalancerRegionLoad>[] getRegionLoads() {
207      @SuppressWarnings("unchecked")
208      Deque<BalancerRegionLoad>[] loads = new Deque[testRegions.length];
209      for (int i = 0; i < testRegions.length; i++) {
210        Deque<BalancerRegionLoad> dq = new ArrayDeque<>();
211        dq.add(new BalancerRegionLoad(createMockRegionMetrics(testRegions[i])) {
212        });
213        loads[i] = dq;
214      }
215      return loads;
216    }
217  }
218
219  /**
220   * Creates a mocked RegionMetrics for the given region.
221   */
222  private static RegionMetrics createMockRegionMetrics(RegionInfo regionInfo) {
223    RegionMetrics regionMetrics = Mockito.mock(RegionMetrics.class);
224
225    // Important
226    int storeFileSizeMb = REGION_TO_STORE_FILE_SIZE_MB.get(regionInfo.getRegionId());
227    when(regionMetrics.getRegionSizeMB()).thenReturn(new Size(storeFileSizeMb, Size.Unit.MEGABYTE));
228    when(regionMetrics.getStoreFileSize())
229      .thenReturn(new Size(storeFileSizeMb, Size.Unit.MEGABYTE));
230
231    // Not important
232    when(regionMetrics.getReadRequestCount()).thenReturn(0L);
233    when(regionMetrics.getCpRequestCount()).thenReturn(0L);
234    when(regionMetrics.getWriteRequestCount()).thenReturn(0L);
235    when(regionMetrics.getMemStoreSize()).thenReturn(new Size(0, Size.Unit.MEGABYTE));
236    when(regionMetrics.getCurrentRegionCachedRatio()).thenReturn(0.0f);
237    return regionMetrics;
238  }
239}