			高速通信機能
			実装仕様書
						2014.3.31 数理技研

はじめに
	本ドキュメントは、Gfarm ファイルシステムのlinux カーネルドライバで
	vm のreadpages 要求をInfiniBand を利用して他ノードのキャッシュから
	データを得ることで高速に満たす仕様を記述するものである。

1. 実装概要
1.1. readpages 要求
	readpages はlinux のvm が発行する先読み要求で、ユーザーのIOの
	延長で、先読みが有効であると判断されたら、当該ファイルに関して
	複数のページの先読み要求がファイルシステムに対して出される。

	この時、ページはまだファイルのページキャッシュとしては登録されて
	おらず、ページは必ずしも連続したブロックを示すわけではない。
	また、この要求を非同期で行うかどうかはファイルシステムに任されている。

	本開発では、以下のように実装する。
	1. ページをファイルのページキャッシュとして登録する。
	   読み込みを行っている間に他の書き込みや読み込みで同じブロックに
	   別のページがアサインされるのを防ぐため、キャッシュ登録を行い
	   ページをロックする。
	2. InfiniBand でのキャッシュコピーは非同期で行う。
	   gfsd からの要求はgfarm に非同期処理が組み込まれていないので
	   同期的に行う。
	3. gfsd からのgfarm のバッファを利用して、ここからページにコピーする。
	   これは、前回のページへの直接IOが、連続した仮想空間を得られないページ
	   に対する転送フォーマットを設けず、ページサイズのRPCとなってむしろ
           性能の低下を招いたことから、gfarm の機能を利用することに戻した。
	   但し、ページ単位の要求を繰り返さないようコールバック関数を設ける。
	4. readpages 要求は、先ず、gfarm のバッファからコピーを行い、
	   足りない分は他ノードのキャッシュから得るか、gfsd から得るかする。

1.2. InfiniBand データ転送
	本システムは大量のノードが存在して、この間でキャッシュデータの交換を		行うため、コネクション型の接続では接続に時間がかかることもさりながら、
	接続ポート数が足りない問題がある。
	このため、ReliableConnection に依るRDMAは諦め、UnreliableDatagram に
	よる転送を行う。

	UDのSEND処理では通常データのコピーが伴うがこれを避け直接転送を行う。

	本開発では、以下のように実装する。
	1. 各ノードはRPC要求受信用のQPを予め交換する。
	2. データ転送の要求を行うノードは要求毎にQPをアロケートし、
	   ここに受信用のページを受信バッファとして登録する。
	3. データ転送要求ノードは、このQPの情報と要求キャッシュの情報を
	   転送要求にまとめ、相手ノードのRPC要求受信用QPに送る。
	4. 相手ノードは自身のページキャッシュを探し、必要なページを
	   指定されたQPに向けて送信する。

2. readpages の実装
2.1. パラメタ
	以下のパラメタが指定できる。

	1. readahead=npage
		先読みページ数を指定する。
		デフォルトはlinux最大値(32ページ)なので指定の意味はない。
	2. ra_sync={0,1}
		先読みを非同期に行うかの指定でデフォルトは非同期(1)。

2.2. gfs_pio_pread_page 関数
	typedef gfarm_off_t (*gfs_pageio_t)(char *, gfarm_off_t, int, void*);
	gfs_pio_pread_page(GFS_File gf, gfarm_off_t off, int size, int force,
        	gfs_pageio_t cb, void* arg)

	本関数は、仮想アドレスを与えられていないページにgfarm のデータバッファの
	データをコピーするために新設した関数である。

     【引数】
	off, size は読み込み要求の開始オフセットと、連続していないかもしれないが
	要求最終位置までの長さである。

	gfs_pageio_t cb はコールバック関数で、関数値は次の読み込みオフセットを
	示す。
	arg はコールバック関数への引数である。
	force フラグはバッファに要求データが無いときにgfsd からの読み込みを
	行うかどうかのフラグである。

	コールバック関数へはバッファポインタとそのファイルオフセット、バッファ長
	が渡される。

     【処理】
	1. 現在保持しているバッファに要求オフセットが含まれていない場合、
	   force 指定であれば、gfsd から読み出す。
	2. 現在保持しているバッファに要求オフセットが含まれていれば、
	   保持バッファデータ情報をコールバック関数に渡す。
	3. コールバック関数が正の値を返したらこれを新しい要求オフセットとして
	   1. に戻る。

     【問題】
	EOFを明示的に表す値がないので、ページ処理を行っているコールバック関数は
	最終ページであるかの判断がつかない。
	現在は、古いファイルサイズの値で判断している。

