2009年12月28日月曜日

MapReduceをJUnitでTDDで作る(9 さらにリファクタリング)

こんばんは、yoshitsuguです。もう、年の瀬ですね。
2009年は皆さんにとってどんな1年だったでしょうか?

私は怒濤のように過ぎていった1年でした。

さてさて、
前回、テストを追加して、JUnitによるテストがGREENになるところまで実行しました。
TDDの開発は、RED→GREEN→Refactoringの繰り返しであるというのは最初の頃に述べました。
ということで、再びリファクタリングしましょう。

LogAnalysisMapper.java
package jp.co.littel.hadoop;

import java.io.IOException;

import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;

/**
 * ログ解析Mapper
 * @author yoshitsugu
 */
public class LogAnalysisMapper extends Mapper {

    /**
     * map処理を実行します。
     * @param key キー
     * @param value 値
     * @param cotext コンテクスト
     */
    @Override
    protected void map(LongWritable key, Text value, Context context)
            throws IOException, InterruptedException {
        if (value.toString().contains("INFO")) {
            context.write(value, new LongWritable(1));
        }
    }
}

このプログラムで気になるのは、"INFO"というリテラル値がハードコーディングされている点ですね。
Eclipseのリファクタリング機能でpublic定数化してしまいましょう。

    /** ログ種別:情報 */
    public static final String INFO = "INFO";

このように定数ができました。public定数化した理由は、以下INFOを用いる際にはこのクラスを参照させればよくなるからです。(それが他のクラスに強く依存している場合は除きます)

続いてテストクラスもリファクタリングしてしまいましょう。

TestLogAnalysisMapper.java
package jp.co.littel.hadoop;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Matchers.any;


import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.internal.verification.Times;

/**
 * ログ解析Mapperのテストクラスです。
 * @author yoshitsugu
 */
public class TestLogAnalysisMapper {

    /**
     * @throws java.lang.Exception
     */
    @Before
    public void setUp() throws Exception {
    }
    /**
     * @throws java.lang.Exception
     */
    @After
    public void tearDown() throws Exception {
    }

    /**
     * 抽出するパターン
     * @throws Exception 例外発生時
     */
    @Test
    public void testMapExtract() throws Exception {
        Mapper.Context context = mock(Mapper.Context.class);
        String[] lines = {
                "2009-12-14 00:00:26,340 INFO  hogehoge @ abcdefg hijklmn opqrstu"
        };
        LogAnalysisMapper mapper = new LogAnalysisMapper();
        mapper.map(null, new Text(lines[0]), context);
        verify(context, new Times(1)).write(new Text(lines[0]), new LongWritable(1));
    }
   
    /**
     * 抽出する行が複数の場合
     * @throws Exception 例外発生時
     */
    @Test
    public void testMapExtract_ComplexRows() throws Exception {
        Mapper.Context context = mock(Mapper.Context.class);
        String[] lines = {
                "2009-12-14 00:00:26,340 INFO  hogehoge @ abcdefg hijklmn opqrstu",
                "2009-12-14 00:00:26,341 WARN  fugafuga @ 11111111111111111111111",
                "2009-12-14 00:00:26,341 ERROR fatal @ exception has occurred",
        };
        LogAnalysisMapper mapper = new LogAnalysisMapper();
       for (String line : lines) {
         mapper.map(null, new Text(line), context);
       }
        verify(context, new Times(1)).write(new Text(lines[0]), new LongWritable(1));
        verify(context, new Times(1)).write(any(Text.class), any(LongWritable.class));
    }
}

気になるのは黄色く着色した部分が繰り返している点と、逆に同じロジックの筈なのに異なっているオレンジの着色をした部分ですね。

contextとmapperオブジェクトはテストクラスのフィールドとして持たせても何ら問題ないと思われます。現在のテストの形なら各メソッドで一度ずつインスタンスを生成してそれきりだからです。
オレンジの部分は2つめのメソッドのロジックで統一が可能だと言うことに気がつきます。

では、そのように修正してみましょう。

Eclipseの機能をフル活用します。
testMapExtract()メソッド内のcontextを選択して、「リファクタリング」→「ローカル変数をフィールドに変換」を実行します。初期化はフィールド宣言を選択します。
 フィールド宣言で初期化され、testMapExtract()メソッド内の記述は消えました。
また、testMapExtract_ComplexRows()メソッド内のcontextのインスタンス生成処理は冗長なので削除してしまいましょう。

同じく、testMapExtract()メソッドのmapperオブジェクトもフィールドに変換してしまいます。mapperオブジェクトの初期化もフィールド宣言で行ってしまいましょう。
また、同様にtestMapExtract_ComplexRows()メソッド内のmapperオブジェクトの初期化も削除してしまいましょう。

あとはオレンジの部分ですね。
まず、testMapExtract_ComplexRows()メソッド内のfor文を選択し、リファクタリング→メソッドの抽出を実行します。メソッド名はmapでよいでしょう。
for文がmap()メソッドで置き換えられました。

mapper.map()処理を呼び出している部分もmap()メソッドで置き換えてしまいます。

リファクタリング完了。実行してみましょう。

見事にGREENのままです。


今回はここまで。

ここまでの修正で、ソースは次のようになりました。

LogAnalysisMapper.java

package jp.co.littel.hadoop;

import java.io.IOException;

import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;

/**
 * ログ解析Mapper
 * @author yoshitsugu
 */
public class LogAnalysisMapper extends Mapper {

    /** ログ種別:情報 */
    public static final String INFO = "INFO";

    /**
     * map処理を実行します。
     * @param key キー
     * @param value 値
     * @param cotext コンテクスト
     */
    @Override
    protected void map(LongWritable key, Text value, Context context)
            throws IOException, InterruptedException {
        if (value.toString().contains(INFO)) {
            context.write(value, new LongWritable(1));
        }
    }
}
TestLogAnalysisMapper.java


package jp.co.littel.hadoop;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Matchers.any;

import java.io.IOException;


import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.internal.verification.Times;

/**
 * ログ解析Mapperのテストクラスです。
 * @author yoshitsugu
 */
public class TestLogAnalysisMapper {

    private Mapper.Context context = mock(Mapper.Context.class);
    private LogAnalysisMapper mapper = new LogAnalysisMapper();

