pseudo のすべて: 第 3 回 学んだ教訓

そのつもりでやったのです

この連載の第 3 回目となる今回の記事では、Peter Seebach が pseudo を開発する中でおかした多くの誤りの中から、いくつかを選んで振り返ります。その内容は、ためになるだけでなく、他の人の誤りを堂々と楽しめるようなものになっています。

Peter Seebach, member of technical staff, Wind River Systems

Author photoPeter Seebach はソース・コードをいじることが流行になる以前からソース・コードをいじってきました。彼は言語の標準化からマウスのドライバー作成に至るまで、あらゆるものに従事した経験があります。



2011年 6月 24日

私が猫を飼っていて学んだことに、「何かやってはいけないことをしてしまったときには、わかっていてやったかのようなふりをするのがクールに見せるためのポイントである」というものがあります。このことは、多くの教師が「さて、私がわざと間違えたことに誰か気付きましたか?」と言うことからも学びました。

pseudo プロジェクトでは、その出だしからの変わった誤りや、奇妙なバグ、そして他にも新たな教訓を学んだ経験などがプロジェクト全体をとおして数多くありました。それらのなかには、非常に変わった特殊なケースのものもあれば、とてもコードが正しく動作するとは思えないこともありました。しかしこの記事では、コンパイルすらできないコードを何百回も作成してしまったことを詳しく取り上げるような無慈悲なことはしません。何しろ、単純なタイプミスを芸術の域にまで高めてしまったのですから。

意図的にそうしたのだ、と言い張りましょう

pseudo の初期リリースでは、リネーム処理が実際には正常に動作しませんでした。しかし、それによる実害はまったくありませんでした。というのも、リネーム後にアクセスした場合、pseudo デーモンによってデータベースのエントリーが自動的に修正されたからです。

最初に学んだ教訓

「このインフラストラクチャーの部分は新しいものに変えた方がよいと思う」というような提言をする人は、たとえその人のスケジュールがすでに一杯であったとしても、それを実行することになります。このことは必ずしも悪いことではありません。しかし頭に入れておかなければならないのは、一連の作業を提案する場合、多くの人にとってはその作業を提案することと、それを自発的に行うこととは切っても切り離せない 1 つのことであるということです。変更を主張する人は、その変更を実装する準備をしてください。何かを実装したい場合には、その実装をサポートする準備をしてください。

これは私にとって素晴らしい経験でした。私はその当時、自分が何を約束しているのかわかっていませんでした。わかっていたら、そのような提言はしていなかったかもしれません。しかし今となっては、提言して良かったと思っています。

SQL エンジンの 1 つである SQLite には、LIKE による比較にインデックスを使用することができないという制約があります。ディレクトリーをリネームする場合、当然、そのディレクトリー内のファイルを指定する際のディレクトリー名も変更しなければなりません。つまりディレクトリー /foo/bar にリネームする場合には、/foo/ で始まるすべてのパスのストリング /foo/bar で置き換える必要があります。

しかしそれを SQLite の節 (path LIKE ? || '/') を使って行う場合には、インデックスを使用することができず、恐ろしいほど処理に時間がかかります。私は、あちこちブラウズしまくった挙句、愉快なほどに変わっている対策、(path > (? || '/') AND path < (? || '0')) を見つけました。

ASCII 文字で構成されるシステムであれば、この節はまさに、後に任意の文字が続く path/ を示すことになります。それでも、この節で使われているのは単なるリレーショナル演算子なので、インデックスが使われていました。このようにすることで、小さなファイルシステムの場合ですら約 2 万倍も高速化されるのです。

しかし、この方法に変更している際、私は非常に小さなミスをしてしまいました。そのミスとは、実質的にパラメーターの関連付けの順序を間違ってしまったことにより、/foo/bar にリネームすると、/foo/ で始まるすべてのパスの /bar が /foo に変更されてしまうというものでした。結果的には何も変更されませんでしたが、少なくとも、何も変更されない処理が迅速に行われたのです。

