また一口メモ。
hdfsadmin -report は権限が必要なさそうだが、hadoopのsuperユーザー権限が必要。
デフォルトではhadoopユーザー。
2010年10月24日日曜日
2010年5月20日木曜日
/etc/hostsの罠
皆様お久しぶりです。
会社でHadoopのテスト環境を構築していたのですが、思いっきり、はまったのでPOSTしてみます。
Hadoop周りの各種設定ファイル(core-site.xml, hdfs-site.xml, mapred-site.xmlなど)の設定は一通り完了し、テスト環境なのでファイアウォールもSELinuxもオフにした状態で、start-all.shでHadoopを起動したのですが、namenodeにdatanodeが認識されません。ちゃんとそれぞれのホストでプロセスが動作しているにも関わらず、です。
色々試していくうちに次のことに気がつきました。
127.0.0.1:9000でListenしてやがる事が判明。
/sbin/ifconfig で 192.168.xxx.yyy のethが起動していることは確認できている、ということは・・・
/etc/hostsを開くと、案の定、
/etc/hostsからnamenode.FQDNとnamenodeを取り除くと正常に動作しました。
はぁ・・・。
関連リンクぅ~~
/etc/hosts 再び
会社でHadoopのテスト環境を構築していたのですが、思いっきり、はまったのでPOSTしてみます。
Hadoop周りの各種設定ファイル(core-site.xml, hdfs-site.xml, mapred-site.xmlなど)の設定は一通り完了し、テスト環境なのでファイアウォールもSELinuxもオフにした状態で、start-all.shでHadoopを起動したのですが、namenodeにdatanodeが認識されません。ちゃんとそれぞれのホストでプロセスが動作しているにも関わらず、です。
色々試していくうちに次のことに気がつきました。
- telnet namenodeホスト名 9000 では、Namenodeプロセスに接続されない
- telnet localhost 9000 では、Namenodeプロセスに接続される
- netstat -nl
127.0.0.1:9000でListenしてやがる事が判明。
/sbin/ifconfig で 192.168.xxx.yyy のethが起動していることは確認できている、ということは・・・
- ping namenode
> PING namenode.FQDN(127.0.0.1) 56(84) bytes of data.・・・・ /etc/hosts またおのれか
> (以下略)
/etc/hostsを開くと、案の定、
127.0.0.1 namenode.FQDN namenode localhost.localdomain localhostという記述が見つかりました。大きなお世話だっつーの・・・(ToT)
/etc/hostsからnamenode.FQDNとnamenodeを取り除くと正常に動作しました。
はぁ・・・。
関連リンクぅ~~
/etc/hosts 再び
2010年1月27日水曜日
MapReduce をJUnitでTDDで作る(10 さらにテスト追加)
こんにちは、yoshitsuguです。
前回からかなり時間が空いてしまいました。
今回はさらにテストを追加してみたいと思います。
(明らかなバグがあるので)
このプログラムの仕様をおさらいすると
というものでした。
現在実装済みのテストでは、
以下の場合を考えてみましょう
では、これを試すテストを追加して実施してみましょう。
テストはこのようになります。実行結果はどうなるでしょうか?
REDになりました。
これは、LogAnalysisMapperクラスのmap()メソッド内で、INFOがvalueに含まれているものを全て出力しているからです。仕様を満たすにはどうすればいいのでしょうか?
では実際にプログラムを書き換えてやってみます。
(上記の正規表現はそのままJavaのプログラムに貼り付けると’\’がエスケープシーケンスとして判別されてコンパイルエラーとなってしまいます。プログラム中は’\\’と記述すればコンパイルエラーは回避されます。)
map処理を以下のように書き換えました。
では実行してみます。どうなるでしょうか?
見事にGREENになりました。
今日はここまでにします。
前回からかなり時間が空いてしまいました。
今回はさらにテストを追加してみたいと思います。
(明らかなバグがあるので)
このプログラムの仕様をおさらいすると
ログの仕様:
yyyy-MM-dd hh:mm:ss,sss XXXX [PG名] @ [ログ出力内容]
要求仕様:
上記XXXX=INFOのものを抜き出して出力する。
というものでした。
現在実装済みのテストでは、
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",
};
以下の場合を考えてみましょう
String[] lines = {この場合、2行目、3行目のログの種別はWARNとERRORなので、抽出されてはいけない筈です。
"2009-12-14 00:00:26,340 INFO hogehoge @ INFO=abcdefg hijklmn opqrstu",
"2009-12-14 00:00:26,341 WARN fugafuga @ INFO :11111111111111111111111",
"2009-12-14 00:00:26,341 ERROR fatal INFO = exception has occurred",
};
では、これを試すテストを追加して実施してみましょう。
/**
* 抽出する行のログの中身にINFOを含む場合
* @throws Exception 例外発生時
*/
@Test
public void testMapExtract_ComplexContents() throws Exception {
String[] lines = {
"2009-12-14 00:00:26,340 INFO hogehoge @ INFO=abcdefg hijklmn opqrstu",
"2009-12-14 00:00:26,341 WARN fugafuga @ INFO :11111111111111111111111",
"2009-12-14 00:00:26,341 ERROR fatal INFO = 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));
}
テストはこのようになります。実行結果はどうなるでしょうか?
REDになりました。
Wanted 1 time but was 3となっています。writeメソッドが1回しか呼ばれない想定なのに3度も呼ばれたということです。
これは、LogAnalysisMapperクラスのmap()メソッド内で、INFOがvalueに含まれているものを全て出力しているからです。仕様を満たすにはどうすればいいのでしょうか?
ログの仕様:とあるので、"^[\d]{4}-[\d]{2}-[\d]{2}\s[\d]{2}:[\d]{2}:[\d]{2},[\d]{3}\sINFO\s.*"の正規表現にマッチするものを出力するとすれば良さそうです。
yyyy-MM-dd hh:mm:ss,sss XXXX [PG名] @ [ログ出力内容]
要求仕様:
上記XXXX=INFOのものを抜き出して出力する。
では実際にプログラムを書き換えてやってみます。
(上記の正規表現はそのままJavaのプログラムに貼り付けると’\’がエスケープシーケンスとして判別されてコンパイルエラーとなってしまいます。プログラム中は’\\’と記述すればコンパイルエラーは回避されます。)
map処理を以下のように書き換えました。
/**
* 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)) {
if (value.toString().matches("^[\\d]{4}-[\\d]{2}-[\\d]{2}\\s[\\d]{2}:[\\d]{2}:[\\d]{2},[\\d]{3}\\sINFO\\s.*")) {
context.write(value, new LongWritable(1));
}
}
では実行してみます。どうなるでしょうか?
見事にGREENになりました。
今日はここまでにします。
2010年1月5日火曜日
あけましておめでとうございます
yoshitsuguです。
明けましておめでとうございます。
いや、仕事始めは昨日だったんだけれども。
クラウドコンピューティングがもてはやされてます。Hadoopはその中心的な技術のような扱いですね。
しかし、実は、私、Hadoopとクラウドの関係がいまいち解ってません。
大量データの処理がなんでクラウドコンピューティングと紐付くのでしょうか。
クラウドコンピューティングの大まかな定義は、
「『ネットの向こう側=雲の向こう側』で実行されるアプリケーション群を利用すること」
だと認識しています。
代表はGMail、GoogleDocumnent、GoogleAppEngine、AmazonEC2など。
広い意味ではWebMail系のWebアプリケーションも含まれるのでしょうか。
GoogleDocumentやGoogleAppEngineの裏側でGFSやBigTableやMapReduceが動いているのは理解できるのです。Yahoo.comやfacebookの裏側でHadoopが動いているのも理解できるのです。
だが、だからHadoop=Cloudだとはいえないと思うのです。
Hadoopをバックエンドで動かすシステムはいくらでも構築可能です。でもそれはクラウドコンピューティングとは違う。クラウド環境の構築は、何万アクセス何億アクセスにも耐えきれるネットワークインフラ、サーバーインフラの構築、使いやすいWebインターフェースの構築なくしてあり得ない。
いくらバックエンドにHadoopを用いても従来のままのWebサービスはクラウドコンピューティングではないのです。
日本でクラウドコンピューティングインフラをどうやって実現していくか?
世界の巨人たちに見劣りしないサービスをどうやって提供するか?
日本のIT会社がクラウドで食っていくためには、それを実現しなければお話にならないと思います。
「クラウドで食えるサービスづくり」
それが今年の私の目標です。
明けましておめでとうございます。
いや、仕事始めは昨日だったんだけれども。
クラウドコンピューティングがもてはやされてます。Hadoopはその中心的な技術のような扱いですね。
しかし、実は、私、Hadoopとクラウドの関係がいまいち解ってません。
大量データの処理がなんでクラウドコンピューティングと紐付くのでしょうか。
クラウドコンピューティングの大まかな定義は、
「『ネットの向こう側=雲の向こう側』で実行されるアプリケーション群を利用すること」
だと認識しています。
代表はGMail、GoogleDocumnent、GoogleAppEngine、AmazonEC2など。
広い意味ではWebMail系のWebアプリケーションも含まれるのでしょうか。
GoogleDocumentやGoogleAppEngineの裏側でGFSやBigTableやMapReduceが動いているのは理解できるのです。Yahoo.comやfacebookの裏側でHadoopが動いているのも理解できるのです。
だが、だからHadoop=Cloudだとはいえないと思うのです。
Hadoopをバックエンドで動かすシステムはいくらでも構築可能です。でもそれはクラウドコンピューティングとは違う。クラウド環境の構築は、何万アクセス何億アクセスにも耐えきれるネットワークインフラ、サーバーインフラの構築、使いやすいWebインターフェースの構築なくしてあり得ない。
いくらバックエンドにHadoopを用いても従来のままのWebサービスはクラウドコンピューティングではないのです。
日本でクラウドコンピューティングインフラをどうやって実現していくか?
世界の巨人たちに見劣りしないサービスをどうやって提供するか?
日本のIT会社がクラウドで食っていくためには、それを実現しなければお話にならないと思います。
「クラウドで食えるサービスづくり」
それが今年の私の目標です。
2009年12月28日月曜日
MapReduceをJUnitでTDDで作る(9 さらにリファクタリング)
こんばんは、yoshitsuguです。もう、年の瀬ですね。
2009年は皆さんにとってどんな1年だったでしょうか?
私は怒濤のように過ぎていった1年でした。
さてさて、
前回、テストを追加して、JUnitによるテストがGREENになるところまで実行しました。
TDDの開発は、RED→GREEN→Refactoringの繰り返しであるというのは最初の頃に述べました。
ということで、再びリファクタリングしましょう。
LogAnalysisMapper.java
Eclipseのリファクタリング機能でpublic定数化してしまいましょう。
/** ログ種別:情報 */
public static final String INFO = "INFO";
このように定数ができました。public定数化した理由は、以下INFOを用いる際にはこのクラスを参照させればよくなるからです。(それが他のクラスに強く依存している場合は除きます)
続いてテストクラスもリファクタリングしてしまいましょう。
TestLogAnalysisMapper.java
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
ではでは、よいお年をお迎えください!!
2009年は皆さんにとってどんな1年だったでしょうか?
私は怒濤のように過ぎていった1年でした。
さてさて、
前回、テストを追加して、JUnitによるテストがGREENになるところまで実行しました。
TDDの開発は、RED→GREEN→Refactoringの繰り返しであるというのは最初の頃に述べました。
ということで、再びリファクタリングしましょう。
LogAnalysisMapper.java
package jp.co.littel.hadoop;このプログラムで気になるのは、"INFO"というリテラル値がハードコーディングされている点ですね。
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));
}
}
}
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;TestLogAnalysisMapper.java
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));
}
}
}
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を含む行が来ることが予想されるので、追加するテストは、次のようなテストデータを用いればいいと予想できます。
さらに、writeの呼び出しは一度しか行われない筈なので、次のverifyを追加しましょう。
テストメソッド名は testMapExtract_ComplexRows()にしてみました。しかし、anyがimportされてないので、コンパイルが通りません。次のstatic import文を追加して下さい。
テストメソッドは次のようになります。
では、実行してみましょう。REDになるはずなのですが・・・。
GREENになってしまいました。
これはおかしいですね。REDになるはずなのにGREENになってしまったということは、テストがどこかおかしいと言うことです。(GREENの理由を明確に説明できるのならばいいのですが、今回はREDを予想していたので明らかにおかしい)
GREENになってしまったということは、context.write();が一度しか呼ばれていないということです。
今のmap()メソッド内では無条件にcontext.write()を呼んでいるので、つまりmapメソッドが一度しか呼ばれていないと言うことですね。
つまり、map()メソッドを引数を変えて複数回呼べばREDになるはずですね。
ということで以下のように変更します。
全行をテストで呼ぶように変更しました。
では気を取り直して、テストを再実行してみます。
ようやく想定通り、REDになりました。
(一つ目はGREENのままです。これも想定通りです)
失敗内容を確認してみましょう。
「想定は1回なのに3回呼ばれた」と言っています。これは想定通りの結果です。
(テストデータは3行なのでmap()メソッドは3回呼ばれる。map()メソッド内部では無条件にcontext.write()メソッドを呼び出すので3回呼ばれてしまう)
では、ソースを修正して、REDをGREENにします。
INFOの場合のみ取り出せばいいので、
でよさそうです。
(優秀な開発者の方はこの時点でバグを見つけられると思います。またテストを追加してそれを明らかにしていこうと思います。)
では、実行してみます。
おめでとうございます。すべてGREENになりました!
GREENになったらリファクタリングです。
リファクタリングは次回に。。。
最近のアメリカではHappy Holidays!!と言うのが一般的だそうです。
12/25で年始まで休日か・・うらやましい(ぉぃ)
閑話休題。前回で、リファクタリングを行い、
valueに与えられた文字列をそのままmapperがwriteするようにしました。
しかし、これでは仕様と異なることが明白です。
常にINFOを含む行が来るという仮定ならば、これでよいのですが、そうとは限りません。
では、実際にテストしてREDとなることを確認しましょう。
INFOを含む行だけでなく、WARNINGを含む行やERRORを含む行が来ることが予想されるので、追加するテストは、次のようなテストデータを用いればいいと予想できます。
String[] lines = {また、INFOを含む行のみが抽出されるはずなので、context.writeの呼び出しは次のようになるはずです。
"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",
};
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) {せっかく複数行のテストデータを用意したのに最初の1行しかテストに用いてなかったようです。
mapper.map(null, new Text(line), context);
}
全行をテストで呼ぶように変更しました。
では気を取り直して、テストを再実行してみます。
ようやく想定通り、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のリファクタリング)
前回からだいぶ経ってしまいました。
今日は冬至ですね。
かぼちゃとゆず湯が私を待っている・・・。
リファクタリングに入ります。
要リファクタリング箇所
Eclipseの「問題」ビューを見てみましょう。
これらの原因は実は同じで、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してしまいましょう。
ちゃんとGREENになりました。リファクタリング成功です!!
ということで今回はこの辺で。
今回までのソースです。
TestLogAnalysisMapper.java
LogAnalysisMapper.java
今日は冬至ですね。
かぼちゃとゆず湯が私を待っている・・・。
リファクタリングに入ります。
要リファクタリング箇所
- 警告が色々出ている
- mapperが固定値のハードコーディングでcontext.write()を読んでいる。
Eclipseの「問題」ビューを見てみましょう。
Mapper.Context は raw 型です。総称型 Mapperという警告がLogAnalysisMapper.javaとTestLogAnalysisMapper.javaに出ています。また、.Context への参照は、パラメーター化する必要があります。
型の安全性: メソッド write(Object, Object) は raw 型 TaskInputOutputContext に属しています。総称型 TaskInputOutputContextという警告がLogAnalysisMapper.javaとTestLogAnalysisMapper.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-ちょっとおさらい)
いつまで続くんだろう、このシリーズ・・・
今回は、リファクタリングの予定を変更して予定の確認と振り返りをしたいと思います。
今後のロードマップです。
少しコーヒーブレーク。今までを振り返りたいと思います。
INFOというログをログファイルから抽出するプログラムをMap(/Reduce)で作成しようとしています。
まず、実装より先にテストを作りました。
テストの内容は、「context.write();メソッドがある引数で1度だけ呼ばれればOK」というものでした。
次に実際のMapperクラスにコーディングしました。(リテラル値のハードコーディングですが)
そうしてようやくテストがOKになりました。
では、そもそも何でこのテストになったのでしょうか?
Map/Reduceを作成する上でネックとなるのは、「デバッグがしにくい」という点が挙げられます。
ちょっと反省として、
「"INFO"というログを抽出する」という仕様と、今回のテスト仕様がどう繋がるのかわかりづらいかな、というのがあります。
まず、テストデータとして
ということで、
Mapperの入力はKEY=LongWritable, VALUE=Text、出力はKey=LongWritable, VALUE=TextKey=Text, VALUE=LongWritableとしました。出力のKeyには行番号が連番で振られます。出力のVALUEはログの実際の抽出対象となった行の文字列です。出力のKeyはログの実際の抽出対象となった行の文字列です。出力のVALUEは1です。
この辺りの仕様をどうするかは、設計者に委ねられています。UnitTestはあくまで「プログラム仕様を満たしているかどうかをテストする」もので、業務仕様を満たしているかのテストではないのです。そこは、結合テストを実施して出力結果を検討し、業務仕様を満たしている結果になっているか確認する必要があります。(その前にUnitTestのレビューを行い、業務仕様に沿ったプログラム仕様でUnitTestが作られているかを確認するのがよいでしょう)
閑話休題。
次のリファクタリングでは、グリーンのままでどのようにリファクタリングをやっていくか、を説明したいと思います。明日時間があれば・・・。
今回は、リファクタリングの予定を変更して予定の確認と振り返りをしたいと思います。
今後のロードマップです。
- Mapper作成(リファクタリング)
- Mapper作成(テスト追加)
- Mapper作成(レッド→グリーン→リファクタリング)
- Driverのテスト(スタンドアローンモードについて)
- Driverのテスト(テスト実行)
- まとめ
少しコーヒーブレーク。今までを振り返りたいと思います。
INFOというログをログファイルから抽出するプログラムをMap(/Reduce)で作成しようとしています。
まず、実装より先にテストを作りました。
テストの内容は、「context.write();メソッドがある引数で1度だけ呼ばれればOK」というものでした。
次に実際のMapperクラスにコーディングしました。(リテラル値のハードコーディングですが)
そうしてようやくテストがOKになりました。
では、そもそも何でこのテストになったのでしょうか?
Map/Reduceを作成する上でネックとなるのは、「デバッグがしにくい」という点が挙げられます。
- データが大量であるため、デバッグログを吐いてもどれがどのログかわかりづらい
- 分散環境で実行するので、デバッグログを吐いてもどれがどのログかわかりづらい
- HadoopをWindows環境で動かしにくい
ちょっと反省として、
「"INFO"というログを抽出する」という仕様と、今回のテスト仕様がどう繋がるのかわかりづらいかな、というのがあります。
まず、テストデータとして
String[] lines = {を用意しています。これには"INFO"が含まれているので、これをMapperのmap処理に渡せば抽出対象としてcontext.writeを呼び出してくれるはずです。
"2009-12-14 00:00:26,340 INFO hogehoge @ abcdefg hijklmn opqrstu"
};
ということで、
verify(context, new Times(1)).write(new Text(lines[0]), new LongWritable(1));をテストコードとして記述しました。
Mapperの入力はKEY=LongWritable, VALUE=Text、出力は
この辺りの仕様をどうするかは、設計者に委ねられています。UnitTestはあくまで「プログラム仕様を満たしているかどうかをテストする」もので、業務仕様を満たしているかのテストではないのです。そこは、結合テストを実施して出力結果を検討し、業務仕様を満たしている結果になっているか確認する必要があります。(その前にUnitTestのレビューを行い、業務仕様に沿ったプログラム仕様でUnitTestが作られているかを確認するのがよいでしょう)
閑話休題。
次のリファクタリングでは、グリーンのままでどのようにリファクタリングをやっていくか、を説明したいと思います。明日時間があれば・・・。
MapReduceをJUnitでTDDで作る(5-Mapper作成(3))
前回までで、テストを失敗させるところまで来ました。
ここまでのソースコードです。
前回のエラーの内容を振り返ってみると、
とありました。
「context.writeメソッドが1, とlines[0]で呼ばれるってテストには書いてあるんだけど、呼ばれなかったよ?」と言っています。
ということは、テストをグリーンにするには、context.writeメソッドを呼び出す仕組みを作れば良さそうです。
つまり、Mapper.mapメソッドを呼び出さねば!
Mapper.map(key, value, context)メソッドの呼び出しは次のように行います。
早速、ソースコードのtestMapExtract()メソッドのverifyメソッドの手前に追加しましょう。
もちろんコンパイルエラーになりますね。LogAnalysisMapperクラスなんて存在しないからです。
Eclipseのクイックフィックス機能でクラスを追加しましょう。[Ctrl]+[1]キーを押下して下さい。
「クラス'LogAnalysisMapper'を作成します」をクリックして下さい。
「新規」ダイアログが表示されるので、
で「完了」ボタンを押下します。
LogAnalysisMapper.javaファイルが生成されました。が、まだコンパイルエラーはなくなっていません。
LogAnalysisMapper.javaをEclipseのJavaエディタで開き、右クリックして、「ソース」→「メソッドのオーバーライド/実装」をクリックしましょう。
mapメソッドにチェックを入れて、「OK」ボタンをクリックします。
クラス内にmapメソッドが追加されます。
コンパイルエラーが解消されたので、実行してみます。
またレッド(失敗)になりました。前回とは少し異なるようです。
今度は、「呼ばれたけど引数が違う!」というエラーのようです。
さきほど作成したメソッドの中でスーパークラスの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 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メソッド内で
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になりましたが、エラーが発生しています。
先ほどnullで初期化したのがまずかったようです。
とあるので、nullではなくモックインスタンスで初期化すればいいと解ります。contextの初期化の所を
と書き換え、
というimport static文を追加しましょう。
再びコンパイルが通りました。実行してみます。
またREDになりました。今度は、ちゃんとテストが失敗したようです。

