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.junit.jupiter.api.Assertions.assertTrue;
021
022import java.io.IOException;
023import java.util.HashSet;
024import java.util.Hashtable;
025import java.util.Iterator;
026import java.util.List;
027import java.util.Map;
028import java.util.Random;
029import java.util.Set;
030import java.util.concurrent.ThreadLocalRandom;
031import javax.management.MBeanAttributeInfo;
032import javax.management.MBeanInfo;
033import javax.management.MBeanServerConnection;
034import javax.management.ObjectInstance;
035import javax.management.ObjectName;
036import javax.management.remote.JMXConnector;
037import javax.management.remote.JMXConnectorFactory;
038import org.apache.hadoop.conf.Configuration;
039import org.apache.hadoop.hbase.HBaseTestingUtil;
040import org.apache.hadoop.hbase.HConstants;
041import org.apache.hadoop.hbase.JMXListener;
042import org.apache.hadoop.hbase.ServerName;
043import org.apache.hadoop.hbase.TableName;
044import org.apache.hadoop.hbase.client.RegionInfo;
045import org.apache.hadoop.hbase.coprocessor.CoprocessorHost;
046import org.apache.hadoop.hbase.testclassification.MediumTests;
047import org.apache.hadoop.hbase.testclassification.MiscTests;
048import org.apache.hadoop.hbase.util.Threads;
049import org.apache.hadoop.net.DNSToSwitchMapping;
050import org.junit.jupiter.api.AfterAll;
051import org.junit.jupiter.api.BeforeAll;
052import org.junit.jupiter.api.Disabled;
053import org.junit.jupiter.api.MethodOrderer;
054import org.junit.jupiter.api.Tag;
055import org.junit.jupiter.api.Test;
056import org.junit.jupiter.api.TestMethodOrder;
057import org.slf4j.Logger;
058import org.slf4j.LoggerFactory;
059
060@Tag(MiscTests.TAG)
061@Tag(MediumTests.TAG)
062@TestMethodOrder(MethodOrderer.MethodName.class)
063@Disabled
064public class TestStochasticBalancerJmxMetrics extends BalancerTestBase {
065
066  private static final Logger LOG = LoggerFactory.getLogger(TestStochasticBalancerJmxMetrics.class);
067  private static HBaseTestingUtil UTIL = new HBaseTestingUtil();
068  private static int connectorPort = 61120;
069  private static StochasticLoadBalancer loadBalancer;
070  /**
071   * a simple cluster for testing JMX.
072   */
073  private static int[] mockCluster_ensemble = new int[] { 0, 1, 2, 3 };
074  private static int[] mockCluster_pertable_1 = new int[] { 0, 1, 2 };
075  private static int[] mockCluster_pertable_2 = new int[] { 3, 1, 1 };
076  private static int[] mockCluster_pertable_namespace = new int[] { 1, 3, 1 };
077
078  private static final String TABLE_NAME_1 = "Table1";
079  private static final String TABLE_NAME_2 = "Table2";
080  private static final String TABLE_NAME_NAMESPACE = "hbase:namespace";
081
082  private static Configuration conf = null;
083
084  /**
085   * Setup the environment for the test.
086   */
087  @BeforeAll
088  public static void setupBeforeClass() throws Exception {
089
090    conf = UTIL.getConfiguration();
091
092    conf.setClass("hbase.util.ip.to.rack.determiner", MockMapping.class, DNSToSwitchMapping.class);
093    conf.setFloat("hbase.regions.slop", 0.0f);
094    conf.set(CoprocessorHost.REGIONSERVER_COPROCESSOR_CONF_KEY, JMXListener.class.getName());
095    Random rand = ThreadLocalRandom.current();
096    for (int i = 0; i < 10; i++) {
097      do {
098        int sign = i % 2 == 0 ? 1 : -1;
099        connectorPort += sign * rand.nextInt(100);
100      } while (!HBaseTestingUtil.available(connectorPort));
101      try {
102        conf.setInt("regionserver.rmi.registry.port", connectorPort);
103
104        UTIL.startMiniCluster();
105        break;
106      } catch (Exception e) {
107        LOG.debug("Encountered exception when starting cluster. Trying port " + connectorPort, e);
108        try {
109          // this is to avoid "IllegalStateException: A mini-cluster is already running"
110          UTIL.shutdownMiniCluster();
111        } catch (Exception ex) {
112          LOG.debug("Encountered exception shutting down cluster", ex);
113        }
114      }
115    }
116  }
117
118  @AfterAll
119  public static void tearDownAfterClass() throws Exception {
120    UTIL.shutdownMiniCluster();
121  }
122
123  /**
124   * In Ensemble mode, there should be only one ensemble table
125   */
126  @Test
127  public void testJmxMetrics_EnsembleMode() throws Exception {
128    loadBalancer = new StochasticLoadBalancer();
129
130    conf.setBoolean(HConstants.HBASE_MASTER_LOADBALANCE_BYTABLE, false);
131    loadBalancer.setClusterInfoProvider(new DummyClusterInfoProvider(conf));
132    loadBalancer.initialize();
133
134    TableName tableName = HConstants.ENSEMBLE_TABLE_NAME;
135    Map<ServerName, List<RegionInfo>> clusterState = mockClusterServers(mockCluster_ensemble);
136    loadBalancer.balanceTable(tableName, clusterState);
137
138    String[] tableNames = new String[] { tableName.getNameAsString() };
139    String[] functionNames = loadBalancer.getCostFunctionNames();
140    Set<String> jmxMetrics = readJmxMetricsWithRetry();
141    Set<String> expectedMetrics = getExpectedJmxMetrics(tableNames, functionNames);
142
143    // printMetrics(jmxMetrics, "existing metrics in ensemble mode");
144    // printMetrics(expectedMetrics, "expected metrics in ensemble mode");
145
146    // assert that every expected is in the JMX
147    for (String expected : expectedMetrics) {
148      assertTrue(jmxMetrics.contains(expected),
149        "Metric " + expected + " can not be found in JMX in ensemble mode.");
150    }
151  }
152
153  /**
154   * In per-table mode, each table has a set of metrics
155   */
156  @Test
157  public void testJmxMetrics_PerTableMode() throws Exception {
158    loadBalancer = new StochasticLoadBalancer();
159
160    conf.setBoolean(HConstants.HBASE_MASTER_LOADBALANCE_BYTABLE, true);
161    loadBalancer.setClusterInfoProvider(new DummyClusterInfoProvider(conf));
162    loadBalancer.initialize();
163
164    // NOTE the size is normally set in setClusterMetrics, for test purpose, we set it manually
165    // Tables: hbase:namespace, table1, table2
166    // Functions: costFunctions, overall
167    String[] functionNames = loadBalancer.getCostFunctionNames();
168    loadBalancer.updateMetricsSize(3 * (functionNames.length + 1));
169
170    // table 1
171    TableName tableName = TableName.valueOf(TABLE_NAME_1);
172    Map<ServerName, List<RegionInfo>> clusterState = mockClusterServers(mockCluster_pertable_1);
173    loadBalancer.balanceTable(tableName, clusterState);
174
175    // table 2
176    tableName = TableName.valueOf(TABLE_NAME_2);
177    clusterState = mockClusterServers(mockCluster_pertable_2);
178    loadBalancer.balanceTable(tableName, clusterState);
179
180    // table hbase:namespace
181    tableName = TableName.valueOf(TABLE_NAME_NAMESPACE);
182    clusterState = mockClusterServers(mockCluster_pertable_namespace);
183    loadBalancer.balanceTable(tableName, clusterState);
184
185    String[] tableNames = new String[] { TABLE_NAME_1, TABLE_NAME_2, TABLE_NAME_NAMESPACE };
186    Set<String> jmxMetrics = readJmxMetricsWithRetry();
187    Set<String> expectedMetrics = getExpectedJmxMetrics(tableNames, functionNames);
188
189    // printMetrics(jmxMetrics, "existing metrics in per-table mode");
190    // printMetrics(expectedMetrics, "expected metrics in per-table mode");
191
192    // assert that every expected is in the JMX
193    for (String expected : expectedMetrics) {
194      assertTrue(jmxMetrics.contains(expected),
195        "Metric " + expected + " can not be found in JMX in per-table mode.");
196    }
197  }
198
199  private Set<String> readJmxMetricsWithRetry() throws IOException {
200    final int count = 0;
201    for (int i = 0; i < 10; i++) {
202      Set<String> metrics = readJmxMetrics();
203      if (metrics != null) {
204        return metrics;
205      }
206      LOG.warn("Failed to get jmxmetrics... sleeping, retrying; " + i + " of " + count + " times");
207      Threads.sleep(1000);
208    }
209    return null;
210  }
211
212  /**
213   * Read the attributes from Hadoop->HBase->Master->Balancer in JMX
214   */
215  private Set<String> readJmxMetrics() throws IOException {
216    JMXConnector connector = null;
217    ObjectName target = null;
218    MBeanServerConnection mb = null;
219    try {
220      connector =
221        JMXConnectorFactory.connect(JMXListener.buildJMXServiceURL(connectorPort, connectorPort));
222      mb = connector.getMBeanServerConnection();
223
224      Hashtable<String, String> pairs = new Hashtable<>();
225      pairs.put("service", "HBase");
226      pairs.put("name", "Master");
227      pairs.put("sub", "Balancer");
228      target = new ObjectName("Hadoop", pairs);
229      MBeanInfo beanInfo = mb.getMBeanInfo(target);
230
231      Set<String> existingAttrs = new HashSet<>();
232      for (MBeanAttributeInfo attrInfo : beanInfo.getAttributes()) {
233        existingAttrs.add(attrInfo.getName());
234      }
235      return existingAttrs;
236    } catch (Exception e) {
237      LOG.warn("Failed to get bean!!! " + target, e);
238      if (mb != null) {
239        Set<ObjectInstance> instances = mb.queryMBeans(null, null);
240        Iterator<ObjectInstance> iterator = instances.iterator();
241        System.out.println("MBean Found:");
242        while (iterator.hasNext()) {
243          ObjectInstance instance = iterator.next();
244          System.out.println("Class Name: " + instance.getClassName());
245          System.out.println("Object Name: " + instance.getObjectName());
246        }
247      }
248    } finally {
249      if (connector != null) {
250        try {
251          connector.close();
252        } catch (Exception e) {
253          e.printStackTrace();
254        }
255      }
256    }
257    return null;
258  }
259
260  /**
261   * Given the tables and functions, return metrics names that should exist in JMX
262   */
263  private Set<String> getExpectedJmxMetrics(String[] tableNames, String[] functionNames) {
264    Set<String> ret = new HashSet<>();
265
266    for (String tableName : tableNames) {
267      ret.add(StochasticLoadBalancer.composeAttributeName(tableName, "Overall"));
268      for (String functionName : functionNames) {
269        String metricsName = StochasticLoadBalancer.composeAttributeName(tableName, functionName);
270        ret.add(metricsName);
271      }
272    }
273
274    return ret;
275  }
276}