    /**
     * @throws java.lang.Exception
     */
    @Before
    public void setUp() throws Exception {
    }

    /**
     * @throws java.lang.Exception
     */
    @After
    public void tearDown() throws Exception {
    }

    /**
     * 抽出するパターン
     * @throws Exception 例外発生時
     */
    @Test
    public void testMapExtract() throws Exception {
        String[] lines = {
                "2009-12-14 00:00:26,340 INFO  hogehoge @ abcdefg hijklmn opqrstu"
        };
        map(lines);
        verify(context, new Times(1)).write(new Text(lines[0]), new LongWritable(1));
    }
   
    /**
     * 抽出する行が複数の場合
     * @throws Exception 例外発生時
     */
    @Test
    public void testMapExtract_ComplexRows() throws Exception {
        String[] lines = {
                "2009-12-14 00:00:26,340 INFO  hogehoge @ abcdefg hijklmn opqrstu",
                "2009-12-14 00:00:26,341 WARN  fugafuga @ 11111111111111111111111",
                "2009-12-14 00:00:26,341 ERROR fatal @ exception has occurred",
        };
        map(lines);
        verify(context, new Times(1)).write(new Text(lines[0]), new LongWritable(1));
        verify(context, new Times(1)).write(any(Text.class), any(LongWritable.class));
    }

    /**
     * map処理を呼び出します。
     * @param lines 行の配列
     * @throws IOException
     * @throws InterruptedException
     */
    private void map(String[] lines) throws IOException, InterruptedException {
        for (String line : lines) {
            mapper.map(null, new Text(line), context);
        }
    }
}

ではでは、よいお年をお迎えください!!

2009年12月25日金曜日

MapReduceをJUnitでTDDで作る(8 テストの追加)

Merry, Xmas!
最近のアメリカではHappy Holidays!!と言うのが一般的だそうです。
12/25で年始まで休日か・・うらやましい(ぉぃ)

閑話休題。前回で、リファクタリングを行い、
valueに与えられた文字列をそのままmapperがwriteするようにしました。
しかし、これでは仕様と異なることが明白です。

常にINFOを含む行が来るという仮定ならば、これでよいのですが、そうとは限りません。
では、実際にテストしてREDとなることを確認しましょう。

INFOを含む行だけでなく、WARNINGを含む行やERRORを含む行が来ることが予想されるので、追加するテストは、次のようなテストデータを用いればいいと予想できます。

        String[] lines = {
                "2009-12-14 00:00:26,340 INFO  hogehoge @ abcdefg hijklmn opqrstu",
                "2009-12-14 00:00:26,341 WARN  fugafuga @ 11111111111111111111111",
                "2009-12-14 00:00:26,342 ERROR fatal @ exception has occurred",
        };
また、INFOを含む行のみが抽出されるはずなので、context.writeの呼び出しは次のようになるはずです。
        verify(context, new Times(1)).write(new Text(lines[0]), new LongWritable(1));

さらに、writeの呼び出しは一度しか行われない筈なので、次のverifyを追加しましょう。

        verify(context, new Times(1)).write(any(Text.class), any(LongWritable.class));

テストメソッド名は  testMapExtract_ComplexRows()にしてみました。しかし、anyがimportされてないので、コンパイルが通りません。次のstatic import文を追加して下さい。

import static org.mockito.Matchers.any;
これでコンパイルまで通った筈です。

テストメソッドは次のようになります。
    /**
     * 抽出する行が複数の場合
     * @throws Exception 例外発生時
     */
    @Test
    public void testMapExtract_ComplexRows() throws Exception {
        Mapper.Context context = mock(Mapper.Context.class);
        String[] lines = {
                "2009-12-14 00:00:26,340 INFO  hogehoge @ abcdefg hijklmn opqrstu",
                "2009-12-14 00:00:26,341 WARN  fugafuga @ 11111111111111111111111",
                "2009-12-14 00:00:26,341 ERROR fatal @ exception has occurred",
        };
        LogAnalysisMapper mapper = new LogAnalysisMapper();
        mapper.map(null, new Text(line[0]), context);
        verify(context, new Times(1)).write(new Text(lines[0]), new LongWritable(1));
        verify(context, new Times(1)).write(any(Text.class), any(LongWritable.class));
    }


では、実行してみましょう。REDになるはずなのですが・・・。

 GREENになってしまいました。

これはおかしいですね。REDになるはずなのにGREENになってしまったということは、テストがどこかおかしいと言うことです。(GREENの理由を明確に説明できるのならばいいのですが、今回はREDを予想していたので明らかにおかしい)

GREENになってしまったということは、context.write();が一度しか呼ばれていないということです。
今のmap()メソッド内では無条件にcontext.write()を呼んでいるので、つまりmapメソッドが一度しか呼ばれていないと言うことですね。
つまり、map()メソッドを引数を変えて複数回呼べばREDになるはずですね。

ということで以下のように変更します。
        mapper.map(null, new Text(lines[0]), context);
         for (String line : lines) {
            mapper.map(null, new Text(line), context);
        }
せっかく複数行のテストデータを用意したのに最初の1行しかテストに用いてなかったようです。
全行をテストで呼ぶように変更しました。

では気を取り直して、テストを再実行してみます。


ようやく想定通り、REDになりました。
(一つ目はGREENのままです。これも想定通りです)
失敗内容を確認してみましょう。
org.mockito.exceptions.verification.TooManyActualInvocations:
context.write(
    isA(org.apache.hadoop.io.Text),
    isA(org.apache.hadoop.io.LongWritable)
);
Wanted 1 time but was 3
    at jp.co.littel.hadoop.TestLogAnalysisMapper.testMapExtract_ComplexRows(TestLogAnalysisMapper.java:72)
Caused by: org.mockito.exceptions.cause.UndesiredInvocation:
(以下略)
呼び出しが多すぎるというエラーになっています。
「想定は1回なのに3回呼ばれた」と言っています。これは想定通りの結果です。
(テストデータは3行なのでmap()メソッドは3回呼ばれる。map()メソッド内部では無条件にcontext.write()メソッドを呼び出すので3回呼ばれてしまう)

