たごもりすメモ

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

shib v0.2.0

shib v0.2 beta なのを書いてる - tagomorisのメモ置き場

書いてましたが、APIをひととおりfixしたのでgithub上でv0.2.0のタグを打ちました。お仕事で既にこのバージョンを使ってます。
移行したい人は上記エントリの注意書きを読んでください。

http://github.com/tagomoris/shib

これまでとの違いについて整理したところ以下のような感じ。

  • node v0.8.x および node v0.10.x での動作
  • データ保存まわりの大幅な書き換え
  • Hiveクエリ実行エンジンまわりの設計の大幅な変更(複数の選択肢への対応を見越して)
  • ブラウザローカルなクエリのブックマーク機能の追加など、UI修正がいくつか

設計として hiveserver, hiveserver2 の両対応とか MRv1 / YARN の両対応とかを目的として実装を入れ替え可能にするための設計変更でしたが、実用上の問題がいくつかあって結局今のところ動作するのは hiveserver (not 2) のみです。クエリ動作状況の詳細取得・および中断には Huahin Manager が必要となりますが、これについては特に CDH4 + MRv1 のみしか対応していません*1

SELECTだけできるHiveのWebクライアントが欲しいんだ! クエリにpermalink欲しいんだ! Hue使いたくない! 他のサーバからHTTPでHiveクエリを実行したい! という人にお薦めのツールとなっております。そのような方はどうぞ。

*1:クエリをgive upしたときに裏側で終わるまで走り続けることを許容できる場合は hiveserver だけ動いてればCDH3でもCDH4+YARNでも使えます

shib v0.2 beta なのを書いてる

だいぶ前に最初のバージョンをつくった shib というHive Webフロントエンドツール、ずっと使ってるんだけど色々こまめに手を入れたりしてた。

tagomoris/shib · GitHub
Hive Client Webアプリケーション shib をつくった - tagomorisのメモ置き場
Hive WebClient shib をアップデート - tagomorisのメモ置き場

で、KT用のライブラリとかが全然バージョンアップしなかったりThriftのnode用ライブラリがnpmjs.orgでひどい状況だったりして、細かい対応ではさすがにもうやってられなくなった。
ので、今かなり大規模に手を入れてる。

  • ローカルのファイルシステム + SQLiteを使うようにして負荷対策 + KT free化
  • Hive実行に関しての hiveserver2 対応などを見越した設計変更
  • MRv1/YARN環境どちらでも制御可能にすることを見越した設計変更

などなど。まだ予定の範囲の実装が終わってないんだけどとりあえず内部で使える状態になったのでmasterブランチにマージした。セットアップも簡単になったし将来的にあれこれ対応できるようになったし、馬鹿でかい結果を吐くようなクエリを走らせても固まらなくなったし、めでたしめでたし。

注意点として、KyotoTycoonを使っていたのをやめたのでデータストアが完全に切り替わり、データの互換性がなくなった。つまり過去のクエリやクエリ結果が失われる。
まあshibのストレージ機能にそんなに深く依存した設計にする人などおるまいと思っていたのでがつんとやった。もし今使ってる人がアップデートすることがあったら大いに注意してほしい。過去のバージョンをそのまま使いたい人は v0.1 タグを参照して使ってください。

tagomoris/shib at v0.1 · GitHub

それはともかくまだ実装が終わってないので、これから使いたい人はあと数日待ったほうがいいと思う。なんか最近shibを使ってる/これから使う的な話をいくつか聞いたので、現段階でも注意喚起としてエントリに書いてみた。

CDH4で YARN から MRv1 にスイッチ

CDH4.2 + YARN + Hive でしばらく動かしてたんだけど、なんか一部のクエリが失敗しまくる。おかしいなーと思ったらこれにひっかかってた。

https://issues.cloudera.org/browse/DISTRO-461

なんだかなー。

いい回避策がないかあれこれやってみたんだけどどうにもうまくないので、しょうがないから MRv1 に変えよう、ということにした。
ところで YARN と MRv1 は設定をスイッチして再起動すればまあいけるんじゃね? ということは聞いてたが実際のところはどうなのよ、ということをここにまとめる次第。結論から言うとできた。

