5. NFS の性能を最適化する

クライアントとサーバの両面から、環境を注意深く分析することが、 NFS の性能を最適化する際の最初のステップになります。 前半のセクションでは、一般にクライアントの方で重要になる点を扱います。 後半 (Section 5.3 以降) では、サーバ側の点を議論します。 サーバ・クライアント両側での項目は、 互いに影響を及ぼしあうことがないわけではありませんが、 原因・結果をはっきりさせるためには、この 2 つを分けておくと便利かと思います。

十分なネットワーク容量、高速な NIC、全二重の設定にして衝突を減らす、 スイッチやハブでネットワークスピードを一致させる、 といったようなネットワーク関連の設定を除くと、 クライアントを最適化するために最も重要な設定は、 NFS データ転送のバッファサイズでしょう。 これは mount コマンドの rsize, wsize 各オプションで設定します。

5.1. ブロックサイズを設定して転送速度を最適化する

mount コマンドの rsize オプションと wsize オプションは、 クライアントとサーバがデータをやりとりするときの データの転送単位を指定するものです。 それぞれのオプションが指定されないときのデフォルト値は、 使っている NFS のバージョンによって異なります。 ほとんどの場合のデフォルトは 4K (4096 バイト) ですが、 2.2 カーネルにおける TCP ベースでのマウントや、 2.4 カーネル以降でのあらゆるマウントでは、 サーバがデフォルトのブロックサイズを指定します。

NFS V2 プロトコルの理論上の上限は 8K です。 V3 プロトコルでの上限はサーバによって異なります。 Linux サーバでの最大ブロックサイズは、 カーネルソースの ./include/linux/nfsd/const.h にあるカーネル定数 NFSSVC_MAXBLKSIZE で決まります。 2.4.17 の時点では、カーネルの最大ブロックサイズは 8K (8192 バイト) ですが、 2.4 系に対する NFS over TCP/IP 転送を実装したパッチでは、 執筆時点で 32K (このパッチでは 32*1024) が最大ブロックサイズになっています。

すべての 2.4 系クライアントは、現時点で最大 32 K までのブロック転送サイズをサポートしており、 Solaris のような他のサーバからの NFS 転送で標準となっている 32K のブロック転送を、クライアントを修正することなく利用できます。

デフォルトの値は大きすぎ/小さすぎかもしれません。 全ての、あるいは大抵の設定に有効なサイズ、というものはありません。 例えば Linux カーネルとネットワークカードの組み合わせ (ほとんどは古いマシンでの話) によっては、あまり大きなブロックは扱えません。 一方大きなブロックが扱えれば、大きなサイズの転送は高速になります。

実験を行って、動作する限りで最速となるような rsizewsize を決定しましょう。ある設定にしたときの転送速度は、 ネットワークが混雑していなければ、 いくつかの簡単なコマンドで調べられます。 実際の結果は場合によって大きく変わってしまうかもしれません。 その際には Bonnie, Bonnie++, IOzone といった、 より複雑なベンチマークを使うことになります。

最初に実行すべきコマンドは、16k のブロック 16384 個を、 特殊なファイル /dev/zero (読み込むと 0 を「非常に」高速に吐き出してきます) からマウントしたパーティションに転送するものです。 どのくらい時間がかかるかは time で測りましょう。 クライアントマシンから次のように入力します。

    # time dd if=/dev/zero of=/mnt/home/testfile bs=16k count=16384

こうすると (バイトデータの) 0 で埋めつくされた、 大きさが 256Mb のファイルができます。 一般には、サーバに積んである RAM のサイズの、 少なくとも 2 倍の大きさのファイルを作るべきです (ただしディスクに空きがあるか、確認を忘れないこと!)。 次にそのファイルを、クライアントのブラックホール (/dev/null) に読み出します。 次のように入力してください。

    # time dd if=/mnt/home/testfile of=/dev/null bs=16k

これを何回か繰り返して、かかった時間を平均してください。 対象のファイルシステムを毎回アンマウント→再マウントして (クライアントと、こだわる方はサーバでも)、 キャッシュの効果をすべてクリアするのを忘れずに。

終わったらアンマウントし、ブロックサイズを増減して もう一度マウントしてください。サイズは 1024 の倍数にし、 またシステムでの最大ブロックサイズは越えないようにしましょう。 NFS Version 2 の最大サイズは、 NFSSVC_MAXBLKSIZE での定義にかかわらず 8K です。 Version 3 は、許可されていれば 64K までサポートします。 ブロックサイズは 2 倍ずつ変えていくのが良いでしょう。 転送に関連するパラメータ (ファイルのシステムブロックサイズや ネットワークのパケットサイズなど) も、 たいていは 2 倍ずつ変わるからです。 ただし、ブロックサイズを 2 の冪乗以外の値にして、 より良い結果を得たユーザもいます。 しかしその場合でも、システムのブロックサイズや ネットワークパケットサイズの整数倍にはなっていました。