では、ソースを修正して、REDをGREENにします。
INFOの場合のみ取り出せばいいので、
        if (value.toString().contains("INFO")) {
            context.write(value, new LongWritable(1));
        }

でよさそうです。
(優秀な開発者の方はこの時点でバグを見つけられると思います。またテストを追加してそれを明らかにしていこうと思います。)

では、実行してみます。
おめでとうございます。すべてGREENになりました!



GREENになったらリファクタリングです。
リファクタリングは次回に。。。

2009年12月22日火曜日

MapReduceをJUnitでTDDで作る(7 Mapperのリファクタリング)

前回からだいぶ経ってしまいました。
今日は冬至ですね。
かぼちゃとゆず湯が私を待っている・・・。

リファクタリングに入ります。

要リファクタリング箇所
  • 警告が色々出ている
  • mapperが固定値のハードコーディングでcontext.write()を読んでいる。
まず、警告を消していきます。
Eclipseの「問題」ビューを見てみましょう。
Mapper.Context は raw 型です。総称型 Mapper.Context への参照は、パラメーター化する必要があります。
という警告がLogAnalysisMapper.javaTestLogAnalysisMapper.javaに出ています。また、
型の安全性: メソッド write(Object, Object) は raw 型 TaskInputOutputContext に属しています。総称型 TaskInputOutputContext への参照はパラメーター化される必要があります
という警告がLogAnalysisMapper.javaTestLogAnalysisMapper.javaに出ています。

これらの原因は実は同じで、Mapper<KEYIN, KEYOUT, VALUEIN, VALUEOUT>に具体的な型を指定する必要があると言うことを言っています。

今回のMapの入力のキーはLongWritableです(これはほぼお約束)。入力の値はファイルの各行なので、Text型になります。出力のキーは、抽出した行になるので、Text型です。出力の値は、実は特に何でもいいのですが、今回はLongWritableとしましょう。

つまり、
Mapper<LongWritable, Text, Text, LongWritable>.Contextと書けばいいことになります。
実際に書いてみましょう。
Test側は、contextの初期化の所に警告が残りました。
ソースファイルはコンパイルエラーとなってしまいました。

コンパイルエラーはよくないので、早速消します。
Mapper.write()がコンパイルエラーになっています。
どうやら、mapメソッドの引数の型とMapper<LongWritable, Text, Text, LongWritable>で指定した型が一致してないようです。Object型を指定しているところをLongWritable, Textで書き換えます。

コンパイルが通りました。一方、Test側のコードですが、こちらは消えそうにないですね。
こちらは放置しましょう。

では、コンパイルが通ったので実行します。
ロジックを一切触っていないので、JUnitはGREENになるはずです。
GREENになることを確認したら、次のリファクタリングに進みます。

今回、map関数内でwriteしている文字列はもともとVALUEとして引き渡されてくるものですね。
では、引数のvalueをそのままwriteしてしまいましょう。
context.write(value, new LongWritable(1));
では、JUnitを実行してみます。

ちゃんとGREENになりました。リファクタリング成功です!!

ということで今回はこの辺で。

今回までのソースです。

TestLogAnalysisMapper.java

/*
 * TestLogAnalysisMapper.java
 */
package jp.co.littel.hadoop;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;

import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.internal.verification.Times;

/**
 * ログ分析Mapperプログラム テストプログラム
 * @author yoshitsugu
 */
public class TestLogAnalysisMapper {

    /**
     * @throws java.lang.Exception
     */
    @Before
    public void setUp() throws Exception {
    }

    /**
     * @throws java.lang.Exception
     */
    @After
    public void tearDown() throws Exception {
    }

    /**
     * 抽出するパターン
     * @throws Exception 例外発生時
     */
    @Test
    public void testMapExtract() throws Exception {
        Mapper<LongWritable, Text, Text, LongWritable>.Context context = mock(Mapper.Context.class);
        String[] lines = {
                "2009-12-14 00:00:26,340 INFO  hogehoge @ abcdefg hijklmn opqrstu"
        };
        LogAnalysisMapper mapper = new LogAnalysisMapper();
        mapper.map(null, new Text(lines[0]), context);
        verify(context, new Times(1)).write(new Text(lines[0]), new LongWritable(1));
    }
}

LogAnalysisMapper.java
/*
 * LogAnalysisMapper.java
 */
package jp.co.littel.hadoop;

import java.io.IOException;

import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;

/**
 * ログ解析Mapperプログラム
 * @author m.yoshitsugu
 */
public class LogAnalysisMapper extends Mapper {

    /* (non-Javadoc)
     * @see org.apache.hadoop.mapreduce.Mapper#map(java.lang.Object, java.lang.Object, org.apache.hadoop.mapreduce.Mapper.Context)
     */
    @Override
    protected void map(LongWritable key, Text value, Context context)
throws IOException, InterruptedException {
        context.write(value, new LongWritable(1));
    }
}

2009年12月16日水曜日

MapReduceをJUnitでTDDで作る(6-ちょっとおさらい)

いつまで続くんだろう、このシリーズ・・・
今回は、リファクタリングの予定を変更して予定の確認と振り返りをしたいと思います。

今後のロードマップです。
  • Mapper作成(リファクタリング)
  • Mapper作成(テスト追加)
  • Mapper作成(レッド→グリーン→リファクタリング)
  • Driverのテスト(スタンドアローンモードについて)
  • Driverのテスト(テスト実行)
  • まとめ
うーん、結構長いな・・・。

少しコーヒーブレーク。今までを振り返りたいと思います。

INFOというログをログファイルから抽出するプログラムをMap(/Reduce)で作成しようとしています。
まず、実装より先にテストを作りました。
テストの内容は、「context.write();メソッドがある引数で1度だけ呼ばれればOK」というものでした。
次に実際のMapperクラスにコーディングしました。(リテラル値のハードコーディングですが)
そうしてようやくテストがOKになりました。

では、そもそも何でこのテストになったのでしょうか?

Map/Reduceを作成する上でネックとなるのは、「デバッグがしにくい」という点が挙げられます。
  • データが大量であるため、デバッグログを吐いてもどれがどのログかわかりづらい
  • 分散環境で実行するので、デバッグログを吐いてもどれがどのログかわかりづらい
  • HadoopをWindows環境で動かしにくい
