🌀

Apache Ozone におけるhsync/hflushについて

2025/01/20に公開

本記事では、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();
}

本稿では、 hflushhsync と等価である[4]ものとし、以降特に言及しない限りは hsync のみについて記載する。

Hadoopにおけるhsyncの実装

Hadoop(HDFS)では、DFSOutputStream において hsync を実行した場合データパケットのヘッダ情報に sync する旨の情報が書き込まれ、そのヘッダを受け取った DataNode は最終的にデータ書き込み時に org.apache.hadoop.io.IOUtils#fsync() を呼び出す。

hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/io/IOUtils.java
  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相当のことが実施できていることが期待される。

References

脚注
  1. ここではデバイスにあるキャッシュなどについては深く言及しない ↩︎

  2. https://hadoop.apache.org/docs/stable/hadoop-project-dist/hadoop-common/filesystem/outputstream.html#Syncable.hsync.28.29 ↩︎

  3. https://hadoop.apache.org/docs/current/hadoop-project-dist/hadoop-common/filesystem/outputstream.html#Syncable.hflush.28.29 ↩︎

  4. 細かいことを言うと、hflush は Clientのバッファからデータがなくなり、他のreaderからそのデータが可視となればOK、 hsynchflush に加えてストレージにデータが書かれていることを保証しなければだめ、という違いがある。つまり hsync のほうが重い処理となる ↩︎

  5. https://issues.apache.org/jira/browse/HDDS-11753 ↩︎

  6. https://issues.apache.org/jira/browse/HDDS-9915 ↩︎

Discussion