大きなサイズでマウントしたら、 そのファイルシステムに cd し、ls するなどして、 ファイルシステムの中味が正しく見えるか調べてみて下さい。 rsizewsize が大き過ぎると、妙な兆候が現われ、 ファイルの信頼性が 100% でなくなります。 よくある例としては 「ls してもすべてが表示されない、 エラーメッセージも出ない」とか 「エラーメッセージは出ないのにファイルの読み込みになぜか失敗する」 などがあります。 さて、与えた rsize/wsize でシステムが正しく動作していることがわかったら、 もう一度速度のテストをしてみましょう。 サーバの OS が違うと最適なサイズも異なる場合が多いです。

最後に /etc/fstab を編集して、 決まった rsize/wsize の値を反映させるのを忘れないように。

結果が一貫しなかったり、疑わしかったりした場合には、 rsizewsize の値を変化させながら、 ネットワークをもっと真面目に解析する必要があるかもしれません。 その場合には、ベンチマークソフトを使うと良いかもしれません。 いくつかポインタを挙げておきます。

非常にさまざまなファイルサイズや、 さまざまな IO 形式 (読み出し・書き込み、再読み出し・再書き込み、ランダムアクセスなどなど) など、最も広い範囲を扱えて、かつ最も簡単なベンチマークは、 IOzone のように思います。 推奨される IOzone の実行方法としては (これには root 権限が必要ですが)、 テストするディレクトリをアンマウント・再マウントして キャッシュが関与しないようにし、 ファイルクローズ時間の測定をテストに入れるようなやり方です。 すでにサーバ foo/tmp を制限なしでエクスポートしており、 IOzone をローカルディレクトリにインストール済みであるとすると、 次のようなコマンド群になります。

    # echo "foo:/tmp /mnt/foo nfs rw,hard,intr,rsize=8192,wsize=8192 0 0"
    >> /etc/fstab
    # mkdir /mnt/foo
    # mount /mnt/foo
    # ./iozone -a -R -c -U /mnt/foo -f /mnt/foo/testfile > logfile

ベンチマークには最大で 2〜3 時間かかります。 そして、もちろん対象の rsizewsize を変更するたびに実行する必要があります。 web サイトにはパラメータを網羅した文書がありますが、 上記で用いたオプションについては以下で説明します。

5.2. パケットサイズとネットワークドライバ

Linux のネットワークカードドライバの多くは優れたものですが、 中には、比較的標準的なカードのものも含め、 極めて出来の悪いものもあります。 自分のネットワークカードを直接テストして、 最高の状態で動作させるにはどうすれば良いかを知っておくのは、 やるだけの価値があることだと言えます。

2 台のマシンの間で ping をやり取りしてみましょう。 その際 -f オプションと -s オプションを用いて (詳細は ping(8) を見てください) 大きなパケットを使い、 大量のパケットロスが起きていないか、 応答に時間がかかっていないかを見てみましょう。 このような障害が起きている場合は、 ネットワークカードの性能に問題があるかと思われます。

NFS の動作に特化した解析をより詳しく行うには、 nfsstat コマンドをつかって NFS トランザクション、クライアント/サーバの統計、 ネットワークの統計などを見てください。 "-o net" オプションを用いると、 トランザクションの総パケット数に対するパケット落ちの回数が表示されます。 UDP トランザクションで最も重要な統計は再送数で、 これはパケット落ち、ソケットのバッファオーバーフロー、 一般的なサーバの過負荷、タイムアウトなどによって生じます。 これは NFS の性能に非常に重要な影響を与えるので、 注意深く見る必要があります。 なお nfsstat はまだ カウンタをゼロにリセットする -z オプションを実装していません。 したがって、ベンチマークを行う前に、まず nfsstat カウンタの現在値を見ておく必要があります。

ネットワークの問題を修正するには、 ネットワークカードの用いているパケットサイズを再設定するといいでしょう。 2 台のマシンの間でやり取りできるパケットサイズの最大値は、 ほとんどの場合ネットワークのどこか (例えばルータ) において、 ネットワークカードのものより小さな値に制限されています。 TCP ではネットワークに対して適切なパケットサイズを 自動的に見つけるようになっていますが、 UDP では単にデフォルトの値を使うだけです。 従って、特に UDP 上で NFS を使っている場合には、 適切なパケットサイズを決めることは非常に重要です。