そこで、mockitoを用いてモックオブジェクトを使って、Hadoopを動かさなくてもJUnit上で模擬的にMap/ReduceのロジックをUnitテストしましょう、というのが今回ここまでのお話。

ちょっと反省として、
「"INFO"というログを抽出する」という仕様と、今回のテスト仕様がどう繋がるのかわかりづらいかな、というのがあります。

まず、テストデータとして
String[] lines = {
"2009-12-14 00:00:26,340 INFO hogehoge @ abcdefg hijklmn opqrstu"
};
を用意しています。これには"INFO"が含まれているので、これをMapperのmap処理に渡せば抽出対象としてcontext.writeを呼び出してくれるはずです。

ということで、
verify(context, new Times(1)).write(new Text(lines[0]), new LongWritable(1));
をテストコードとして記述しました。

Mapperの入力はKEY=LongWritable, VALUE=Text、出力はKey=LongWritable, VALUE=TextKey=Text, VALUE=LongWritableとしました。出力のKeyには行番号が連番で振られます。出力のVALUEはログの実際の抽出対象となった行の文字列です。出力のKeyはログの実際の抽出対象となった行の文字列です。出力のVALUEは1です。
この辺りの仕様をどうするかは、設計者に委ねられています。UnitTestはあくまで「プログラム仕様を満たしているかどうかをテストする」もので、業務仕様を満たしているかのテストではないのです。そこは、結合テストを実施して出力結果を検討し、業務仕様を満たしている結果になっているか確認する必要があります。(その前にUnitTestのレビューを行い、業務仕様に沿ったプログラム仕様でUnitTestが作られているかを確認するのがよいでしょう)

閑話休題。
次のリファクタリングでは、グリーンのままでどのようにリファクタリングをやっていくか、を説明したいと思います。明日時間があれば・・・。

MapReduceをJUnitでTDDで作る(5-Mapper作成(3))

前回までで、テストを失敗させるところまで来ました。

ここまでのソースコードです。

package jp.co.littel.hadoop;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;

import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.internal.verification.Times;

/**
*
* @author littel
*/
public class TestLogAnalysisMapper {

/**
* @throws java.lang.Exception
*/
@Before
public void setUp() throws Exception {
}

/**
* @throws java.lang.Exception
*/
@After
public void tearDown() throws Exception {
}

/**
* 抽出するパターン
* @throws Exception 例外発生時
*/
@Test
public void testMapExtract() throws Exception {
Mapper.Context context = mock(Mapper.Context.class);
String[] lines = {
"2009-12-14 00:00:26,340 INFO hogehoge @ abcdefg hijklmn opqrstu"
};
verify(context, new Times(1)).write(new LongWritable(1), new Text(lines[0]));
verify(context, new Times(1)).write(new Text(lines[0]), new LongWritable(1));
}
}


前回のエラーの内容を振り返ってみると、
Wanted but not invoked

とありました。
「context.writeメソッドが1, とlines[0]で呼ばれるってテストには書いてあるんだけど、呼ばれなかったよ?」と言っています。

ということは、テストをグリーンにするには、context.writeメソッドを呼び出す仕組みを作れば良さそうです。

つまり、Mapper.mapメソッドを呼び出さねば!

Mapper.map(key, value, context)メソッドの呼び出しは次のように行います。


LogAnalysisMapper mapper = new LogAnalysisMapper();
mapper.map(null, new Text(lines[0]), context);


早速、ソースコードのtestMapExtract()メソッドのverifyメソッドの手前に追加しましょう。
もちろんコンパイルエラーになりますね。LogAnalysisMapperクラスなんて存在しないからです。

Eclipseのクイックフィックス機能でクラスを追加しましょう。[Ctrl]+[1]キーを押下して下さい。
「クラス'LogAnalysisMapper'を作成します」をクリックして下さい。
「新規」ダイアログが表示されるので、

ソースフォルダ:LogAnalysisHadoop/src
スーパークラス:org.apache.hadoop.mapreduce.Mapper
継承された抽象メソッド:チェックあり

で「完了」ボタンを押下します。

LogAnalysisMapper.javaファイルが生成されました。が、まだコンパイルエラーはなくなっていません。

LogAnalysisMapper.javaをEclipseのJavaエディタで開き、右クリックして、「ソース」→「メソッドのオーバーライド/実装」をクリックしましょう。
mapメソッドにチェックを入れて、「OK」ボタンをクリックします。
クラス内にmapメソッドが追加されます。

コンパイルエラーが解消されたので、実行してみます。
またレッド(失敗)になりました。前回とは少し異なるようです。

org.mockito.exceptions.verification.junit.ArgumentsAreDifferent:
Argument(s) are different! Wanted:
context.write(
1,
2009-12-14 00:00:26,340 INFO hogehoge @ abcdefg hijklmn opqrstu
);
context.write(
2009-12-14 00:00:26,340 INFO hogehoge @ abcdefg hijklmn opqrstu1,
);
at jp.co.littel.hadoop.TestLogAnalysisMapper.testMapExtract(TestLogAnalysisMapper.java:50)
Caused by: org.mockito.exceptions.cause.ActualArgumentsAreDifferent:
Actual invocation has different arguments:
context.write(
null,
2009-12-14 00:00:26,340 INFO hogehoge @ abcdefg hijklmn opqrstu
);


今度は、「呼ばれたけど引数が違う!」というエラーのようです。

さきほど作成したメソッドの中でスーパークラスのmapメソッドをそのまま呼び出しているため、キーのnullがそのまま引き渡されてしまっているようです。

では、このレッドをグリーンに変えましょう。どうすればいいでしょうか?

「mapメソッドに、仕様を満たすようにINFOのログを抜き出す処理を記述すればいいんだよ」

残念ながら難しく考えすぎです。

正解は
「mapメソッド内でcontext.write(new LongWritable(1), new Text("<省略>"));context.write(new new Text("<省略>"), LongWritable(1));と記述する」です。
TDDでは、とにかくシンプルに作成することを第一に考えます。
context.writeの呼び出し引数にLongWritable(1)とText("<省略>")が渡されればいいのですから、それをそのまま呼び出せばいいのです。

「おいおい、それじゃ別のテストケースに対応できないじゃん?」
その通り。それはテストがグリーンになった後でリファクタリングしましょう。

