CentOS 6 で QJM ベースの NameNode HA + 自動フェイルオーバを構成した時のメモ

Hadoop HDFS の NameNode は長い間単一障害点だったのですが、 CDH 4 から、 NFS 上の edits ログを共有する形でのアクティブ/スタンバイ構成が可能になりました。しかし、フェイルオーバが中途半端になると共有 edits ログが破壊されるとか、 NFS が新しい単一障害点になってしまうとかで、評判が良くなかったようです。

そこで CDH 4.1 からは、 JournalManager という、 edits ログを書き込む専門のデーモンが追加されました。アクティブな NameNode は、 Quorum Journal Manager (QJM) を通じて、共有 edits ログを JournalNode クラスタに書き込めるようになりました。

今回、 QJM ベースの NameNode 冗長化を試すため、 HDFS クラスタを作ってみました。同時に ZooKeeper を使った NameNode 自動フェイルオーバも試してみました。その時のメモを書きます。

ソフトウェア

OS CentOS 6
Hadoop Distro CDH 4.1.2

マシン構成

ホスト名 役割 パッケージ
hjt JournalNode, ZooKeeper hadoop-hdfs-journalnode, zookeeper-server
hnn1, hnn2 NameNode, JournalNode, ZooKeeper hadoop-hdfs-namenode, hadoop-hdfs-zkfc, hadoop-hdfs-journalnode, zookeeper-server
hslave1 - hslave4 DataNode hadoop-hdfs-datanode
hclient Hadoop クライアント hadoop-client

SecondaryNameNode / CheckpointNode は必要ありません。スタンバイ NameNode がチェックポイントを行うためです。

Hadoop と ZooKeeper の設定

core-site.xml
プロパティ
fs.default.name hdfs://localns

ホスト名の代わりに、 dfs.nameservices のネームサービス名を設定します。

hdfs-site.xml
プロパティ
dfs.nameservices localns
dfs.ha.namenodes.localns hnn1,hnn2
dfs.namenode.rpc-address.localns.hnn1 hnn1.localnet:8020
dfs.namenode.http-address.localns.hnn1 hnn1.localnet:50070
dfs.namenode.rpc-address.localns.hnn2 hnn2.localnet:8020
dfs.namenode.http-address.localns.hnn2 hnn2.localnet:50070
dfs.namenode.shared.edits.dir qjournal://hjt.localnet:8485; hnn1.localnet:8485;hnn2.localnet:8485/localns (本当はスペースなし)
dfs.client.failover.proxy.provider.localns org.apache.hadoop.hdfs.server. namenode.ha.ConfiguredFailoverProxyProvider (本当はスペースなし)
dfs.ha.automatic-failover.enabled true
dfs.ha.fencing.methods sshfence
dfs.ha.fencing.ssh.private-key-files ${user.home}/.ssh/id_rsa
ha.zookeeper.quorum hjt.localnet:2181, hnn1.localnet:2181, hnn2.localnet:2181

dfs.ha.namenodes.localns には NameNode (以下 NN) の ID を指定します。常識的にはホスト名をそのまま指定するものでしょう。 HA 構成の管理コマンドを発行する時 (hdfs haadmin ...) は、この ID を使います。

dfs.ha.fencing.methods には sshfence だけを指定しています。この場合、何かがおかしくなって NN の自動フェイルオーバが発生すると、新しくアクティブになった側の ZKFC が、以前アクティブだった側の NN を、 SSH 経由で殺しに行きます。各 NN の hdfs ユーザ同士は、 SSH で鍵認証できるようにする必要があります。

CDH 4.0 時点から提供されていた、 NFS ベースの NN 冗長化では、自らがアクティブだと認識している NN が複数存在すると、 edits ログがぐちゃぐちゃに破壊されてしまう可能性がありました。このため、自動フェイルオーバ時に、古い側の NN に確実にとどめを刺して二度と NFS にアクセスできないようにするため、 sshfence に加えてクラスタ固有のコマンドでフェンシングを行う必要がありました *1 。 QJM ベースの冗長化では、古い側の NN からの書き込みを JournalNode が弾くため、 edits ログが破壊されることはありません *2 。このため、 sshfence だけで OK な場合もあるんじゃないかなーという感じです。