ネットワークパケットサイズのテストは tracepath コマンドによって行えます。 クライアントマシンから単に tracepath server 2049 と入力すれば、末尾に path MTU が表示されます。 次に ifconfig の MTU オプションを使って、 ネットワークカードの MTU を path MTU の値と同じにし、 パケット落ちが少なくなるか確認してください。 MTU の再設定方法の詳細は ifconfig の man ページを見てください。

さらに netstat -s を使えば、 サポートされているプロトコル全てに対して収集された統計が表示されます。 また /proc/net/snmp を見れば、 現在のネットワークの動作状況に関する情報がわかります。 詳細は次の節を見てください。

5.3. フラグメントされたパケットのオーバーフロー

network の MTU (多くのネットワークでは通常 1500) よりも大きな rsizewsize を用いると、NFS over UDP を使っている場合には IP パケットはフラグメント化されます。 IP パケットのフラグメント化と再構成は、 ネットワーク接続の両側で、相当量の CPU 資源を必要とします。 さらにパケットのフラグメント化が行われている状態では、 UDP パケットのフラグメントがなんらかの理由で落ちたときに RPC リクエスト全体を再送しなければならないため、 ネットワーク転送をずっと不安定にします。 RPC 再送の増加は、タイムアウトを増加させることになりかねず、 NFS over UDP の性能を悪化させる最悪の原因となります。

パケット落ちはいろいろな理由で生じます。 ネットワークの形状が複雑だと、フラグメントの経路が異なるかも知れず、 サーバでの再構成時に全てが到着しないかもしれません。 カーネルがバッファできるフラグメントの数には上限があり、 これを越えるとパケットは破棄されるため、 NFS サーバの収容能力も問題になります。 /proc ファイルシステムをサポートするカーネルでは、 /proc/sys/net/ipv4/ipfrag_high_thresh/proc/sys/net/ipv4/ipfrag_low_thresh ファイルで確認できます。 未処理のフラグメント化パケットが ipfrag_high_thresh (バイト単位) を越えると、カーネルは単純にパケットのフラグメントを捨てはじめ、 サイズの合計が ipfrag_low_thresh に指定した値になるまで捨て続けます。

別のモニターカウンタとして、 /proc/net/snmp ファイルにある IP: ReasmFails があります。 この数は、フラグメントの再構成に失敗した回数です。 重いファイル操作の際にこの値があまりに急激に上昇する場合は、 おそらく問題が生じています。

5.4. NFS over TCP

新しい機能である NFS over TCP は、 2.4 カーネルでも 2.5 カーネルでも利用できますが、 まだ執筆時点ではメインストリームのカーネルには統合されていません。 TCP の利用には、UDP に対してはっきりした利点・欠点があります。 利点は、ロスの多いネットワークにおいて UDP よりずっと良く動作することです。 TCP を使った場合は、パケットがひとつ落ちると単にそれが再送され、 RPC リクエスト全体を再送するようなことは起こりません。 よってロスの多いネットワークではより良い性能を示します。 さらに TCP は、下層のネットワークレベルでのフロー制御のおかげで、 ネットワーク速度の違いを UDP よりもうまく扱えます。

TCP を用いることの欠点は、 TCP が UDP のようなステートレスのプロトコルではないことです。 パケット送信の最中にサーバがクラッシュすると、 クライアントはハングしてしまい、 すべての共有をアンマウント・再マウントする必要が生じます。

TCP プロトコルはオーバーヘッドを必要とするため、 理想的なネットワーク環境の下では UDP に比べて少々性能が低下します。 しかしそのコストはあまり厳しいものではなく、 注意深く測定しないと気付かない場合が多いでしょう。 通信の端から端まで gigabit イーサネットを使っているような場合は、 巨大なフレームの利用を試みてみるといいかもしれません。 高速なネットワークでは、特にネットワークが全二重の場合には、 フレームサイズを大きくしても衝突レートが増加しないからです。

5.5. タイムアウトと再送の値