おおざっぱにまとめると以下のようにした。

  • 現状の設定ファイルセットを丸ごとコピーして MRv1 用に一部のみ書き換え
    • 切り替えは alternatives で読み込むパスを変更することで行う
  • 変更するファイルは mapred-site.xml と hive-site.xml のみ (yarn-site.xml は優雅に無視する)
  • 環境変数をいじらないといけないので hadoop-env.sh と hive-env.sh も変更対象
  • HDFSまわりは断固変更しない

手順は以下の通り。(HDFS関連は起動しっぱなしで触らない)

  1. hiveserverを停止
  2. resourcemanager / nodemanager を停止
  3. yum install hadoop-0.20-mapreduce-jobtracker ならびに tasktracker でインストール
  4. alternativesで設定ファイルの参照パスを変更
  5. mapred.local.dir で指定したディレクトリの作成・パーミッション変更
  6. jobtracker / tasktracker 起動
  7. hiveserver起動

無事動いたのでこれで多分よいのでしょう。戻すときは逆にすればいい。たぶん。

設定のdiff

まず簡単なところから言うと、YARN設定時に指定してあった HADOOP_MAPRED_HOME があるとうまくmapreduceが走らなったので、これを *-env.sh からコメントアウトする。理屈は知らん。*1

hive-site.xml にJobTrackerなどの情報が書いてあるので、これを変更する。

  <property>
    <name>mapred.job.tracker</name>
    <!--YARN <value>master.local:10020</value> -->
    <value>master.local:8021</value>
  </property>

  <property>
    <name>mapred.job.tracker.http.address</name>
    <!--YARN <value>master.local:19888</value> -->
    <value>master.local:50030</value>
  </property>

あとの変更点は mapred-site.xml のみ。親切な人にもらった設定例をベースにがりがり書き換えて、こんな感じ。これで全部。YARN時の mapred-site.xml を丸ごと置き換えた。

<configuration>

  <property>
    <name>mapreduce.framework.name</name>
    <value>classic</value>
  </property>

  <property>
    <name>mapred.job.tracker</name>
    <value>master.local:8021</value>
  </property>

  <!-- ここは見易さのために改行した -->
  <property>
    <name>mapred.local.dir</name>
    <value>
      /var/hadoop/disk01/mapred/local,/var/hadoop/disk02/mapred/local,
      /var/hadoop/disk03/mapred/local,/var/hadoop/disk04/mapred/local
    </value>
  </property>

  <property>
    <name>io.seqfile.local.dir</name>
    <value>
      /var/hadoop/disk01/mapred/tmp,/var/hadoop/disk02/mapred/tmp,
      /var/hadoop/disk03/mapred/tmp,/var/hadoop/disk04/mapred/tmp
    </value>
    <description>HADOOP-5219</description>
  </property>

  <property>
    <name>mapred.system.dir</name>
    <value>/mapred/system</value>
  </property>

  <property>
    <name>mapreduce.jobtracker.staging.root.dir</name>
    <value>/mapred/staging</value>
  </property>

  <!-- LOG -->
  <property>
    <name>mapreduce.map.log.level</name>
    <value>WARN</value>
  </property>

  <property>
    <name>mapreduce.reduce.log.level</name>
    <value>WARN</value>
  </property>

  <property>
    <name>mapred.userlog.retain.hours</name>
    <value>96</value>
  </property>

  <property>
    <name>mapred.compress.map.output</name>
    <value>false</value>
  </property>

  <property>
    <name>mapred.output.compress</name>
    <value>true</value>
  </property>

  <property>
    <name>mapred.output.compression.type</name>
    <value>BLOCK</value>
  </property>

  <property>
    <name>mapred.output.compression.codec</name>
    <value>org.apache.hadoop.io.compress.GzipCodec</value>
  </property>

  <property> <!-- ADD HERE for MRv1 (and newlines for blog entry)-->
    <name>io.compression.codecs</name>
    <value>
      org.apache.hadoop.io.compress.DefaultCodec,
      org.apache.hadoop.io.compress.GzipCodec,
      org.apache.hadoop.io.compress.BZip2Codec,
      org.apache.hadoop.io.compress.DeflateCodec,
      org.apache.hadoop.io.compress.SnappyCodec
    </value>
  </property>

  <!-- JobTracker -->
  <property>
    <name>mapred.job.tracker.handler.count</name>
    <value>150</value>
  </property>

  <property>
    <name>hadoop.job.history.location</name>
    <value>/mrv1-history</value>
  </property>

  <property>
    <name>mapreduce.job.maps.speculative.execution</name>
    <value>false</value>
    <description>default: true</description>
  </property>

  <!-- for MRv1 ? -->
  <property>
    <name>mapred.map.tasks.speculative.execution</name>
    <value>false</value>
    <description>default: true</description>
  </property>
  <property>
    <name>mapred.reduce.tasks.speculative.execution</name>
    <value>false</value>
    <description>default: true</description>
  </property>

  <property>
    <name>mapreduce.job.reduces.speculative.execution</name>
    <value>false</value>
    <description>default: true</description>
  </property>

  <!-- TaskTracker -->
  <property>
    <name>mapred.tasktracker.map.tasks.maximum</name>
    <value>6</value>
  </property>

  <property>
    <name>mapreduce.map.memory.mb</name>
    <value>1024</value>
  </property>

  <property>
    <name>mapred.tasktracker.reduce.tasks.maximum</name>
    <value>2</value>
  </property>

  <property>
    <name>mapreduce.reduce.memory.mb</name>
    <value>2048</value>
  </property>

  <property>
    <name>mapred.child.java.opts</name>
    <value>-Xmx2048m</value>
  </property>


  <property>
    <name>tasktracker.http.threads</name>
    <value>120</value>
  </property>

  <property>
    <name>mapred.job.reuse.jvm.num.tasks</name>
    <value>1</value>
  </property>

