Apache Sliderを使ってStormもYARNに乗せる

本日は、Hadoopと良く一緒に語られるApache Stormと、そのStormを真の意味でのHadoopファミリーとする上でのキーとなるミドルウェアである Apache Sliderについてちょこっとだけ書きたいと思います。

# Apache Sliderとは？

YARNの登場により、様々な分散アプリケーションが1つのクラスタを総合的なリソース管理のもとに使いまわすことができるようになりました。
ただし、YARNを利用したい分散アプリケーションにはいくつかの制約が発生します。

その中でもとりわけ大きいものとして、

* YARNに対してのリソースの要求や開放等のコードを書かなくてはならない。
* ジョブ1つにつきリソースの確保やデーモン等の立ち上げという仕事が発生するため、スタンドアローンに比べてジョブの立ち上がりが遅い。

(ここで言うスタンドアローンとは、YARNを使わずにその分散アプリケーションがネイティブにデーモンを常時起動させて動かすモードを指します。)

が挙げられます。

Apache Sliderを使うと上記の問題がまるっと解決できます。

端的に言うとSliderを使うことで、

1. 分散アプリケーションのコードを一切変更せずにYARN上で動かすことができる。
2. リソース確保やデーモン立ち上げを最初に行い、そのまま継続利用することができる。
   (すなわち、通常のYARNアプリケーションの1ジョブ毎におきていたリソース確保とデーモン立ち上げがスキップできる。)

となります。
(ちゃんと調べてませんが上記の1の、アプリケーションを変更せずにYARNに"スライド"できる、という特徴がSliderの名前の由来な気がします。だとしたら、すごく安直なネーミングですね（笑）わかりやすくて良いですが。)

また、Slider経由で起動した分散アプリケーションにはリソースの動的な拡張、縮退、サスペンド、障害発生時のサーバの再割り当て、監視等についての機能が付与されます。
また、Apache Ambariで自動で管理ができるようになる等の恩恵もあるとのことです。

## で、Apache Slider とは何なのさ？

とりあえず、長々と上に書いてみたのですが、

* 現時点でのSliderとは、HBase、Storm、Accumulo等の常時起動するタイプの分散アプリケーションをYARNのリソース管理に押し込むための仕組み

と思っておけばまず問題ないです。たぶん。

継続的に処理を動作させることを目指したStormや、リージョンの情報をメモリに持ちながらgetやputリクエストに答えるHBaseやAccumuloは、そもそもバッチ処理が終わったらリソース開放して終了という現時点のYARNの想定する一般的なアプリケーションスタイルにはいまいちはまらないのです。
だからといって、HBase、Storm、Accumuloのために別途専用クラスタを立ち上げるというのも色々と面倒なので、YARNのリソース管理の仕組みの元で動かしたいというのは自然な発想と感じます。

# Sliderの仕組みというか基本的なアイデア

では、Sliderとはどういう仕組みなのでしょうか？

Sliderは、YARNから見ると単なる分散アプリケーションの1つにすぎません。
Sliderが他の分散アプリと違うのは、Slider自身がYARNから確保したリソース（具体的にはコンテナ）内で他のアプリケーションを起動するという点です。

Sliderのアイデアは非常に単純です。

Sliderが分散アプリケーションを起動する際に、

* 各プロセスが動作に使う各種ディレクトリ(設定置き場やログ出力先)を他の分散アプリと被らない所に入れ替える。
* 各プロセスが動作に使うポート番号を他の分散アプリと被らない所に入れ替える。

程度のことをしてあげれば、それらのアプリをYARNの通常アプリが走行に使うファイルシステムのサブツリーに押し込めて動作させることができるし、CPUリソースの制限自体はcgroupsでカーネルレベルで管理しているので該当のプロセスグループさえわかればここもそのまま制御できちゃうよね？というのが基本的なアイデアです。

ただし、分散アプリケーションが、

* 固定的なファイルPATHを使わない。

  + 設定ファイル置き場やログの出力箇所などが全てパラメタで指定できる。

* 通信に使うポート番号やアドレスが全て設定で変更可能。