pseudo では執拗にサニティー・チェックを行っているため、決して不正な結果が発生することはなく、ただ単に大量の警告がログ・ファイルに記録されるだけでした。


シリアライズ (逐次化) が十分ではない

当初、pseudo にはシリアライズ (逐次化) の問題は起こらないだろうと想定していました。すべての処理はサーバーでシリアライズされるので、指定された 1 つのクライアントからの連続する 2 つの処理の順序が入れ替わることはあり得ないからです。これは「640K あれば誰にとっても十分」という想定ほどに見誤った想定ではありませんが、この想定が深刻な誤りであったことは確かです。

当初の設計では、処理が試みられ、その結果が成功した場合にサーバーにレポートされていました。プログラムが 1 つの場合には、これで問題ありませんでしたが、複数のプログラムがある場合には、レース・コンディションが発生する可能性がありました。

例えば次のような場合を考えてみます。プロセス A が inode 番号 12345 で一時ファイルを作成します。次にプロセス A はこの一時ファイルを削除します。この一時ファイルが削除された後、プロセス B が新しいファイルを作成し、このファイルが inode 番号 12345 を再利用します。しかし pseudo デーモンが、プロセス A からのリンク解除 (UNLINK) メッセージを認識する前にプロセス B からのファイル作成メッセージを認識したような場合には何が起こるのでしょう?

pseudo デーモンはプロセス B からのファイル作成メッセージを受信すると、同じ inode 番号を持つ古いエントリー (A の一時ファイル) がデータベースの中にあることを認識します。pseudo デーモンはその矛盾をログに記録し、そのエントリーを削除します。そして新しいデータベース・エントリーを作成します。しかし状況はさらに悪くなります。ファイル削除メッセージを受信すると、pseudo デーモンは同じ inode 番号を持つ古いエントリー (B のファイル) がデータベースの中にあることを認識します。pseudo デーモンは本来の対象ではないそのエントリーを削除し、続いて A の一時ファイルもデータベースから削除しようとします。結局、B のファイルはもはやデータベースには記録されていないという結果になります。

この問題を修正するために私が行った最初の試みは、惨めな失敗に終わりました。私は UNLINK の処理を変更し、対象ファイルの以前のデータベース・エントリーを返した上で、クライアントに UNLINK メッセージを送信させ、大本のシステム・コールが失敗した場合には再度ファイルとリンクされるようにしたのです。この修正によって確かにレース・コンディションは発生しなくなりましたが、さらに悪い障害が発生しました。つまり中にファイルを含むディレクトリーに対して rmdir(2) を実行すると、そのディレクトリー内のファイルすべてに対するデータベース・エントリーが削除されてしまったのです (ディレクトリーを削除すると、そのディレクトリーの中身がすべて削除されるからです)。

ファイルに対して「削除中」フラグを追加し、また MAY_UNLINKDID_UNLINKCANCEL_UNLINK というメッセージを追加することで、ようやくこの問題を修正することができました。これらのメッセージにより、データベースには、あるファイルがこれから削除されるところである、ということが記録されます。そのため、そのファイルに対する作成メッセージによってエラーが発生することはありません。すると、DID_UNLINK メッセージによってファイルが削除されるのは、そのファイルに削除中フラグが立っている場合のみ、ということになります。このようにして、ようやくこの問題がなくなりました。


3 つのバグによって 1 つの問題が発生

私達は、ディレクトリーをリネームすると、そのディレクトリー内のすべてのファイルがなくなってしまうという不可解な問題を経験しました。この問題は 3 つのまったく異なるバグによって生じたものであり、それらのバグのどれを修正しても、問題の動作はなくなりました。