注意点は以下の通り。

  • mapred.system.dir と mapreduce.jobtracker.staging.root.dir , hadoop.job.history.location
    • わざとこれまでに使っていなかった hdfs dir を指定
    • yarn使用の場合とかぶせるとpermissionまわりのエラーが怖いため
  • mapred.tasktracker.map.tasks.maximum と mapred.tasktracker.reduce.tasks.maximum
    • tasktracker毎の指定であることを忘れててクラスタ全体のつもりで指定したらjvm上がりすぎて死にかけた
  • mapred.child.java.opts
    • 指定しないと -Xmx200m でtaskが起動してheap不足で死にまくった、ので指定した
  • mapred.map.tasks.speculative.execution と mapred.reduce.tasks.speculative.execution
    • タスクの投機的実行は抑制したかったので mapreduce.job.*.speculative.execution を指定してたんだけど、これ効いてないっぽい?
    • ので追加で指定した(象本英語3版 p216)
    • けどまだなんか効いてないような……なぜだ

これでYARNのときと変わらない感じでHiveクエリが走るようになった。やれやれ。

いっぽうlog4jが謎のエラー

ところでMRv1に変えたところ、ジョブ完了時に投機的実行タスク*2がkillされるタイミングで該当タスクに以下のようなログが残ってた。

