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}