おっさんエンジニアの滝澤です。「アトミックなファイル操作なんて考えずにデータベースを使えばいいじゃない」と言われそうでgkbrです。
前回に引き続き、アトミック(atomic)なファイル操作について紹介します。この内容は弊社の社内勉強会で話した内容をまとめ直したものです。
- そのファイル、安全に更新できていますか?(アトミックなファイル操作:前編)
- そのファイル、安全に作成できていますか?(アトミックなファイル操作:中編)←今回
- そのファイル、安全にロックできていますか?(アトミックなファイル操作:後編)
2回目の今回は「みなさん、安全にファイルの作成ができていますか?」ということについて、考えてみましょう。
そのファイル、安全に作成できていますか?
アプリケーションがあるディレクトリ内に存在するファイルを読み込んで処理するシステムがあるとします。このとき、そのディレクトリ内に直接ファイルを作成してみました。アプリケーションがファイルを読み込むタイミングをあなたが制御できていれば問題はありません。ファイルの作成が完了するまでファイルを読み込まないようにすればよいだけです。しかし、アプリケーションがファイルを読み込むタイミングをあなたが制御できない場合はどうでしょうか。
ファイルを作成したときのファイルの状態を追ってみましょう。
作成前 | ファイルが存在しない |
作成中 | 不完全(データの一部分) |
作成完了後 | 完全 |
ファイルの作成中はデータが不完全な状態にあるため、アプリケーションがそのファイルを読み込んだ場合には作成中の中途半端なデータが読み込まれてしまい、問題が生じるでしょう。これはファイルの作成処理(直接ファイルを作成して、そこにデータを書き込む方法)がアトミックではないためです。ファイルの作成がアトミックであるとは、ファイルの作成途中の中途半端な状態ではなく、ファイルが存在しないか、あるいは作成済みの完全なファイルが存在するかのどちらかになることです。それでは、アトミックにファイルを作成するには具体的にどうしたらよいでしょうか。
アトミックにファイルを作成するには
アトミックにファイルを作成するには、前回と同じくrename()というファイル名を変更するシステムコールを使います。rename()については前回説明したのでそちらを参照してください。mvコマンドはこのrename()を使ってファイル名の変更を行っています。
特定のファイル名のファイルをアトミックに作成する場合と、ディレクトリそのものをスプールのように扱う場合の2つのケースについて説明します。
特定のファイル名のファイルをアトミックに作成する場合は、特定のファイル名とは異なるファイル名のファイルを作成します。ファイルの作成が完了したら、rename()を使って特定のファイル名に名前を変えればよいです。ディレクトリエントリの書き換えはアトミックに行われるため、特定のファイル名のファイルは「ファイルが存在しない」か「完全な状態のファイルが存在する」のどちらかになり、安全にファイルを作成できます。
ディレクトリそのものをスプールのように扱う場合は、同じファイルシステム上にある別のディレクトリにてファイルを作成します。ファイルの作成が完了したら、rename()を使ってそのファイルを目的のディレクトリに移動すればよいです。ディレクトリエントリの書き換えはアトミックに行われるため、目的のディレクトリにおいては「ファイルが存在しない」か「完全な状態のファイルが存在する」のどちらかになり、安全にファイルを作成できます。
Maildir形式のメールボックスでの例
アトミックなファイル作成の例として、Maildir形式のメールボックスについて考えてみましょう。
Maildir形式のメールボックスは、Maildirというディレクトリ内にnew, cur, tmpという3つのサブディレクトリがあります。それぞれ、新着メール、既読・処理中メール、一時ファイルを格納するディレクトリです。
/home / foo / Maildir / new / cur / tmp /
メールボックス内のファイルはPOP3サーバなどのプロセスからいつアクセスがあるかわかりません。そのため、作成中などの不完全な状態のファイルに対してPOP3サーバが読み込みを行ったら、不完全なメールが処理されてしまうのです。
Maildir形式のメールボックスを扱うソフトウェアはこの問題を防ぐために、作成中のファイルをtmpというディレクトリ内にユニークなファイル名(例えば、ホスト名 . プロセスID . UNIX時間)で作成します。
/home / foo / Maildir / new / cur / tmp / example.jp.4726.1380907208 ←作成中のファイル
作成が完了して完全な状態になったら、newディレクトリ内にrename()で移動するのです。
/home / foo / Maildir / new / example.jp.4726.1380907208 cur / ↑rename()により移動 tmp / example.jp.4726.1380907208
newディレクトリ内へ移動はアトミックに行われるため、newディレクトリ内は常に完全な状態のファイルしかありません。
/home / foo / Maildir / new / example.jp.4726.1380907208 ←作成完了後のファイル cur / tmp /
このようにすることにより、newディレクトリ内に安全にファイルを作成できます。
スクリプト言語の場合
スクリプト言語を使う場合でも、rename()を内部的に使っている関数やメソッドがあるのでそれを使えばアトミックにファイルを作成できます。たいていはrename()という名前になっています。
Pythonの例を次に示します。osモジュールのrename()関数を使います。テンポラリのファイルを作成して、それをrename()により指定したファイルに移動します。
#!/usr/bin/python import sys import os def main(): tmp_filename = '/home/foo/tmp/file.dat' target_filename = '/home/foo/data/file.dat' try: with open(tmp_filename, 'w+') as tmpfile: tmpfile.writelines("OK\n") tmpfile.flush() except IOError: print "%s" % (sys.exc_value) sys.exit(1) try: os.rename(tmp_filename, target_filename) except OSError: print "%s" % (sys.exc_value) sys.exit(1) if __name__ == "__main__": main()
シェルスクリプトによるファイルの作成例
シェルスクリプトでアトミックにファイルを作成する例を紹介します。
ここで紹介するスクリプトは次の内容になります。指定したファイル名の場所にファイルを作成します。
#!/bin/bash PATH=/opt/local/bin:/usr/bin:/bin export PATH TARGET_FILE=/path/to/data/file.dat TMPDIR=/path/to/tmp TMPFILE=${TMPDIR}/$(basename ${TARGET_FILE}).$$.$(date +%s) chmod 644 ${TMPFILE} mv ${TMPFILE} ${TARGET_FILE} exit 0
主要な部分について説明します。
1. 作成するファイルとテンポラリのディレクトリの名前を次のように変数に定義します。
TARGET_FILE=/path/to/data/file.dat TMPDIR=/path/to/tmp
2. 一時的に利用するファイル名を生成します。
basenameコマンドで作成する目的のファイル名を取得して、テンポラリディレクトリのディレクトリ名に付けます。
TMPFILE=${TMPDIR}/$(basename ${TARGET_FILE})
このとき次のようなファイル名が生成できます。
/path/to/tmp/file.dat
ファイル名にユニーク性が欲しいときには、プロセスIDやUNIX時間などを付ければそこそこユニーク性を保てるでしょう。
TMPFILE=${TMPDIR}/$(basename ${TARGET_FILE}).$$.$(date +%s)
このとき次のようなファイル名が生成できます。
/path/to/tmp/file.dat.4726.1380907208
3. ファイルの作成を行います。
echo "OK" > ${TMPFILE}
4. パーミッションを調整が必要であればここで行います。
chmod 644 ${TMPFILE}
5. mvコマンドによりファイルを目的のディレクトリに移動します。
mv ${TMPFILE} ${TARGET_FILE}
以上の処理によりアトミックにファイルの作成を行うことができました。
最後に
Maildir形式のメールボックスについて調べると、安全にファイル操作を行うことに対してよく考えられていることがわかります。調べてみるとおもしろいでしょう。