2.3. gfsk_readpages 関数
	int gfsk_readpages(struct file *file, struct address_space *mapping,
		struct list_head *head, unsigned nr_pages)

	linux vm から呼ばれる先読み処理関数

     【処理】
	1. ページ配列管理構造(pages)をアロケートする。
	2. head で示されるページリストをファイルのキャッシュに登録し、
	   ページ配列管理構造(pages)に登録する。
	3. 読み込むべきページがあれば、gfarm のバッファからデータをコピーする。
	   gfs_pio_pread_page()にgfsk_read_page_cb とpagesを渡す。
	4. 読み込むべきページがあれば、他ノードのキャッシュから読み込む。
	   mount 初期化時に InfiniBand 情報(gfsk_fsp->gf_cc_ctxp)が
	   とれていて、キャッシュ問い合わせ関数(gfarm_cc_find_host) が
	   ノード情報を答えればキャッシュデータ取得(gfsk_cc_read)を行う。
	   キャッシュ取得は通常非同期で行うので、その場合これ以上行わない。
	5. 読み込むべきページがあれば、gfsd から読み込む。
	   gfs_pio_pread_page()にgfsk_read_page_cb とpagesをforce 指定で渡す。

     【問題】
	非同期でキャッシュデータの取得を行って得られなかった場合、
	残りのページへの先読みは行われないことになり、性能が低下する。

3. InfiniBandデータ転送の実装
3.1. IBアドレス
	本開発はデータ転送をUDで行うので、IBのアドレス情報として
	以下の値が必要である。
		gid : グローバルユニークID
			但し、今回開発ではこの値での試験は行っていない。
		lid : ローカルID
			サブネットマネジャから動的に割り付けられる。
		qpn : キューペア番号
			ホストチャネルアダプタから割り付けられる。
		qkey: マルチキャストやUDで使う相互確認キー
		sl  : サービスレベル

	本開発では、RPC要求受信用とキャッシュデータ受信用の2種類の
	IBアドレスを用いる。
	RPC 受信用はマウント時に作成し、クライアントノード間で交換する。
	データ受信用はRPC要求時に作成し、RPCデータとして相手ノードに通知する。

     【問題】
	gid には対応していない。

3.2. UDによる直接転送
	UD による転送では一般的に送信側に受信バッファを知らせることができない。
	本開発では、RPC要求毎に異なるQPをアサインし、このQPに受け取りたい
	メモリバッファを割り付け、一度限りのデータ転送を要求する。
	要求受信側では、要求データの順序に合わせたキャッシュデータを、
	指定されたQPに順序どおりに送る。

	----------------------------------------------------------------
	要求側						応答側
			RPC用QP		公開QP	RPC用QP
	----------------------------------------------------------------
	1. RPC用QPを準備
	2. 受信バッファ作成
	3. QPに受信WRを登録
	4. 公開QPにRPCを要求	------->
	5. 				RPC要求を処理
	6. 					RPC要求用のQPを準備
	7. 					送信バッファを作成
	8. 					QPに送信WRを登録
	9. 			<--------------	受信したQPに向かって送信
	10. 受信完了でページを開放
	----------------------------------------------------------------

     【問題】
	qkey をQP毎に変えれば誤受信の危険が減るので、RPC要求は
	公開QPを使って行い、RPC用のQPは通知したqkey で行うようにした方がよい。
	しかし、qkey の設定にはQPのリセットが必要なので、このコストとの
	勘案となる。

