libhdfsのビルド仕方@64bit環境(hadoo-0.18.3)

libhdfsはC言語からHDFSへアクセスするためのライブラリです。が、ちょっとコンパイルでハマったのでメモしておきます。32bit環境だと問題なくビルドできるかも知れませんが、実験はしていません。また、これは0.18.3(現時点でのstable release)での話で、最新のバージョンでは直っていそうな雰囲気です(http://issues.apache.org/jira/browse/HADOOP-3344)。

libhdfsをビルドするための準備として環境変数JAVA_HOMEをexportしておく必要があります。これは$(HADOOP_HOME)/conf/hadoop-env.shに書かれているJAVA_HOMEと同一のもので大丈夫です。次にHADOOP_HOMEディレクトリで以下のコマンドを実行します。

$ ant compile-libhdfs -Dlibhdfs=1

本当はこれでビルドできるらしいのですが、以下のようなエラーがでました。

compile-libhdfs:
    [mkdir] Created dir: /home/nobu/hadoop-0.18.3/build/libhdfs
     [exec] gcc -g -Wall -O2 -fPIC -m32 -I/usr/lib/jvm/java-6-sun/include -I/usr/lib/jvm/java-6-sun/include/linux -c hdfs.c -o /home/nobu/hadoop-0.18.3/build/libhdfs/hdfs.o
     [exec] In file included from /usr/include/features.h:354,
     [exec]                  from /usr/include/sys/types.h:27,
     [exec]                  from hdfs.h:22,
     [exec]                  from hdfs.c:19:
     [exec] /usr/include/gnu/stubs.h:7:27: error: gnu/stubs-32.h: No such file or directory
     [exec] make: *** [/home/nobu/hadoop-0.18.3/build/libhdfs/hdfs.o] Error 1

libhdfsはそのままだと-m32付きでビルドされてしまいます。なので、$(HADOOP_HOME)/src/c++/libhdfs/Makefile内の-m32を手動で-m64に置き換えます。そして、再度上記のantをHADOOP_HOMEにて実行します。警告が何個か出ますが一応ビルドに成功し、$(HADOOP_HOME)/build/libhdfsに.soなどが出力されます。

自分のアプリケーションでlibhdfsをリンクしたい場合は、LD_LIBRARY_PATHなどで

  • $(JAVA_HOME)/jre/lib/$(OS_ARCH)/server (OS_ARCHはamd64など)
  • $(HADOOP_HOME)/build/libhdfs

にパスを通しておき、"-lhdfs"をつけてコンパイルすればOKです。上のパスはlibjvm.so用です。

検索に苦労したので一応エラーメッセージなどをそのまま載せています。関係のない話でたどり着いてしまった人はごめんなしあ。


そしてantの使い方が良くわからんよ!!

$ ant compile-libhdfs -Dlibhdfs=1

なんでこのように二重にフラグを立てているような感じになっているのでしょうか(compile-libhdfsだけで良さそうな気がしてしまう)。教えて偉い人。

libhdfsを使う(その0)

コンパイルから実行までに1時間くらいかかったわけですが、95%はCLASSPATHの効率的な指定方法を調査していた時間ですw 以下、pathは適当に自分の環境に合うように置き換えて読んでください。

libhdfsを使ったソースファイルは以下のようにコンパイルします。Linuxに詳しくないので、詳しい人はもっとちゃんとした方法を教えてください。まず、LD_LIBRARY_PATHをexportしておきます。

export LD_LIBRARY_PATH=/usr/lib/jvm/java-6-sun/jre/lib/amd64/server/:$(HADOOP_HOME)/build/libhdfs/

次にlibhdfsをリンクしつつコンパイルします。

$ g++ -L$(HADOOP_HOME)/build/libhdfs -lhdfs その他のオプションなど

makeを使えば軽減されますがめんどくさす。

コンパイルしたものは以下のように実行します。LD_LIBRARY_PATHは前述のものをあらかじめexportしておくか、以下のように指定しつつ実行します。

$ LD_LIBRARY_PATH=/usr/lib/jvm/java-6-sun/jre/lib/amd64/server/:$(HADOOP_HOME)/build/libhdfs/ ./a.out

これで実行されれば良いのですが、libhdfsのためにhadoopの各種.jarを指定したCLASSPATHを与えなければ動きません(http://hadoop.apache.org/core/docs/r0.20.0/libhdfs.html:ここを参照)。そこでCLASSPATHを設定しなければならないわけですが…。

CLASSPATHに含めるものは良く分からなかったので、とりあえず$(HADOOP_HOME)と$(HADOOP_HOME)/libに含まれているjarファイルをすべてCLASSPATHに含めることにしました。lsしてみると分かりますが結構な数ですね非常に面倒くさいです。javaコマンドで起動する場合はワイルドカード指定が可能なようですが、環境変数として指定する場合はワイルドカードは使えないようです。Javaに詳しい方、なんか良い方法があったら教えて欲しいです(「eclipse使え」以外で)。

まあそんなわけで以下のようなものをexportしたら無事に動きました。

export CLASSPATH=$(HADOOP_HOME)/hadoop-0.18.3-core.jar:$(HADOOP_HOME)/hadoop-0.\
18.3-tools.jar:$(HADOOP_HOME)/lib/commons-cli-2.0-SNAPSHOT.jar:$(HADOOP_HOME)/l\
ib/commons-codec-1.3.jar:$(HADOOP_HOME)/lib/commons-httpclient-3.0.1.jar:$(HADO\
OP_HOME)/lib/commons-logging-1.0.4.jar:$(HADOOP_HOME)/lib/commons-logging-api-1\
.0.4.jar:$(HADOOP_HOME)/lib/commons-net-1.4.1.jar:$(HADOOP_HOME)/lib/jets3t-0.6\
.0.jar:$(HADOOP_HOME)/lib/jetty-5.1.4.jar:$(HADOOP_HOME)/lib/junit-3.8.1.jar:$(\
HADOOP_HOME)/lib/kfs-0.1.3.jar:$(HADOOP_HOME)/lib/log4j-1.2.15.jar:$(HADOOP_HOM\
E)/lib/oro-2.0.8.jar:$(HADOOP_HOME)/lib/servlet-api.jar:$(HADOOP_HOME)/lib/slf4\
j-api-1.4.3.jar:$(HADOOP_HOME)/lib/slf4j-log4j12-1.4.3.jar:$(HADOOP_HOME)/lib/x\
mlenc-0.52.jar

/(^o^)\

libhdfsを使う

libhdfsのAPIは$(HADOOP_HOME)/libhdfs/docs/apiに詳しいドキュメントがあるので、そこを見ながらやると簡単です。サンプルが必要ないくらいに色々書いてあります。ただちょっと良く分からなかった点があったので、いくつかメモを残しておきます。

Connect, Disconnect

HDFSへはhdfsConnectを使って接続します。この関数にはhost,portを与えます。サンプルプログラムでは"default",0を指定していますが、このように指定するとローカルファイルシステムが使用されました。HDFSのhost,portを指定したらちゃんとHDFSが使用されました。

hdfsConnectで作成したhdfsFSはhdfsDisconnectで解放します。

相対パスの扱われ方

libhdfsで関数に相対パスを与えると、デフォルトでは/user/ユーザー名/からの相対パスとして解釈されるようです。この挙動はhdfsSetWorkingDirectoryという関数で変更可能です。現在のWorkingDirectoryはhdfsGetWorkingDirectoryで取得可能です。

hdfsSetWorkingDirectory(fs, "");

のように空のものを指定すると次のようなエラーメッセージをstderrに吐きました。

Exception in thread "main" java.lang.IllegalArgumentException: Can not create a Path from an empty string
        at org.apache.hadoop.fs.Path.checkPathArg(Path.java:82)
        at org.apache.hadoop.fs.Path.<init>(Path.java:90)
Can't construct instance of class org.apache.hadoop.fs.Path for

エラーメッセージは出力されましたが、hdfsSetWorkingDirectoryは普通に-1を返すだけでプログラムは終了しませんでした。その後fsを使いまわしても特になんの問題もありませんでした。

hdfsFileInfo

hdfsGetPathInfoやhdfsListDirectoryが返すhdfsFileInfoの中身が謎だったので調査してみました。mKind以外は名前通りなので省略。

mKindは良く分かりませんが、ファイルだと70、ディレクトリだと68でした。二進法に直すとそれぞれ1000110と1000100です。謎。ドキュメントにはhdfsFileInfoについて詳しくかかれていません。mKindはtObjectKind型(enum)らしいです。tObjectKindについてドキュメントには

typedef enum tObjectKind tObjectKind

port

ソースを読めという暗黙的ですが親切なメッセージが書いてあります。見てみました。

    typedef enum tObjectKind {
        kObjectKindFile = 'F',
        kObjectKindDirectory = 'D',
    } tObjectKind;

/(^o^)\

さっきのportという謎のメッセージは、下のように書いたせいで出力されていたようです。

    typedef int32_t   tSize; /// size of data for read/write io ops             
    typedef time_t    tTime; /// time type                                      
    typedef int64_t   tOffset;/// offset within the file                        
    typedef uint16_t  tPort; /// port                                           
    typedef enum tObjectKind {
        kObjectKindFile = 'F',
        kObjectKindDirectory = 'D',
    } tObjectKind;

doxygenコメントって、"//!<"こう書かないと次の行に対するコメントになってしまうのでは。

ちなみに、mNameは次のようなhdfs://から始まる絶対パスでした。

hdfs://localhost:50000/user/nobu/test/test.txt

ファイルへの書き込み

hdfsOpenFileでファイルを開いてそれに書き込む形になります。

hdfsFile hdfsOpenFile(hdfsFS fs,
                      const char* path,
                      int flags,
                      int bufferSize,
                      short replication,
                      tSize blocksize
                      )  	

これは通常のファイル関係のAPIと同じで、ファイル名や読み書きフラグなどを引数に与えます。それに加えてHDFS上でのブロックサイズやレプリケーション数なども与えることができるようです。この辺は0を指定するとデフォルトの値が使用されます。

そんで読み書きフラグがちょっと気になったのでhdfs.hを見てみたのですが、

#ifndef O_RDONLY
#define O_RDONLY 1
#endif

#ifndef O_WRONLY
#define O_WRONLY 2
#endif

となっていました。fcntl.hでは

#define O_RDONLY             00
#define O_WRONLY             01

こうなってます。これはさすがにヤバいですね…。既に問題になっていたようです。でも、hdfs.hではfcntl.hを先にincludeしているので、LinuxではO_RDONLYは0, O_WRONLYは1になるそうです。それもそれでヤバいのではw まあLinuxだけで使っている分には大丈夫そうですね。とりあえず下手に自分でaliasなどを作らない方が良さそうです。

それでは本題のファイル書き込み。hdfsWriteを使って書き込みます。書き込んだ結果はhdfsFlushかhdfsCloseを呼び出すと反映されます。既存のファイルを書き込みモードで開くとO_TRUNC的な挙動をします。追記はできません。追記はできませんが、hdfsWriteの呼び出しは何度かに分けて行うことができます。

hdfsWrite(fs, file, "test\n", 5);
hdfsWrite(fs, file, "test2\n", 6);
hdfsCloseFile(fs, file);

次のように、hdfsFlushを複数回呼んでも大丈夫でした。

hdfsWrite(fs, file, "test\n", 5);
hdfsFlush(fs, file);
hdfsWrite(fs, file, "test2\n", 6);
hdfsFlush(fs, file);
hdfsCloseFile(fs, file);

さらに、hdfsCloseFileを呼び出さずにプログラムを終了しても大丈夫でした(絶対やらないけど)。さすが。追記ができないのであまり嬉しくはないですが、書き込み途中に不慮の事故などでプログラムが落ちてしまっても大丈夫そうです。

追記

正確には、HDFS上へ変更が反映されるのはhdfsCloseFileを呼び出したときだけかもしれないです。詳しくは分からず。サーセン。とりあえず書き込み中に情報を取得したりすることはあまりないと思うので大丈夫かな。