Apache HadoopのHDFS Router-based Federationを触ってみる

# はじめに

RBF（Router-Based Federation）はApache HadoopのHDFSの新しい機能です。RBFを有効にすることで、ユーザーにとって複数のHadoopクラスタを１つのHadoopクラスタとして透過的に扱うことができます。

RBFが登場した背景や開発状況は下記の資料を参照してください。

* https://qiita.com/ajis_ka_old/items/d132c040f98836f2f3de
* https://www.slideshare.net/techblogyahoo/apache-hadoop-hdfs2018dbts2018

この記事では具体例を用いてRBFのセットアップ方法について説明します。（※Hadoopのバージョンは現在リリースVOTE中の3.2.0を用いています）

# Router-Based Federationのシステム構成例

本記事では下記のシステム構成でRBFをセットアップします。

* **Router x 2**
  * クライアントのリクエストを受け取り、StateStoreのマウント情報を参照し、適切なNameNodeへルーティングします
* **StateStore（ZooKeeper）**
  * HDFSに関するHadoopクラスタのマウント情報を保存しています。つまり、複数のHadoopクラスタのHDFSを束ねて、１つの仮想的なディレクトリツリーを保持しています
* **Hadoopクラスタ x 2**
* **Gateway（Hadoopクライアントがインストールされたノード）**

<img width="500" alt="rbf_system.png" src="https://qiita-image-store.s3.amazonaws.com/0/106566/06a81e34-27a6-47ed-a3a0-cb7902922171.png">

# クラスタ側の設定

クラスタ側ではRouterの存在を意識する必要はありません。クラスタＡ、Ｂともに下記のように通常のNameNode HAの設定になっています。（※設定のXMLはHadoopの3系から簡易フォーマットで記述できるようになりました。）

* クラスタＡの`hdfs-site.xml`のHA設定

```xml
<!-- ネームサービス -->
<property name="dfs.nameservices" value="auction" />

<!-- NameNodeID -->
<property name="dfs.ha.namenodes.auction" value="nn1,nn2" />

<!-- RPCアドレス -->
<property name="dfs.namenode.rpc-address.auction.nn1"  value="auction-nn1:8020" />
<property name="dfs.namenode.rpc-address.auction.nn2"  value="auction-nn2:8020" />

<!-- HTTPアドレス -->
<property name="dfs.namenode.http-address.auction.nn1" value="auction-nn1:9870" />
<property name="dfs.namenode.http-address.auction.nn2" value="auction-nn2:9870" />
```

* クラスタＢの`hdfs-site.xml`のHA設定

```xml
<!-- ネームサービス -->
<property name="dfs.nameservices" value="shopping" />

<!-- NameNodeID -->
<property name="dfs.ha.namenodes.shopping" value="nn1,nn2" />

<!-- RPCアドレス -->
<property name="dfs.namenode.rpc-address.shopping.nn1"  value="shopping-nn1:8020" />
<property name="dfs.namenode.rpc-address.shopping.nn2"  value="shopping-nn2:8020" />

<!-- HTTPアドレス -->
<property name="dfs.namenode.http-address.shopping.nn1" value="shopping-nn1:9870" />
<property name="dfs.namenode.http-address.shopping.nn2" value="shopping-nn2:9870" />
```

# Routerの設定

Routerの設定値も`hdfs-site.xml`に記述します。

