投稿日: 2015/03/03
本記事は、Cloudera のソフトウェア・エンジニア Yongjun Zhang による記事を翻訳したものです。原文についてはこちらをご覧ください。
HDFSの重要な設計要件のひとつとして、連続的かつ正確な動作の保証が挙げられる。ネットワークやノード障害時に、HDFSへの書き込みの正確さを保証することは複雑な問題のひとつと言えるだろう。これは、リースリカバリ、ブロックリカバリ、あるいはパイプラインリカバリプロセスにより保証される。そのため、いつ、なぜこれらのリカバリプロセスがコールされるのか、またそれらが何をするべきなのかを理解することは、ユーザーにとっても開発者にとっても、自身のHDFSクラスタがどのように動作するのか理解する手助けになると思われる。
この記事では、これらのリカバリプロセスを詳細に説明する。まずはHDFSの書き込みパイプラインやそのリカバリプロセスを簡単に説明するところから始めよう。次にブロックとレプリカの状態/世代スタンプといった重要なコンセプトについて述べ、各リカバリプロセスに踏み込んでいこう。最終的に、関連するIssueをいくつかリストし(解決済み/オープン中どちらも対象とする)、結びとする。
この記事は2つにわけて公開する予定である。その1ではリースリカバリ、ブロックリカバリの詳解とし、その2ではパイプラインリカバリについて詳細を説明する。より詳しく知りたい場合は実装にあたってのデザインドキュメント design specification: Append, Hflush, and Read を参照するとよいだろう。
はじめに
HDFSではファイルがブロックに分割され、ファイルへのアクセスはmulti-reader, single-writerセマンティクスに従っている。障害耐性の要件を満たすため、複数のブロックレプリカが異なるデータノードに格納される。レプリカの数は複製係数と呼ばれる。新しいファイルブロックが作成されるか、または既存のファイルが追記のために開かれたとき、HDFSの書き込み処理はレプリカの受信と保存のためにデータノードのパイプラインを作成する(複製係数は一般に、パイプライン内のデータノードの数を決定する)。そのブロックへの連続書き込みは、同じパイプライン(図1)を通過することになる。
読み取り操作の際は、クライアントはブロックのコピーを保持しているデータノードのひとつを選択し、データ転送を要求する。
以下は、障害耐性のための要件の必要性を強調したふたつのアプリケーションのシナリオである。
・HBaseのリージョンサーバー(RS)は、WAL(先行書き込みログ)というHDFSファイルに書き込むことでデータの損失を防いでいる。あるRSがダウンした場合新たなRSが開始されるが、WALを読み込むことによって前のRSの状態を再構築している。RSがダウンした際に書き込みパイプラインが終了してなかった場合、パイプライン内のデータノード同士が同期していない可能性が考えられる。HDFSは、正しいRSの状態を再構築するためにWALから必要なデータがすべて読み込まれることを保証しなければならない。
・FlumeクライアントがHDFSファイルにデータをストリーミングしているときには、パイプライン内のいくつかのデータノードに障害が発生したり応答が停止しても、連続して書き込むことができなければならない。
リースリカバリ、ブロックリカバリ、およびパイプラインリカバリはこの種の状況において作用するものである。
・クライアントはHDFSファイルを書き込む前に、リースを取得しなければならない。これは本質的にロックであり、single-writerセマンティクスを保証するものである。クライアントが書き込みを維持したい場合には、リースが所定の時間内に更新されなければならない。リースが明示的に更新されなかったり、それを保持しているクライアントが機能していない場合は、期限切れとなる。これ(リース失効)が発生すると、HDFSはファイルを閉じ、他のクライアントがファイルに書き込むことができるようにクライアントに代わってリースを放棄する。このプロセスはリースリカバリと呼ばれる。
・書き込み中のファイルの最後のブロックがパイプライン内のすべてのデータノードに伝播されていない場合にリースリカバリが発生すると、書き込まれたデータ量がノード間で異なる事態が起きるだろう。リースリカバリはファイルをクローズさせる前に、最後のブロックのすべてのレプリカが同じファイル長であることを保証する必要がある。このプロセスはブロックリカバリと呼ばれている。ブロックリカバリはリースリカバリ処理中にのみ引き起こされ、リースリカバリはあるファイルの最後のブロックがCOMPLETE状態(後のセクションで定義する)でない場合にのみブロックリカバリを始動する。
・書き込みパイプライン動作時には、パイプライン内のデータノードが失敗することがある。この場合、パイプラインの下流での書き込み操作がただ失敗して終わりとすることはできない。代わりにHDFSは、パイプラインが継続し、クライアントがファイルへの書き込みを続行できるようにエラーから回復しようとする。パイプラインのエラーから回復するためのメカニズムは、パイプラインリカバリと呼ばれている。
以下のセクションでは、より詳細にこれらのプロセスを説明する。
ブロック、レプリカとその状態
ネームノードのコンテキストにおけるブロックとデータノードのコンテキストにおけるブロックを区別するために、我々は前者をブロック、後者をレプリカと呼ぶことにしている。
データノードのコンテキストにおけるレプリカは、次のいずれかの状態をとる (org.apache.hadoop.hdfs.server.common.HdfsServerConstants.java の ReplicaState も参照)。
・FINALIZED:レプリカがこの状態にあるときには、レプリカが追記のためにリオープンされていない限り、レプリカへの書き込みは終了し、レプリカ内のデータは「frozen(長さが確定)」されている。同じ世代スタンプ(GSと呼ばれる。以下で定義する)を有するブロックのすべての確定済みの (finalized) レプリカは同じデータを持つべきである。確定したレプリカのGSはリカバリの結果としてインクリメントされる場合がある。
・RBW(Replica Being Written):これは、ファイルが書き込み用に作成または追記のためにリオープンされたかどちらかの、書き込み途中のレプリカの状態である。RBW状態にあるレプリカは、かならずオープン中のファイルの最後のブロックである。データはまた複製中であり、確定していない。RBW状態のレプリカのデータ(必ずしもすべてではないが)は、クライアントから参照可能である。なにか障害が発生した場合、RBW状態にあるレプリカ内のデータは保護されようとする。
・RWR(Replica Waiting to be Recovered):データノードが停止、再起動する場合、すべてのRBWレプリカはRWR状態に変更される。RWR状態のレプリカは古く、したがって、廃棄されるかリースリカバリに参加する。
・RUR(Replica Under Recovery):リースリカバリに参加しているとき、TEMPORARYでないレプリカはRURに変更される。
・TEMPORARY:(レプリケーションモニタまたはクラスタバランサのいずれかによって)ブロック複製のために作成されている一時レプリカである。これは、そのデータがすべての読み込みクライアントから見えないことを除き、RBWのレプリカに似ているといえる。ブロックの複製が失敗した場合、一時レプリカは削除される。
一方、ネームノードのコンテキストにおけるブロックは以下のいずれかの状態をとる(org.apache.hadoop.hdfs.server.common.HdfsServerConstants.java の BlockUCState も参照)。
・UNDER_CONSTRUCTION:これは、書き込まれているときの状態である。UNDER_CONSTRUCTIONブロックは、オープンしているファイルの最後のブロックである。その長さと世代スタンプがまだ変更可能であり、(必ずしもすべてではないが)そのデータがクライアントから参照可能である。ネームノード内のUNDER_CONSTRUCTIONブロックは、書き込みパイプライン(有効なRBWのレプリカの位置)、及びそのRWRのレプリカの位置を追跡する。
・UNDER_RECOVERY:対応するクライアントのリースが期限切れになったときにファイルの最後のブロックがUNDER_CONSTRUCTION状態の場合、ブロックリカバリが開始されるとUNDER_RECOVERY状態に変更される。
・COMMITTED:COMMITTEDは、ブロックのデータと世代スタンプが(追記のために再オープンされない限り)変更不可であることを意味する。また、FINALIZEDとレポートされている同じGS/長さのレプリカの数が最小複製数で以下であることを意味する。サービスに読み取りを要求するために、COMMITTEDブロックはRBWのレプリカの位置、FINALIZEDレプリカのGSと長さを追跡しなければならない。ネームノードは、クライアントから新しいブロックをファイルに追加する、あるいはファイルをクローズすると指示があったらUNDER_CONSTRUCTIONブロックをCOMMITTEDに変更する。最後あるいは最後から2番目のブロックがCOMMITTED状態にある場合、ファイルはクローズできず、クライアントが再試行する必要がある。
・COMPLETE:ネームノードがGSと長さの一致するFINALIZEDなレプリカが最小複製数に達したことを確認したら、COMMITTEDブロックはCOMPLETEに変更される。すべてのブロックがCOMPLETEとなった場合にのみファイルは閉じることができる。ブロックは最小複製数に満たない場合にも強制的にCOMPLETEとすることができる。たとえば、クライアントが新しいブロックを要求し、以前のブロックがまだCOMPLETEではない場合などである。
データノードはレプリカの状態をディスクに保持するが、ネームノードはブロックの状態をディスクに保持しない。ネームノードが再起動すると、以前オープンしていたファイルの最後のブロックの状態をUNDER_CONSTRUCTIONに変更し、その他のすべてのブロックの状態をCOMPLETEにする。
レプリカとブロックの簡略化された状態遷移図を、図2、図3に示す。詳細はデザインドキュメントを参照のこと。
世代スタンプ
世代スタンプ(GS)は、ネームノードによって継続的に維持される各ブロック用に用いられる、単調増加の8バイトの数値である。ブロックとレプリカのための世代スタンプ(Design Specification: HDFS Append and Truncates)の目的は以下である。
・無効なレプリカを検知する。つまり、レプリカのGSがブロックのGSより古い場合。たとえば追記操作があるレプリカでなぜかスキップされてしまったなどが該当する
・データノードにある期限切れのレプリカを検知する。これはデータノードが長い間停止しており、クラスタに再度参加したなどが該当する。
以下のいずれかが発生すると、新たな世代スタンプが必要となる
・新しいファイルが作成された
・クライアントが追記またはTRUNCATEため既存のファイルをオープンした
・クライアントがデータノードへデータを書き込み途中にエラーが発生し、新たな世代スタンプを要求した
・ネームノードがファイルのリースリカバリを開始した
リースリカバリとブロックリカバリ
リースマネージャ
リースは、ネームノードでリースマネージャによって管理される。ネームノードは各クライアントが書き込み用にオープンしているファイルを追跡する。クライアントがリースを更新する際、書き込み用にオープンしている各ファイルを一つ一つ数え上げる必要はない。代わりに、定期的にネームノードに対して一度にすべてを更新するような要求を送っている(この要求は org.apache.hadoop.hdfs.protocol.proto.ClientNamenodeProtocolProtos.RenewLeaseResponseProto で、HDFSクライアントとネームノード間のPRCプロトコルである)。
ネームノードはその名前空間に関連付けられているすべてのクライアントのリースを管理するため、単一のリースマネージャを持ちそれぞれが単独のHDFSの名前空間を管理している。フェデレーション化されたHDFSクラスタでは、独自のリースマネージャーとそれぞれ複数の名前空間を持つこともあるだろう。リースマネージャは失効期限のためにソフトリミット(1分)とハードリミット(1時間)を保持し、リースマネージャによって維持されるすべてのリースは同じソフト/ハードリミットを守ることになる。
ソフトリミットが失効する前は、ファイルのリースを保持しているクライアントはファイルへの排他書き込みアクセスを持っている。ソフトリミットが失効し、クライアントがリースを更新しなかったりファイルをクローズした場合、別のクライアントが強制的にリースを引き継ぐことができる。ハードリミットが失効し、クライアントがリースを更新しない場合、HDFSはクライアントが停止していると判断し、クライアントに代わって自動的にファイルをクローズし、その後リースのリカバリを行う。
ファイルのリースがあるクライアントによって保持されているからといって、他のクライアントによるそのファイルの読み込みを妨げるというわけではない。ファイルは並列に読み込むことが可能であるし、あるクライアントが書き込み中であってもこれは同様である。
リースマネージャがサポートしている操作を以下に挙げる:
・クライアントとパスのリースを追加する(クライアントが既にリースをもっている場合、そのパスをリースへ追加する。そうでなければ新しいリースを作成し、そのパスをリースへ追加する)
・クライアントとパスのリースを削除する(リース内の最後のパスである場合、リースを削除する)
・ソフトあるいはハードリミットが失効しているかを確認し、
・任意のクライアントのためにリースを更新する
リースマネージャは定期的にリースが失効済みのハードリミットを持っているかどうかをチェックするための監視スレッドをもっている。もし失効していれば、リース内のファイルのためにリースリカバリを始動する。
HDFSクライアントはorg.apache.hadoop.hdfs.LeaseRenewer.LeaseRenewerクラスによりリースを更新する。LeaseRenewerはユーザのリストを維持し、ネームノードのユーザにつき1スレッドを実行する。このスレッドはネームノードを定期的にチェックし、クライアントのリース期間が半分を過ぎるとすべてのリースを更新する(HDFSクライアントはひとつのネームノードとのみ通信する。org.apache.hadoop.hdfs.DFSClientのコンストラクタも参考になるだろう。もし同じアプリケーションが、フェデレーションを有効にしたクラスタにおける別々のネームノードにて管理される異なるファイルにアクセスしたい場合、それぞれのネームノードに対して独立したクライアントを生成する必要がある)。
リースリカバリとブロックリカバリ
リースリカバリのプロセスは、監視スレッドがハードリミット満了を検知するか、ソフトリミットが失効してクライアントが別のクライアントへリースを引き継ぐようなときに、ネームノードで始動される。その際に、ファイルが同じクライアントにより書き込み用にオープンされていないかチェックし、そのファイルの最後のブロックがCOMPLETE状態にない場合にブロックリカバリを実施、ファイルをクローズする。あるファイルのブロックリカバリは、ファイルのリースリカバリ時にのみ始動される。
以下はあるファイルfに対するリースリカバリのアルゴリズムである。クライアントが停止すると、同じアルゴリズムが書き込みのためにオープンされているファイルに対して適用される。
1. fの最後のブロックが含まれているデータノードを取得
2. データノードのひとつをプライマリデータノードpとしてアサイン
3. pはネームノードから新しい世代スタンプを取得
4. pは各データノードからブロック情報を取得
5. pは最小のブロック長を計算
6. pは、新たな世代スタンプと最小ブロック長を用い、有効な世代スタンプでデータノードを更新
7. pはネームノードが更新したことを確認
8. ネームノードはBlockInfoを更新
9. ネームノードはfのリースを削除(他のクライアントがfに書き込むとリースを取得できる)
10. ネームノードはeditログに変更をコミット
上記の3から7のステップが、ブロックリカバリのアルゴリズムである。あるファイルがブロックリカバリを必要とする場合、ネームノードはファイルの最後のブロックを持つデータノードをプライマリとして選び、そのデータノードに対して他のノードとブロックリカバリを行うよう指示を出す。プライマリデータノードは処理が完了するとネームノードに報告する。ネームノードはその後、このブロックの内部の状態を更新し、リースを削除、editログに変更をコミットする。
クラスタ管理者であればときには、ハードリミットが切れる前に強制的にファイルのリースリカバリを行う必要があるだろう。その際はCLIのdebugコマンドが有効である(Hadoopのリリース2.7、CDH5.3以降に含まれている)。
hdfs debug recoverLease [-path <path>] [-retries <num-retries>]
まとめ
リースリカバリ、ブロックリカバリ、およびパイプラインリカバリはHDFSの耐障害性における本質部分といえる。これらが協調することで、ネットワーク/ホスト障害下においても堅牢性、一貫性を保証するのである。この記事を通して、なぜリースリカバリ、ブロックリカバリがおき、どう動作するのかが理解できたら幸いである。その2では、パイプラインリカバリについて詳しく見ていこう。