では、メソッドにcotext.write();を記述して、テストを実行してみます。

おめでとうございます!見事にグリーンになりました!!!


LogAnalysisMapper.javaのソースは次のようになっています。


package jp.co.littel.hadoop;

import java.io.IOException;

import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;

/**
*
* @author yoshitsugu
*/
public class LogAnalysisMapper extends Mapper {

/* (non-Javadoc)
* @see org.apache.hadoop.mapreduce.Mapper#map(java.lang.Object, java.lang.Object, org.apache.hadoop.mapreduce.Mapper.Context)
*/
@Override
protected void map(Object key, Object value, Context context)
throws IOException, InterruptedException {
context.write(new LongWritable(1), new Text("2009-12-14 00:00:26,340 INFO hogehoge @ abcdefg hijklmn opqrstu"));
context.write(new Text("2009-12-14 00:00:26,340 INFO hogehoge @ abcdefg hijklmn opqrstu"), new LongWritable(1));
}
}


次はリファクタリングしてこのソースをもっと洗練します。

MapReduceをJUnitでTDDで作る(4-Mapper作成(2))

前回は、コンパイルを通すところまででした。
では、コンパイルが通ったので実行してみましょう。

パッケージエクスプローラー上でTestLogAnalysisMapper.javaを右クリックして、「実行」→「JUnit Test」をクリックします。

どうでしょうか?REDになりましたか?

確かにREDになりましたが、エラーが発生しています。
org.mockito.exceptions.misusing.NullInsteadOfMockException:
Argument passed to verify() is null!
Examples of correct verifications:
verify(mock).someMethod();
verify(mock, times(10)).someMethod();
verify(mock, atLeastOnce()).someMethod();
Also, if you use @Mock annotation don't miss initMocks()
at jp.co.littel.hadoop.TestLogAnalysisMapper.testMapExtract(TestLogAnalysisMapper.java:47)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
at java.lang.reflect.Method.invoke(Unknown Source)
at org.junit.internal.runners.TestMethodRunner.executeMethodBody(TestMethodRunner.java:99)
at org.junit.internal.runners.TestMethodRunner.runUnprotected(TestMethodRunner.java:81)
at org.junit.internal.runners.BeforeAndAfterRunner.runProtected(BeforeAndAfterRunner.java:34)
at org.junit.internal.runners.TestMethodRunner.runMethod(TestMethodRunner.java:75)
at org.junit.internal.runners.TestMethodRunner.run(TestMethodRunner.java:45)
at org.junit.internal.runners.TestClassMethodsRunner.invokeTestMethod(TestClassMethodsRunner.java:66)
at org.junit.internal.runners.TestClassMethodsRunner.run(TestClassMethodsRunner.java:35)
at org.junit.internal.runners.TestClassRunner$1.runUnprotected(TestClassRunner.java:42)
at org.junit.internal.runners.BeforeAndAfterRunner.runProtected(BeforeAndAfterRunner.java:34)
at org.junit.internal.runners.TestClassRunner.run(TestClassRunner.java:52)
at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:45)
at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:460)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:673)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:386)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:196)


先ほどnullで初期化したのがまずかったようです。
org.mockito.exceptions.misusing.NullInsteadOfMockException:

とあるので、nullではなくモックインスタンスで初期化すればいいと解ります。contextの初期化の所を
Mapper.Context context = mock(Mapper.Context.class);

と書き換え、
import static org.mockito.Mockito.mock;

というimport static文を追加しましょう。

再びコンパイルが通りました。実行してみます。

またREDになりました。今度は、ちゃんとテストが失敗したようです。


TDDの流れを思い出して下さい。
レッド(失敗)→グリーン(成功)→リファクタリングを繰り返すとあった筈です。
ようやくその最初までたどり着きました。
(同じレッドでも意図していない実行時エラーのレッドはTDDの流れには含みません)

では、なぜ、わざわざテストを実装前に走らせて一度失敗させるのでしょうか?
その目的は
「実装前に失敗させることで、テストの対象があっている事を確認するため」
です。
実装していないのにテスト結果がグリーン(成功)になるのは明らかにおかしいと解ります。テストの対象が間違っているかテストケースが間違っている可能性があると考えられるのです。

では、今回はここまでにします。

MapReduceをJUnitでTDDで作る(3-Mapper作成(1))

前回の投稿ではEclipse上の環境を構築しました。

今回はいよいよプログラムの作成に入ります。

TDDなので、まずはテストケースから作成します。

  1. Eclipseのパッケージエクスプローラー上で、testフォルダーで右クリック、「新規」→「JUnitテスト・ケース」をクリック。
  2. 「新規JUnit テスト・ケース」ダイアログで「新規 JUnit 4 テスト」を選択、パッケージにパッケージ構造を入力。今回は「jp.co.littel.hadoop」と入力しました。
  3. 名前に「TestLogAnalysisMapper」を入力。「setUp」と「tearDown」、お好みで「コメントの生成」にチェックを入れ「完了」ボタンをクリックします。
  4. 「JUnit4がビルド・パスにありません。追加しますか?」と聞かれるので、「次のアクションを実行」が選択され、「JUnit4ライブラリをビルド・パスに追加」が選択されていることを確認して「OK」ボタンをクリック。
  5. testフォルダー配下のjp.co.littel.hadoopパッケージ内にTestLogAnalysisMapperが追加されました。
さて、いよいよコーディングに入ります!!

まずはテストを作成しましょう。
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

/**
 *
 * @author yoshitsugu
 */
public class HadoopGrepMapperTest {
    /**
     * @throws java.lang.Exception
     */
    @Before
    public void setUp() throws Exception {
    }

    /**
     * @throws java.lang.Exception
     */
    @After
    public void tearDown() throws Exception {
    }
    /**
     * 抽出するパターン
     * @throws Exception 例外発生時
     */
    @Test
    public void testMapExtract() throws Exception {
        String[] lines = {
            "2009-12-14 00:00:26,340 INFO  hogehoge @ abcdefg hijklmn opqrstu"
        };
        verify(context, new Times(1)).write(new LongWritable(1), new Text(lines[0]));
        verify(context, new Times(1)).write(new Text(lines[0]), new LongWritable(1));
    }
}