hadoop-env.sh

インストールした時点では /etc/hadoop/conf 下に hadoop-env.sh が入っていないので *3 、ソースから持って来ます。次の二点を修正すればとりあえず動きます。

変数 変更内容
JAVA_HOME /usr/java/default
HADOOP_IDENT_STRING 削除

HADOOP_IDENT_STRING を持って来たままにしておくと、 init スクリプトHadoop の各サービスを制御した時に、 start したサービスが status も stop もできなくなってしまいます *4 。とりあえず消しとけば大丈夫です。

zoo.cfg

追加部分のみ。

プロパティ
server.1 hjt.localnet:2888:3888
server.2 hnn1.localnet:2888:3888
server.3 hnn2.localnet:2888:3888

「server.X」の X の部分が、 ZooKeeper Server ごとの myid です。

手順

冗長化関係ないとこもそれなりに書きます。

前準備
  • NTP で各ノードが時刻同期するようにする
  • SELinux 切る
  • iptables 切る (ちゃんと設定する方が良いんだと思うけど大変)
  • limits.conf で hdfs ユーザと mapred ユーザがファイルをたくさん開けるようにする (nofile)
  • 各サーバで Oracle JDK, CDH の各パッケージを入れる
  • ZooKeeper と Hadoop の設定ファイルを各ノードにばらまく
名前解決の確認

FQDN で正引きできることを確認します。

taku@hclient ~$ nslookup hnn1.localnet
Server:		192.168.**.**
Address:	192.168.**.**#53

Name:	hnn1.localnet
Address: 192.168.XX.YY

それから、逆引きで FQDN が返ることを確認します。

taku@hclient ~$ nslookup 192.168.XX.YY
Server:		192.168.**.**
Address:	192.168.**.**#53

YY.XX.168.192.in-addr.arpa	name = hnn1.localnet.
NN 同士が SSH で鍵認証できるようにする

NN サーバの hdfs ユーザ同士が SSH 鍵認証できるようにします。まず hnn1 の鍵を hnn2 に登録します。

hdfs@hnn1$ ssh-keygen
...
hdfs@hnn1$ ssh-copy-id hnn2
...

hnn2 から hnn1 へも同様に。

hdfs@hnn2$ ssh-keygen
...
hdfs@hnn2$ ssh-copy-id hnn1
...

CDH のパッケージでインストールすると、 hdfs ユーザのホームディレクトリ (/var/lib/hadoop-hdfs) のパーミッションが 775 になっています。これだと鍵による認証ができないので、 755 にします。

hdfs@hnn1$ chmod g-w ~hdfs
hdfs@hnn2$ chmod g-w ~hdfs

互いにパスワードなしで認証できれば OK 。

hdfs@hnn1$ ssh hnn2 hostname
hnn2.localnet
hdfs@hnn2$ ssh hnn1 hostname
hnn1.localnet
ZooKeeper クラスタの構築, 起動

hjt, hnn1, hnn2 の各マシンで ZooKeeper 用のディレクトリを初期化します。指定する myid は zoo.cfg で設定したものです。

root@hjt# service zookeeper-server init --myid=1
Using myid of 1
root@hnn1# service zookeeper-server init --myid=2
Using myid of 2
root@hnn2# service zookeeper-server init --myid=3
Using myid of 3

hjt, hnn1, hnn2 の各マシンで zookeeper-server を上げます。

root@hjt# service zookeeper-server start
...
ZKFC の初期化, 起動

ZKFC 用の znode を作成します。 ZooKeeper をインストールした任意のマシンで次のコマンドを実行します。

root@hjt$ sudo -u hdfs hdfs zkfc -formatZK
...

そんで、 NN のマシン (hnn1, hnn2) で hadoop-hdfs-zkfc を上げます 。

root@hnn1# service hadoop-hdfs-zkfc start
...
JournalNode 起動

