Основываясь на примере wordcount из Hadoop - The Definitive Guide, я разработал задание mapreduce для подсчета появления неупорядоченных кортежей строк. Ввод выглядит так (просто больше):
аб
куб.см
дд
ба
объявление
дд
Запуск mapreduce Я ожидаю, что выход будет (для этого примера):
cc 1
дд 1
ab 2
объявление 1
дд 1
Это означает, что я хочу, чтобы кортежи a, b и b, a считались одинаковыми. Вопрос уже задан здесь: Hadoop MapReduce: два значения в качестве ключа в Mapper-Reducer и, вероятно, были решены здесь https://developer.yahoo.com/hadoop/tutorial/module5.html#keytypes.
Для больших входных файлов я получаю такой вывод, первый столбец - hashCode of resp. ключ:
151757761 aa 62822
153322274 ab 62516
154886787 ac 62248
156451300 объявление 62495
153322274 ba 62334
154902916 bb 62232
158064200 bd 62759
154886787 ca 62200
156483558 cb 124966
158080329 cc 62347
159677100 dc 125047
156451300 да 62653
158064200 db 62603
161290000 dd 62778
Как можно видеть, некоторые ключи являются дубликатами, например 153322274 для a, b и b, a. Для других, таких как c, b (и b, c) и c, d (и d, c), счет правильный. Примерно вдвое больше, чем другие, потому что тестовые данные рисуются равномерно случайным образом.
Я искал эту проблему в течение некоторого времени и теперь исчерпываю идеи, почему все еще могут быть ключевые дубликаты после фазы сокращения.
Ниже приведен код, который я использую:
Сначала код для моего пользовательского WritableComparable
import org.apache.hadoop.io.WritableComparable;
import org.apache.hadoop.io.WritableUtils;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.math.BigInteger;
public class Pair implements WritableComparable<Pair> {
private String first;
private String second;
public Pair(String first, String second) {
this.first = first;
this.second = second;
}
public Pair() {
this("", "");
}
@Override
public String toString() {
return this.hashCode() + "\t" + first + "\t" + second;
}
@Override
public void write(DataOutput out) throws IOException {
WritableUtils.writeString(out, first);
WritableUtils.writeString(out, second);
}
@Override
public void readFields(DataInput in) throws IOException {
first = WritableUtils.readString(in);
second = WritableUtils.readString(in);
}
@Override
public int hashCode() {
BigInteger bA = BigInteger.ZERO;
BigInteger bB = BigInteger.ZERO;
for(int i = 0; i < first.length(); i++) {
bA = bA.add(BigInteger.valueOf(127L).pow(i+1).multiply(BigInteger.valueOf(first.codePointAt(i))));
}
for(int i = 0; i < second.length(); i++) {
bB = bB.add(BigInteger.valueOf(127L).pow(i+1).multiply(BigInteger.valueOf(second.codePointAt(i))));
}
return bA.multiply(bB).intValue();
}
@Override
public boolean equals(Object o) {
if (o instanceof Pair) {
Pair other = (Pair) o;
boolean result = ( first.compareTo(other.first) == 0 && second.compareTo(other.second) == 0 )
|| ( first.compareTo(other.second) == 0 && second.compareTo(other.first) == 0 );
return result;
}
return false;
}
@Override
public int compareTo(Pair other) {
if (( first.compareTo(other.first) == 0 && second.compareTo(other.second) == 0 )
|| ( first.compareTo(other.second) == 0 && second.compareTo(other.first) == 0 ) ) {
return 0;
} else {
int cmp = first.compareTo( other.first );
if (cmp != 0) {
return cmp;
}
return second.compareTo( other.second );
}
}
}
И остальное:
import java.io.IOException;
import java.util.StringTokenizer;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.util.GenericOptionsParser;
public class PairCount {
public static void main(String[] args) throws Exception {
Configuration conf = new Configuration();
String[] otherArgs = new GenericOptionsParser(conf, args).getRemainingArgs();
if (otherArgs.length < 2) {
System.err.println("Usage: paircount <in-dir> <out-dir>");
System.exit(2);
}
Job job = new Job(conf, "word count");
job.setJarByClass(PairCount.class);
job.setMapperClass(TokenizerMapper.class);
job.setReducerClass(IntSumReducer.class);
job.setMapOutputKeyClass(Pair.class);
job.setMapOutputValueClass(IntWritable.class);
job.setOutputKeyClass(Pair.class);
job.setOutputValueClass(IntWritable.class);
for (int i = 0; i < otherArgs.length - 1; ++i) {
FileInputFormat.addInputPath(job, new Path(otherArgs[i]));
}
FileOutputFormat.setOutputPath(job, new Path(otherArgs[otherArgs.length - 1]));
System.exit(job.waitForCompletion(true) ? 0 : 1);
}
public static class TokenizerMapper extends Mapper<Object, Text, Pair, IntWritable> {
private final static IntWritable one = new IntWritable(1);
public void map(Object key, Text value, Context context) throws IOException, InterruptedException {
StringTokenizer itr = new StringTokenizer(value.toString());
while (itr.hasMoreTokens()) {
context.write(new Pair(itr.nextToken(), itr.nextToken()), one);
}
}
}
public static class IntSumReducer extends Reducer<Pair, IntWritable, Pair, IntWritable> {
private IntWritable result = new IntWritable();
public void reduce(Pair key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
int sum = 0;
for (IntWritable val : values) {
sum += val.get();
}
result.set(sum);
context.write( key, result);
}
}
}
Edit: Я добавил модульные тесты для функций hashCode() и compareTo(). Они отлично работают.
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
public class Tests {
@Test
public void testPairComparison() {
assertTrue( 0 == new Pair("a", "a").compareTo(new Pair("a", "a")) );
assertTrue( 0 == new Pair("a", "b").compareTo(new Pair("b", "a")) );
assertTrue( 0 == new Pair("a", "c").compareTo(new Pair("c", "a")) );
assertTrue( 0 == new Pair("a", "d").compareTo(new Pair("d", "a")) );
assertTrue( 0 == new Pair("b", "b").compareTo(new Pair("b", "b")) );
assertTrue( 0 == new Pair("b", "c").compareTo(new Pair("c", "b")) );
assertTrue( 0 == new Pair("b", "d").compareTo(new Pair("d", "b")) );
assertTrue( 0 == new Pair("c", "c").compareTo(new Pair("c", "c")) );
assertTrue( 0 == new Pair("c", "d").compareTo(new Pair("d", "c")) );
assertTrue( 0 == new Pair("d", "d").compareTo(new Pair("d", "d")) );
assertTrue( 0 > new Pair("a", "a").compareTo(new Pair("b", "b")) );
assertTrue( 0 > new Pair("a", "a").compareTo(new Pair("c", "b")) );
assertTrue( 0 < new Pair("d", "d").compareTo(new Pair("c", "b")) );
assertTrue( 0 < new Pair("c", "d").compareTo(new Pair("c", "a")) );
}
@Test
public void testPairHashcode(){
assertTrue( 0 != new Pair("a", "a").hashCode());
assertTrue( 0 != new Pair("a", "b").hashCode());
assertTrue( 0 != new Pair("a", "c").hashCode());
assertTrue( 0 != new Pair("a", "d").hashCode());
assertTrue( 0 != new Pair("b", "b").hashCode());
assertTrue( 0 != new Pair("b", "c").hashCode());
assertTrue( 0 != new Pair("b", "d").hashCode());
assertTrue( 0 != new Pair("c", "c").hashCode());
assertTrue( 0 != new Pair("c", "d").hashCode());
assertTrue( 0 != new Pair("d", "d").hashCode());
assertEquals( new Pair("a", "a").hashCode(), new Pair("a", "a").hashCode() );
assertEquals( new Pair("a", "b").hashCode(), new Pair("b", "a").hashCode() );
assertEquals( new Pair("a", "c").hashCode(), new Pair("c", "a").hashCode() );
assertEquals( new Pair("a", "d").hashCode(), new Pair("d", "a").hashCode() );
assertEquals( new Pair("b", "b").hashCode(), new Pair("b", "b").hashCode() );
assertEquals( new Pair("b", "c").hashCode(), new Pair("c", "b").hashCode() );
assertEquals( new Pair("b", "d").hashCode(), new Pair("d", "b").hashCode() );
assertEquals( new Pair("c", "c").hashCode(), new Pair("c", "c").hashCode() );
assertEquals( new Pair("c", "d").hashCode(), new Pair("d", "c").hashCode() );
assertEquals( new Pair("d", "d").hashCode(), new Pair("d", "d").hashCode() );
assertNotEquals( new Pair("a", "a").hashCode(), new Pair("b", "b").hashCode() );
assertNotEquals( new Pair("a", "b").hashCode(), new Pair("b", "d").hashCode() );
assertNotEquals( new Pair("a", "c").hashCode(), new Pair("d", "a").hashCode() );
assertNotEquals( new Pair("a", "d").hashCode(), new Pair("a", "a").hashCode() );
}
}
Но я понял, что, меняя compareTo(), чтобы всегда возвращать 0, каждая пара будет считаться той же, что и результат:
156483558 cb 1000000
в то время как изменение hashCode() всегда возвращает 0 (для тех же входных данных, что указано выше) приведет к тому же результату, что и выше, только с нулевыми клавишами.
0 aa 62822
0 ab 62516
0 ac 62248
0 до 62495
0 ba 62334
0 bb 62232
0 bd 62759
0 ca 62200
0 cb 124966
0 cc 62347
0 dc 125047
0 да 62653
0 дБ 62603
0 дд 62778
Редактировать:
Я исследовал дальше, делая compareTo() печатаем то, что сравнивается. Это показало, что некоторые ключи, такие как a, b и b, a, никогда не сравниваются друг с другом, поэтому не группируются.
Я думаю, что есть какая-то крошечная вещь, которую мне не хватает. Я рад за любые идеи! Заранее большое спасибо.
с наилучшими пожеланиями
Проблема заключается в функции compareTo(). Сначала проверьте, равны ли они в терминах a, b равно b, a. Если это не так, сначала сравните меньшие значения пар и, если они совпадут, сравните большие значения. пар. Это решает проблему.
Вот как я его реализовал сейчас:
@Override
public int compareTo(Pair other){
int cmpFirstFirst = first.compareTo(other.first);
int cmpSecondSecond = second.compareTo(other.second);
int cmpFirstSecond = first.compareTo(other.second);
int cmpSecondFirst = second.compareTo(other.first);
if ( cmpFirstFirst == 0 && cmpSecondSecond == 0 || cmpFirstSecond == 0 && cmpSecondFirst == 0) {
return 0;
}
String thisSmaller;
String otherSmaller;
String thisBigger;
String otherBigger;
if ( this.first.compareTo(this.second) < 0 ) {
thisSmaller = this.first;
thisBigger = this.second;
} else {
thisSmaller = this.second;
thisBigger = this.first;
}
if ( other.first.compareTo(other.second) < 0 ) {
otherSmaller = other.first;
otherBigger = other.second;
} else {
otherSmaller = other.second;
otherBigger = other.first;
}
int cmpThisSmallerOtherSmaller = thisSmaller.compareTo(otherSmaller);
int cmpThisBiggerOtherBigger = thisBigger.compareTo(otherBigger);
if (cmpThisSmallerOtherSmaller == 0) {
return cmpThisBiggerOtherBigger;
} else {
return cmpThisSmallerOtherSmaller;
}
}
Это означает, что, в отличие от моего предположения, группировка вывода карты выполняется с использованием транзитивного отношения, а не перекрестного произведения ключей. Необходим стабильный порядок ключей. Это дает полный смысл, как только вы это знаете и понимаете.
Учитывая исходные требования, чтобы {a, b} =: = {b, a} было бы нелегко иметь упорядоченные в конструкторе элементы кортежа?
public Pair(String first, String second) {
boolean swap = first.compareTo(second) > 0;
this.first = swap ? second : first;
this.second = swap ? first : second;
}
Это упростит методы, такие как compareTo и equals, а также сделает ненужным выполнение Partitioner.
Думаю, я вижу здесь эту проблему. Вы не реализовали разделитель.
Когда вы говорите, что сталкиваетесь с проблемами с большим набором данных, я предполагаю, что вы используете несколько редукторов. Если вы используете один редуктор, код будет работать. Но в случае нескольких редукторов вам нужен разделитель, чтобы сообщить фреймворку, что ab & ba по сути являются одними и теми же ключами и должны перейти к одному и тому же редуктору.
Вот пояснительная ссылка: LINK