stderr logs
log4j:ERROR setFile(null,true) call failed.
java.io.FileNotFoundException: /var/log/hadoop (Is a directory)
	at java.io.FileOutputStream.openAppend(Native Method)
	at java.io.FileOutputStream.<init>(FileOutputStream.java:192)
	at java.io.FileOutputStream.<init>(FileOutputStream.java:116)
	at org.apache.log4j.FileAppender.setFile(FileAppender.java:294)
	at org.apache.log4j.RollingFileAppender.setFile(RollingFileAppender.java:207)
	at org.apache.log4j.FileAppender.activateOptions(FileAppender.java:165)
	at org.apache.log4j.config.PropertySetter.activate(PropertySetter.java:307)
	at org.apache.log4j.xml.DOMConfigurator.parseAppender(DOMConfigurator.java:295)
	at org.apache.log4j.xml.DOMConfigurator.findAppenderByName(DOMConfigurator.java:176)
	at org.apache.log4j.xml.DOMConfigurator.findAppenderByReference(DOMConfigurator.java:191)
	at org.apache.log4j.xml.DOMConfigurator.parseChildrenOfLoggerElement(DOMConfigurator.java:523)
	at org.apache.log4j.xml.DOMConfigurator.parseCategory(DOMConfigurator.java:436)
	at org.apache.log4j.xml.DOMConfigurator.parse(DOMConfigurator.java:1004)
	at org.apache.log4j.xml.DOMConfigurator.doConfigure(DOMConfigurator.java:872)
	at org.apache.log4j.xml.DOMConfigurator.doConfigure(DOMConfigurator.java:778)
	at org.apache.log4j.helpers.OptionConverter.selectAndConfigure(OptionConverter.java:526)
	at org.apache.log4j.LogManager.<clinit>(LogManager.java:127)
	at org.apache.log4j.Logger.getLogger(Logger.java:104)
	at org.apache.commons.logging.impl.Log4JLogger.getLogger(Log4JLogger.java:289)
	at org.apache.commons.logging.impl.Log4JLogger.<init>(Log4JLogger.java:109)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:39)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:27)
	at java.lang.reflect.Constructor.newInstance(Constructor.java:513)
	at org.apache.commons.logging.impl.LogFactoryImpl.createLogFromClass(LogFactoryImpl.java:1116)
	at org.apache.commons.logging.impl.LogFactoryImpl.discoverLogImplementation(LogFactoryImpl.java:914)
	at org.apache.commons.logging.impl.LogFactoryImpl.newInstance(LogFactoryImpl.java:604)
	at org.apache.commons.logging.impl.LogFactoryImpl.getInstance(LogFactoryImpl.java:336)
	at org.apache.commons.logging.impl.LogFactoryImpl.getInstance(LogFactoryImpl.java:310)
	at org.apache.commons.logging.LogFactory.getLog(LogFactory.java:685)
	at org.apache.hadoop.mapred.Child.<clinit>(Child.java:61)

なんでっしゃろねコレ。誰か知ってたら教えてください。

/var/log/hadoopHADOOP_LOG_DIR に指定してある。YARN_LOG_DIR でも同じ指定(今回はたぶん関係ないけど)。うーん、MRv1なログ出力の指定がなにか足りないのかなあ。
他のエラーが出るときはこのログの後に出てたので、まあ実害はなさそうな気はする。気はするんだけどなんか怖い。ううむ。

*1:なんだかなあ。

*2:まだ有効だったとき

CDH4でHiveServerを使う際の注意点

CDHでHadoopを使っている良い子のみんな、hiveserverまわりで罠があるので気をつけましょう! という話。ふたつあるので順番に。

hive-server 再起動時にログがtruncateされる

hiveserverがなんかおかしかったりすると、原因究明はやるとしてとりあえず再起動、という運用をしてしまう気持ちはよくわかります。が、やってしまうとその日の分のログが消える(!)ので注意が必要だ。

原因は /etc/init.d/hive-server *1の hive_start() 中の以下の部分。

    log_success_msg "Starting $desc (${NAME}): "
      $SU -c "cd $HIVE_HOME ; $exec_env nohup \
               $EXE_FILE --service $service_name $PORT \
             > $LOG_FILE 2>&1 < /dev/null & "'echo $! '"> $PID_FILE"
    RETVAL=$?

start時にログを '> $LOG_FILE' でopenしてるので、その時点の内容が消えてしまう。
ClouderaのJIRAにぶちこんだんだけど、その後にリリースされたCDH4.2でも直ってないので注意が必要。1バイト書き足してくれればいいだけなのに……。

ということで、自分で以下のように書き直しましょう。リダイレクトを追記('>>')にするだけですね!

    log_success_msg "Starting $desc (${NAME}): "
      $SU -c "cd $HIVE_HOME ; $exec_env nohup \
               $EXE_FILE --service $service_name $PORT \
             >> $LOG_FILE 2>&1 < /dev/null & "'echo $! '"> $PID_FILE"
    RETVAL=$?

