TT Lab
はじめる
学ぶ 学習パス コース

Apache Hadoop — 1つのPodにHDFSとYARNを立てて運用する

NameNode は名前を、DataNode はバイトを持つ

TT Labで続きを見る

一言でいうと

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番目に渡します。

HDFSの書き込みの流れです。クライアントはNameNodeに新しいブロックを要求し、ブロックIDと3台のDataNodeの一覧だけを受け取ります。バイトは最初のDataNodeにだけ送り、各DataNodeは受け取った断片を書きながら次のDataNodeに渡します。DataNodeは別にNameNodeにハートビートとブロックレポートを送ります

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人だけです。途中を直すことはありません。ログのように積み上げるデータには合い、行単位で直すデータには合いません。

実務で本当に大切なこと

次のラボですること

まずhdfs getconfで、デフォルトのファイルシステムアドレス・レプリケーション係数・ブロックサイズがどんな値になっているかを尋ね、管理レポートでDataNodeが1台つながっている様子を見ます。アクセスログ1つをHDFSにアップロードしたあと、シェルで行数を数え、同じディレクトリをWebHDFSのLISTSTATUSでHTTPリクエストを送って、JSONの一覧として受け取ります。最後に、ファイルを別のディレクトリに移動したあとfsckでブロックを確認し、移動の前後でfileIdとブロックIDがそのままであること、つまり名前の操作がバイトに触れないことを、目で確認します。