たごもりすメモ

コードとかその他の話とか。

Hoopの性能を確認してみたらもうlibhdfsとかオワコンでHoop使えって結果になった

前に書いた エントリ の通りHoopが有望な感じだったんだけどどのくらいの性能が出るのか見てみないことには本番投入して性能出ませんでした乙、ということになりかねない。ので見てみた。

なお検証に関係する環境としては以下の通り。ちなみに前はCDH3u1で試してたけど、今回はCDH3u2 (JDK6u29) on CentOS5。メモリが問題になることは全くないので全て省略。

  • ベンチ用サーバ
  • データ中継サーバ (deliver)
  • Hadoop NameNode (+JobTracker) (namenode)
  • Hadoop DataNode + TaskTracker x9

Hoopを起動したのはデータ中継サーバとNameNodeの2ヶ所で、それぞれに対して試してみた。上記サーバはすべて別のラックにありGbEで接続されている。Hadoopクラスタの各ノードはNameNodeと同一および隣のラックに配置。

なお データ中継サーバ(deliver)はこのテスト中も scribed が走っていて、やってくるログをhdfs(scribed with libhdfs)に書き込んでいる。が、scribedによるCPU負荷やネットワーク使用率が問題になることはあるまいということで、そのまま実施した。(実際に、多分問題なかった。)

書き込み/読み込みのスループット

とりあえずナイーブな書き込みおよび読み込みについて、hadoop fsコマンドと較べてどのくらいの数字差が出るか。以下の条件でやってみた。

  • 2GBのファイルのPUTを以下の順序で(最初にそのまま実行、次に time コマンドをかませて計測)
    1. hadoop fs -put による書き込み
    2. curl でNameNode上のHoop REST APIを使って書き込み(op=create)
    3. curl で中継サーバ上のHoop REST APIを使って書き込み(op=create)
  • 2GBのファイルのGETを同じように実行
    1. hadoop fs -get による読み出し
    2. curl でNameNode上のHoop REST APIを使って読み込み
    3. curl で中継サーバ上のHoop REST APIを使って読み込み

結果は以下のとおり。まずPUT。

hadoop fs -put 2gdata.txt /tmp/put1.txt ;\
time hadoop fs -put 2gdata.txt /tmp/put2.txt ;\
curl -X POST 'http://namenode:14000/tmp/put3.txt?op=create&user.name=hoge' --data-binary @2gdata.txt --header "content-type: application/octet-stream" ;\
time curl -X POST 'http://namenode:14000/tmp/put4.txt?op=create&user.name=hoge' --data-binary @2gdata.txt --header "content-type: application/octet-stream" ;\
curl -X POST 'http://deliver:14000/tmp/put5.txt?op=create&user.name=hoge' --data-binary @2gdata.txt --header "content-type: application/octet-stream" ;\
time curl -X POST 'http://deliver:14000/tmp/put6.txt?op=create&user.name=hoge' --data-binary @2gdata.txt --header "content-type: application/octet-stream"

# hadoop fs -put
real	1m8.564s
user	0m8.736s
sys	0m1.635s

# curl - Hoop(namenode)
real	1m2.889s
user	0m0.830s
sys	0m1.676s
    
# curl - Hoop(deliver)
real	1m2.837s
user	0m0.811s
sys	0m1.532s

どれも対して変わらない、っていうか hadoop fs コマンドが一番遅いって……。あとから考えたがJVMの起動コスト分ですかねえ。user 8.736s の大部分がそれかな。2GB/60secだとビットレートにして266Mbps。わお。
で、つぎはGET。

hadoop fs -get /tmp/put1.txt - > /dev/null ;\
time hadoop fs -get /tmp/put1.txt - > /dev/null ;\
curl -X GET -s 'http://namenode:14000/tmp/put3.txt?user.name=hoge' > /dev/null ;\
time curl -X GET -s 'http://namenode:14000/tmp/put3.txt?user.name=hoge' > /dev/null ;\
curl -X GET -s 'http://deliver:14000/tmp/put5.txt?user.name=hoge' > /dev/null ;\
time curl -X GET -s 'http://deliver:14000/tmp/put5.txt?user.name=hoge' > /dev/null

# hadoop fs -get
real	0m31.224s
user	0m12.519s
sys	0m4.926s

# curl - Hoop(namenode)
real	0m49.797s
user	0m2.741s
sys	0m4.727s

# curl - Hoop(deliver)
real	0m30.389s
user	0m1.354s
sys	0m2.404s

これもdeliver経由だとhadoop fs -getより速くて、やっぱりJVM起動コストですかね。2GB/30secだと532Mbps? わーお! namenodeはCPU弱いサーバなので、たぶんそこで差が出ちゃったかな。本当は同一スペックのサーバで試せればどこにHoopを置くか決まって良かったかもしれないけど、でもまあ49秒で終わる程度にパフォーマンス出るなら実質充分。

継続的な追記のスループット

とはいえ単発のGET/PUTの性能はあんまり関係なくて、継続的に大量のログを書くときの中継点としてHoopが使えるのか知りたい。ので、特定のパスにひたすら追記(append)を繰り返す処理を試してみた。

