HBaseのJuliet Pauseとリージョン復旧について

HBaseにはJuliet Pauseというよく知られた障害のシナリオがあります。

The HBase development team has affectionately dubbed this scenario a Juliet Pause ― the master (Romeo) presumes the region server (Juliet) is dead when it’s really just sleeping, and thus takes some drastic action (recovery). When the server wakes up, it sees that a great mistake has been made and takes its own life. Makes for a good play, but a pretty awful failure scenario!

Avoiding Full GCs in Apache HBase with MemStore-Local Allocation Buffers: Part 1 - Cloudera Engineering Blog

馬本10.2.5から訳を引用します。

HBaseの開発チームは、愛情を込めてこのシナリオをジュリエットポーズと呼んでいます。マスター(ロミオ)は、実は眠り込んでいるだけのリージョンサーバー(ジュリエット)のことを死んでしまったと思い込んでしまい、思い切った行動(リカバリ)にでます。目覚めたリージョンサーバーは、大変な間違いが起きてしまったことを知り、自らの命を絶つのです。演劇としては素晴らしいですが、障害のシナリオとしてはかなり困ったものです。

簡単にいうとGCのStop the Worldで一時停止している間にZooKeeperとのセッションがタイムアウトしてリージョンサーバーがダウンするというものです。

リージョンサーバーのログをsleptでgrepして以下のようになっていたらこの問題に遭遇している可能性があります。この辺は馬本の12.5.3.2も参照

WARN org.apache.hadoop.hbase.util.Sleeper: We slept 28678ms instead of 3000ms, this is likely due to a long garbage collecting pause and it's usually bad, see http://hbase.apache.org/book.html#trouble.rs.runtime.zkexpired

sleptに続いて以下のようになっていたらZooKeeperとのセッションがタイムアウトしていますね。

org.apache.zookeeper.KeeperException$SessionExpiredException: KeeperErrorCode = Session expired
        at org.apache.hadoop.hbase.zookeeper.ZooKeeperWatcher.connectionEvent(ZooKeeperWatcher.java:389)
        at org.apache.hadoop.hbase.zookeeper.ZooKeeperWatcher.process(ZooKeeperWatcher.java:286)
        at org.apache.zookeeper.ClientCnxn$EventThread.processEvent(ClientCnxn.java:521)
        at org.apache.zookeeper.ClientCnxn$EventThread.run(ClientCnxn.java:497)

最終的には以下のようにケンシロウ的なメッセージを残してリージョンサーバーがダウンします。

org.apache.hadoop.hbase.YouAreDeadException: org.apache.hadoop.hbase.YouAreDeadException: Server REPORT rejected; currently processing リージョンサーバのFQDN,ポート番号,スタートコード as dead server

回避策としてはヒープを増やしたりMapReduceジョブのスロット数を減らしたりzookeeper.session.timeoutを増やしたりといったあたりです。

OKWaveではJava7を使ってこの問題を回避しているようです。

長時間のGCが起きるとリージョンダウン扱いになってしまう。
そうするとZooKeeperがリージョンをクラスタから切り離して、GCが終わっても復帰できない。。
対策はGarbage-First GC(G1GC)の実行。
G1GCを実行するとStop the Worldの目標時間が設定できるので、停止目標10秒にする。
ただし, G1GCはJava7以降でないと正常に機能しないのでJava6以下の場合はバージョンアップが必要。

http://hatacomp.hateblo.jp/entry/dont-stop-hbase

リージョンサーバーがダウンするとリージョン移動が発生します。

で、ダウンしたリージョンサーバーを復旧して起動すると受け持っているリージョン数が0になっているのでこれを戻す必要があります。

これを手動で戻す方法について書いてみます。

moveコマンドでリージョン移動するわけですがhelpを見ると以下のようになっています。
リージョンサーバーのFQDNとポート番号はいいとしてスタートコードなるものも必要です。
これはWeb UIを見ればわかりますがruby(というかjruby)で取得することもできます。

Command: move
Move a region.  Optionally specify target regionserver else we choose one
at random.  NOTE: You pass the encoded region name, not the region name so
this command is a little different to the others.  The encoded region name
is the hash suffix on region names: e.g. if the region name were
TestTable,0094429456,1289497600452.527db22f95c8a9e0116f0cc13c680396. then
the encoded region name portion is 527db22f95c8a9e0116f0cc13c680396
A server name is its host, port plus startcode. For example:
host187.example.com,60020,1289493121758
Examples:

  hbase> move 'ENCODED_REGIONNAME'
  hbase> move 'ENCODED_REGIONNAME', 'SERVER_NAME'

ruby(というかjruby)で取得する場合はこんな感じですね。

include Java

import org.apache.hadoop.hbase.HBaseConfiguration
import org.apache.hadoop.hbase.client.HBaseAdmin

config = HBaseConfiguration.create()
admin = HBaseAdmin.new(config) 
serverInfos = admin.getClusterStatus().getServerInfo()
for server in serverInfos
        puts server.getServerName()
end

ダウンしたリージョンサーバーがどのリージョンを持っていたかは.META.を見ればわかります。
とはいえダウンした後はリージョン移動してしまっているためダウン前の情報が必要です。
なので定期的に.META.の情報をscanしておいたほうが良さげです。

scan '.META'すると以下のようになっています。

 t1,,1356771578584.5d3e512ddd794ab2a8f6ae0412cca1e7. column=info:regioninfo, timestamp=1356771578616, value={NAME => 't1,,1356771578584.5d3e512ddd794ab2a8f6ae0412cca1e7.', STARTKEY => '', ENDKEY => '', ENCODED => 5d3e512ddd794ab2a8f6ae0412cca1e7,}
 t1,,1356771578584.5d3e512ddd794ab2a8f6ae0412cca1e7. column=info:server, timestamp=1356771578691, value=リージョンサーバのFQDN:ポート番号
 t1,,1356771578584.5d3e512ddd794ab2a8f6ae0412cca1e7. column=info:serverstartcode, timestamp=1356771578691, value=1356771556838

一つのリージョンに対してinfoというcolumn familyがありregioninfo, server, serverstartcodeという3つのqualifierがあります。

最終的には以下のようなスクリプトを実行すればリージョン移動ができます。どうせなら全部rubyで書いたほうがいいんでしょうけど、僕のruby(というかjruby)力が足りなくてシェルとコンボになってます。

down_regionserver_name=...
scan_meta_file=...
#ダウンしたリージョンサーバーの「リージョンサーバのFQDN,ポート番号,スタートコード」を取得する。
server_name=`hbase org.jruby.Main reginserver.rb | grep ${down_regionserver_name}`

#scan '.META.'した結果からダウンしたリージョンサーバーがもともとどのリージョンを保持していたかを取得する。
grep ${down_regionserver_name} ${scan_meta_file} | awk -F'.' '{print $2}' | while read region_name
do
        #リージョン移動
        echo "move '${region_name}', '${server_name}'" | hbase shell
done