2010年10月24日日曜日

hdfsadminコマンドの実行と権限

また一口メモ。

hdfsadmin -report は権限が必要なさそうだが、hadoopのsuperユーザー権限が必要。
デフォルトではhadoopユーザー。

HDFSのPATHに関する制限

まさにメモなのですが・・・
ソースコード読んでいて見つけたのでメモしておきます。

パスの文字数制限:8000文字
パスの階層:1000

これだけ。

2010年5月20日木曜日

/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プロセスに接続される
DNSに問題はないし、9000ポートでListenしてるプロセスはいるわけだから・・・

  • 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です。
前回からかなり時間が空いてしまいました。

今回はさらにテストを追加してみたいと思います。
(明らかなバグがあるので)

このプログラムの仕様をおさらいすると

ログの仕様:
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 = {
    "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",
};
この場合、2行目、3行目のログの種別はWARNとERRORなので、抽出されてはいけない筈です。
では、これを試すテストを追加して実施してみましょう。

    /**
     * 抽出する行のログの中身に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に含まれているものを全て出力しているからです。仕様を満たすにはどうすればいいのでしょうか?


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

要求仕様:
上記XXXX=INFOのものを抜き出して出力する。
とあるので、"^[\d]{4}-[\d]{2}-[\d]{2}\s[\d]{2}:[\d]{2}:[\d]{2},[\d]{3}\sINFO\s.*"の正規表現にマッチするものを出力するとすれば良さそうです。

では実際にプログラムを書き換えてやってみます。
(上記の正規表現はそのまま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会社がクラウドで食っていくためには、それを実現しなければお話にならないと思います。

「クラウドで食えるサービスづくり」
それが今年の私の目標です。

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になったらリファクタリングです。
リファクタリングは次回に。。。