「待てぇ~い!コンパイルが全然通らんではないか。」というお叱りの声が聞こえてきそうです。
TDDは、まず「何をテストしているのか」を意識します。まず、何をテストするのかを書くのです。コンパイルを通すのはその後です。
「ちょっと待て、テストから書くのならJUnitで見慣れたassertEqualsなどのメソッドがあるはずだろう?どこにも見あたらないぞ?」

ここが今回の肝、モックオブジェクトライブラリ mockitoです。
verify(context, new Times(1)).write(new LongWritable(1), new Text(lines[0]));
verify(context, new Times(1)).write(new Text(lines[0]), new LongWritable(1));

に注目して下さい。
ここで、contextはorg.apache.hadoop.mapreduce.Mapper.Contextクラスのモックインスタンスです。

verifyメソッドはcontext.write(KEY, VALUE)メソッドが、「KEYがどんな値で」「VALUEがどんな値で」「何度」呼ばれるかを判別します。

上記の例では、context.writeが、KEY=new LongWritable(1)という値で、VALUE=new Text(lines[0])という値では、1度(1 time)だけ呼ばれれば、テストOKであるという事になります。

では、テストができたので、コンパイルを通せるようにしましょう。

コンパイルエラーとなっている部分をマウスでポイントして、[Ctrl]+[1]キーを押下することで、LongWritableやTextはimport文を追加することができるはずです。

つづいて、contextのコンパイルエラーをなくすために、contextを宣言します。
次の一文をtestMapExtract()メソッドの一文目に追加しましょう。
Mapper.Context context;

そうするとMapperがコンパイルエラーになるはずです。
[Ctrl]+[1]キーでorg.apache.hadoop.mapreduce.Mapperのimport文を追加しましょう。
まだverifyがコンパイルエラーになっていますね。

これはEclipse任せでは解消できないので次のimport static文を追加して下さい。
import static org.mockito.Mockito.verify;


そうすると、再びcontextが初期化されてないのでコンパイルエラーになると思います。
とりあえず、context = nullで初期化しましょう。

ここまでで、コンパイルが一通り通りました。

今回はここまでにします。

MapReduceをJUnitでTDDで作る(2-環境設定)

前回の投稿ではTODOリストを作成しました。

TODOリスト
  1. 環境設定(Eclipse上にテスト環境の作成)
  2. ログファイルからXXXX=INFOのものを抽出する
今回は、「1.環境設定」をやります。

TODOリストには1行で軽く『環境設定(Eclipse上にテスト環境の作成)』と書きましたが、実はやらなければならないことがたくさんてんこ盛りです。EclipseのインストールやHadoop等各種ライブラリのダウンロードは省きます。
ちなみに開発環境はWindowsです。(私のはXPですがVistaや7でも問題ありません)
Windows上でHadoopが動くのか?JUnitを使えば動くんです。

TODOリスト in 環境設定
  • Eclipseプロジェクトの作成
  • testソースフォルダの作成
  • 必要ライブラリのプロジェクトへの登録