pseudo はディレクトリーをリネームする際、そのディレクトリーがすでに pseudo のデータベースに認識されているかどうかをチェックします。認識されていない場合には、リネーム前の名前のディレクトリーを作成することで、リネーム処理が正常に行われるように (ひいてはそのディレクトリーに含まれていて、すでに pseudo に認識されているすべてのファイルについてもディレクトリー名が変更されるように) します。こうしたことが起こり得るのは、例えば pseudo 環境外でディレクトリーを作成した後、pseudo 環境内で実行している間にそのディレクトリー内にファイルを作成した場合などです。

問題は pseudo の 3 つの動作の組み合わせによって発生しました。1 つ目は、pseudo はあるファイルにリンクする際に、そのファイルと同じ名前の既存ファイルに対するリンクを解除するというものでした。2 つ目は、pseudo はあるディレクトリーに対するリンクを解除する際に、そのディレクトリーに含まれるファイルやディレクトリーへのリンクを解除するというものでした。これら 2 つと、リネーム処理による暗黙的なリンクが組み合わされると、まだデータベースに記録されていなかったディレクトリーをリネームする場合に、pseudo はそのディレクトリー内にある、すでにデータベースに記録されていたファイルのエントリーをすべて失ってしまうのです。

この問題のみであれば、私達のビルド・システムに影響を与えなかったはずです。影響を与える原因となったのは、私のまったく説明不能な判断による試みでした。その試みとは、複数のファイルシステムにわたって 1 つのファイルを rename(3) でリネームする場合の処理を改善しようとするものでした。実際には、その処理が改善されるはずがありません。しかしなぜか、私はそのサポートを実装しようとしたばかりか、非常に不適切な実装をしてしまいました。つまりリネーム用のラッパーは、必ずデータベース内の古い名前にリンクしてからリネームするようにしたのです。その結果、中にファイルを含むディレクトリーを移動すると、それらのファイルは必ずデータベースから削除されるようになってしまいました。

私達はこれらの大きなエラーを修正しました。現在は、LINK の処理によって暗黙的にリンクを解除する場合には、指定された名前のファイルのみを削除し、そのファイルに含まれているように見えるファイルはどれも削除しないようになっています。また、リネーム処理の際にリンクを作成しようとする無駄な試みはなくなりました。その結果、ディレクトリーをリネームしても、何かに影響が出ることはなくなりました。


5 つの条件が揃って発生する問題

皆さんはエッジ・ケース (境界ぎりぎりの特別なケース) やコーナー・ケース (ごく稀にしか起こらないケース) について聞いたことがあると思います。ここで紹介する問題は、私がこれまで長年ソフトウェアを扱ってきた中で初めて見る、5 つの条件が揃って発生する問題です。

先ほど「削除中」フラグの追加について説明しましたが、このフラグを追加したということは、IPC 用として pseudo が使用するデータ構造が変化したということです。私はデータ構造のバージョン管理をしなかったので、使用する IPC メッセージのバージョンがクライアントとサーバーで異なることは理論的にあり得ます。しかし私達のビルド・システムではさまざまなコンポーネントが同時にリビルドされるため、バージョンの違いは起こり得ません。

ところが、ある 1 つのプログラムがビルドの特定のポイントでときどき失敗する、という非常に奇妙な問題が発生しました。「失敗」というのは、「デーモンからの応答を待った状態で永遠にハングアップする」という意味です。一方、デーモンはソケットからの入力を待っていました。

問題発生の前提となる条件

まずは pseudo のプロトコルについて、もう少し詳しく説明しましょう。クライアントが起動すると、クライアントはまず、PSEUDO_MSG_PING メッセージをサーバーに送信します。このメッセージに含まれている情報には、クライアントの PID、クライアントのバイナリーの名前、そのクライアントからのイベントをログに記録するためのオプションの「タグ」メッセージがあります。タグ・メッセージがない場合には、単純にタグ・メッセージは省略されます (名前とタグは「パス」として送信され、名前とタグの長さは pathlen フィールドに記述されます)。