mount コマンドの 2 つのオプション、 timeoretrans は、 クライアントがパケット落ち・ネットワーク負荷などによってタイムアウトしたときの、 UDP リクエストの動作を制御するものです。 -o timeo オプションは時間の長さを 1/10 秒単位で指定するもので、 クライアントはこの時間を越えるとサーバから応答がもらえなかったと判断し、 リクエストを再送しようと試みます。 デフォルトは 0.7 秒です。 -o retrans オプションは、 クライアントがあきらめるまでに許されるタイムアウト回数で、 これを越えると Server not responding というメッセージが表示されます。 デフォルトは 3 回です。 クライアントがこのメッセージを表示したら、 リクエストを送信しようとし続けますが、 次のタイムアウト 1 回で、エラーメッセージを表示します。 接続が復帰すると、クライアントはふたたび元の retrans の値を用いるようになり、 Server OK というメッセージを表示します。

すでに大量の再送が起きていたり (nfsstat コマンドの出力を見てください)、 あるいはタイムアウト・再送を引き起こすことなく ブロック転送サイズを増加させたい場合には、 これらの値を調整するといいでしょう。 適切な調整値は環境に依存しますが、 ほとんどの場合では現在のデフォルトで問題ないはずです。

5.6. NFSD のインスタンスの数

Linux でも他の OS でも、ほとんどの起動スクリプトでは、 nfsd のインスタンスを 8 つ起動します。 NFS の最初の頃に Sun はこの値を経験則として決め、 その後はみんなこの値をコピーしているのです。 どのくらいのプロセス数が最適かを決める良い基準はありませんが、 トラフィックの大きいサーバではより大きな値にするのが良いでしょう。 最低でもプロセッサ当たりひとつのデーモンは起動すべきで、 プロセッサ当たり 4 から 8 というのがだいたいの目安になるでしょう。 2.4 以降のカーネルを使っている人は、 各 nfsd スレッドがどのくらい使われているかを /proc/net/rpc/nfsd で見てみるといいでしょう。このファイルの th 行の最後の 10 個の数字は、 割り当て可能な最大値に対する各パーセンテージに そのスレッドがあった秒数を示しています。 最初の 3 つの値が大きいときは、 nfsd のインスタンスを増やすほうが良いでしょう。 これを行うには、nfsd を起動するときの コマンドラインオプションでインスタンスの数を与えます。 NFS の起動スクリプト (Red Hat なら /etc/rc.d/init.d/nfs) では RPCNFSDCOUNT で指定します。 詳細は nfsd(8) の man ページを見てください。

5.7. 入力キューのメモリ制限

2.2 と 2.4 のカーネルでは、ソケットの入力キュー (処理中のリクエストが待つところ) のデフォルトのサイズ制限値 (rmem_default) は小さく、64k しかありません。 このキューは読み込み負荷が大きいクライアントで、 また書き込み負荷の大きいサーバで重要です。 例えば、サーバで nfsd のインスタンスを 8 つ走らせているとすれば、 各々には処理対象のリクエストを保存する場所が 8k ずつしかないことになります。 さらに、ソケットの出力キュー (書き込み負荷の大きなクライアント・読み込み負荷の大きなサーバで重要) も、デフォルトのサイズ (wmem_default) は小さくなっています。

NFS ベンチマーク SPECsfs の実行結果がいくつか公開されていますが、それらでは [rw]mem_default[rw]mem_max の両方にずっと大きな値を指定しています。 これらの値は、少なくとも 256k にまで増やすことを考えるべきです。 読み書きの上限値は、(例えば) proc ファイルシステムの /proc/sys/net/core/rmem_default/proc/sys/net/core/rmem_max を用いて設定します。 rmem_default の値を増加させるには 3 つの段階を踏みます。以降に示す方法はちょっとあらっぽいですが、 ちゃんと動作しますし、問題を起こすこともないはずです。

この最後のステップは不可欠で、 これらの値を長い間変えたままにしておくと、 マシンがクラッシュするというレポートも受けています。

5.8. NIC とハブの自動ネゴシエーションを無効にする

ネットワークカードの中には、 動作速度や全二重・半二重が異なるハブ、スイッチ、ポートなどとの 自動ネゴシエーションがうまくできず、 大量のコリジョン・パケット落ちなどによって、 性能が非常に劣化するものがあります。 nfsstat の出力に大量のパケット落ちがあったり、 あるいは一般にネットワークの性能がでない場合は、 ネットワーク速度と全/半二重の設定をいじってみてください。 可能なら 100BaseT 全二重のサブネットを確立することに集中しましょう。 全二重による仮想的なコリジョン回避は、 NFS over UDP における最も大きな性能低下の原因を取り除いてくれるからです。 カードの自動ネゴシエーション機能を切るときには注意が必要です。 カードが接続されているハブやスイッチは、 別の方法で (例えば並列検知など) をもちいて全/半二重の設定を決めますが、 カードによっては (古いハブでもサポートされているという理由から) デフォルトが半二重になっていることがあるからです。 ドライバがサポートしているのであれば、 100BaseT 全二重でネゴシエーションするようカードに強制するのが最善です。