tmpwatchが走って /tmp 下が掃除されるとhiveserverが正常動作しなくなる

これは hive 0.8 および 0.9 が対象、なので CDH 4.0.x および 4.1.x が対象。4.2.0 は hive 0.10 ベースで、そっちだと直ってたのを確認した。hiveserver2で該当するかどうかはわからないが、しそうな気がする。/tmp以下に同じようなxmlが吐かれてたら多分該当する。

hive 0.8 および 0.9 あたりの hiveserver は設定を読み込んだらそれを /tmp/hive-default-xxxx.xml というパスのファイルにいったんダンプし、以降はそれを読んで動作する。パスを変更する手段はなさそうだった。
で、hiveserverを起動しっぱなしにしておくとこのファイルは更新されずにずっと置かれたままで、tmpwatchのクリーニング対象になってしまう。そして消されたら hiveserver は「起動はしている(TCP接続を受け付ける)がクエリが正常に実行されない」状態になる。TCP接続での監視しかしていないと気付けない。

https://issues.apache.org/jira/browse/HIVE-3596

対処方法としては以下のどちらか。

  • CDH 4.2 に更新する metadataの互換性がなかった、全部吹っ飛ばしたくなかったらやめておけ
  • 該当ファイルを touch しつづける cron job でも動かすか tmpwatch の対象から該当ファイルを除外する

この問題にぶちあたってるかどうかは hiveserver のログを見れば、該当ファイルが見付からない! というエラーが出るのでわかるはず。ただし前述の「再起動するとログが消える」状態だと調査が困難を極めるというか迷宮入りになるので注意な。

*1:hive-server2 でも同様なことを確認してある

YARN/Hiveな環境で大量の一時ファイルとログをごりごり削除する

YARN使っててHDFSにログを吐くよう設定してるとアホみたいにたまっていくので削除しないとやばい。
とりあえず10日経過したら削除するようにした。

(
    for searchpath in '/tmp/hive-*' '/mr-history/done/*/*' '/mr-history/tmp/*' '/yarn/user/*/.staging' '/yarn/log/apps/*/logs';
    do
        hdfs dfs -ls "$searchpath" | grep -v '^Found [0-9]* items' | \
            perl -MTime::Piece -MTime::Seconds \
            -e '$t = ((scalar(localtime(time))) - ONE_DAY*10)->strftime("%Y%m%d");' \
            -e 'while(<>){ chomp; @f = split(/ +/, $_, 8); $d = $f[5]; $d =~ s/-//g; if($d < $t){ print $f[-1],"\n"; } }'
    done
) | xargs hdfs dfs -rm -r

perl 5.10 以降で使える、かな? あと /mr-history とか /yarn/log/apps とかは設定による。

DataNodeのIPアドレス変更

クラスタでちょっとネットワーク設計まずってた! slave nodeだけ直したい! という話がありましてIPアドレスの変更を伴うので、やってた。結論だけ言うと以下の手順でいけた。これを1台ずつ、NameNodeでちゃんとノード認識が戻ったことを確認しながらやる。

  1. yarn nodemanager / hdfs datanode 停止
  2. ネットワーク設定(IPアドレス・ホスト名)変更
  3. hdfs datanode / yarn nodemanager 起動

問題はIPアドレスを変更されたDataNodeが再度認識されたとき、変更前と同一のノードと扱われるかどうか。これによりDataNodeに保存されている hdfs block がそのまま使えるのか、全部失われてレプリケーションしなおしになるのかが変わる。希望としてはそのまま認識されて使えることだったんだけど、迂闊に信じられないのがhadoopのおそろしいところ。

で、おそるおそる確認しながらやってたんだけど、大丈夫でした。CDH4.1.2にて。めでたしめでたし。

CDH3u5でWebHDFS

いろいろあって待ち望んでいたHadoop CDH3u5がリリースされましたね!