※2019年6月追記
[HDFS-14516](https://issues.apache.org/jira/browse/HDFS-14516)により`hdfs-rbf-site.xml`にも記述できるようになりました。（Hadoop-3.3.0以降）

```xml
<!-- ネームサービス（クラスタ側の設定と合わせなくて良い） -->
<property name="dfs.nameservices" value="ns1,ns2" />

<!-- NameNodeID（クラスタ側の設定と合わせなくて良い） -->
<property name="dfs.ha.namenodes.ns1" value="nn1,nn2" />
<property name="dfs.ha.namenodes.ns2" value="nn1,nn2" />

<!-- クラスタＡ(ns1)のアドレス -->
<property name="dfs.namenode.rpc-address.ns1.nn1"  value="auction-nn1:8020" />
<property name="dfs.namenode.rpc-address.ns1.nn2"  value="auction-nn2:8020" />
<property name="dfs.namenode.http-address.ns1.nn1" value="auction-nn1:9870" />
<property name="dfs.namenode.http-address.ns1.nn2" value="auction-nn2:9870" />

<!-- クラスタＢ(ns2)のアドレス -->
<property name="dfs.namenode.rpc-address.ns2.nn1"  value="shopping-nn1:8020" />
<property name="dfs.namenode.rpc-address.ns2.nn2"  value="shopping-nn2:8020" />
<property name="dfs.namenode.http-address.ns2.nn1" value="shopping-nn1:9870" />
<property name="dfs.namenode.http-address.ns2.nn2" value="shopping-nn2:9870" />

<!-- デフォルトのネームサービスはクラスタＡを指定 -->
<property name="dfs.federation.router.default.nameserviceId" value="ns1" />

<!-- StateStoreはZooKeeperを指定 -->
<property name="dfs.federation.router.store.driver.class"
  value="org.apache.hadoop.hdfs.server.federation.store.driver.impl.StateStoreZooKeeperImpl" />

<!-- ルーティング先のNameNodeを指定 -->
<property name="dfs.federation.router.monitor.namenode" value="ns1.nn1,ns1.nn2,ns2.nn1,ns2.nn2" />

<!-- RouterとNameNodeが同じホストならtrue -->
<property name="dfs.federation.router.monitor.localnamenode.enable" value="false" />

<!-- グローバルなクォータ設定ができるようにする -->
<property name="dfs.federation.router.quota.enable" value="true" />

<!-- 複数クラスタのマウント機能を有効にする場合 -->
<property name="dfs.federation.router.file.resolver.client.class"
  value="org.apache.hadoop.hdfs.server.federation.resolver.MultipleDestinationMountTableResolver" />
```

現在の仕様では、Routerの設定ファイルにルーティング対象の全てのNameNodeのRPCアドレスとHTTPアドレスを記述する必要があります。クラスタの数が増えるとそれも大変なので、将来的にはサービスディスカバリの機能が実装される予定です。

※2019年6月追記
[HDFS-14118](https://issues.apache.org/jira/browse/HDFS-14118)によりDNSラウンドロビンを使ってNameNodeやRouterの名前解決ができる機能が追加されました。これにより設定ファイルに実ホスト名を記述する必要がなくなり、ホストの追加・削除が容易になります。（Hadoop-3.3.0以降）

# Routerの起動と停止
ここまでの設定に問題がなければRouterを起動することができます。

```
// 起動
[router]$ hdfs --daemon start dfsrouter

// 停止
[router]$ hdfs --daemon stop dfsrouter
```

# クライアント側（Gateway）の設定

クライアント側では特に何も設定しなくても、NameNodeの代わりにRouterに直接リクエストを投げることで、Hadoopコマンドを実行することができます。

```
[gateway]$ hadoop fs -ls hdfs://router1:8888/
```

あるいは今回のように冗長化したRouterに対してNameNode HAと同様な設定を行うことで、クライアントのリクエストに対するフェイルオーバー機能を有効にできます。

```xml
<!-- Routerに向き先を設定する-->
<property name="dfs.nameservices" value="ns-fed" />
<property name="dfs.ha.namenodes.ns-fed" value="r1,r2" />
<property name="dfs.namenode.rpc-address.ns-fed.nn1"  value="router1:8888" />
<property name="dfs.namenode.rpc-address.ns-fed.nn2"  value="router2:8888" />

<!-- フェイルオーバーの設定 -->
<property name="dfs.client.failover.proxy.provider.ns-fed"
  value="org.apache.hadoop.hdfs.server.namenode.ha.ConfiguredFailoverProxyProvider" />
<property name="dfs.client.failover.random.order"  value="true" />
```

```
[gateway]$ hadoop fs -ls hdfs://ns-fed/
```

この場合片方のRouterに障害が起きても、クライアントからのリクエストは失敗しません。

# サンプルデータの準備

事前にそれぞれのクラスタでサンプルデータを作成して起きます。

```
// クラスタＡ
[gateway]$ hadoop fs -mkdir hdfs://auction-nn1:8020/foo
[gateway]$ echo bar > bar.txt && hadoop fs -put bar.txt hdfs://auction-nn1:8020/foo
hadoop fs -ls -R hdfs://auction-nn1:8020/
drwxr-xr-x   - tasanuma users          0 2018-11-30 01:21 hdfs://auction-nn1:8020/foo
-rw-r--r--   3 tasanuma users          4 2018-11-30 01:21 hdfs://auction-nn1:8020/foo/bar.txt

// クラスタＢ
[gateway]$ hadoop fs -mkdir hdfs://shopping-nn1:8020/fizz
[gateway]$ echo buzz > buzz.txt && hadoop fs -put buzz.txt hdfs://shopping-nn1:8020/fizz
hadoop fs -ls -R hdfs://shopping-nn1:8020/
drwxr-xr-x   - tasanuma users          0 2018-11-30 01:23 hdfs://shopping-nn1:8020/fizz
-rw-r--r--   3 tasanuma users          5 2018-11-30 01:23 hdfs://shopping-nn1:8020/fizz/buzz.txt
```

この時点でRouterに対してルートディレクトリをリストしてみると、クラスタＡのデータを参照しています。

```
[gateway]$ hadoop fs -ls -R hdfs://ns-fed/
drwxr-xr-x   - tasanuma users          0 2018-11-30 01:21 hdfs://ns-fed/foo
-rw-r--r--   3 tasanuma users          4 2018-11-30 01:21 hdfs://ns-fed/foo/bar.txt
```

これはRouterの設定で`dfs.federation.router.default.nameserviceId=ns1`としたので、フォールバックによりクラスタＡを参照したためです。

# マウントの管理

マウントの管理は`hdfs dfsrouteradmin`コマンドで行います。（※基本的にこのコマンドはRouterが稼働しているホストで実行します。）

```
Usage: hdfs dfsrouteradmin :
	[-add <source> <nameservice1, nameservice2, ...> <destination> [-readonly] [-order HASH|LOCAL|RANDOM|HASH_ALL] -owner <owner> -group <group> -mode <mode>]
	[-update <source> <nameservice1, nameservice2, ...> <destination> [-readonly] [-order HASH|LOCAL|RANDOM|HASH_ALL] -owner <owner> -group <group> -mode <mode>]
	[-rm <source>]
	[-ls <path>]
	[-setQuota <path> -nsQuota <nsQuota> -ssQuota <quota in bytes or quota size string>]
	[-clrQuota <path>]
	[-safemode enter | leave | get]
	[-nameservice enable | disable <nameservice>]
	[-getDisabledNameservices]
```

### クラスタのマウント/アンマウント

```
// マウント
$ hdfs dfsrouteradmin -add <マウント先のパス> <ネームサービス> <マウント元のクラスタ側パス>

// アンマウント
$ hdfs dfsrouteradmin -rm <マウント先のパス>
```

ネームサービスはRouterの設定の`dfs.nameservices`で指定した値です。
ここでは仮想的なディレクトリツリーの`/auc`にクラスタＡ、`/shp`にクラスタＢのルートディレクトリをマウントしてみます。

```
[router]$ hdfs dfsrouteradmin -add /auc ns1 /
[router]$ hdfs dfsrouteradmin -add /shp ns2 /

// マウントテーブルの参照
[router]$ hdfs dfsrouteradmin -ls
Mount Table Entries:
Source                    Destinations              Owner                     Group                     Mode                      Quota/Usage
/auc                      ns1->/                    tasanuma                  users                     rwxr-xr-x                 [NsQuota: -/-, SsQuota: -/-]
/shp                      ns2->/                    tasanuma                  users                     rwxr-xr-x                 [NsQuota: -/-, SsQuota: -/-]
```

クライアント側（Gateway）で参照できるか確認してみます。

```
[gateway]$ hadoop fs -ls -R hdfs://ns-fed/
drwxrwxrwx   - tasanuma users          0 2018-11-30 01:46 hdfs://ns-fed/auc
drwxr-xr-x   - tasanuma users          0 2018-11-30 01:21 hdfs://ns-fed/auc/foo
-rw-r--r--   3 tasanuma users          4 2018-11-30 01:21 hdfs://ns-fed/auc/foo/bar.txt
drwxr-xr-x   - tasanuma users          0 2018-11-30 01:21 hdfs://ns-fed/foo <--- hdfs://ns-fed/auc/fooと同じ
-rw-r--r--   3 tasanuma users          4 2018-11-30 01:21 hdfs://ns-fed/foo/bar.txt
drwxrwxrwx   - tasanuma users          0 2018-11-30 01:46 hdfs://ns-fed/shp
drwxr-xr-x   - tasanuma users          0 2018-11-30 01:23 hdfs://ns-fed/shp/fizz
-rw-r--r--   3 tasanuma users          5 2018-11-30 01:23 hdfs://ns-fed/shp/fizz/buzz.txt
```
１つのHDFSとして２つのクラスタを参照できました。

マウント時のオプションで読み込み専用やオーナー、グループ、パーミッションの指定も可能です。

```
[router]$ hdfs dfsrouteradmin -add /auc_data ns1 /data -readonly -owner alice -group analysts -mode 770
Successfully added mount point /auc_data

[router]$ hdfs dfsrouteradmin -ls /auc_data
Mount Table Entries:
Source                    Destinations              Owner                     Group                     Mode                      Quota/Usage
/auc_data                 ns1->/data                alice                     analysts                  rwxrwx---                 [NsQuota: -/-, SsQuota: -/-]
```

### マウントしたネームサービスの無効化/有効可

クラスタ自体をデコミッションしたいときはアンマウントするよりもネームサービスを無効化した方が楽です。

```
// ネームサービスの無効化
[router]$ hdfs dfsrouteradmin -nameservice disable ns1

// ネームサービスの有効化
[router]$ hdfs dfsrouteradmin -nameservice enable ns1
```

### グローバルなクォータ設定

マウントポイントごとにネームスペースクォータ（ファイル数の制限）とスペースクォータ（データ量の制限）を設定できます。

```
[router]$ hdfs dfsrouteradmin -setQuota /auc -nsQuota 100 -ssQuota 1024
```

### 複数クラスタのマウント

１つのマウントポイントに複数のクラスタを紐づけることができます。

```
[router]$ hdfs dfsrouteradmin -add /data ns1,ns2 /data -order HASH

[router]hdfs dfsrouteradmin -ls /data
Mount Table Entries:
Source                    Destinations              Owner                     Group                     Mode                      Quota/Usage
/data                     ns1->/data,ns2->/data     tasanuma                  users                     rwxr-xr-x                 [NsQuota: -/-, SsQuota: -/-]
```

この場合`/data`をリストすると、クラスタＡとＢ両方の`/data`ディレクトリ配下の全てのファイル・ディレクトリが表示されます。

# RouterのWebUI

RouterはNameNodeライクなWeb UIを提供しています。デフォルトのURLは`http://<Router hostname>:50071`です。

### トップページ

<img width="600" alt="rbf_top.png" src="https://qiita-image-store.s3.amazonaws.com/0/106566/460583ca-4141-6759-7a5b-5e801efc9959.png">

### ルーティング先のクラスタ情報

<img width="600" alt="rbf_sub_clusters.png" src="https://qiita-image-store.s3.amazonaws.com/0/106566/403d1741-cf09-3092-ec84-6951beddd8eb.png">

### Router一覧

<img width="600" alt="rbf_routers.png" src="https://qiita-image-store.s3.amazonaws.com/0/106566/526fbe47-bc6f-752d-4b5a-9ad4a1510ad6.png">

### マウントテーブル

<img width="600" alt="rbf_mount_tables.png" src="https://qiita-image-store.s3.amazonaws.com/0/106566/3baaa361-9a5e-299e-85eb-daaf169b045d.png">

# 感想

クラスタ、Router、クライアントで設定値が異なるため少し注意が必要ですが、とても便利な機能だと思います。特にクライアント側に負担が少ないのが良いところです。複数のHadoopクラスタを運用している場合は是非検討してみてください。

# 公式ドキュメント

* https://hadoop.apache.org/docs/r3.1.1/hadoop-project-dist/hadoop-hdfs-rbf/HDFSRouterFederation.html
* https://hadoop.apache.org/docs/r3.1.1/hadoop-project-dist/hadoop-hdfs-rbf/hdfs-rbf-default.xml