5.9. NFS の同期動作と非同期動作

nfs-utils の Version 1.11 より前の exportfs では、 NFS の Version 2 と Version 3 プロトコルのどちらにおいても、 デフォルトのエクスポートは「非同期 (asynchronous)」で行ないます (なお Version 1.11 は CVS ツリーには存在しますが、 2002 年 1 月の段階ではまだパッケージになっていません)。 このデフォルトにおいては、サーバはクライアントからのリクエストに対し、 処理をローカルのファイルシステムに渡したところで返答することが許されており、 データが永続的なストレージに書き込まれることを待つ必要はありません。 これはサーバのエクスポートリストの async オプションによって識別できます。 非同期動作は性能が向上しますが、 書き込んでいないデータやメタデータがキャッシュにあるときに サーバがリブートすると、データが壊れる可能性があります。 このデータ破壊は実際に起こるまでわかりません。 async オプションを指定すると、 用いているプロトコルに関らず、 サーバはクライアントに対して、 データはすべて実際に永続的なストレージに書き込まれた、と嘘をつくからです。

「同期 (synchronous)」動作は NFS をサポートする商用製品システム (Solaris, HP-UX, RS/6000 など) の多くで、 また最新版の exportfs でもデフォルトになっています。 この動作をさせるには、Linux サーバのファイルシステムを sync オプションでエクスポートする必要があります。 なお同期的なエクスポートをした場合は、 サーバのエクスポートリストにはオプションが表示されません。

カーネルが /proc ファイルシステムをサポートするように コンパイルされた場合は、 /proc/fs/nfs/exports ファイルによっても すべてのエクスポートオプションのリストが表示できます。

同期動作を指定すると、サーバは NFS version 2 のリクエストに対して、 ローカルファイルシステムがすべてのデータ・メタデータをディスクに書き込むまで 動作を完了しません (つまりクライアントに応答しません)。 同期動作の NFS version 3 においては、 サーバはこの遅延を行うことなく返答を 行い、 クライアントにデータの状態を返して、 どのデータをキャッシュに保持しておくべきか、 またどのデータは捨ててよいかを判断できるようにします。 include/linux/nfs.h の enum 型変数 nfs3_stable_how には、3 つの状態値があります。

上記の同期動作の定義に加え、 クライアントから (プロトコルによらず) 明示的に完全な同期動作を行うような指定もできます。 これにはファイルをオープンする際に O_SYNC オプションを指定します。 この場合、クライアントからのリクエストに対する応答は、 データが実際にサーバのディスクに書き込まれるまで行なわれません。 これはプロトコルによりません (つまり NFS version 3 に対しては、 すべてのリクエストが NFS_FILE_SYNC リクエストとなり、 これはサーバに対して必ずこの状態を返すように求めるのです)。 この場合、NFS version 2 と NFS version 3 の性能は事実上同じになります。

しかし、古いデフォルトである async 動作が用いられていた場合には、 いずれの NFS バージョンにおいても O_SYNC オプションは全く意味を持ちません。 サーバは書き込みの完了を待つことなくクライアントに応答してしまうからです。 この場合も、バージョンの違いによる性能差は現われません。

最後に一言。NFS version 3 プロトコルのリクエストでは、 ファイルをクローズするときや fsync() の時に NFS クライアントから「後出し」の commit リクエストが発行されますが、 これによってサーバは以前書き込みを完了していなかった データ・メタデータをディスクに書き込むよう強制されます。 そしてサーバは、 sync 動作に従うのであれば、 この書き込みが終了するまでクライアントに応答しません。 一方もし async が用いられている場合は、 commit は基本的に no-op (何も行なわない動作) です。 なぜならサーバは再びクライアントに対して、 データは既に永続的なストレージに送られた、と嘘をつくからです。 するとクライアントはサーバがデータを永続的なストレージに保存したと信じて 自分のキャッシュを捨ててしまうので、 これはやはりクライアントとサーバをデータ破壊の危険に晒すことになります。

5.10. サーバの性能をあげる NFS 以外の方法

一般に、サーバの性能とサーバのディスクアクセス速度は NFS の性能にも重要な影響を及ぼします。 良好に機能するファイルサーバの設定に対するガイドラインを提供することは、 この文書で扱う範囲を越えていますが、 いくつかのヒントを提供しておく価値はあるでしょう。