ファイルへのappendは Hoop REST API 経由で、ベンチマークのクライアント側はRubyでコードを書いた。これは一定時間、指定されたデータソースを指定されたHoopサーバにappendしつづける。データソースは最初にメモリに読み込むのでクライアント側のdisk I/O負荷は考えなくていい。
continuous write over hoop

これを一度起動すると、指定したサーバに対して指定した秒数だけappendを繰り返し*1、最終的に何度のappendが成功したか、何度失敗したか*2、概算の平均スループットはどうだったか、およびtimeコマンドの出力を以下のように出す。

written chunk:13421, failed:0
rate: 149 Mbps

real    120m0.573s
user    0m7.917s
sys     0m37.009s

これをシェルスクリプトで並列起動させ、以下のような処理になるようにした。

  • 最初の60分
    • 1つ起動、10MBのファイルをメモリに読み込んでひたすらappend
  • 次の60分
    • 2つめも起動(2並列)、おなじくappendを別ファイルへ
  • 次の60分
    • 3つめも起動(3並列)、おなじくappendを別ファイルへ

結果がこちら。まずnamenode上のHoop、次にdeliver上のHoopに実行している。コメントは説明のため加えた。

starting namenode ....
Mon Oct 31 20:24:39 JST 2011

# 1st process (namenode)
written chunk:17644, failed:0
rate: 130 Mbps

real    180m0.816s
user    0m10.253s
sys     0m49.879s

# 2nd process (namenode)
written chunk:10285, failed:0
rate: 114 Mbps

real    120m0.642s
user    0m5.701s
sys     0m28.964s

# 3rd process (namenode)
written chunk:4360, failed:0
rate: 96 Mbps

real    60m0.704s
user    0m8.196s
sys     0m41.168s

ended master101.analysis
Mon Oct 31 23:24:40 JST 2011

starting deliver
Tue Nov  1 00:24:40 JST 2011

# 1st process (deliver)
written chunk:20787, failed:0
rate: 153 Mbps

real    180m0.165s
user    0m12.570s
sys     0m57.139s

# 2nd process (deliver)
written chunk:13421, failed:0
rate: 149 Mbps

real    120m0.573s
user    0m7.917s
sys     0m37.009s

# 3rd process (deliver)
written chunk:6088, failed:0
rate: 135 Mbps

real    60m0.326s
user    0m16.180s
sys     1m14.040s

ended deliver101.att.scribe.admin
Tue Nov  1 03:24:42 JST 2011

これだとわかりにくいよねー、ってことで、ずばり上記の試験期間中のグラフはこんな。

ベンチマークサーバ(ネットワークトラフィックCPU使用率)


namenode(ネットワークトラフィックCPU使用率)


deliver(ネットワークトラフィックCPU使用率)


まとめ

性能を見る限り deliver でのスコアは3並列で430Mbpsくらい出ていて*3、そのいっぽう該当の時間帯のdeliverサーバのCPU使用率を見ても1コアを使い切った数値(12.5%)にまったく届いていない。Hoop自体のパフォーマンスとしては430Mbps出て、まだ余裕あり、というところだろう。
2並列から3並列への数値の伸びを見るに1.5倍にはなってないので、このあたりでネットワークなりなんなりの要素が絡んできてるかなーという気はする。これ以上やろうと思うとネットワークまわりの環境をもっといじったりしないと難しいかも。

とはいえ、ぶっちゃけこんだけ出てれば何も問題ない。400Mbps書き込めることがわかってればなんでもできるでしょ。というかlibhdfsとか要らないってこれ。
libhdfsみたいにJVMに依存しJNIに依存しHadoopのバージョンであれこれあり、というものに較べてクライアントは軽量なHTTP REST APIを叩くだけでこんだけ性能出るんだもん。もう全部これでいいんじゃないですかね。

Hoop REST APIの罠

今回やってて気付いたこと。REST API経由でのはなし。

  • Append時に対象のパスが存在しなかった場合はエラー(FileNotFound)になる
  • Create時に対象パスのディレクトリが存在しなかった場合は勝手に作られる
  • Createのデフォルトオプションは overwrite=true
    • つまりファイルが存在した場合は無かったことになって、新たにCreate時のデータで新規作成される

これはどうしたもんかなーという挙動。本当は逆で、以下みたいなのがいいんだけど。

  • Append時に対象のパスが存在しなかった場合は新規作成されて書き込まれる
  • Create時に対象パスのディレクトリが存在しなかった場合はエラー

とはいえこうなってないものは仕方無い。ので、実用上は以下のようにするしか無いと思う。

  • 書き込みはすべてAppendでやる
    1. エラーになったらリトライとしてCreateを overwrite=false でやる
    2. Createもエラーになったら*4Appendを再度試みる

これで誰かと競合しても大丈夫かなあ。ということでHoopを経由して書き込むときは競合でデータが失われないよう、みなさん気をつけましょう。

*1:最初の一度だけはcreateだけど

*2:REST APIのレスポンスコード依存

*3:deliver側のトラフィックを見ると約480Mbpsになってるけど、これは通常のログ書き込みが50Mbpsくらいあるため

*4:誰かが作ったということなので