ファイルのロックについて
WWWでは、当然のことですが同じページに複数の人が同時にアクセスしています。サーバーも当然のように複数のアクセスを同時に処理します。
このような環境で、ファイルに書き込みを行うとファイル(のデータ)は簡単に壊れてしまいます。
何故、壊れるのでしょうか?
例えば、カウンターの場合、必ず前回までのカウンターの値を持ったファイルがあり、アクセスがある毎に
CGIプログラムがこのファイルから前回の値を読み出して+1して保存するという動作をしています。
1.現在のカウンタ値をファイルから読み出す。
2.カウンタ値を+1する
3.+1したカウンタ値をファイルに書き出す。
いま、カウンターファイルに123という値が書かれているとします。
Aさんと Bさんが全く同時にアクセスしてきた場合、二人のアクセスは同時に処理されるため、
Aさんを処理している CGIも、Bさんを処理している CGIも、現在のカウンタ値である 123を読み出し +1した 124をファイルに書き出します。
本来なら 125にならなければいけないカウンタ値も実際には +1しかされないのです。
でも、これだと別に致命的な破損ではないですよね?
実は、致命的な破損は、上に書いた「1」と「3」が同時に実行されたときに起きるのです。
Aさんがアクセスした少し後に(といってもコンピュータの処理時間ですからほぼ同時なのですが) Bさんがアクセスしたとします。
Bさんを処理する CGIは最初のステップであるカウンタファイルを読み出します。
しかし、ちょうどその時、少し先を処理をしている Aさんの CGIは、まさに新しいカウンタ値をファイルに書きこんでいる最中なのです。
この時に何が起きるのでしょうか?
Aさんの CGIはカウンタファイルに 124 と書き込みますが、124の 12まで書き込んだちょうどその瞬間に
Bさんの CGIがカウンタを読み出すと「現在のカウンタ値は 12」であると認識してしまいます。
その結果、Bさんの CGIがステップ3でカウンタの値を書き出す時には 12を +1した「13」をファイルに書き込んでしまうのです。
また、Aさんの CGIが 124と書く直前(書き込みモードでファイルを開いた直後)には、それまで書いてあった 123という値は削除されているので、ちょうどその時に Bさんの CGIが値を読み出すと、カウンタ値を 0と認識したり、CGIの作りによっては(何も読み出せないので)エラーとなったりするかも知れません。
一般的に、ファイルロックの方法としては、flockを使う方法と、symlink や mkdir を使う方法があります。
これらのどれを使うべきかはサーバーの構成により変わってきますので一概には言えませんが、
結論だけ言うと弊社の環境では flock を使う方法が最良です。
もし、これらの特徴やメリット・デメリットなどを知りたい場合にはインターネット上でたくさんの議論されていますので
google などの検索エンジンで、「CGI」「ロック」「flock」「symlink」などをキーワードにして検索して見てください。
flock を使えば、オープンしたファイルをクローズするまで、他のプロセス(例えばCGI)が、そのファイルをオープンできなくなり、
同時アクセスによるファイルの破損を防止することができます。
(ファイルのロックが必要なのはファイルに書き込みを行う必要がある場合のみです。
読み出すだけのファイルであれば、ロックを行わなくともファイルが破損することはありません。
ロックしない方が快適なレスポンスが得られます)
さて、flockでファイルロックを行う場合、
1.ファイルをオープンする。
2.ファイルをロックする。
3.ファイルに書き込みを行う。
4.ファイルをクローズする。
で問題なさそうですが、実際にはもう少し複雑な処理が必要となります。
例えば、上の例では、実際にファイルがロックされるのは2が実行された後ですから、
ある CGIが1を実行した後、2が実行されるより前に、別の CGIが1を実行すると2つの CGIがファイルをオープンできてしまいます。
実は、隙のないファイルロックを行うことはとても大変なのです。この他にもプロセスが強制終了した場合のことなども考えなければなりません。
壊れないカウンターを作るには、
・ファイル操作を行う部分全体をロックする必要がある。
・再起動などによって CGIが途中で終了するとファイルが壊れる場合がある。
ということを考慮する必要があります。
例えばこれらを考慮したカウンターは以下のようになります。
open( LOCK, ">$lock_file" ); # ロック用のファイルを作る(※1)
flock( LOCK, 2 ); # ファイルをロックする
(これでこれ以降の処理は同時に1つしか実行されなくなる)
open( COUNT, "$counter_file" ); # カウンターの値が入ったファイルを開く
$count = <COUNT>; # カウンター値を読み出す
close( COUNT ); # カウンターファイルを閉じる
$count++; # カウンターを+1する
open( CTEMP, ">$counter_temp" ); # 更新したカウンタの値を別のファイルに一時的に書き出す(※2)
print CTEMP $count;
close( CTEMP ); # 一時的に書き出したファイルを閉じる(書き込み完了)
rename( "$counter_temp", "$counter_file" ); # 一時的に書き出したファイルを本来のファイル名にリネーム
unlink( "$lock_file" );
flock( LOCK, 8 );
close( LOCK );
※1 先にも書いたように、操作するファイル自身をロックしたのでは「隙間」ができてしまうので、ロック用の別ファイルを作ってそのファイルをロックします。
※2 ファイル書き込み途中でCGIが異常終了するとカウンターが壊れてしまうので、まずは別のファイルに書き出し、書き出しが完了してから本来のファイル名にリネームします。
ここまで書いてきたように、きちんと処理されたCGIプログラムでは通常カウンタや掲示板のログが壊れることはありません。
ですが、通常見かける CGIでは、ファイルロックはしているものの、ロックが不完全(隙間がある)であったり、symlinkを使ってロックをしているものを良く見かけます。これらの CGIはアクセスが少ないうちは問題がないように見えてもアクセス数が増えるほど危険度が増します。また、弊社の場合、レスポンスを良くするために、たくさんの httpdを動かしているので、ユーザが待たずにアクセスできる反面、HIT数の多いページでは同時アクセスが発生する可能性も高くなっていると思われます。
さて、最後に symlinkに関する危険性について触れておきます。
symlinkは flockに並んでファイルをロックするためによく使われる手法ですが、きちんと排他的にロックができないことがあるのでお勧めしません。
symlinkは単なるライブラリ関数なので、その実体は複数のシステムコールの組み合わせです。実行中に別のプログラムへ切り替わる(隙間ができる)ことがあります。それほどシビアな衝突は希にしか起きないのですがアクセスが集中した場合には当然危険も高くなります。
これに対して flockは元々ファイルをロックするための特別な関数で、カーネルによって実行中に別のプログラムへ切り替わらないことが保証されているのです。