CGIを扱う上で最も重要と言っていいのがファイルの保護です。
CGIの最大の特徴である、外部アクセスによるサーバ上のファイルの実行・読み書きという機能は、同時にファイルを破壊することもできるということですから、トラブルの元となるような動作を起こさないようにしなければなりません。
もちろんサーバのOS等のシステムそのものの機能によって、ある程度そういったトラブルは防げるようになっていますが、それだけでは防ぎきれないトラブルというものがありますので、それについては各自で防ぐしかありません。その方法の一つがロック処理です。
ここでは、mkdir関数を用いた排他処理によるロックのやり方について、なぜそれが必要なのか、どうしてそうしなければならないか、といった概念的なことから解説します。
A B ロック開始 ↓ ↓ ロック失敗→↓ ファイル読込 ↓ ↓ ↓ 各種処理 ↓ ↓ ロック解除待ち ファイル書込 ↓ ↓ ↓ ロック解除 ↓ ↓ ロック開始←← ↓ ↓ |
これは排他処理によるロックの簡単な概念図です。
1つのCGIに対してA・B2つのリクエストがほぼ同時にあって、Bの方がやや遅れて処理されていると思ってください。
排他処理とは、このように複数の処理(プロセス)が同時に行われている中で、1つの処理が作業している間、他の処理にそれを邪魔されないようにガードする。言い換えれば、他の処理が作業している間、その作業が終わるのを待つ仕組みのことです。
この場合は、ファイルの読み込みから書き込みまでの間をロックし、ファイルに発生する不具合を防いでいます。
ではなぜこのようなことをする必要があるのでしょうか?
A B ファイル読込 ↓ ↓ ファイル読込 各種処理 ↓ ↓ 各種処理 ファイル書込 ↓ ↓ ファイル書込 ↓ ↓ |
先ほどの処理をロックなしでやった場合の図です。
一見問題なさそうにも見えますが、例えばこれが掲示板の発言処理だったとします。
すると、A・B共に同じ状態のファイルを読み込んで、各々自分の発言内容を追加して保存していることになります。つまり、Aがファイルを読み込み、発言内容を追加して保存した後、BはAの発言内容が反映される前に読み込んだ内容に発言を追加して保存してしまっているのです。それでは結果的にAの発言が残らないことになってしまいます。
こうならないためには、Bが確実にAの発言処理が終わった後にファイルを読み込む必要があります。それを可能にしているのがロックというわけです。
CGIは、不特定多数の人が同じプログラムに同時にアクセスできるため、このような事態が発生することがあります。
プログラムの処理は基本的に一瞬ですが、チャットや人気サイトの掲示板のような場所では、その一瞬に複数の人が同時に発言を行うこともありえます。また、1つのCGIに限らずアクセスが集中して、サーバそのものが重くなると、その一瞬も幾分長くなります。低い確率に思えますが、最小でたった2人のアクセスで発生できてしまうのですから、見過ごせない状況になるというわけです。
A B ファイル読込 ↓ ↓ ファイル読込 各種処理 ↓ ↓ 各種処理 ロック開始 ↓ ↓ ロック失敗→↓ ファイル書込 ↓ ↓ ロック解除待ち ロック解除 ↓ ↓ ロック開始←← ↓ ↓ ↓ ファイル書込 ↓ ↓ |
時々、このようにファイルの書き込みの前後でだけロックを行っているプログラムを見かけます。
先ほど説明したとおり、他の処理の書き込みが終わってから読み込みを始めるようにしないといけないので、このようなロックの仕方は全く意味がありません。
ロックはファイルを読み込む前に行う、というのが鉄則ですので、忘れないようにしてください。
A B C D ロック開始 ↓ ↓ ↓ ↓ ロック失敗→↓ ↓ ↓ 処理 ↓ ↓ ↓ ↓ ↓ ロック失敗→↓ ↓ ロック解除 ↓ ↓ ロック失敗→↓ ↓ ロック開始←← ↓ ↓ ↓ ↓ ↓ ↓ ↓ 処理 ↓ ↓ ↓ ↓ ↓ ↓ ↓ ロック解除 ↓ ↓ ↓ ↓ ロック開始←← ↓ ↓ ↓ ↓ ↓ ↓ ↓ 処理 ↓ ↓ ↓ ↓ ↓ ↓ ↓ ロック解除 ↓ ↓ ↓ ↓ ロック開始←← ↓ ↓ ↓ ↓ ↓ ↓ ↓ 処理 ↓ ↓ ↓ ↓ ↓ ↓ ↓ ロック解除 ↓ ↓ ↓ ↓ |
排他処理によるロックは、ファイルを保護するために必要なわけですが、「ロック中他の処理の割り込みを一切認めない」という強固なものであるが故に問題もあります。
ロック中の処理が長ければ長いほど待ち時間も長くなってしまうので、図のようにたくさんのリクエストが同時に行われた場合、同時に待ちになる処理が多くなってしまうのです。
サーバが一つの処理を行っている間は、その処理に必要なメモリやCPUを食われてしまうので、こうして延々待たされ続ければ、サーバがパンクしてしまいかねません。
サーバ側にもそれを防ぐ機能が備わっていますが、ロック処理そのものにもそうならないような仕組みを組み込むべきでしょう。
また、ロック中の処理をできるだけ軽くするという努力も忘れてはいけません。ロック後に読み込むファイルの内容が無くとも出来るような処理は、ロックの前にやっておくとよいでしょう。
# ロック開始 # sub lock{ my $dir = $_[0]; # 古いロック(3分以前)は削除 if(-e $dir){ my $t = (stat($dir))[9]; if($t < time - 180){ &unlock($dir); } } # ロックできなかったら5回まで1秒間待ってみる local $retry = 5; while(!mkdir($dir,0755)){ if(--$retry <= 0){ return 0; } sleep(1); } return 1; } # ロック解除 # sub unlock{ rmdir("$_[0]"); } |
実用的なロック処理の例です。ロック処理はサブルーチンで関数化して使用するのが一般的です。
mkdir関数を利用したこのロック関数は、基本的には指定されたディレクトリを作ろうとしてみて、作ることが出来たらロック成功ということで1を返し、出来なかったら1秒間待ってもう一度試そうとします。
そのままでは、もし何らかの理由でロックがかかりっぱなしになっていた場合、永久に待ち続けることになってしまうため、ループの回数をカウントし、5回試してまだ駄目だったら0を返して終了しています。
また、stat関数はファイルのさまざまな情報を得られる関数なのですが、これによってディレクトリの更新日時を見て、3分以上前に作られたものだった場合にロックを解除することで、なんらかの理由でロックがかかりっぱなしになっても自動的に復旧するようにもしています。
ロック解除処理は、指定されたディレクトリをrmdir関数で削除するだけなので関数化する必要もないように思いますが、分かりやすい関数名で用意しておくという意味があります。
一般的なロックの方法は、このようにキーとなるファイルやディレクトリの存在の有無によって、ロックされているかどうかの判断を行います。ここではmkdir関数を使っていますが、他にもrename関数を使った方法などがあります。
実際にこの関数を使う場合は、次の様にします。
# ロック開始 &lock('lockdir') or &error('只今混雑しています。 |
lock関数は、キーになるディレクトリを引数として指定します。また、成功したら1、失敗したら0を返すので、5回リトライして失敗した場合はエラー処理を実行させます。error関数は各自用意しましょう。
unlock関数もキーになるディレクトリを引数として指定します。当然ですが、正しく対応したディレクトリを指定しましょう。
# ロックA開始 &lock('lockdir_a') or &error('只今混雑しています。 |
一つのシステムで複数のファイルの読み書きを行う場合で、それぞれのファイルに特に関連性が無い場合、両方で同じロック処理を行ってしまうと、待たせる必要の無い処理まで待たせてしまうことになります。
そういう場合は処理ごとにロックに使用するディレクトリ名を変えてやると、他の処理の邪魔をしません。ロック中は一切他の処理が割り込めませんので、このようにして少しでも無駄なロックをしないよう工夫しましょう。
A B ロックP開始 ↓ ↓ ロックQ開始 ロックQ開始 ↓ ↓ ロックP開始 ? ↓ ? |
2種類のロックP・Qを同時に扱うシステムがあり、一つのプログラムではP→Qの順にロックしており、他のプログラムではQ→Pの順でロックしていたとします。
これらのプログラムが図のようにほぼ同時に動いた場合どうなるでしょう?
答えは「互いにロック解除待ちになる」です。
このような互いにロックを掛け合ったような状況をデッドロックといって、データベースを利用したシステムなどで見られる現象です。