注目するところは人それぞれだろうけど、個人的にはWebHDFSのサポートが入ったのが大変嬉しい。前にCDH4で試したりはしていたけどCDH4に移行するのもいろいろアレそうだし、と思っていたのでWebHDFSサポートの入るCDH3u5をマジで待ってた。のでさっそくちょっと確認して、すぐ置き換えてみた。
なおCDH3u5にはHttpFsは入ってないようだ。これはちょっと残念。

  • 移行前: CDH3u2 + Hoop
  • 移行後: CDH3u5 + WebHDFS

設定変更点

WebHDFSを有効にするため、およびHoop/WebHDFSでappendを有効にするために hdfs-site.xml に以下の設定を加えた。

  <property>
    <name>dfs.webhdfs.enabled</name>
    <value>true</value>
  </property>

  <property>
    <name>dfs.support.append</name>
    <value>true</value>
  </property>

  <property>
    <name>dfs.support.broken.append</name>
    <value>true</value>
  </property>

dfs.support.append および dfs.support.broken.append はCDH3u4以降で必要になっているはず。WebHDFSはCDH3u5から。WebHDFSでappendするにも dfs.support.broken.append は必要になる。
これをくっつけて stop-dfs および start-dfs すればよい。WebHDFSの場合は書き込み操作は直接 DataNode に対してアクセスが飛ぶので、DataNode単独できちんと上記の設定が有効になるよう読み込まれている必要があることに注意。DFSClientやNameNodeを経由しないからこの注意が必要になる。

クライアント

WebHDFSクライアントはたぶんまだ世の中にあんまり無い、けどPerlRubyにはクライアントライブラリがあるぞ!

あとはFluentdからなら fluent-plugin-webhdfs が使える。

性能について

まだ限界まで負荷がかかるようなことをしてみてないんだけど、通常時の書き込みを見てる限り以下のような状況。

  • Hoop Server を立てていたNameNodeの通信量が激減
    • 実際のデータ書き込みがDataNodeに直接行くようになったため
    • CPU使用率に10%程度見えていた SoftIRQ (割り込みによるCPU使用率) がほぼゼロに
  • DataNode側の通信量・CPU使用率のiowaitなどはほぼ変化なし
    • ただしわずかに SoftIRQ の値の上昇が見られるようになった(2〜4%程度)
  • クライアント側から確認できていた書き込み先への通信エラーがなくなった
    • HoopのケースだとMapReduceジョブが走っているときにしばしばクライアントがサーバに接続できない状況が起きていた*1
    • WebHDFSだと無くなった、かも*2

Hoop Serverのように1ヶ所を必ず経由する通信パターンに較べてWebHDFSだと通信負荷がバラけるのは当然のことだけど、やっぱりその影響は大きいかな。

もうひとつ実際には重要なこととして、Tomcatで動くHoop ServerとJettyで動くWebHDFSハンドラ、という違いはあると思う。*3
WebHDFSはHadoop DataNodeが前から動かしているWebコンソールと同じところで処理される。これはJettyで動作しているもので、実際にWebHDFSを叩いてみるとHTTPヘッダにもJetty云々な行がある。

データを継続的に書き込むような通信パターンだとデータ(ペイロード)の受信時間が長く、Tomcatのように同期的に処理するアーキテクチャのサーバだと並列度が高くなるならそのぶんだけスレッド数を増やすしかない。スレッド数を増やせばそのぶんだけコンテキストスイッチのコストがかさむ。スレッド数が足りなければクライアントは接続に失敗する。
その点Jettyは非同期I/Oをサポートしたアプリケーションサーバなので、Network I/OならびにDisk I/O待ちの間は他の接続の相手にCPUを使える*4。それなりに多くの通信の並列処理も得意なはずだから、特に大きいサイズのデータについての通信処理ではTomcatに較べて状況が改善するのは納得できる。

数日これで様子見て、特別問題がなければそのままWebHDFSを使い続ける予定。さてどうかな。

*1:Fluentdのリトライの範囲で、問題にならない程度になんとかなっていた

*2:もっと重いジョブが走ったときの状況を確認する必要があるけど

*3:HttpFsはどっちだたっけ……?

*4:ようになっている、というか DataNode はそう作られている、はず