/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * <p> * http://www.apache.org/licenses/LICENSE-2.0 * <p> * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.hadoop.hdfs.server.datanode.metrics; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import org.apache.hadoop.test.GenericTestUtils; import org.apache.log4j.Level; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.Timeout; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import static org.junit.Assert.assertTrue; /** * Unit tests for {@link OutlierDetector}. */ public class TestSlowNodeDetector { public static final Logger LOG = LoggerFactory.getLogger(TestSlowNodeDetector.class); /** * Set a timeout for every test case. */ @Rule public Timeout testTimeout = new Timeout(300_000); private final static double LOW_THRESHOLD = 1000; private final static long MIN_OUTLIER_DETECTION_PEERS = 3; // Randomly generated test cases for median and MAD. The first entry // in each pair is the expected median and the second entry is the // expected Median Absolute Deviation. The small sets of size 1 and 2 // exist to test the edge cases however in practice the MAD of a very // small set is not useful. private Map<List<Double>, Pair<Double, Double>> medianTestMatrix = new ImmutableMap.Builder<List<Double>, Pair<Double, Double>>() // Single element. .put(new ImmutableList.Builder<Double>() .add(9.6502431302).build(), Pair.of(9.6502431302, 0.0)) // Two elements. .put(new ImmutableList.Builder<Double>() .add(1.72168104625) .add(11.7872544459).build(), Pair.of(6.75446774606, 7.4616095611)) // The Remaining lists were randomly generated with sizes 3-10. .put(new ImmutableList.Builder<Double>() .add(76.2635686249) .add(27.0652018553) .add(1.3868476443) .add(49.7194624164) .add(47.385680883) .add(57.8721199173).build(), Pair.of(48.5525716497, 22.837202532)) .put(new ImmutableList.Builder<Double>() .add(86.0573389581) .add(93.2399572424) .add(64.9545429122) .add(35.8509730085) .add(1.6534313654).build(), Pair.of(64.9545429122, 41.9360180373)) .put(new ImmutableList.Builder<Double>() .add(5.00127007366) .add(37.9790589127) .add(67.5784746266).build(), Pair.of(37.9790589127, 43.8841594039)) .put(new ImmutableList.Builder<Double>() .add(1.43442932944) .add(70.6769829947) .add(37.47579656) .add(51.1126141394) .add(72.2465914419) .add(32.2930549225) .add(39.677459781).build(), Pair.of(39.677459781, 16.9537852208)) .put(new ImmutableList.Builder<Double>() .add(26.7913745214) .add(68.9833706658) .add(29.3882180746) .add(68.3455244453) .add(74.9277265022) .add(12.1469972942) .add(72.5395402683) .add(7.87917492506) .add(33.3253447774) .add(72.2753759125).build(), Pair.of(50.8354346113, 31.9881230079)) .put(new ImmutableList.Builder<Double>() .add(38.6482290705) .add(88.0690746319) .add(50.6673611649) .add(64.5329814115) .add(25.2580979294) .add(59.6709630711) .add(71.5406993741) .add(81.3073035091) .add(20.5549547284).build(), Pair.of(59.6709630711, 31.1683520683)) .put(new ImmutableList.Builder<Double>() .add(87.352734249) .add(65.4760359094) .add(28.9206803169) .add(36.5908574008) .add(87.7407653175) .add(99.3704511335) .add(41.3227434076) .add(46.2713494909) .add(3.49940920921).build(), Pair.of(46.2713494909, 28.4729106898)) .put(new ImmutableList.Builder<Double>() .add(95.3251533286) .add(27.2777870437) .add(43.73477168).build(), Pair.of(43.73477168, 24.3991619317)) .build(); // A test matrix that maps inputs to the expected output list of // slow nodes i.e. outliers. private Map<Map<String, Double>, Set<String>> outlierTestMatrix = new ImmutableMap.Builder<Map<String, Double>, Set<String>>() // The number of samples is too low and all samples are below // the low threshold. Nothing should be returned. .put(ImmutableMap.of( "n1", 0.0, "n2", LOW_THRESHOLD + 1), ImmutableSet.<String>of()) // A statistical outlier below the low threshold must not be // returned. .put(ImmutableMap.of( "n1", 1.0, "n2", 1.0, "n3", LOW_THRESHOLD - 1), ImmutableSet.<String>of()) // A statistical outlier above the low threshold must be returned. .put(ImmutableMap.of( "n1", 1.0, "n2", 1.0, "n3", LOW_THRESHOLD + 1), ImmutableSet.of("n3")) // A statistical outlier must not be returned if it is within a // MEDIAN_MULTIPLIER multiple of the median. .put(ImmutableMap.of( "n1", LOW_THRESHOLD + 0.1, "n2", LOW_THRESHOLD + 0.1, "n3", LOW_THRESHOLD * OutlierDetector.MEDIAN_MULTIPLIER - 0.1), ImmutableSet.<String>of()) // A statistical outlier must be returned if it is outside a // MEDIAN_MULTIPLIER multiple of the median. .put(ImmutableMap.of( "n1", LOW_THRESHOLD + 0.1, "n2", LOW_THRESHOLD + 0.1, "n3", (LOW_THRESHOLD + 0.1) * OutlierDetector.MEDIAN_MULTIPLIER + 0.1), ImmutableSet.of("n3")) // Only the statistical outliers n3 and n11 should be returned. .put(new ImmutableMap.Builder<String, Double>() .put("n1", 1029.4322) .put("n2", 2647.876) .put("n3", 9194.312) .put("n4", 2.2) .put("n5", 2012.92) .put("n6", 1843.81) .put("n7", 1201.43) .put("n8", 6712.01) .put("n9", 3278.554) .put("n10", 2091.765) .put("n11", 9194.77).build(), ImmutableSet.of("n3", "n11")) // The following input set has multiple outliers. // - The low outliers (n4, n6) should not be returned. // - High outlier n2 is within 3 multiples of the median // and so it should not be returned. // - Only the high outlier n8 should be returned. .put(new ImmutableMap.Builder<String, Double>() .put("n1", 5002.0) .put("n2", 9001.0) .put("n3", 5004.0) .put("n4", 1001.0) .put("n5", 5003.0) .put("n6", 2001.0) .put("n7", 5000.0) .put("n8", 101002.0) .put("n9", 5001.0) .put("n10", 5002.0) .put("n11", 5105.0) .put("n12", 5006.0).build(), ImmutableSet.of("n8")) .build(); private OutlierDetector slowNodeDetector; @Before public void setup() { slowNodeDetector = new OutlierDetector(MIN_OUTLIER_DETECTION_PEERS, (long) LOW_THRESHOLD); GenericTestUtils.setLogLevel(OutlierDetector.LOG, Level.ALL); } @Test public void testOutliersFromTestMatrix() { for (Map.Entry<Map<String, Double>, Set<String>> entry : outlierTestMatrix.entrySet()) { LOG.info("Verifying set {}", entry.getKey()); final Set<String> outliers = slowNodeDetector.getOutliers(entry.getKey()).keySet(); assertTrue( "Running outlier detection on " + entry.getKey() + " was expected to yield set " + entry.getValue() + ", but " + " we got set " + outliers, outliers.equals(entry.getValue())); } } /** * Unit test for {@link OutlierDetector#computeMedian(List)}. */ @Test public void testMediansFromTestMatrix() { for (Map.Entry<List<Double>, Pair<Double, Double>> entry : medianTestMatrix.entrySet()) { final List<Double> inputList = new ArrayList<>(entry.getKey()); Collections.sort(inputList); final Double median = OutlierDetector.computeMedian(inputList); final Double expectedMedian = entry.getValue().getLeft(); // Ensure that the median is within 0.001% of expected. // We need some fudge factor for floating point comparison. final Double errorPercent = Math.abs(median - expectedMedian) * 100.0 / expectedMedian; assertTrue( "Set " + inputList + "; Expected median: " + expectedMedian + ", got: " + median, errorPercent < 0.001); } } /** * Unit test for {@link OutlierDetector#computeMad(List)}. */ @Test public void testMadsFromTestMatrix() { for (Map.Entry<List<Double>, Pair<Double, Double>> entry : medianTestMatrix.entrySet()) { final List<Double> inputList = new ArrayList<>(entry.getKey()); Collections.sort(inputList); final Double mad = OutlierDetector.computeMad(inputList); final Double expectedMad = entry.getValue().getRight(); // Ensure that the MAD is within 0.001% of expected. // We need some fudge factor for floating point comparison. if (entry.getKey().size() > 1) { final Double errorPercent = Math.abs(mad - expectedMad) * 100.0 / expectedMad; assertTrue( "Set " + entry.getKey() + "; Expected M.A.D.: " + expectedMad + ", got: " + mad, errorPercent < 0.001); } else { // For an input list of size 1, the MAD should be 0.0. final Double epsilon = 0.000001; // Allow for some FP math error. assertTrue( "Set " + entry.getKey() + "; Expected M.A.D.: " + expectedMad + ", got: " + mad, mad < epsilon); } } } /** * Verify that {@link OutlierDetector#computeMedian(List)} throws when * passed an empty list. */ @Test(expected=IllegalArgumentException.class) public void testMedianOfEmptyList() { OutlierDetector.computeMedian(Collections.<Double>emptyList()); } /** * Verify that {@link OutlierDetector#computeMad(List)} throws when * passed an empty list. */ @Test(expected=IllegalArgumentException.class) public void testMadOfEmptyList() { OutlierDetector.computeMedian(Collections.<Double>emptyList()); } private static class Pair<L, R> { private final L l; private final R r; Pair(L l, R r) { this.l = l; this.r = r; } L getLeft() { return l; } R getRight() { return r; } static <L, R> Pair of(L l, R r) { return new Pair<>(l, r); } } }