TDDの流れを思い出して下さい。
レッド(失敗)→グリーン(成功)→リファクタリングを繰り返すとあった筈です。
ようやくその最初までたどり着きました。
(同じレッドでも意図していない実行時エラーのレッドはTDDの流れには含みません)
では、なぜ、わざわざテストを実装前に走らせて一度失敗させるのでしょうか?
その目的は
「実装前に失敗させることで、テストの対象があっている事を確認するため」
です。
実装していないのにテスト結果がグリーン(成功)になるのは明らかにおかしいと解ります。テストの対象が間違っているかテストケースが間違っている可能性があると考えられるのです。
では、今回はここまでにします。
では、コンパイルが通ったので実行してみましょう。
パッケージエクスプローラー上で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なので、まずはテストケースから作成します。
まずはテストを作成しましょう。
「待てぇ~い!コンパイルが全然通らんではないか。」というお叱りの声が聞こえてきそうです。
TDDは、まず「何をテストしているのか」を意識します。まず、何をテストするのかを書くのです。コンパイルを通すのはその後です。
「ちょっと待て、テストから書くのならJUnitで見慣れたassertEqualsなどのメソッドがあるはずだろう?どこにも見あたらないぞ?」
ここが今回の肝、モックオブジェクトライブラリ mockitoです。
に注目して下さい。
ここで、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がコンパイルエラーになるはずです。
[Ctrl]+[1]キーでorg.apache.hadoop.mapreduce.Mapperのimport文を追加しましょう。
まだverifyがコンパイルエラーになっていますね。
これはEclipse任せでは解消できないので次のimport static文を追加して下さい。
そうすると、再びcontextが初期化されてないのでコンパイルエラーになると思います。
とりあえず、context = nullで初期化しましょう。
ここまでで、コンパイルが一通り通りました。
今回はここまでにします。
今回はいよいよプログラムの作成に入ります。
TDDなので、まずはテストケースから作成します。
- Eclipseのパッケージエクスプローラー上で、testフォルダーで右クリック、「新規」→「JUnitテスト・ケース」をクリック。
- 「新規JUnit テスト・ケース」ダイアログで「新規 JUnit 4 テスト」を選択、パッケージにパッケージ構造を入力。今回は「jp.co.littel.hadoop」と入力しました。
- 名前に「TestLogAnalysisMapper」を入力。「setUp」と「tearDown」、お好みで「コメントの生成」にチェックを入れ「完了」ボタンをクリックします。
- 「JUnit4がビルド・パスにありません。追加しますか?」と聞かれるので、「次のアクションを実行」が選択され、「JUnit4ライブラリをビルド・パスに追加」が選択されていることを確認して「OK」ボタンをクリック。
- 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リスト
TODOリストには1行で軽く『環境設定(Eclipse上にテスト環境の作成)』と書きましたが、実はやらなければならないことがたくさんてんこ盛りです。EclipseのインストールやHadoop等各種ライブラリのダウンロードは省きます。
ちなみに開発環境はWindowsです。(私のはXPですがVistaや7でも問題ありません)
Windows上でHadoopが動くのか?JUnitを使えば動くんです。
TODOリスト in 環境設定
・Eclipseプロジェクトの作成
※JUnitを用いる際のポリシーとしてテストソースをどこに格納するかという問題がつきまといます。私は別ソースフォルダに同じパッケージ構造を作成してそこにテストソースを格納します。
・必要ライブラリのプロジェクトへの登録
※私はこれを行き当たりばったりでやってしまいがちなのですが・・・(必要なライブラリが先に解っているとは限らないので・・)。今回、MapperをTDDで作るに当たって必要となるライブラリのみを登録します。
これでとりあえずの環境はできました。
ここまでで左の画像のような状態になるはずです。
追加したライブラリについて説明しておきます。
TODOリスト
- 環境設定(Eclipse上にテスト環境の作成)
- ログファイルからXXXX=INFOのものを抽出する
TODOリストには1行で軽く『環境設定(Eclipse上にテスト環境の作成)』と書きましたが、実はやらなければならないことがたくさんてんこ盛りです。EclipseのインストールやHadoop等各種ライブラリのダウンロードは省きます。
ちなみに開発環境はWindowsです。(私のはXPですがVistaや7でも問題ありません)
Windows上でHadoopが動くのか?JUnitを使えば動くんです。
TODOリスト in 環境設定
- Eclipseプロジェクトの作成
- testソースフォルダの作成
- 必要ライブラリのプロジェクトへの登録
・Eclipseプロジェクトの作成
- ファイル→新規(N)→Javaプロジェクト
- 新規Javaプロジェクト作成ダイアログが開くのでプロジェクト名に適当なプロジェクト名を入力して「次へ」をクリック。(今回はLogAnalysisHadoopとします。)
※JUnitを用いる際のポリシーとしてテストソースをどこに格納するかという問題がつきまといます。私は別ソースフォルダに同じパッケージ構造を作成してそこにテストソースを格納します。
- 「新規Javaプロジェクト」ダイアログの「新規ソース・フォルダーの作成」リンクをクリック。
- フォルダー名に「test」と入力して「完了」をクリック。
- 「test」ソースフォルダーが作成される。
- 完了ボタンを押下するとプロジェクトが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で組むとします。
のみを抜き出す。
TDDの大まかな流れは
となります。
テストから書くことで仕様を明確にし、
シンプルな実装を心がけること、テストが常に存在していることから変更に強いプログラムを書くことができるというのがメリットとされています。
ちなみにデメリットは、テストの工数が掛かることです。
では、やってみましょう。
ログの仕様:
要求仕様:
TODOリスト
今回はMapperのみで良さそうですね。
続きは次の投稿で。
次のように一般的な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リスト
- 環境設定(Eclipse上にテスト環境の作成)
- ログファイルからXXXX=INFOのものを抽出する
今回はMapperのみで良さそうですね。
続きは次の投稿で。
2009年12月14日月曜日
/etc/hosts再び
新規インストールした環境で、処理が途中で止まってしまって進まなくなるという現象が多発しました。
これ、/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日木曜日
2009年11月17日火曜日
Hive楽だわ~
MapReduce用のテストデータを実データから抽出しようとしているのですが、Windows環境に持ってきて秀丸でgrepとかやってると、PC全体が重くなって使い物になりません。
ちょうどテスト環境に実験的にHiveを入れて、実験データにテストの実データを入れていたので、それを試しに使ってみた。
検索結果の件数が少ない条件を抽出したいな~なんて時は、何度も検索を実行する訳なんですが、CD-ROM一枚分くらいのデータを1分~2分足らずで抽出してくれるので楽です。(VMWare4台構成)
お試しあれ
ちょうどテスト環境に実験的にHiveを入れて、実験データにテストの実データを入れていたので、それを試しに使ってみた。
検索結果の件数が少ない条件を抽出したいな~なんて時は、何度も検索を実行する訳なんですが、CD-ROM一枚分くらいのデータを1分~2分足らずで抽出してくれるので楽です。(VMWare4台構成)
お試しあれ
2009年11月14日土曜日
Hadoop The Definitive Guide の邦訳版
昨日のHadoop カンファレンス Japan 2009の懇親会中
「『Hadoop The Definitive Guide』の邦訳が1月に出ます!」
というアナウンスがありました!
ようやく日本語のアナログ情報がでる~♪
「『Hadoop The Definitive Guide』の邦訳が1月に出ます!」
というアナウンスがありました!
ようやく日本語のアナログ情報がでる~♪
Hadoop カンファレンス Japan 2009
こんにちは。yoshitsuguです。
昨日、HadoopカンファレンスJapan 2009に参加してきました。
ちょっとレポートできるほど時間の余裕がないのですが、Hadoopが、今、米国ですごくホットな技術であり、日本でもだいぶホットになってきたんだというのを実感できました。
だいたい、みなさん1年くらい前からテスト的に使い出し、今年に入って本格的に運用に持って行っている所という感じでした。
今、キャズムの一歩手前かな。キャズムを超えられるかはこれからのシステムの安定性・信頼性の確保にかかっていると思う。
詳しくはのちほど。
ではでは
昨日、HadoopカンファレンスJapan 2009に参加してきました。
ちょっとレポートできるほど時間の余裕がないのですが、Hadoopが、今、米国ですごくホットな技術であり、日本でもだいぶホットになってきたんだというのを実感できました。
だいたい、みなさん1年くらい前からテスト的に使い出し、今年に入って本格的に運用に持って行っている所という感じでした。
今、キャズムの一歩手前かな。キャズムを超えられるかはこれからのシステムの安定性・信頼性の確保にかかっていると思う。
詳しくはのちほど。
ではでは
2009年11月10日火曜日
SafeModeのちょいワザ
こんにちは、yoshitsuguです。
HDFS上のデータが壊れたりして、HDFSがSafeModeに入りっぱなしになり
困ったことはありませんか?
(Safe Modeだとファイルの追加・削除ができないうえ、fsck -deleteも使えません)
そんな時に一時的にSafe Modeを抜けるワザです。
(Hadoopのドキュメントに普通に書いてあるので裏技ではありません)
SafeModeを抜ける必要のある作業が終わったら
コマンドでSafeModeに戻すことも可能です。
※なお、-safemode enterは手動でSafeModeに入るため、SafeModeから抜けるためにはもう一度-safemode leaveコマンドを実行する必要があります。
参考資料:Apache Hadoop コマンドガイド(英語です)
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 コマンドガイド(英語です)
登録:
投稿 (Atom)