最近のプログラミングブームの牽引役といえば、それは間違いなく CGI だろう。 以前の C 言語ブームのころに比べると、Perl スクリプトはとっつきもよいし、 なにしろ Web 掲示板やチャットやカウンタは普通の人々に対して大きな需要がある。 カスタマイズ心もくすぐられるようで、多くの CGI 紹介サイトが(他のよくできた CGI を紹介するだけに留まらず)独自設計のプログラムを掲載している。
だがそうした独自性を強調する割に、プログラムの中身の方はオソマツなものが多い。 たとえば Web 掲示板の場合、ゼロから書き上げられたプログラムは大変少ない。 ほとんどが、有名な掲示板プログラムを改造していたり、コアの部分をそっくりコピーしていたりして作られている。 そうした「コードを盗む」ということそのものは、プログラミングの上達の近道でもあるわけで、それ自体は別に悪くないと思う。 問題なのは、そうしたコードのコピーの際、オリジナルの欠点や間違いもそのままコピーしてしまうことだ。
CGI は常に競合という問題を抱えている。 競合なんて聞いたことないというプログラマは多いかもしれないが、 それは臨界を知らない作業員みたいなもんで、いざという時大変なことになる可能性がある。 幸い、ディスプレイが青く光っても健康には害を及ぼさないが…。
たとえば掲示板 CGI を考えてみよう。 掲示板 CGI は、過去の書き込みをファイルという形で保存している(はずだ)。 利用者が掲示板を読む時は、このファイルを CGI が HTML 形式に整形して出力している。 逆に書き込む時は、利用者の送ってくる文章をファイルへ追加し、新たにファイルを読み直して追加された記事を表示する。 この流れを簡単にまとめると、次のようになる。
- 書き込みを受け取る
- 掲示板ファイルを読み込む
- 書き込みを追加する
- 掲示板ファイルへ書き込む
- 掲示板を再表示する
多くの掲示板が(オリジナルがそうだったように)過去100個までの記事を保存するように作られている。 101個目の記事が到着すると、101個前の記事が捨てられ、新しい記事が追加される。 このため、処理はもう少し複雑になるのだが、まあそれは関係ない。
ここで、べるのちゃん(22)が掲示板に発言を書き込もうとしているとしよう。 べるのちゃんが [書き込み] ボタンを押すと、プログラムが起動して順番に処理を始める。 しかし、ステップ2が終った直後、からかさくん(19、フリーター)が同じように書き込みをしたとする。 そしてプロセス切替のタイミングによって、べるのちゃんの処理は一旦停止し、からかさくんの処理が始まる。
からかさ君のプロセスもまた、ステップ1、ステップ2 と処理を行う。 ステップ3 でからかさ君の記事が追加され、ステップ4 で書き戻される。 ここで、さきほどサスペンドされたべるのちゃんのプロセスが復活、先ほどの続きであるステップ3 から始まる。 しかしここで追加される掲示板の内容には、からかさ君の記事は含まれていない。 べるのちゃんのプロセスが読み込んだのは、からかさ君が追加する前のことだからだ。 こうしてべるのちゃんの処理が進むと、からかさ君の発言のない掲示板が書き戻され、プロセスは終了してしまう。 からかさ君の発言は闇へと葬り去られるのである。
また、最悪のタイミングとして、べるのちゃんがファイルへ書き込もうとして新規モードでファイルを開いた瞬間、からかさ君がファイルを読み込む場合もありうる。 からかさ君は 新規モードで開いただけの空のファイル を読んでしまい、 自分の記事だけになったファイルを書き戻す。 これにより、大盛り上がりだった掲示板の中身がばっさり消えてなくなるという事故になるのだ。
これと同じことはヒットカウンタでも起こる。 カウンタは、数字の書き込まれた小さなファイルを扱うだけの簡単なプログラムだ。
- アクセスを検出する
- カウントファイルを読み込む
- +1する
- カウントファイルへ書き込む
- カウント数を表示する
見比べてみればわかる通り、本質的には同じことをしているプログラムだ。 カウンタは、ページヒットごとにひっきりなしに実行されるから、それだけ事故の起こる可能性も高くなる。 人気のあるページの場合、1秒間に100ヒットすることだってあり得るのだから、事故が起こらないワケがないのだ。
このような問題で頭を悩ます CGI プログラマ(または利用者)は少なくない。 ここでちょっと、Yahoo! で紹介されているような CGI サイトがどのような解決方法を提示しているか調べてみよう。 まず ホーム > コンピュータとインターネット > インターネット > WWW > CGI > スクリプト を見てみよう。 クールサイトアイコンのついた2つのサイトが見つかるはずだ (Aug-14-2004)。
各サイトを見てみると、まさにこの問題を取り扱った FAQ がある。 ログが突然消えてしまいました とか、 ログやカウンタが消えるのですが などがそうだ。 前者のページでは、自作 CGI での対応を簡単に紹介しているだけで、解決策は示されていない。 そこで後者の方を見てみよう。
こちらはコード断片まで用意してあって、かなり本格的にみえる(最近の該当ページでは、解説を放棄してしまったようである。 以下は1999年当時のページに基づく)。 同じアルゴリズムを使って、カウンタを増やすサブルーチンを作ってみよう。sub CounterIncrement { local($retry) = 3; local($count, *COUNTER); # まずロックする while (!symlink(".", $lockfile)) { if ($retry-- < 0) { unlink($lockfile); print "BUSY"; exit; } sleep 2; } # カウンタを読んで、増やして、書き戻す open(COUNTER, $countfile); $count = <COUNTER>; $count++; open(COUNTER, ">$countfile"); print COUNTER $count; close(COUNTER); # ロック解除 unlink($lockfile); }このコードは、シンボリックリンクを排他処理フラグの代わりに使うという大変ポピュラーな方法を示している。 symlink 関数は、シンボリックリンクを作成しようとするが、成功すれば 1、失敗すれば 0 を返す。 あるプロセスがリンクの作成に成功すると、それ以降ファイルを消してロックを解除するまでは、処理を行えるのは最初のプロセスだけということになる。 後から起動されたプロセスは、ファイルが削除されるまで、2秒ずつ3回だけ終了を待つ。 最大6秒間待ったところで解除されないと、サーバがビジーであると判断して終了する。
実はこのコードは完全に間違っている。 symlink 関数は、単なるシステムコールであって、それ自体はアトミックな処理ではない (ついでにいわせてもらえれば、Perl の symlink はシステムコールでさえない。 システムコールへ翻訳されるスクリプトにすぎない)。 つまり symlink 関数の実行中に、プロセス切替が起こり得るのである。 たとえばべるのちゃんのプロセスが symlink を実行し、リンクを作成しようとしていたとする。 ファイルが存在しないことを確認した時点で、からかさ君のプロセスに切り替わり、やはり symlink を実行し始める。 からかさ君はべるのちゃんに気づかずリンクを作成、再開したべるのちゃんも(チェックはOKだったので)リンクを作った気になって終了…。 となるのだ。
このような競合状態のチェックは、普通の方法では決して解決することができない。 普通のプログラムは常にプロセス切替の対象であるから、チェックそのものの有効性が保証できないのだ。 保証できないチェックを100回繰り返したところで、その有効性はちっとも増えないのである。
では本当のところ、どうやれば本当にロックできるのだろうか。 Unix なら簡単にできる。 Perl で例を示そう。
sub CounterIncrement { local($count, *LOCK, *COUNTER); # ロック open(LOCK, ">$lockfile"); flock(LOCK, 2); # カウンタを増やす open(COUNTER, $countfile); $count = <COUNTER>; $count++; open(COUNTER, ">$countfile"); print COUNTER $count; close(COUNTER); # ロック解除 flock(LOCK, 8); close(LOCK); unlink(); }動作を簡単に説明しておこう。
- flock を実行すると、もしそのプロセスが最初にロックするものであれば、そのまま flock は終了する。 すでに LOCK がロックされているときは、そのプロセスはブロックする。 つまり前にロックしたプロセスが解除するまで、停止状態になる。
- ロックの解除と同時に、ブロックしていた他のプロセスは「何事もなかったかのように」再開される。 sleep を利用したビジーウエイトとは違って、すべて自動的に行われる。
- flock は特別なコードで、実行中にプロセス切替が起こる心配がない。 これはカーネルが提供する特別な機能であって、symlink などとは本質的に異なっている。 衝突を心配する必要はまったくない。
- flock はプロセスが異常終了してもきちんと解除される。 だからロックファイルが残ったままになっても心配はまったくない。
- どうしてカウントファイルそのものをロックしないかというと、 カウントファイルは途中で再オープンしなければならず、そうするとロックを維持できないからだ。 ロックは「読んで、書き戻す」間ずっと有効でないと意味がない。 だからロックファイルは別に作る必要がある。
symlink にまつわる伝説は、誰がいつ作り出したのだろうか。 そして、なぜそれに気づかず、使い続ける人が絶えないのだろうか。
実際のところ排他処理は、大変面倒である。 僕自身、自分で作った掲示板では排他処理をしていない。 というのも書き込み動作は、問題が起こるほど頻繁に発生しないと(勝手に)思っていたからだ。 事実、自分でもビビるほどの大量書き込みに使われたことがあるが、一度も事故はなかった(と思う。気づいてないだけかも)。 これがもしカウンタだったら、僕でもきちんとロックするように書く。 さもないと間違いなく誤動作するからだ。 そういうわけでロックは、どうしても必要な技術なのである。 それなのに、なぜ flock は普及せず、symlink ばかりが使われるのだろうか。
たとえば、flock は遅いという迷信もある。 白状すると、ファイルロックをよく知らなかったころ、僕自身 flock は大変遅いと思い込んでいたことがあった。 ところが実際に試してみると、flock そのものは大変高速に動作する。 symlink なんかと比べても、同じぐらいか、それ以上に速いんじゃないかと思われる。 しかし flock で正しくロックするようプログラムを作成すると、危険領域での処理の並列化ができなくなり、全体としてパフォーマンスは犠牲になる。 これは安全性を優先すれば仕方のないことなのだが、それをちゃんと理解しないでいると「flock はだめだ」という間違った結論に達してしまうのだ。 本来なら、ロック時間を短くするよう工夫するのが本筋なのだが…。
しかしそれにしても symlink を使ったコードは質が悪過ぎる。 仮に symlink が排他処理だったとしても、ビジーウエイトで2分も3分も平気で止まってしまうなんて、 「flock が遅い」なんていう話とは10000桁も違う。
というわけで、きたるべき 2000年においては、Y2K なんかよりファイルロック問題の方が深刻なんじゃないかなー
なんて思ってみたりなんかしちゃったりして、ちょんちょん。
Dec-30-1999
すでに5年が過ぎてしまったが、未だロック問題は熱い話題のようである。 このページもリンクを頂いたり、意見を頂戴したりするのだが、 それらも合わせてアップデートしておきたい。
よく頂くのは flock が使えない処理系は多いが、symlink ならたいてい動く。 だから symlink の方が優れているとか、symlink を使わざるを得ない、という意見だ。 flock が使えないなら、flock に代わるマトモなロック機構を使うべきであり、 symlink はそうではないというのが僕の意見だ。 たとえば Windows には Mutex を使ったロック機構がある。 ちょっとググッてみると、Win32::Mutex という Perl モジュールがあるようだ。 Mutex はセマフォ並みに汎用性が高いのでかなり使えると思うが、 ちょっと探した限りではロックのサンプルコードは見つからなかった (一方で「難しくて解らん」という意見はあった)。 あいにく僕は IIS 環境は持っていないので実証できないが、 いずれにせよ flock が使えないからといって symlink の正当性が 1% でも上昇するわけではない。
同様の意見として、NFS 環境では flock が動かないというのもよく頂いた。 複数のサーバ間で競合している状況では、確かに flock は無力である。 しかし再度指摘しておくが、symlink も同様に無力なのだ。 保証できないチェックを100回繰り返しても、その有効性はちっとも増えないのである。 この問題についてスマートな解決策を示せないが、 ロックを受領するサーバを作るか探すかして、 通信しながら競合を避けるなどの工夫でいけると思う。 そんなことは面倒だって? じゃあ複数のサーバに処理を分散することをやめたほうがいい。
蛇足だが、たまたまロックしたいファイルが NFS 上にある、 なんていうケースについては、ロックファイルを /var/tmp に作ったら? と申し上げたい。 今時ディスクレスの Web サーバなんて存在しないと思うのだけれど。
Aug-14-2004