blob: 9bd0687166a51882f3e61b1982f52e71dd1f1afc [file] [log] [blame]
/**
* 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);
}
}
}