3.2.1. RPC要求
	struct gfcc_obj {
		gfarm_ino_t     co_ino;
		gfarm_uint64_t  co_gen;
		gfarm_off_t     co_off;
		gfarm_off_t     co_len;
	};
	struct gfcc_pblk {      /* data segment */
		uint64_t        cs_index;       /* page index */
		uint64_t        cs_npage;       /* # page */
	};
	struct gfcc_rpc2_req {
		uint8_t         r2_ver[8];	/* バージョンマジック*/
		uint64_t        r2_id;		/* 要求ID */
		struct gfcc_obj r2_obj;		/* 要求ファイル情報 */
		uint32_t        r2_qkey;        /* QP で使うKEY */
		uint32_t        r2_cmd;
		uint32_t        r2_psize;       /* KB ページサイズ */
		int     r2_npage;	        /* 要求ページ数 */
		int     r2_npblk;
		struct gfcc_pblk        r2_pblk[1]; /* 要求ページ情報 */
	};
	struct gfcc_rpc2_res {
		uint8_t         r2_ver[8];	/* バージョンマジック*/
		uint64_t        r2_id;		/* 要求ID */
		int             r2_err;		/* 応答 */
		int             r2_npage;       /* 応答ページ数 */
		int             r2_nseg;	/* 応答セグメント数 */
	};

	rpc 要求は待ち受けQP情報と、要求ファイル情報、及び要求ページインデクス
	とページ数の配列から成る。
	QP情報は、待ち受けQPを使って送信するので、相手側で受信情報として一部
	取得できるため、割愛している。

	rpc 応答は成否と成功の場合の送信ページ数とその分割セグメント数を返す。

3.2.2. QPとメモリバッファ
	UD で転送するのでIBの1パケットはmtu の制約を受ける。
	mtu はノード間で同じという前提であるが、マウントパラメタで指定もできる。

	UD の受信では先頭にGlobalRoutingHeader がつくので、各受信WRで
	sge の1番目は同じGRHバッファを指すようにする。
	sge の2番目はDMAマップしたページをmtu に満つるように指させる。
	先頭のWRのみはRPCの応答バッファも1番目のバッファに含める。

	受信ページを以下のように複数のWRに分けてQPに登録する。

	受信バッファ		WR		QP
	+------------+				+-------+
	|  GRH       |<----+			| QP    |
	+------------+     |			|	|
	|  rpc_res   |     |			+-------+
	+------------+     |			   ^
			   |
	page		   |
	+------------+	   |	+-------+----------^
	|            |     +----|GRH+res|
	|mtu-GRH-res |     |    +-------+
	|            |<---------|page up|
	|	     |	   |	+-------+
	|	     |	   |	+-------+----------^
	|	     |	   +----|GRH	|
	+------------+	   |	+-------+
	|pagesize残り|<---------|page bo|
	+------------+	   |	+-------+
			   |
	+------------+	   |	+-------+----------^
	|            |     +----|GRH    |
	|mtu-GRH-res |     |    +-------+
	|            |<---------|page up|
	|	     |	   |	+-------+
	|	     |	   |	+-------+----------^
	|	     |	   +----|GRH	|
	+------------+	    	+-------+
	|pagesize残り|<---------|page bo|
	+------------+	    	+-------+


	送信ページではGRHは不要であるが、受信バッファと同じサイズの
	セグメントになるように複数のWRに分けてQPに登録する。
	最初のWRはRPCの応答を含むのでsgeは二つになるが、次のWRからは
	データのみのパケットとなる。

	送信バッファ		WR		QP
	              				+-------+
	              				| QP    |
	+------------+     			|	|
	|  rpc_res   |<----+			+-------+
	+------------+     |			   ^
			   |
	page		   |
	+------------+	   |	+-------+----------^
	|            |     +----| res   |
	|mtu-GRH-res |          +-------+
	|            |<---------|page up|
	|	     |	    	+-------+
	|	     |
	|	     |
	+------------+	    	+-------+----------^
	|pagesize残り|<---------|page bo|
	+------------+	    	+-------+

	+------------+
	|            |
	|mtu-GRH-res |	    	+-------+----------^
	|            |<---------|page up|
	|	     |	    	+-------+
	|	     |
	|	     |
	+------------+	    	+-------+----------^
	|pagesize残り|<---------|page bo|
	+------------+	    	+-------+

     【問題】
	1ページ毎にWRを作っているので、mtuを使い切っていない。
	sge を３つにして、2ページの断片をを一つのパケットにまとめるように
	した方がよい。