これだけでもかなりの分量があるな・・・(^^;

・Eclipseプロジェクトの作成
  1. ファイル→新規(N)→Javaプロジェクト
  2. 新規Javaプロジェクト作成ダイアログが開くのでプロジェクト名に適当なプロジェクト名を入力して「次へ」をクリック。(今回はLogAnalysisHadoopとします。)
・testソースフォルダの作成
※JUnitを用いる際のポリシーとしてテストソースをどこに格納するかという問題がつきまといます。私は別ソースフォルダに同じパッケージ構造を作成してそこにテストソースを格納します。

  1. 「新規Javaプロジェクト」ダイアログの「新規ソース・フォルダーの作成」リンクをクリック。
  2. フォルダー名に「test」と入力して「完了」をクリック。
  3. 「test」ソースフォルダーが作成される。
  4. 完了ボタンを押下するとプロジェクトがEclipseのworkspaceに追加される。


・必要ライブラリのプロジェクトへの登録
※私はこれを行き当たりばったりでやってしまいがちなのですが・・・(必要なライブラリが先に解っているとは限らないので・・)。今回、MapperをTDDで作るに当たって必要となるライブラリのみを登録します。
  • パッケージエクスプローラーで追加したプロジェクトを右クリックし「新規」→「フォルダー」をクリック
  • 「新規フォルダー」ダイアログのフォルダー名に「lib」と入力して「完了」ボタンをクリック。

  • 以下のjarファイルをlibフォルダーに追加する。
    • commons-logging-1.0.4.jar
    • hadoop-0.20.1-core.jar
    • mockito-all-1.7.jar

  • libフォルダーの下に追加されたjarファイルを右クリック、「ビルド・パス」→「ビルド・パスに追加」をクリック。


これでとりあえずの環境はできました。
ここまでで左の画像のような状態になるはずです。










追加したライブラリについて説明しておきます。
  • commons-logging-1.0.4.jar:ロギングAPIライブラリ。JDKのロギングAPIとLog4JのAPIをラップする。
  • hadoop-0.20.1-core.jar:言わずと知れたhadoopのライブラリです。

  • mockito-all-1.7.jar:モックオブジェクトライブラリ。今回のこの連載(?!)の肝です。

MapReduceをJUnitでTDDで作る(1-TODOリスト作成)

今日は、TDD(Test Driven Development)をMap/Reduceの開発に適用するにはどうするか、に焦点をあてて行きます。

次のように一般的なWebシステムのデバッグログ(hoge.log.20091214)からINFOレベルの情報のみを抜き出すロジックをMap/Reduceで組むとします。
2009-12-14 00:00:00,525 INFO hogeAccessLog @ http://hogehoge.com/index.html
2009-12-14 00:00:00,528 DEBUG hogeAccessLog @ aaa=bbb, ccc.ddd=222
2009-12-14 00:00:00,528 DEBUG hogeAccessLog @ eee=333, fff.ggg=232
2009-12-14 00:00:00,530 WARN hogeAccessLog @ http://hogehoge.com/bbb.html 404 not found!!


2009-12-14 00:00:00,525 INFO hogeAccessLog @ http://hogehoge.com/index.html

のみを抜き出す。

TDDの大まかな流れは

  • 仕様をまとめてToDoリストに記述

  • 以下を繰り返し


    • まずテストを書く

    • テストを実行する(未実装なので当然RED)

    • シンプルな実装をしてテストを通す(GREEN)

    • リファクタリング



となります。

テストから書くことで仕様を明確にし、
シンプルな実装を心がけること、テストが常に存在していることから変更に強いプログラムを書くことができるというのがメリットとされています。

ちなみにデメリットは、テストの工数が掛かることです。

では、やってみましょう。

ログの仕様:
yyyy-MM-dd hh:mm:ss,sss XXXX [PG名] @ [ログ出力内容]


要求仕様:
上記XXXX=INFOのものを抜き出して出力する。
上記より

TODOリスト

  1. 環境設定(Eclipse上にテスト環境の作成)

  2. ログファイルからXXXX=INFOのものを抽出する


今回はMapperのみで良さそうですね。

続きは次の投稿で。

2009年12月14日月曜日

/etc/hosts再び

新規インストールした環境で、処理が途中で止まってしまって進まなくなるという現象が多発しました。

これ、/etc/hostsに原因があって、

127.0.0.1 localhost localhost.localdomain
192.168.x1.y1 name
192.168.x1.y2 data1
192.168.x1.y3 data2
192.168.x1.y4 data3

っていうのを

127.0.0.1 localhost localhost.localdomain
192.168.x1.y1 name name.hoge.jp
192.168.x1.y2 data1 data1.hoge.jp
192.168.x1.y3 data2 data2.hoge.jp
192.168.x1.y4 data3 data3.hoge.jp

とフルドメイン名を含めた形に書き換えるとうまく動くようになります。

2009年12月3日木曜日

原因不明・・・

Hadoop環境を構築した時、たま~に何の誤りもないのに動かないことがあります。
そして、クラスター全体を再起動したら動く・・・謎だ・・・。

2009年11月17日火曜日

Hive楽だわ~

MapReduce用のテストデータを実データから抽出しようとしているのですが、Windows環境に持ってきて秀丸でgrepとかやってると、PC全体が重くなって使い物になりません。
ちょうどテスト環境に実験的にHiveを入れて、実験データにテストの実データを入れていたので、それを試しに使ってみた。
検索結果の件数が少ない条件を抽出したいな~なんて時は、何度も検索を実行する訳なんですが、CD-ROM一枚分くらいのデータを1分~2分足らずで抽出してくれるので楽です。(VMWare4台構成)
お試しあれ

2009年11月14日土曜日

Hadoop The Definitive Guide の邦訳版

昨日のHadoop カンファレンス Japan 2009の懇親会中
「『Hadoop The Definitive Guide』の邦訳が1月に出ます!」
というアナウンスがありました!

ようやく日本語のアナログ情報がでる~♪

Hadoop カンファレンス Japan 2009

こんにちは。yoshitsuguです。

昨日、HadoopカンファレンスJapan 2009に参加してきました。

ちょっとレポートできるほど時間の余裕がないのですが、Hadoopが、今、米国ですごくホットな技術であり、日本でもだいぶホットになってきたんだというのを実感できました。

だいたい、みなさん1年くらい前からテスト的に使い出し、今年に入って本格的に運用に持って行っている所という感じでした。

今、キャズムの一歩手前かな。キャズムを超えられるかはこれからのシステムの安定性・信頼性の確保にかかっていると思う。

詳しくはのちほど。

ではでは

2009年11月10日火曜日

SafeModeのちょいワザ

こんにちは、yoshitsuguです。

HDFS上のデータが壊れたりして、HDFSがSafeModeに入りっぱなしになり
困ったことはありませんか?
(Safe Modeだとファイルの追加・削除ができないうえ、fsck -deleteも使えません)

そんな時に一時的にSafe Modeを抜けるワザです。
(Hadoopのドキュメントに普通に書いてあるので裏技ではありません)


$HADOOP_HOME/bin/hadoop dfsadmin -safemode leave


SafeModeを抜ける必要のある作業が終わったら


$HADOOP_HOME/bin/hadoop dfsadmin -safemode enter


コマンドでSafeModeに戻すことも可能です。

※なお、-safemode enterは手動でSafeModeに入るため、SafeModeから抜けるためにはもう一度-safemode leaveコマンドを実行する必要があります。

参考資料:Apache Hadoop コマンドガイド(英語です)

2009年10月16日金曜日

Hadoopで何ができるのか

こんにちは、yoshitsuguです。

仕事でHadoopのプログラムをゴリゴリやっているわけなんですが、例えば「何か簡単なサンプルアプリを書け。word count以外で、だ。」と言われると結構困ってしまいます。

HadoopというよりMap/Reduceというアルゴリズムでできることとは、一体なんでしょうか?

ある程度DBのシステムを組んだことがある人には、
  • Map≒WHERE + GROUP BY(※集約キー指定のみ)
  • Reduce≒GROUP BY(※キーでデータが集約されてくる), COUNT(), SUM(), MAX(), MIN()などの集約関数
であるという説明が案外わかりやすい気がします。
(私の業務では、DBに近い使い方が結構多いからかもしれません)

具体例で示します。
次のプログラムはApacheのアクセスログから404アクセスしたクライアントのIPアドレスとリクエストURLを抜き出してカウントする処理です。

------- Map処理(クラス定義等は省略) ---------------
/**
 * mapメソッドです。<BR>条件を絞って key, valueの組を作ります。
 */
public void map(LongWritable key, Text value, Context context) {
    String code = getCode(value); //
    if (code.matches("404")) {
        // URLを抜き出してvalueとする。
        String url = getURL(value);
        String ip = getIP(value);
        // 出力する
        context.write(new Text(ip), new Text(url));
    }
}

------- Reduce処理(クラス定義等は省略) ---------------
/**
 * reduce処理。>br<クライアントIPアドレス毎に要求URLをカウントします。
*/
public void reduce(Text key, Iterable value, Context context) {
    Map urlMap = new HashMap();
    for (Iterator iter = value.iterator();         iter.hasNext();) {
        String url = iter.next().toString();
        Integer count = urlMap.get(url);
        if (count == null) {
            urlMap.put(url, new Integer(1));
        } else {
            urlMap.put(url, new Integer(count.intValue() + 1));
        }
    }
    StringBuilder sb = new StringBuilder();
    Iterator entries = .entrySet().iterator();
    for (Map.Entry entry : entries) {
        sb.append(entry.getKey()).append("\t").append(entry.getValue()).append("\t")
    }
    sb.setlength(sb.length - 1);
    context.write(key, sb.toString);
}


データベースの場合、集約によるCOUNT()ならば数値を加算するのみですが、Hadoopの場合は上記のように文字列を集約してさらに数えたり等いろいろな事ができるのです。

結構お客様から「データベースとの比較データを出せ」などと言われることが多いのですが、データベースの速度もIndexを張っていたりいなかったりやSQLのクエリの仕方次第で大きく変わるので、一概に比較することは難しいと思います。

やはりバッチ処理として、大量データに対するマッチング処理やキーワードを抽出して検索Index作成、ログデータ中のキーワードの頻度測定などの用途が向いていると思います。

2009年9月9日水曜日

Too many fetch-failures

こんばんは、yoshitsuguです。

Map処理は完了しているのに、Reduce処理がなかなか進みません。
で、ログを確認してみると、"Too many fetch-failures"というエラーが出ていました。

そこで、Google検索をかけて、あるページにたどり着きました。


以下、引用(邦訳:yoshitsugu)

ReducerがHDFS内でデータをコピーするのに失敗した。チェックすべきは、LinuxのネットワークとHadoopの設定だ。
(The Reducer was failed to copy data through the HDFS, what we should do is to double check your Linux network and Hadoop configuration :)
1.必要な全パラメータがhadoop-site.xmlに設定されているかどうか確認し、Hadoop内の全ノードが同じ設定であることを確認する。
(1. Make sure that all the needed parameters are configured in hadoop-site.xml, and all the worker nodes should have the same content of this file.)

2.TaskTrackerやHDFSのURIにはIPアドレスではなくホスト名を使用するべきだ。URIにIPアドレスを使用しているHadoopクラスターをいくつかみてきたが、それらはサービスを開始してJobの実行をすることはできても正常に終了したことがない。
(2. URI for TaskTracker and HDFS should use hostname instead of IP address. I saw some instances of Hadoop cluster using IP address for the URI, they can start all the services and execute the jobs, but the task never finished successfully.)

3.全ノードの/etc/hostsファイルを確認し、ホスト名とIPアドレスのバインドがなされていることを確認する(ローカルホストではない)とともに全ホストがお互いにホスト名で通信できることを確認する。
(3. Check the file /etc/hosts on all the nodes and make sure that you’re binding the host name to its network IP, not the local one (127.0.0.1), don’t forget to check that all the nodes are able to communicate to the others using their hostname.)

今回の私の環境は、見事に/etc/hostsの中身が間違っていてこれが原因だとわかりました。

2009年9月4日金曜日

HDFSがおかしいとき

こんにちは、yoshitsuguです。
hadoopを動かしていると稀にHDFSのファイルが壊れることがあります。
(特にPseudo-Distributed Operationで1台でテストしているとよく起こる)

そんなときのTipsです。

次のようなコマンドを実行すると、ファイルシステムのチェックができます。
$ $HADOOP_HOME/bin/hadoop fsck <チェック対象のpath> [-delete | -move]
# $HADOOP_HOME:hadoopのインストールディレクトリ
# -delete : 異常があるファイルを削除
# -move : 異常があるファイルを移動して隔離

それでも直らないようならば、
テンポラリファイルの削除
# rm -r /tmp/<ユーザー名>-hadoop*
と、HDFSの初期化
$ $HADOOP_HOME/bin/hadoop namenode -format

を行います。(この時、HDFSのファイルはすべて削除されてしまいます。)

2009年7月7日火曜日

Hadoop The Definitive Guide エッセンス翻訳(1)

yoshitsugu です。
Hadoopが0.20.0で大きくAPIが変わったのですが、それに関する記事を「Hadoop The Definitive Guide」で見つけたので、概訳しておきます。

Hadoop 0.20.0 リリースでは、一部で"Contextオブジェクト"と呼ばれている、将来の発展性を高めるように設計された新たなJava MapReduce APIが含まれた。新しいAPIは旧APIとの型互換性はない。そのため、新しいAPIを利用するためには今までのアプリケーションを書き換える必要がある。

新旧のAPIにはいくつか注意すべき相違点がある
  • 新APIはインターフェースよりも抽象クラスをより多く用いている。それは抽象クラスのほうがより発展させやすいからである。たとえば、抽象クラスには、クラスの古い実装を影響することなく(デフォルトの実装を備えた)メソッドを追加することができる。新APIではMapperとReducerインターフェースは抽象クラスになっている。
  • 新しいAPIは、org.apache.hadoop.mapreduceパッケージとサブパッケージに含まれている。旧APIはorg.apache.hadoop.mapredパッケージにある。
  • 新APIはContextオブジェクトの拡張的な使用法を提供している。それによってユーザーのプログラムコードとMapReduceシステムとのコミュニケーションが可能となっている。たとえば、MapContextはJobConfとOutputCollectorとReporterの役割を本質的に統一している。
  • 新APIは"push"/"pull"スタイルによる反復処理をサポートしている。新旧両方のAPIで、key-valueのレコードペアがmapperにプッシュされるが、さらに新APIではmapperでmapメソッド内からレコードを取り出すことができるようになった。同じことがreducerにも言える。"pull"スタイルがどれだけ便利であるかの例は、バッチ内のレコードを処理するのに1件1件処理するよりむしろまとめて扱ったほうが便利であるといった形で表れる。
  • 構成設定が統一された。旧APIではJobの設定に特別なJobConfオブジェクトを使用していた。それはHadoopの拡張されてないConfigurationオブジェクトの拡張であった(デーモンの構成設定で用いられていた。「Hadoop The Definitive Guide」の"構成設定API"参照(英語版 p116))。新APIではこのような特別扱いはなくなり、Jobの設定にも拡張されていないConfigurationオブジェクトを用いるようになった。
  • Jobの制御はJobClientクラスではなくJobクラスを通じて行われる。JobClientクラスはもはや新APIには存在しない。(※訳者注:非推奨扱いでは存在している)
以上。

訳におかしなところがあればご指摘ください。

yoshitsugu