ハングアップは ping を実行している時に発生しましたが、1 人の開発者のマシンのみで、しかも一時的にしか発生しませんでした。しかし最終的に、私達は問題を突き止めることができました。

私達が行った変更によって、pseudo メッセージの構造は長さが 4 バイト増えていました。サーバーはインテリジェントであり、ベースとなる構造のサイズを読み取ります。しかし最初の部分に関しては、サーバーは単純に、必ず完全な読み取りを行っているのだと想定しています (私はまだこのバグを修正していません)。

何らかの方法で、4 バイト長い新しい構造の pseudo デーモンを古い pseudo クライアントで実行できたとしても、サーバーは想定どおりの長さのデータを受信するわけではありません。クライアントはパス名とタグも送信しています。このため、この失敗が発生するのは、実行中の実行可能ファイルの名前が 4 文字未満で (名前は sed でした)、しかもタグが設定されていない場合のみでした。そうした場合にしか発生しないとしても、古い pseudo クライアントと新しい pseudo デーモンとを共存させるにはどうすればよいのでしょう。

謎は解けました

私達のビルド・システムでは、ホスト・ツールを事前にビルドしておくことができます。それらのツールは (lndir を使用して) シンボリック・リンクのツリーとしてビルド・ディレクトリーにコピーされます。さらに、いずれかのツールをリビルドして新しいバージョンにする必要がある場合には、それらのツールはリビルドされます。当該開発者は pseudo デーモンとクライアント・ライブラリーを含む古いホスト・ツールを使用しており、それらのツールがプロジェクトのディレクトリーにコピーされ、新しいデーモンと新しいクライアント・ライブラリーを含む新しいツールがプロジェクトのディレクトリーにビルドされていました。

私達はプロジェクトのディレクトリーを指すように LD_LIBRARY_PATH を設定していたので、常に新しいライブラリーを選択しており、すべては順調でした。しかし小さな問題が 1 つありました。実行可能ファイルにはリンカーの検索パスを設定することができ、その方法が 2 つあります。最新でお馴染みの RUNPATH の設定は、皆さんが想定するとおりの使われ方です。しかし、古くてあまり馴染みのない RPATH の設定には変わった特徴があり、LD_LIBRARY_PATH よりも前に処理されます。問題のバイナリーは RPATH を $ORIGIN/../lib:$ORIGIN/../lib64 に設定してビルドされていました。$ORIGIN という魔法のクッキーにより、バイナリーを含むディレクトリーにまでパスが拡張されます。

ツールがシンボリック・リンクによってコピーされると私が言ったことを思い出してください。$ORIGIN クッキーを処理すると、シンボリック・リンクをたどることになります。つまりこの特定の実行可能ファイルを実行すると、ダイナミック・リンカーは結局、LD_LIBRARY_PATH ではなく、ビルド済みのライブラリーのディレクトリーを参照し、古い pseudo クライアント・ライブラリーを取得してしまいます。実行可能ファイルの名前は 4 文字未満なので、異常終了する羽目や診断モードにはならず、ハングアップします。

このバグを再現するためには、以下の条件が必要です。

  • pseudo が 1 週間以上前にビルドされている
  • ソース・ツリーによって新しいバージョンがリビルドされる
  • リビルド不要な実行可能ファイルがビルド済みツリーの中にある
  • その実行可能ファイルの名前は 3 文字を超えない
  • その実行可能ファイルは、$ORIGIN を使用するライブラリー検索パスを RPATH を使用して指定している

この問題の追跡には少し時間がかかりました。長期的な修正としては、メッセージに対するバージョン管理の追加 (理想的には、現在のメッセージには決して含まれない何らかの指標を用います) を始めとし、その他いくつもの改善事項があります。また、RPATH を使用してリンク・パスを示すのをやめることや、バイナリーに対するシンボリック・リンクを作成するのではなく、バイナリーをコピーするようにする、などの改善も必要です。


API の違い

API の違いの補足

