Apache Ozone におけるhsync/hflushについて
本記事では、Apache ozoneにおけるhsync/flushの実装においての詳細を説明する。
hsyncとは
あるファイルシステムにデータを書き込んだ場合、そのデータがストレージデバイスに即座に書かれることは保証されない場合がある。なぜなら多くのファイルシステムで、パフォーマンスなどを目的として、書き込んだデータは一時的にバッファ(つまりはメモリ上)に書かれ、何らかの契機でそのデータがストレージに書きこまれるという実装がされているからである。
このような実装は通常時であれば動作の高速化に寄与するため嬉しい。しかし、バッファ中のデータはそのタイミングで電源断などがあった場合にデータロストしてしまうため、ストレージに書き込んだと思っていたデータがなくなってしまう場合がある。これは、例えばデータベースに保存したと思っていたデータが消えてしまう事態になるため、困ってしまう。
こういった問題を解決するために、POSIXには fsync()
がある。 fsync()
を実行するとデータが確実にストレージに書かれたことが保証される[1]ため、先ほど言及したようなデータデータベースがデータの書き込みをcommitする際に利用される。トランザクションのACID特性でいうなら、Durabilityを担保するための機能である。
hsync
とは fsync
をApache Hadoopの分散ファイルシステムで実現したものである[2]である。
クライアントが hsync
を呼び出した場合、そのデータがHDFSに、もっと言うならその下のファイルシステムに必ず書かれたことが保証される。例えば HDFS上でNoSQLデータベースを実現するApache HBase ではその内部実装で hsync
を利用する。
hflushとは
hflush
は厳密性を無視するなら hsync
と等価である。Hadoopのドキュメント上[3] も、以下のような実装であることを許容している。
public void hflush() throws IOException {
hsync();
}
本稿では、 hflush
は hsync
と等価である[4]ものとし、以降特に言及しない限りは hsync
のみについて記載する。
Hadoopにおけるhsyncの実装
Hadoop(HDFS)では、DFSOutputStream において hsync
を実行した場合データパケットのヘッダ情報に sync する旨の情報が書き込まれ、そのヘッダを受け取った DataNode は最終的にデータ書き込み時に org.apache.hadoop.io.IOUtils#fsync()
を呼び出す。
public static void fsync(FileChannel channel, boolean isDir)
throws IOException {
try {
channel.force(true);
} catch (IOException ioe) {
if (isDir) {
assert !(Shell.LINUX
|| Shell.MAC) : "On Linux and MacOSX fsyncing a directory"
+ " should not throw IOException, we just don't want to rely"
+ " on that in production (undocumented)" + ". Got: " + ioe;
// Ignore exception if it is a directory
return;
}
// Throw original exception
throw ioe;
}
}
このメソッドの中では最終的に上記のように java.nio.channels.FileChannel#force(true)
を実行している。
また、 DFSOutputStream
では、hsync を読んだ時点で Clinet 上のバッファのデータが全てDataNodeに送られ、DataNodeからAckが返ってくるまでを待つという実装になっている。
それ以外は他のデータ書き込みと大きく変わるところはない。割とシンプルである。
Ozoneにおけるhsyncの実装
先にも言及した通り、HDFS上で実装されたHBaseのようなデータベースは hsync
を多用している。Apache OzoneはHDFSの置き換えも狙ったオブジェクトストレージであり、HDFSにあるような機能やHDFS上で動くことが期待されるアプリケーションはOzone上であっても動作するように実装することが求められる。 hsync
もその一つである。
Ozoneでは、あるデータはBlockと呼ばれるデータに分割されて格納されるが、実際にはさらに細かいChunkという単位にClientで分割されてDataNodeに送られる。設定によっては、DataNodeでファイルシステムに書き込まれる単位がChunkであった(FILE_PER_CHUNK)が、Ozone 2.0からは非推奨となる[5]となり、Block単位でファイルとして書き込まれるようになる。その実装は FilePerBlockStrategy
である。
FilePerBlockStrategy
においては、 java.io.RandomAccessFile
がファイル書き込みの実装として用いられる。Syncが有効な場合、 mode == "rws"
としてインスタンス化されるため、確実にストレージにデータが書かれることとなる。
hsyncした場合、DataNodeに対してはfinalizeの処理が実行される。DataNodeはこれを受けて、ブロック書き込み用としてopenしていたファイルをcloseする[6]。このファイルが上記のように mode == "rws"
として作成されているため、 closeした段階で fsync相当のことが実施できていることが期待される。
Discussion