Apache Hadoop — 1つのPodにHDFSとYARNを立てて運用する
NameNode は名前を、DataNode はバイトを持つ
一言でいうと
HDFSは、名前を知る側(NameNode)とバイトを持つ側(DataNode)を最初から分けたファイルシステムです。シェルで読んでもHTTPで読んでも、「NameNodeにどこにあるかを尋ね、バイトはDataNodeから受け取る」という順序は同じです。
なぜ役割を2つに分けたのか
ノートPCのファイルシステムは、1つのディスク上で名前と内容を一緒に管理します。ディレクトリエントリを見つければ、その隣に内容がどこにあるかが書かれていて、両者は同じマシンにあります。HDFSが解こうとした問題は、これとは規模が違いました。
HDFS設計ドキュメントは、冒頭で2つの前提を置いています。ハードウェアの故障は例外ではなく日常であり、HDFS上の典型的なファイルは、ギガバイトからテラバイトの大きさだという前提です。数百台に散らばったディスクに大きなファイルを分けて置くには、誰かが「このファイルの何番目の断片がどのマシンにあるか」を1か所で知っている必要があります。同時に、実際のバイトは、複数のマシンが一斉に送り出してはじめてスループットが出ます。
そこで、役割を分けました。NameNodeは、名前空間(ディレクトリツリー、所有者と権限、ファイルごとのレプリケーション係数)と、「どのブロックがどのDataNodeにあるか」という地図を握ります。DataNodeは、自分のマシンに付いたディスクのブロックを読み書きします。設計ドキュメントは、この構造の重心を1文で書いています。ユーザーデータは、決してNameNodeを経由して流れないように設計されている、というものです。
この1文がなぜ重要かは、逆に考えるとはっきりします。もしNameNodeがバイトまで中継していたら、クラスター全体のスループットが、NameNode 1台のネットワークカードとディスクに縛られたはずです。メタデータだけを扱うので、1台が数百台を指揮できます。その代わり、代償があります。すべての名前の操作がその1台を通るので、NameNodeはクラスターで最も重要な単一のポイントになります。
どう動くのか
読み取り: クライアントがファイルを開くと、まずNameNodeに、そのファイルのブロック一覧と、ブロックごとにレプリカを持つDataNodeを尋ねます。そのあと、バイトはDataNodeに直接接続して受け取ります。設計ドキュメントのレプリカ選択の節は、読む側から最も近いレプリカを選ぶと説明しています。同じラックにあれば、それを優先して使います。
書き込み: 書き込みも同じ形です。クライアントはNameNodeに新しいブロックを要求し、NameNodeは、ブロックIDとレプリカを受け取るDataNodeの一覧を返します。バイトは、一覧の最初のDataNodeにだけ送ります。設計ドキュメントがレプリケーションパイプラインと呼ぶ部分です。最初のDataNodeは、断片を受け取って自分のディスクに書きながら、同時に2番目に渡し、2番目は3番目に渡します。
DataNodeはファイルを知りません: 設計ドキュメントの表現をそのまま移すと、DataNodeはHDFSファイルについて何も知らず、ブロック1つ1つをローカルファイルシステムの別々のファイルとして保存します。起動するとき、ローカルディスクを走査して持っているブロックの一覧を作り、NameNodeに送りますが、これがブロックレポートです。普段は、定期的にハートビートを送ります。間隔はhdfs-default.xmlのdfs.heartbeat.intervalでデフォルト3秒で、ハートビートが途絶えたDataNodeを死んだと判定するまでには、デフォルトで10分を超える保守的な時間を置いています。一瞬不安定になったノードのせいで、レプリケーションストームが起きないようにするためです(ラボのPodは、待ち時間を減らすために、この判定を1分30秒に早めてあります)。
ここで重要な性質が1つ出てきます。設計ドキュメントは、NameNodeが自分からRPCを呼び出すことはなく、DataNodeとクライアントの要求に答えるだけだと書いています。NameNodeがDataNodeに「このブロックを削除せよ、複製せよ」と指示するときも、ハートビートの応答に載せて送ります。
mvはバイトを移動しません: 名前の変更とディレクトリの移動は、設計ドキュメントがNameNodeの仕事として分類した名前空間の操作です。ファイルが新しいパスに移動しても、ブロックはそのままで、ブロックIDも変わりません。数十GBのディレクトリをmvしても一瞬で終わる理由です。逆に、cpはバイトを新しいブロックとして書き直します。
同じファイルを2つの道で見る
HDFSに届く道は複数あります。最も慣れているのはhdfs dfsシェルで、NameNodeのRPCポートで話します。もう1つはHTTPです。WebHDFSドキュメントは、パスの前に/webhdfs/v1を付け、後ろにop=クエリを付けるアドレスの形を定めています。NameNodeのHTTPアドレスは、hdfs-default.xmlでdfs.namenode.http-address、デフォルトは0.0.0.0:9870です。
# 목록은 NameNode 혼자 답한다 — 메타데이터뿐이다
curl -s "http://localhost:9870/webhdfs/v1/user/root?op=LISTSTATUS"
# 내용은 DataNode 로 돌려보낸다 — 307 을 따라가야 바이트가 온다
curl -i "http://localhost:9870/webhdfs/v1/user/root/app.log?op=OPEN"
# HTTP/1.1 307 TEMPORARY_REDIRECT
# Location: http://<DataNode>:<포트>/webhdfs/v1/user/root/app.log?op=OPEN...
HTTPに変えても、役割分担はそのまま現れます。LISTSTATUSは、NameNodeがJSONですぐに答えます。OPENとCREATEは、WebHDFSドキュメントに書かれているとおり、通常はDataNodeに向かう307リダイレクトを返します。curl -Lでたどって初めて、バイトが来ます。シェルが内部で行っていた「尋ねて、受け取りに行く」が、HTTPでは目に見える2回のリクエストになったのです。
もう1つ。セキュリティがオフのクラスターでは、WebHDFSはuser.nameクエリの値をそのまま認証済みユーザーとして受け取ると、ドキュメントが書いています。誰であるかを、言うとおりに信じるという意味で、権限のモジュールであらためて扱います。
現場での姿
1つ目は、ファイルは見えるのに、読むと失敗することです。名前はNameNodeに問題なくあるのに、そのブロックを持つDataNodeがすべて止まっていると、lsはできてもcatは失敗します。2つの役割が分かれているために生じる典型的な症状です。このようなときは、HDFSコマンドのドキュメントのhdfs dfsadmin -reportで、生きているDataNodeと容量から確認します。
2つ目は、WebHDFSで一覧はできるのに、読み取りだけができないことです。ファイアウォールの外側で9870だけを開けておくと、よく遭遇します。一覧はNameNodeが答えますが、読み取りはDataNodeのアドレスにリダイレクトされるため、そのアドレスがクライアントから届かなければ、2回目のリクエストが失敗します。同じ構造が、別の姿で現れたのです。
3つ目は、NameNodeが遅いと、すべてが遅いことです。バイトはDataNodeが運びますが、ls1回、小さなファイルを開く1回でも、NameNodeとの往復をします。小さなファイルが数百万個あるクラスターが、NameNodeから苦しくなる理由が、ここにあります。
4つ目は、ファイルは1回書いて何度も読むことです。設計ドキュメントは、ファイルを作って閉じたあと、追記と切り詰め以外は変更しないモデルを選び、1つのファイルには、常に書き込む人が1人だけです。途中を直すことはありません。ログのように積み上げるデータには合い、行単位で直すデータには合いません。
実務で本当に大切なこと
- NameNodeは名前とブロックの地図を、DataNodeはバイトを持ちます。バイトはNameNodeを通りません。
- 読み取りと書き込みは、「尋ねて、受け取りに行く」2段階です。WebHDFSの307リダイレクトが、その2段階を目に見えるようにします。
- 名前の変更は、メタデータの操作です。ブロックIDがそのままなので、サイズに関係なく即座に終わります。
- 一覧はできるのに読み取りができなければ、まずDataNode側を見ます。生きているノード数と、到達可能なアドレスが、最初の確認対象です。
- NameNodeは、すべての名前の操作が通る1つのポイントです。ファイル数と呼び出し数が、その1台の負担になります。
次のラボですること
まずhdfs getconfで、デフォルトのファイルシステムアドレス・レプリケーション係数・ブロックサイズがどんな値になっているかを尋ね、管理レポートでDataNodeが1台つながっている様子を見ます。アクセスログ1つをHDFSにアップロードしたあと、シェルで行数を数え、同じディレクトリをWebHDFSのLISTSTATUSでHTTPリクエストを送って、JSONの一覧として受け取ります。最後に、ファイルを別のディレクトリに移動したあとfsckでブロックを確認し、移動の前後でfileIdとブロックIDがそのままであること、つまり名前の操作がバイトに触れないことを、目で確認します。