※ 左右のカーソルキーでもページ繰りができます(但しブラウザ依存)
藤原 克則 ( FUJIWARA Katsunori )
前職で、 HPC ( High Performance Computing ) 系システムのために Solaris 向けファイルシステムを実装したのを機に、 OpenSolaris 勉強会に参加。
「入門Mercurial Linux/Windows対応」、 「俺のコードのどこが悪い?―コードレビューを攻略する40のルール」、 「アセンブラで読み解くプログラムのしくみ」 といった書籍の執筆や、 技術系ウェブ媒体への記事の寄稿といったことも。
ホームページ、はてなダイアリー、 Twitter等で情報発信中。
オラクルの「Sun ZFS Storage Appliance」を導入したさくらインターネットのクラウド・サービスで、 2011 年末から障害が多発。
『ストレージの事前検証が十分にできなかった』ことが原因として、 まずは性能限界テストが実施しやすい自社開発の新ストレージを採用する方針が発表された。
ネット上では、 これらの障害があたかも ZFS や Solaris の問題であるかのような言及も見られるが、 それは本当だろうか?
構成に関する記述を見る限りでは、 ストレージサーバ1台あたり、 1000台規模のクライアントがアクセスするものと思われる。
北海道にある4,000ラック規模の石狩データセンターの最初のサービスとして、 〜略〜 「さくらのクラウド」を本日開始しました。 〜略〜 本日のサービス開始までに「Sun ZFS Storage 7320 Appliance」 を複数機採用・稼働しました。 〜 オラクルの「Sun ZFS Storage Appliance」がさくらインターネットのクラウド・サービスのストレージとして稼働開始
北海道にある4,000ラック規模の石狩データセンターの最初のサービスとして、 〜略〜 「さくらのクラウド」を本日開始しました。
〜略〜 本日のサービス開始までに「Sun ZFS Storage 7320 Appliance」 を複数機採用・稼働しました。
〜 オラクルの「Sun ZFS Storage Appliance」がさくらインターネットのクラウド・サービスのストレージとして稼働開始
IP over InfiniBandでTCP/IPが使えるようになっているので、 仮想サーバからは普通にTCP/IPでネットワークが見えます。 ストレージにはNFSでマウントしています。 〜 「さくらのクラウド」のアーキテクチャは、意外なほどシンプルだった 〜
IP over InfiniBandでTCP/IPが使えるようになっているので、 仮想サーバからは普通にTCP/IPでネットワークが見えます。 ストレージにはNFSでマウントしています。
〜 「さくらのクラウド」のアーキテクチャは、意外なほどシンプルだった 〜
さくらのクラウドのストレージの実装はNFSを使っていて、 それをQEMUを通して「/dev/sda」など、 ローカルのブロックデバイスに見せている。 〜 さくらのクラウド、Amazon EC2の半額以下で11月開始へ 〜
さくらのクラウドのストレージの実装はNFSを使っていて、 それをQEMUを通して「/dev/sda」など、 ローカルのブロックデバイスに見せている。
〜 さくらのクラウド、Amazon EC2の半額以下で11月開始へ 〜
クライアントが 1000 台規模になるシステムで NFS ?
本資料では、 100 台規模のシステムでは問題無くても、 1000 台超規模のシステムで問題となるであろう Solaris NFS 実装由来の問題について説明する。
なお、 これらの問題点は、 『Solaris NFS の実装が悪い』というよりも、 元々『NFS が前提としているシステムがせいぜい数 100 台規模』という、 前提条件不一致の問題と言える。
以下、mountd における MOUNT RPC 処理時の流れ:
mnt(){ /* RPC 要求のプロシジャ毎処理への振り分け */ mount(){ /* MOUNTPROC_MNT 処理の実施 */ findentry(path){ check_sharetab(); /* ファイル更新時の再読み込み管理 */ /* sharetab 内容の線形走査 */ } /* 以下は『アクセス許可の記録』の処理 */ mntlist_new(host, path){ mntlist_insert(host, path){ if(!mntlist_contains(host, path)){ /* h_get() によるハッシュテーブルからの引き当て失敗 */ rmtab_insert(); /* /etc/rmtab への追記 */ h_put(); /* ハッシュテーブルへの格納 */ } } } } }
大規模システムにおける mountd での問題
以下の処理が export 対象ファイルシステム数 N に対して O(N) オーダの線形処理となってしまう。
check_sharetab()(); /* ファイル更新時の再読み込み管理 */』 契機で実施される処理が線形処理 findentry(path) での合致ファイルシステム引き当てが 『/* sharetab 内容の線形走査 */』
findentry(path)
/* sharetab 内容の線形走査 */
export 対象ファイルシステムのバリエーションが 100 〜 1000 のオーダになった場合、 MOUNTPROC_MNT 要求における線形処理のコストは無視できない。
b130 より前の実装では、 以下の処理におけるハッシュ関数の定義が不適切なため、 多くのケースで実質線形処理となってしまう。
h_get() によるハッシュテーブルからの引き当て
h_put(); /* ハッシュテーブルへの格納 */
b130 より前の『host + path』に対するハッシュ値算出処理は:
mntentry_has(){ for (i = 0, s = (uchar_t *)m->m_host; *s && i < HASH_MAXHOST; i++) { uchar_t ls = tolower(*s); sum <<= HASH_BITS; /* HASH_BITS(= 3)/SPACE は消費ビットを節約 */ sum += ls - SPACE; *s++; } /* * The first character is usually '/'. * Start with the next character. */ for (i = 0, s = (uchar_t *)m->m_path+1; *s && i < HASH_MAXPATH; i++) { sum <<= HASH_BITS; sum += *s++ - SPACE; } return (sum); }
以下の修正により、 b130 以後の『host + path』に対するハッシュ値算出処理が変更:
changeset: 11211:a6230133d60c user: Thomas Haynes <Thomas.Haynes@Sun.COM> date: Mon Nov 30 15:14:37 2009 -0600 summary: 6882460 Hundreds of NFSv3 client mounts followed by immediate reads causes timeouts
コミット日時から推測するに、 この修正が Solaris の公式リリースに含まれるのは、 早くても Solaris10 update9 (2010/09 リリース) 以後になる筈。
b130 以後の『host + path』に対するハッシュ値算出処理は:
mntentry_str_hash(char *s, uint_t hash){ uint_t g; for (; *s != '\0'; s++) { hash = (hash << 4) + *s; if ((g = (hash & 0xf0000000)) != 0) { hash ^= (g >> 24); hash ^= g; } } return (hash); } mntentry_hash(){ uint_t hash = mntentry_str_hash(m->m_host, 0); return mntentry_str_hash(m->m_path, hash); }
上位桁に対するビット操作によって、 ハッシュ値の衝突可能性が大幅に低減されている。
b130 での修正には、 『h_get() によるハッシュテーブルからの引き当て』から 『h_put(); /* ハッシュテーブルへの格納 */』に至る 『アクセス許可の記録』処理全体を、 専用スレッドで非同期的に実施する機構の追加も含まれているため、 これも性能劣化を低減させている筈。
ハッシュ値算出が『パス名』ベースではなく、 公開対象ファイルシステムにおける『識別子』ベースなので、 衝突はほぼ無いと想定可能。
しかしその一方で、 ハッシュテーブルサイズが 32 (EXPTABLESIZE) 固定なので、 共有対象ファイルシステムが 1000 個規模になった場合、 ハッシュエントリ毎のチェーン数が 32 以上となる点は、 性能劣化に関して一抹の不安がある。
mountd における『操作対象 FS のハンドルの引き当て』 と同等の処理が行われる。
ちなみに、 mountd/nfsd 共に、 操作対象 FS のハンドルの引き当て処理は、 カーネル層まで降りてきて実行される。
ユーザ空間で動作する mountd からは、 NFS 専用システムコールでカーネル層に要求が発行される。
以下、nfsd におけるホスト毎アクセス可否の確認処理の流れ:
common_dispatch(){ checkauth(){ nfsauth_access(){ nfsauth_cache_get(){ /* クライアントのIPベースでアクセス情報をハッシュから引き当て */ /* 以下、キャッシュ上にアクセス情報がない場合 */ nfsauth_retrieve(){ ※ DOOR IPC による mountd との連携 〜{ nfsauth_func(){ nfsauth_access(){ findentry(); /* sharetab 内容の線形走査 */ check_client(); /* クライアント一覧の線形走査 */ } } } 〜 DOOR IPC による mountd との連携 ※ } } } } }
大規模システムにおける nfsd での問題
ホスト毎アクセス可否の確認において、 以下の処理が O(N) オーダの線形処理となってしまう。
findentry()
一度引き当てが実施されてしてしまえば、 以後は各共有対象ファイルシステム毎に、 IPアドレスベースのハッシュテーブルで管理されるため、 性能劣化は比較的低減される筈。
但し、 ハッシュテーブルのサイズが 32 固定のため、 クライアントが 1000 台規模になった場合、 ハッシュエントリ毎のチェーン数が 32 以上となる点で、 性能上の不安は残る。
ちなみに、 nfsauth_cache_get() におけるアクセス可能クライアントの情報キャッシュの管理は、 以下の要領で実装されているため、 同一クライアントからの初回アクセスが並走した場合、 『キャッシュの追加』において、 同一内容のキャッシュが重複して追加される可能性もある。
しかし、特に重複の検査/排除を行っていないのは、 (1) 排他の保持を最小限にするためと、 (2) 余計な線形走査による性能劣化を回避するためと思われる。
また、 キャッシュの有効期間は 600 秒だが、 有効期限切れの際の再読み込みは、 以下の要領で実施されるため、 一旦キャッシュされた後の応答性能はある程度担保される。
期限切れキャッシュの刈り取りも、 バックグランドで実施されるので、 正確には『一旦キャッシュされた後の応答性能はある程度担保』 というのは正しくないのだが、 詳細はソースを参照の事。
要求再送判定は、 実行により副作用を伴う処理の要求の際に、 いずれかの層における再送実施が原因となって、 想定外の挙動となることを防ぐために必要。
再送実施は、 OSI 参照モデルで言うところの 『アプリケーション層』に該当する 『NFS クライアント実装』以外でも実施される可能性がある。
ユーザ空間から利用する観点からは、 トランスポート層として TCP/IP を選択した場合、 『応答パケットの消失』は発生しないように思われるかもしれないが、 システム構成 〜 ドライバ実装によっては、 それほど珍しい現象ではない。
ちなみに、Solaris NFS 実装の場合、 TCP/IP なら 60 秒、 UDP なら 1.1 秒の間に応答が無い場合、 一旦通信層から制御が戻り、 NFS 層主導で再送を行うようになっている。
UDP 接続のようにタイムアウト時間が非常に短い場合、 負荷要因によるサーバ側におけるちょっとした処理の遅延が、 再送の実施 〜 負荷の増大 〜 再送の頻発のループを生じさせてしまうので、 十分な注意が必要。
受け付ける要求を2種類に分類する:
前者に該当する要求には、 IDENPOTENT フラグが立てられている。
static struct rpcdisp rfsdisptab_v3[] = { .... { rfs3_getattr, xdr_nfs_fh3_server, .... xdr_GETATTR3res, .... nullfree, (RPC_IDEMPOTENT | RPC_ALLOWANON), rfs3_getattr_getfh }, .... { rfs3_mkdir, xdr_MKDIR3args, .... xdr_MKDIR3res, .... nullfree, 0, rfs3_mkdir_getfh}, .... }
要求再送の検出は以下のフローで実施される。
common_dispatch(){ if(0 == (disp->disp_flas & RPC_IDEMPOTENT)){ /* 再送検査が必要な要求 */ dupstat = SVC_DUP_EXT(....); /* 要求再送の確認 */ switch(dupstat){ case DUP_INPROGRESS: /* 別スレッドで実行中 */ /* 応答しない */ goto done; case DUP_NEW: /* 再送検出無し */ /* 状態を DUP_INPROGRESS にしておく */ (*disp->dis_proc)(args, res, exi, req, cr); /* 処理実施 */ SVC_DUPDONE_EXT( .... ); /* 再送キャッシュへの登録 */ break; case DUP_DONE: /* 既に応答済みの処理 ⇒ 再送キャッシュから取得した内容で応答 */ break; } } }
SVC_DUP_EXT() や SVC_DUPDONE_EXT() の実装は、 各トランスポート層毎に定義された RPC 処理モジュールにおいて定義されている。
SVC_DUP_EXT()
SVC_DUPDONE_EXT()
要求再送が以下のように実現されているため、 再送管理の十分性と性能低下の間で、 バランスを取らなければならない。
NFS の各プロシジャ毎処理の実装において、 クライアントホスト/共有対象 FS の数に影響を受けるのは、 主に排他競合頻発による性能への影響だと思われる。
Solaris の NFS 実装は、 以下のように比較的配慮の行き届いた実装となっているため、 本資料ではこの観点からは扱わないものとする。
NFS を利用するシステムの場合、 NFS クライアント側実装との組み合わせでも、 システム全体としての性能が左右されるので、 異 OS (or 同 OS 異バージョン) を組み合わせる場合は注意が必要。
再送キャッシュへの登録は、 再送確認を行う SVC_DUP_EXT() 呼び出し時点で確保したエントリへの情報書き出しなので、 大規模システムにおける運用の場合でも、 基本的にコスト低下要因はない。
例えば以下の環境において、 host + path で 32 バイト程度の文字列比較を、 1024 の各リスト要素に対して実施した場合、 1走査あたりの所要時間は 0.1 ミリ秒に及ばない程度であった。
単純な参照であれば、 ハッシュテーブルが効率的に構築されていなくても、 十分な性能が出る筈(要キャッシュメモリ)。
本資料で性能劣化要因の可能性を指摘してきた箇所の多くが、 参照時はハッシュテーブル全体の READ 排他を、 改変時(要素追加/削除等)は WRITE 排他を獲得する実装となっているため、 以下のような性能劣化の可能性が考えられる。
以上の事を踏まえた場合:
1000 台超の大規模システムにおいて NFS を使用する場合、 Solaris NFS 実装の視点で見えてくる注意点は以下の通り。