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