hjt, hnn1, hnn2 の各マシンで hadoop-hdfs-journalnode を上げます。

root@hjt# service hadoop-hdfs-journalnode start
...
NameNode のメタデータ作成, 起動

アクティブ側で NN のメタデータを作成して、起動します。

root@hnn1# sudo -u hdfs hdfs namenode -format
...
root@hnn1# service hadoop-hdfs-namenode start
...

スタンバイ側に NN のメタデータをコピーして、起動します。

root@hnn2# sudo -u hdfs hdfs namenode -bootstrapStandby
...
root@hnn2# service hadoop-hdfs-namenode start
...

各 NN の状態を確認します。ここで指定するのは dfs.ha.namenodes.localns で設定した NN の ID です。

taku@hclient$ sudo -u hdfs hdfs haadmin -getServiceState hnn1
active

taku@hclient$ sudo -u hdfs hdfs haadmin -getServiceState hnn2
standby
DataNode 起動

hslave1〜hslave4 の各マシンで DataNode を上げます。

root@hslave1# service hadoop-hdfs-datanode start
...

これでひと通り HDFS クラスタが完成したはずです。

NameNode 手動フェイルオーバ確認

hnn1 から hnn2 にフェイルオーバします。

taku@hclient ~$ sudo -u hdfs hdfs haadmin -failover hnn1 hnn2
Failover to NameNode at hnn2.localnet/192.168.**.**:8020 successful

taku@hclient ~$ sudo -u hdfs hdfs haadmin -getServiceState hnn1
standby

taku@hclient ~$ sudo -u hdfs hdfs haadmin -getServiceState hnn2
active

次は hnn2 から hnn1 にフェイルオーバします。

taku@hclient ~$ sudo -u hdfs hdfs haadmin -failover hnn2 hnn1
Failover to NameNode at hnn1.localnet/192.168.**.**:8020 successful

taku@hclient ~$ sudo -u hdfs hdfs haadmin -getServiceState hnn1
active

taku@hclient ~$ sudo -u hdfs hdfs haadmin -getServiceState hnn2
standby
NameNode 自動フェイルオーバ確認

アクティブ側の NN プロセスが暴走して応答しなくなったという想定で、自動フェイルオーバを確認します。

ここでは、 iptables を起動して、アクティブ側の NN のポートを塞ぐことでシミュレーションします。デフォルトの設定では 22 番ポートだけ通すようになっているので、これでも sshfence は通るはずです。

root@hnn1# service iptables start
...

hnn2 がアクティブに切り替わっていることを確認します。

taku@hclient ~$ sudo -u hdfs hdfs haadmin -getServiceState hnn2
active

元の NN が殺されていることを確認します。

[root@hnn1 ~]# service hadoop-hdfs-namenode status
Hadoop namenode is dead and pid file exists                [FAILED]

確認できたので、元に戻します。

root@hnn1# service iptables stop
...

root@hnn1# service hadoop-hdfs-namenode start
...

root@hnn1# sudo -u hdfs hdfs haadmin -failover hnn2 hnn1
Failover to NameNode at hnn1.localnet/192.168.**.**:8020 successful
OS 起動時に各サービスが立ち上がるようにする

その他さまざま確認できたら、各マシンのサービスが起動時に立ち上がるようにします。

root@hjt# chkconfig zookeeper-server on
...
root@hjt# chkconfig hadoop-hdfs-journalnode on
...
...
...

所感

書き起こしてみると長いな。

NFS ベースの構成に比べてほとんど欠点が見当たらないので、冗長化構成を取るなら、まず間違いなくこっちを選択することになるんじゃないかと思います。

*1:CDH4.0.0のNameNode HAを触ってみて

*2:Software Configuration for Quorum-based Storage

*3:#HADOOP-8287 etc/hadoop is missing hadoop-env.sh - ASF JIRA

*4:PID ファイルの名前を決めるのに、この変数の名前を使っているのだけど、コマンドによって /etc/default 下の設定値が有効になったり hadoop-env.sh の設定値が有効になったりしてバラバラ。名前の不一致で PID ファイルが参照できなくなる