Javaバッチ処理のNFS向けファイルI/O
この記事は Java EE Advent Calendar 2015の12/7分の記事です。
明日は@btnrougeさんです。
Java EEのAPIが直接関連する話ではなくて恐縮ですが、サーバサイドJavaでファイルI/Oを含むバッチ処理の性能Tipsをまとめます。
テーマはjava.io.BufferedWriterクラスのバッファサイズについてです。
デフォルトは8KBでBufferedWriterのコンストラクタにおいて変更可能ですが、javadocには以下の記載があります。
バッファのサイズは、デフォルト値のままにすることも、特定の値を指定することもできます。デフォルト値は、通常の使い方では十分な大きさです。
http://docs.oracle.com/javase/jp/8/docs/api/java/io/BufferedWriter.html
あまり変更する機会もないせいか、Java SE 7で導入された便利なFiles.newBuffertedWriterメソッドにはバッファサイズを設定する引数がありません。
しかし、NFSへの書き込み時においては、mountオプションnoac*1の有効時にバッファサイズ拡大が効果的なケースがあります。
Javaのバッチ処理のシステム連携において、NFSサーバにファイルを置くファイル連携方式は、業務システムで見かける構成かと思います。
効果測定
手元の仮想マシンでバッファサイズ変更時の書き込み性能を実測してみます。
/nfs_export/batch 192.168.xxx.xxx(rw)
NFSクライアント: マウントオプションnoac
mount -o noac 192.168.xxx.xxx:/nfs_export/batch /nfs
NFSクライアント: /proc/mounts
192.168.xxx.xxx:/nfs_export/batch /nfs nfs4 rw,sync,relatime,vers=4.0,rsize=131072,wsize=131072,namlen=255, acregmin=0,acregmax=0,acdirmin=0,acdirmax=0,hard,noac, proto=tcp,port=0,timeo=600,retrans=2,sec=sys, clientaddr=192.168.xxx.xxx,local_lock=none,addr=192.168.xxx.xxx 0 0
検証コード
ローカルファイルシステム上のddで生成した100MBファイルのダミーデータ(test.src)を、NFSマウント上のパスにコピーするシンプルな処理です。
public class Fcopy { private static final String SRC = "/home/test/input/test.src"; private static final String DST = "/nfs/test.dst"; public static void main(String ... args) throws IOException { int bufSize = Integer.valueOf(args[0]); byte[] buf = new byte[8192]; try ( BufferedInputStream bis = new BufferedInputStream(Files.newInputStream(Paths.get(SRC))); BufferedOutputStream bos = new BufferedOutputStream(Files.newOutputStream(Paths.get(DST)), bufSize)) { long start = System.currentTimeMillis(); for (int readSize = bis.read(buf); readSize >= 0; readSize = bis.read(buf)) { bos.write(buf); } long time = start - System.currentTimeMillis(); System.out.println("BufferedStream bufSize: " + bufSize + " Time(millisec): " + time); } } }
測定結果
以下のような結果となりました。デフォルトのNFSクライアントの非同期書き込みには及ばないものの、BufferedWriterのバッファサイズ変更で、数倍の処理時間差が見られます*2。
BufferedWriterのバッファサイズ | 処理時間(ミリ秒) |
---|---|
8192 (8KB, デフォルト) | 36857 |
65536(64KB) | 18452 |
131072(128KB) | 7888 |
(参考) 8192/NFSクライアント側の非同期書き込み | 407 |
なぜ早くなったか
ここから先はNFS実装の知識が足りず、若干自信なしです。
/proc/mountsの結果を振り返ると、wsizeが131072です。man nfsを見ると、wsizeはNFSクライアントがNFSサーバに一度に書き込むサイズを示しています。wsizeは明示的に指定しない場合、クライアントサーバ間で適切な値を自動的に決定します。
192.168.56.101:/nfs_export/batch /nfs nfs4 rw,sync,relatime,vers=4.0,rsize=131072,wsize=131072 ...
デフォルトの非同期NFSクライアントの場合は、ある程度まとまったデータをwsize単位(例では128KB)でNFSサーバに送ります。しかし、noacにより同期書き込みになった場合は、writeシステムコールの都度サーバに書き込まれるため、JavaのBufferedWriterの単位でNFSサーバに書き出されます。tcpdumpで見ると、noacオプションを付けてマウントした場合はLen:が示すWRITE RPCのサイズがJava側のバッファサイズと同じです。
BuffertedWriterのバッファサイズが8192の場合
11 0.029289000 192.168.56.102 -> 192.168.56.101 NFS 1222 V4 Call WRITE StateID: 0x6366 Offset: 0 Len: 8192 14 0.035469000 192.168.56.101 -> 192.168.56.102 NFS 202 V4 Reply (Call In 11) WRITE ...
BuffertedWriterのバッファサイズが131072の場合
53 0.012420 192.168.56.102 -> 192.168.56.101 NFS 458 V4 Call WRITE StateID: 0x8849 Offset: 0 Len: 131072 55 0.015583 192.168.56.101 -> 192.168.56.102 NFS 202 V4 Reply (Call In 53) WRITE ...
また、nfsstatの結果からも、writeを示すRPC発行回数が減っています。
(データは1MBのファイルコピー処理時のもの)
BuffertedWriterのバッファサイズが8192の場合
(writeが128回。8192B * 128回 = 1MB)
nfsstat -c # ファイルコピー処理前 null read write commit open open_conf 0 0% 0 0% 133228 93% 7915 5% 149 0% 13 0% # 処理後 null read write commit open open_conf 0 0% 0 0% 133356 93% 7915 5% 150 0% 14 0%
BuffertedWriterのバッファサイズが131072の場合
(writeが8回。131072B * 8回 = 1MB)
# ファイルコピー処理前 null read write commit open open_conf 0 0% 0 0% 133356 93% 7915 5% 150 0% 14 0% # 処理後 null read write commit open open_conf 0 0% 0 0% 133364 93% 7915 5% 151 0% 15 0%
JavaのBufferetedWriterのデフォルト8KBの単位での書き出しでは、NFSのwsize(128KB)と書き込み単位が合わず、サイズの小さい断片化したRPCを繰り返し発行していたことが、バッファ拡大による性能向上の理由と思います。
バッファ拡大の注意点
NFSクライアントマシンの不測なクラッシュに備えた同期書き込みを目的にnoacオプションでマウントしていた場合、JavaレイヤでのバッファリングはNFSレイヤでの同期書き込みの意味をなくし、データ損失の可能性を高めるため注意が必要です。
以下のようなケースでは、BufferetedWriterのバッファ拡大もありかと思います。
まとめ
BuffertedWriterのバッファ拡大により、性能差分が発生するあまり見かけないケースをまとめました。実測したのはNFSv4だけですが、ネットワーク経由でアクセスする他のファイルシステムも、同様の注意が必要と思います。