3.2.3. リソース管理
	QP はRPC要求の度に作成/削除するとコストが高いので、再利用する。
	再利用時にリセットするとコストが高いので、そのまま使用する。

	但し、データ受信に用いたQPの登録したWRが全部完了していない（
	サーバー側にデータがないなどで受信完了できなかった）場合は、
	リセットして再利用する。

	QPはRPC用データ構造とともにプール管理する。

	RPC要求待ちの場合にもRPCデータ構造をもって待ち受ける。

	WR に渡すWR_ID は実はこのRPC要求用のアドレスで、RPC要求内の
	rpc_id もこのRPC要求のアドレスである。
	つまり、完了通知では何番目のWRが完了したのかは分からない。
	IDからアドレスへの変換は、アドレスがバリッドでRPC用データ構造の
	マジック値が入っているかでチェックする。


	マウント時のオプションパラメタib_num_rrpc, ib_num_srpc は
	それぞれrpcの同時受信数、同時送信数である。
	この値は完了キューの最大イベント数や公開QPの受信WRの最大数に
	効いてくる。

3.3. 処理構造
3.3.1. イベント処理
	IBのWR完了処理は linux のwork-queue 機構を使って行う。

	InfiniBand のイベントは送信完了と受信完了に分けて通知要求を行い、
	イベント通知関数ではそれぞれのwork-queue に処理依頼を行う。

	完了処理は、各完了キューから完了WRを取り出し、WR_IDが示すRPCデータの
	コールバック関数を呼び出す。

3.3.2. rpc 要求送信
	rpcの要求は、ユーザーの要求コンテクストで以下のことを行う。
	1. RPCデータをQPとともにプールから得る。
	2. ページをdma マップしマップしたアドレスを記録する。
	3. アドレスハンドルを作成する。
		これはQPと共でよかったか			【問題】
	4. 受信用のWRをQPに登録する。
	5. RPC要求を応答待ちリストにつなぎタイマーを掛ける。
	6. RPC要求を作成しQPに送信登録する。
	マウントオプションで同期を指定されない限り、応答は待たない。

3.3.3. rpc 要求受信
	RPC 要求は予め、マウント時に指定されたrrpc_num個の受信登録を行う。

	要求受信コールバック関数で以下の処理を行う。
	1. rpc 要求を解析する。
	2. 要求ファイルのキャッシュのページを探す。
	3. ページをdma マップしマップしたアドレスを記録する。
	4. アドレスハンドルを作成する。
	5. RPC 応答とデータのWRをQPに送信登録する。
	完了は待たない。

     【問題】
	要求ファイルの特定はinode番号と世代番号とで行なっているが
	gfarm の世代番号がinode世代番号とデータ世代番号を兼ねているため、
	自身が書き込みクローズした場合、データ世代番号が上がっているにも拘らず、
	自身はその世代番号を知らないため、問い合わせより古いデータと
	判断してしまう。
	close後に属性取得行う案もあったが、これは自身のデータ番号とは
	限らないので採用しなかった。
	本実装では経過的対応としてwrite-close 時に世代番号を1上げているが、
	これもデータの世代番号を保証するものではない。
	協調キャッシュ機構が、この問題を解決すると期待する。

3.3.4. rpc データ受信完了
	1. 最初の応答でRPCの応答を見る。
		1. エラーなら要求処理を終了させる。
		2. 応答パケットが期待より少なければ記録する。
	2. 期待する応答数が得られれば要求処理を完了させる。
	3. 要求完了では、
		1. データが得られたページに更新フラグを立てる。
		2. 得られなかったページは開放する。
		3. 受信WRがすべて完了していない時はQPをリセットする。
		4. DMA登録アドレスを解除する。
		5. RPCデータをプールに返す。

3.3.5. rpc データ送信完了
	1. ページを開放する。
	2. DMA登録アドレスを解除する。
	3. RPC要求受信の登録を行う。

3.3.6. rpc 応答待ちタイマ処理
	linux のdelayed work queue の機構を用い、完了しないRPC要求を刈り取る。

4. 協調キャッシュ機構連携処理
	以下の関数はgfarm_stub.c に定義されている。

	gfarm_error_t
	gfarm_cc_register(struct gfcc_ibaddr *ibaddr)
		マウント時に自身のIBアドレスを登録しノード間で交換する。

		今回実装では何もしない。

	gfarm_error_t
	gfarm_cc_find_host(struct gfcc_obj *, struct gfcc_ibaddr *)
		obj が示すファイルのキャッシュを持っているノードのIBアドレスを
		ibaddr に 返す。

		今回実装では、ユーザーが手動で/proc/fs/gfarm/{mnt_id}/ibaddr
		に登録したアドレスがあればそれを返すようになっている。