* 分散したプロセスは自律的にお互いを見つけてサービスを構成できる。

  + 要はZookeeper等のコーディネータを使って勝手にお互いの場所を把握できるってこと。

* 動作に必要なバイナリを全て内包している。

等の制約を守って由緒正しく作ってあれば..、という結構面倒な条件があります。
(より詳しくは、公式サイトの[Application Needs](http://slider.incubator.apache.org/docs/slider_specs/application_needs.html)を参照。)

この環境を閉じ込めてポータブルにして実行しようという発想は、Dockerとも似ているとも言えるかもしれません。

ただし、SliderではDockerと違って、起動するアプリはYARNのNodeManagerサーバ上でほぼネイティブに実行されます。
Dockerのように、プロセス空間、ファイルシステム空間およびネットワークがホストサーバから完全に分離されるわけではありません。
あくまで、分散アプリが行儀よくファイルシステムサブツリーの中で動く必要があります。

個人的には実行時のオーバヘッドや処理自体の透過性やデバッグしやすさという面ではこの程度のつくりの方が楽なので初手としては悪くは無いと思います。
([HortonWorksのslider-0.60のアナウンス](http://hortonworks.com/blog/announcing-apache-slider-0-60-0/)によると今後Docker連携とかも予定しているらしいです。)

# Storm on Slider

では、実際にSliderを使ってみましょう。SliderはZookeeperやHadoop(2.6以上)等色々と必要ですので、楽するためにHortonWorks社が提供するHDP2.2のsandboxを使うことにします。

Sliderによるアプリケーション実行の流れは以下のようになります。

* **アプリケーションパッケージのビルド**: Sliderで実行するアプリケーションパッケージの作成
* **アプリケーションパッケージのインストール(install-package)**: Stormのapp-packageをYARN上で起動するためにクラスタに事前登録する。
* **アプリケーションの実行(create)**: YARN管理下の「Stormクラスタ」を起動する。確保する計算リソースはapp-package中の設定が基本的に利用される。

運用という観点だと、

* **Sliderアプリケーションのリスト表示(list)**: YARN上で動作しているSliderアプリケーションインスタンスをリスト表示する。
* **Sliderアプリケーションの停止(stop)**: 一旦、「Stormクラスタ」を停止する。実際にYARN上のStorm関連のプロセスが停止し、計算リソースが開放される。
* **Sliderアプリケーションの再開(start)**: 一度停止した「Stormクラスタ」を起動する。このとき、以前に起動していた場所と同じ所で極力プロセスの起動を試みる。
* **Sliderアプリケーションクラスタの拡張/縮退(flex)**: 「Stormクラスタ」の利用するリソース量を増減する。
* **Sliderアプリケーションクラスタの完全停止(destroy)**: 「Stormクラスタ」を消す。stopでは「Stormクラスタ」の動作ホスト等のデータが保存されていたがそれらも消去されることとなる。

といった機能も有ります。

## 事前準備

### HortonWorks Sandboxの微調整

本筋ではないのであまり詳しくは説明しませんが起動するだけでYARNやHDFS等の諸々が立ち上がっており便利です。(メモリが4GB必要なのでノートPCでの利用は厳しいです。)
私が試したのは、[HDP2.2のPreview版のVBox](http://hortonassets.s3.amazonaws.com/2.2/HDP_2.2_Preview_VirtualBox_r2.ova)となります。

起動後は以下のようにssh経由でアクセスが可能です。（初期パスワードは ``hadoop`` です。)

```console:HDPのsandboxへのSSHアクセス
$ ssh root@127.0.0.1 -p 2222
root@127.0.0.1's password: 
Last login: Tue Nov 11 10:48:38 2014 from 10.0.2.2
[root@sandbox ~]# 
```

なお、YARNのアプリを動かすにはかなり貧弱なので私は、

* VirtualBoxのVMのメモリ量を4GBから8GBに拡大
* /etc/hadoop/conf/yarn-site.xmlのyarn.nodemanager.resource.memory-mbを4GB強に拡大

という変更をして遊んでいます。

### mavenのインストール

ビルドのためのmavenをインストールします。作業ユーザはrootです。

```console:mvnのインストール
# wget http://ftp.yz.yamagata-u.ac.jp/pub/network/apache/maven/maven-3/3.2.5/binaries/apache-maven-3.2.5-bin.tar.gz
# cd /usr/local/
# tar xzf ~/apache-maven-3.2.5-bin.tar.gz
```

## アプリケーションパッケージのビルド

まず、YARNにのせるためにそのアプリケーションが動作するための各種環境等をパッケージとしてまとめる必要があります。
この作業は、触れ込み通りアップストリームのソースコード改変無しで対応が可能です。
(なお、ここからの作業は全てyarnユーザの実行です。rootの場合はプロンプトを``#``にしますのでそこで見分けてください。)

まず、Stormの **バイナリ** tarボールを入手します。今回は2014/12で最新の storm-0.9.3.tar.gz をダウンロードしました。

```console:upstreamからのstorm最新パッケージの入手
$ wget http://ftp.yz.yamagata-u.ac.jp/pub/network/apache/storm/apache-storm-0.9.3/apache-storm-0.9.3.tar.gz
```

次にapache sliderのソースコードをダウンロードして2014/12の最新リリースである0.6のブランチに移動します。

```console:sliderのcloneとリリース版のcheckout
$ git clone https://github.com/apache/incubator-slider.git
$ git checkout -b releases/slider-0.60 origin/releases/slider-0.60
```

インストールが終わったら yarn ユーザになって事前準備で用意したmavenへのパスが通るように``~/.bash_profile``に環境変数M2_HOMEとM2を追記し、PATH環境変数にmavenへのパスを通します。

```bash:/home/yarn/.bash_profileへの編集
(略)
export M2_HOME=/usr/local/apache-maven-3.2.5
export M2=$M2_HOME/bin
PATH=$M2:$PATH:$HOME/bin
(略)
```

sliderのアプリケーションパッケージ用のディレクトリに移動して、先ほどダウンロードしたstormのバイナリtarボールの場所や名前を指定してmvnコマンドを実行します。(このディレクトリの周辺を眺めるとHBaseやAccumulo等のパッケージ作成用のものもあります。)

```console:アプリケーションパッケージのビルド
# cd ~/incubator-slider/app-package/storm
# mvn clean package -Pstorm-app-package -Dpkg.version=0.9.3 -Dpkg.name=apache-storm-0.9.3.tar.gz -Dpkg.src=/home/yarn
```

うまく行くと、

* ``~/incubator-slider/app-packages/storm/target/slider-storm-app-package-0.60.0-incubating.zip``

というパッケージができます。

### Storm アプリケーションパッケージビルド時の注意

SliderのStorm用起動スクリプト名がstorm.pyになっており、Upstreamのパッケージを使うとうまく動かない問題があったので以下の修正をしています。

```diff:
diff --git a/app-packages/storm/package/scripts/params.py b/app-packages/storm/package/scripts/params.py
index 1ccba5e..f111447 100644
--- a/app-packages/storm/package/scripts/params.py
+++ b/app-packages/storm/package/scripts/params.py
@@ -36,7 +36,7 @@ nimbus_host = config['configurations']['storm-site']['nimbus.host']
 nimbus_port = config['configurations']['storm-site']['nimbus.thrift.port']
 rest_api_conf_file = format("{conf_dir}/config.yaml")
 rest_lib_dir = format("{app_root}/external/storm-rest")
-storm_bin = format("{app_root}/bin/storm.py")
+storm_bin = format("{app_root}/bin/storm")
 storm_env_sh_template = config['configurations']['storm-env']['content']
 
 ganglia_installed = config['configurations']['global']['ganglia_enabled']
```

些細な問題に見えますのでいずれ修正されるでしょう。

## アプリケーションパッケージのインストール(install-package)

作成したファイルを storm-cluster-tmplate1 という名前でインストールします。
この作業は実際には、実行ユーザのHDFS上のディレクトリにアプリケーションパッケージを置くだけです。

```console:アプリケーションパッケージのインストール
$ slider install-package --name storm-slider-template1 --package /home/yarn/incubator-slider/app-packages/storm/target/slider-storm-app-package-0.60.0-incubating.zip 
```

なお、同じ名前のパッケージを再ロードしたい場合は``--replacepkg``オプションをつける必要があります。

## アプリケーションの実行(create)

早速実行と行きたいところなのですが、実行前に、

* アプリケーションの設定等が記載された``appConfig-default.json``
* 利用するクラスタリソース情報の書かれた``resources-default.json``

を用意する必要があります。(もし、何も指定しないとアプリケーションパッケージに含まれるデフォルトのファイルが使われます。)
現段階のslider-0.60で作ったアプリケーションパッケージに含まれるappConfigはどうにもうまく動作しないので大幅に書き換える必要がありました。

(例えば、
* HDFS上のアプリケーションパッケージの場所を指定するapplication.defが間違っている。
* java_homeがoracle JDK指定になっている。(sandboxはOpenJDKで動いている。)
* sandboxに含まれていないjmxetric関連のclassをロードする設定になっている。
等の問題が有りました。)

```json:appConfig-default.jsonの変更（一部）
  "global": {
    "application.def": ".slider/package/storm-slider-template1/slider-storm-app-package-0.60.0-incubating.zip"
    "java_home": "/usr/lib/jvm/java-1.7.0-openjdk.x86_64",
    (略)
  }
```

また、resources-default.jsonでもsandbox設定のリソース量が足りないのかジョブ実行管理のプロセスであるSupervisorプロセスが起動しない等の問題があり、DRPCサーバというプロセスについては立ち上がらないようにするなどの設定変更をしています。


```console:Sliderアプリケーションの実行
yarn$ slider create storm_cluster1 --template /home/yarn/appConfig-default.json --resources /home/yarn/resources-default.json
```

これがうまく行くと、``slider list``コマンドで起動しているクラスタを見ることができるようになります。
すごく見づらいですが真ん中あたりからstorm_cluster1が起動していることがわかります。

```console:Slilderアプリケーションをリスト表示
$ slider list
2014-12-22 11:41:46,128 [main] INFO  impl.TimelineClientImpl - Timeline service address: http://0.0.0.0:8188/ws/v1/timeline/
2014-12-22 11:41:47,257 [main] WARN  shortcircuit.DomainSocketFactory - The short-circuit local reads feature cannot be used because libhadoop cannot be loaded.
2014-12-22 11:41:47,268 [main] INFO  client.RMProxy - Connecting to ResourceManager at sandbox.hortonworks.com/10.0.2.15:8050
storm_cluster1                     RUNNING  application_1419236346356_0002           http://sandbox.hortonworks.com:8088/proxy/application_1419236346356_0002/
storm_cl1                     
2014-12-22 11:41:47,780 [main] INFO  util.ExitUtil - Exiting with status 0
```

(ちなみにstorm_cl1というのは昔作って``slider destroy``し忘れたSliderアプリケーション。)

## 起動したStormプロセスを見てみよう

StormのマスターデーモンであるNimbusプロセスについてちょっと見てみます。

```console:Nimbusプロセスのコマンドラインを見てみる
yarn$ ps aux | grep nimbus | grep java
yarn     13646  0.0  0.0 106196  1584 ?        S    11:24   0:00 /bin/bash --login -c env \
JAVA_HOME=/usr/lib/jvm/java-1.7.0-openjdk.x86_64 \
PATH=/usr/lib/jvm/java-1.7.0-openjdk.x86_64/bin:$PATH \
STORM_BASE_DIR=/hadoop/yarn/local/usercache/yarn/appcache/application_1419236346356_0002/container_1419236346356_0002_01_000002/app/install/apache-storm-0.9.3 \
STORM_CONF_DIR=/hadoop/yarn/local/usercache/yarn/appcache/application_1419236346356_0002/container_1419236346356_0002_01_000002/app/install/apache-storm-0.9.3/conf \
/usr/bin/python /hadoop/yarn/local/usercache/yarn/appcache/application_1419236346356_0002/container_1419236346356_0002_01_000002/app/install/apache-storm-0.9.3/bin/storm nimbus > /hadoop/yarn/log/application_1419236346356_0002/container_1419236346356_0002_01_000002/nimbus.out 2>&1
```

厳密にはこのプロセスはJavaプロセスであるNimbusを起動するためのPythonのヘルパースクリプトなのですが、NimbusプロセスをYARNアプリ動作用のサブディレクトリに押しこもうしているのが見て取れるかと思います。
(設定を探すファイルPATHの起点であるSTORM_CONF_DIRとか、バイナリを探す起点であるSTORM_BASE_DIR等の環境変数を設定しています。)

これ以外のプロセスも皆こんな感じで動作場所がすり替えられています。

## Stormトポロジを投入してみる

ここまででSliderアプリケーションとしてStormクラスタ ``storm_cluster1`` が起動しました。
早速、Stormにおけるジョブに相当するトポロジを動かしてみましょう。

しかし、ここで1つ問題があります。通常のStormクラスタではNimbusデーモンは6627番のportでユーザがNimbusを起動したホストでトポロジの受付を行います。
YARNのアプリケーションとしてNimbusを起動しているのでNimbusがどのホストのどのサーバで起動しているのかはわかりません。

Sliderでは``slider regsitry``コマンドを実行することで現在起動しているSliderアプリケーションに配布されている設定がわかります。
(これらの設定データはSliderが使うzookeeper上に保存されます。ちゃんと書いてませんでしたがSliderにはZookeeperは必須です。)
Sliderアプリケーションに外部からアクセスするには、このAPIを使ってクライアントを実装する必要があります。

幸いStormには``storm-slider``という、上記のめんどくさい部分をやってくれるクライアントプログラムがすでにHortonWorksによって用意されています。

前置きが長かったですが、ついに実行をしてみます。

```bash:トポロジの実行
yarn$ tar xzf ~/apache-storm-0.9.3.tar.gz
yarn$ export SLIDER_HOME=/usr/hdp/2.2.0.0-1084/slider/
yarn$ /usr/hdp/2.2.0.0-1084/storm-slider-client/bin/storm-slider --app storm_cluster1 --user yarn jar /usr/hdp/2.2.0.0-1084/storm/contrib/storm-starter/storm-starter-0.9.3.2.2.0.0-1084-jar-with-dependencies.jar storm.starter.WordCountTopology wordcount
```

``storm-slider list``コマンドでstorm_cluster1上で動くトポロジをリストしてみましょう。

```bash:トポロジ動いたかな
yarn$ /usr/hdp/2.2.0.0-1084/storm-slider-client/bin/storm-slider --app storm_cluster1 --user yarn list
(略)
Topology_name        Status     Num_tasks  Num_workers  Uptime_secs
-------------------------------------------------------------------
wordcount            ACTIVE     28         2            172       
```

あ、何か動いたぽい。

と、思いましたがworkerのログを見たところ、storm-sliderが中でつかうstorm関連クラスのシリアルバージョンとupstreamのstorm関連クラスのシリアルバージョンが合わないのでエラーになって動いていませんでした...。

```console:心底腹が立つシリアルバージョンのエラー
java.io.InvalidClassException: Data; local class incompatible: stream classdesc serialVersionUID = -1826836446123557654, local class serialVersionUID = (略)
```

## Stormトポロジを投入してみる（ゴリ押し）

仕方が無いので、``storm_cluster1``のNimbusのポートを自分で調べて通常のstormコマンドでトポロジを起動することにします。
YARNのアプリケーションディレクトリ中の設定ファイルから、``nimbus.thrift.port``と``nimbus.host``プロパティの設定を調べてそこにアクセスさせるだけのはずです。最初からこうすればよかった。

まず、nimbusを動かしているアプリケーションのコンフィグディレクトリを特定する。

```console:nimbusのランニングコンフィグ探し
$ ps aux | grep nimbus | grep java
yarn      7076  0.0  0.0 106192  1576 ?        S    13:47   0:00 /bin/bash --login -c env JAVA_HOME=/usr/lib/jvm/java-1.7.0-openjdk.x86_64 PATH=/usr/lib/jvm/java-1.7.0-openjdk.x86_64/bin:$PATH STORM_BASE_DIR=/hadoop/yarn/local/usercache/yarn/appcache/application_1419252594056_0003/container_1419252594056_0003_01_000002/app/install/apache-storm-0.9.3 STORM_CONF_DIR=/hadoop/yarn/local/usercache/yarn/appcache/application_1419252594056_0003/container_1419252594056_0003_01_000002/app/install/apache-storm-0.9.3/conf /usr/bin/python /hadoop/yarn/local/usercache/yarn/appcache/application_1419252594056_0003/container_1419252594056_0003_01_000002/app/install/apache-storm-0.9.3/bin/storm nimbus > /hadoop/yarn/log/application_1419252594056_0003/container_1419252594056_0003_01_000002/nimbus.out 2>&1
```

上記をよく見ると``/hadoop/yarn/local/usercache/yarn/appcache/application_1419252594056_0003/container_1419252594056_0003_01_000002/app/install/apache-storm-0.9.3/conf``がコンフィグディレクトリなことがわかります。

```console:ランニングコンフィグを見る
$ grep nimbus /hadoop/yarn/local/usercache/yarn/appcache/application_1419252594056_0003/container_1419252594056_0003_01_000002/app/install/apache-storm-0.9.3/conf/storm.yaml
(略)
nimbus.thrift.port: 44781
nimbus.host: 'sandbox.hortonworks.com'
(略)
```

ここで調べたホスト・ポートに対してコマンドを実行してみます。

```console:今度こそトポロジの実行
$ cd ~/apache-storm-0.9.3
$ ./bin/storm -c nimbus.host=sandbox.hortonworks.com -c nimbus.thrift.port=44781 jar examples/storm-starter/storm-starter-topologies-0.9.3.jar storm.starter.WordCountTopology wordcount
```

適当にworker(Stormのタスク)のログを見ると、

```console:何とか動きましたとさ。
$ tail /hadoop/yarn/log/application_1419252594056_0003/container_1419252594056_0003_01_000006/worker-39329.log
2014-12-22T14:12:52.005+0000 b.s.d.task [INFO] Emitting: count default [score, 9417]
2014-12-22T14:12:52.005+0000 b.s.d.task [INFO] Emitting: split default ["two"]
2014-12-22T14:12:52.007+0000 b.s.d.task [INFO] Emitting: split default ["with"]
2014-12-22T14:12:52.008+0000 b.s.d.task [INFO] Emitting: split default ["nature"]
2014-12-22T14:12:52.008+0000 b.s.d.executor [INFO] Processing received message source: split:20, stream: default, id: {}, ["nature"]
2014-12-22T14:12:52.008+0000 b.s.d.task [INFO] Emitting: count default [nature, 9458]
2014-12-22T14:12:52.010+0000 b.s.d.executor [INFO] Processing received message source: split:20, stream: default, id: {}, ["with"]
2014-12-22T14:12:52.010+0000 b.s.d.task [INFO] Emitting: count default [with, 9458]
2014-12-22T14:12:52.013+0000 b.s.d.executor [INFO] Processing received message source: split:22, stream: default, id: {}, [the]
2014-12-22T14:12:52.014+0000 b.s.d.task [INFO] Emitting: count default [the, 37497]
```

動いていることが確認できました！昔、[Stormコマンドのコンフィグ差し替え方法](http://qiita.com/takeuchikzm/items/44e683bc98106b65b9b2)調べてといてよかった！

# まとめとか所感

StormやHBase等の常時起動前提だったりレスポンス重視の分散アプリケーションをYARNのリソース管理化で起動するためのツールであるApache Sliderについて紹介しました。

また、実際にStormをSlider経由で動作させてみました。(細かい部分に刺さって中途半端になってしまったのは残念です。)

現時点では、正直各々のプロダクトがちゃんとYARN対応してくれたほうが安心だなぁとも思います。ただ、非常に面白いプロダクトなのでこれからもウォッチはして行こうと思います。
極力環境に疎なアプリケーションの書き方の雛形という意味でSliderの提示した[分散アプリケーションの要件](http://slider.incubator.apache.org/docs/slider_specs/application_needs.html)は使えるかもしれませんね。