この記事の下書きを終えた後で、pseudo は Mac OS X で動作するように部分的にポーティングされました。Mac OS X の核となるシステム・ユーティリティーは、実際に動作する getxattr()/setxattr() を要求します。従って OS X では、pseudo は単に呼び出しを渡しているだけです。幸いなことに、対象のユーティリティーは setxattr() を使ってファイル・モードを実装しているわけではありません。この問題に取り組む際には、私は背水の陣で臨むつもりです。

最近の一部の Linux マシンでは、昔ながらの単純な /bin/cp を使用してコピーされたファイルには不適切なパーミッション・ビットが設定されてしまいました。調べてみると、getxattr()/setxattr() ファミリーの関数を使用すると、拡張属性だけではなく、POSIX モードの照会や設定もできることがわかりました。ある特定のシステムでは、単純な chmod() を使用する代わりに、getxattr()/setxattr() ファミリーの関数を使用して POSIX モードを照会、設定しています。都合の良いことに、仕様によれば、*xattr() 関数が失敗する場合には代わりにchmod() を使えなければなりません。そのため現状では、pseudo は *xattr() 関数をインターセプトして失敗すると errnoENOTSUP に設定しますが、今後、この動作は修正する必要があるかもしれません。

同様に、大規模なリファクタリングの際に、pseudo のラッパーの多くは、他の関数を呼び出すだけの単純な関数として再実装されました。例えば、open()O_CREAT を使用することで creat() を実装しています。特に、*at() のバリエーションを持つ多くの関数は、dirfd パラメーターとして AT_FDCWD を指定して、対応する *at() 関数を呼び出すことで実装されています。この方法は非常に有効でしたが、openat() を使えないマシンでは、この方法は使えませんでした。

今後しばらく経つと、API サポートの範囲が異なるシステムに対し、もっと完全な処理を開発する必要が出てくると思います。


学んだ教訓と今後の方針

pseudo を開発する初期の段階、そして pseudo を継続的に保守していく中で、私達が遭遇した問題の多くは追跡や診断が比較的容易でした。堅牢さと適切なロギングを重視するという判断を初期の段階で下しましたが、その成果が確実に現れています。その一方で、テスト・スイートをすぐに作成するという計画と現実との間には大きなギャップがあります。もっと早い段階で、テストのサポートを用意して活用していれば、もっと大きく時間を節約できたはずです。

皆さんが行っていることと内容が合う既存のコードやプロジェクトがある場合、それらのコードやプロジェクトを使用するのは妥当なことですが、皆さんが解決しようとしている問題実際に新しい問題である、と結論付けることにためらいを感じてはなりません。そうした問題は起こり得るものです。頻繁には起こらないかもしれません (私にも初めての経験だと思います) が実際に起こるものであり、そうした問題が起きた時には、それに対する準備を整える必要があります。

今後の作業として、堅牢さと診断機能には改善の余地がありますが、次に検討が必要な重要分野はパフォーマンスかもしれません。というのも、ここまで紹介してきた pseudo は必要なことを非常に適切に実行しますが、fakeroot よりもかなり動作が遅いことは否定しようがなく、しかも、かなり大幅にパフォーマンスを改善できそうに思えます。ディスク上の安定したデータベースにそのフォーマットでデータを保存する場合、メモリーのみに保存する場合のように高速になることはあり得ませんが、高速化の余地は多分に残っています。

参考文献

学ぶために

製品や技術を入手するために

  • 皆さんの次期オープンソース開発プロジェクトを IBM ソフトウェアの試用版を使って革新してください。ダウンロードあるいは DVD で入手することができます。

議論するために

  • developerWorks コミュニティーで開発者向けのブログ、フォーラム、グループ、ウィキなどを利用しながら、他の developerWorks ユーザーとやり取りしてください。

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Open source
ArticleID=681541
ArticleTitle=pseudo のすべて: 第 3 回 学んだ教訓
publish-date=06242011