

5月版 Firefoxのプチフリーズ問題から始まった大論争

小崎資広

2009/6/1

今回メインのネタとして取り上げたFirefoxの「プチフリーズ問題」ですが、その後調べたところ、WindowsやMacでも問題になっているようですね。「firefox sqlite」で検索するといっぱいヒットしました。

今回の件は、アプリケーションのミスでもカーネル側で無理やり何とかしてしまうLinuxの実利主義の真骨頂が表れたんじゃないかと僕は思っています。皆さんはどう思いますか？

それでは、どうぞ！

それはFirefoxのプチフリーズ問題から始まった

すでに各方面で話題になっていますが、2.6.30のマージウィンドウでext3のトピックが荒れに荒れ、とんでもない騒ぎが起こっていました。

問題の発端は、あるFirefoxのbugzillaエントリから始まりました（ 注1 ）。「Linux版Firefoxを使っているとプチフリーズが頻発して使い物にならない。原因はfsync()だ」というのです。調査の結果、fsync()はFirefox本体が呼んでいるのではなく、Cookieなどの格納に使っていたSQLiteから呼ばれていることが分かりました。

しかし、その書き込み量はたかだか2.5Mbytes程度であり、数分単位でハングするような量ではないようにみえます。なぜこのようなことが起こるのでしょうか？

秘密は、ext3のジャーナルログの仕様とordered modeの仕様が関係していました。ext3のジャーナリングは、

メモリ上にデータ構造を作る メタデータの複製をジャーナル領域に書き込む オリジナルのメタデータをファイルシステムに書き込む

という3段階を取ります。

2の処理順序は、1のときに構築された処理順のリスト構造で決まります。つまり、システムコール発行時にディスクへの書き込み順が決まるということです。

またordered modeは、

A. メタデータのみをジャーナリング

B. メタデータをジャーナル領域に書き込むとき、実データがディスクに書き込まれるのを待ってから書き込みを行う

という動作を行います。

すると、この2つの仕様により、fsync()によりジャーナル処理リストの末尾要素に対してウェイト処理を行うと、結果として未処理のすべての実データ・メタデータの処理完了を待つことになります。よって、たまたま同じタイミングで別のプロセスが別のファイルに巨大な書き込みを行っていると、その巻き添えを食らって、とんでもないレスポンス劣化が発生してしまうのです。

まず指摘されたのは、Firefoxの設計の悪さでした。たとえクラッシュに備えて各種データを書き込んでおく必要があるとしても、書き込みが終わるまでUI処理をすべて止めるのはナンセンスであり、UIスレッドからfsync()を呼ぶというやり方がそもそも間違っています。

また、DBの書き込みという観点でいうと、メタデータで書き込み必須なのはファイルサイズのみで、更新時刻などは重要ではありません。従ってSQLiteは、fsync()ではなくfdatasync()かsync_file_range()を使うべきです。DBライブラリがfsync()とfdatasync()で速度がまったく違うことを意識していないこともオカシイわけです。

もっというと、ページ遷移ごとに2.5Mbytesの書き込みを行うこと自体、SSDのような書き込み回数に制限のある媒体では自殺行為です。つまり、どう見ても設計が考慮不足だったわけです。

これらについては、Linuxコミュニティからの働き掛けにより、

SQLiteはfdatasync()を使うように書き直す

Firefox は3.1（ 注2 ）以降、オンメモリデータベースとオンディスクデータベースのハイブリッド仕様に移行し、書き込みは15〜30分に1回しかディスクに反映しないようにする

ということになりました。

■ fsync()時のデータ吐き出しに議論が飛び火

普段ならここで「アプリのバグでしたね」といって話が終わるのですが、今回はカーネル側でも「何とかできないのか」といい出す人が続出し、議論が継続します。

次に議論に上ったのは、ブロックレイヤのfsync()時の書き込み処理の効率の悪さでした。

一般にブロックレイヤは、readとwriteとで、動作をまったく変える必要があります。writeの場合は、書き込み処理が多少遅れてもその影響はユーザーには見えません。ですから、積極的にブロックレイヤでキューイングして、書き込みブロックの併合を試みるべきです。一言でいうと「async的」。しかしreadでは、即座にデバイスに読み込み要求を出すべきで、待っていても意味がありません。一言でいうと「sync的」となります。

しかし、fsync()時のデータ吐き出しは、処理自体はwriteであるにもかかわらず、すぐさま書かないといけないというsync的動作が要求されるので、少し特殊です。このため、従来のブロックレイヤではread()とfsync()のwriteに同じSYNCフラグを使っていたのですが、これがさまざまな非効率な動作を生んでいました。

1つ目は、キューのアンプラグ処理です。アンプラグとは、ブロックレイヤのリクエストキューのふたを開けて、デバイスにI/Oの発行を開始することです。ここで、fsyncの延長で書き込みを行うときに毎回アンプラグを行っており、1000ページの書き込みがあれば1000回アンプラグ（≒SCSIコマンド発行）がなされていました。せっかくアプリケーションが性能を考えて大きなバッファ単位で書き込みを行っていても、それが台無しになっていたのです。

2つ目は、CFQの次リード予測機能です。readやDirectIOのwriteの場合、あるI/Oリクエストの処理が終わったとき、すぐさま次のI/Oリクエストが隣接セクタに対して発行される可能性が高いため、即座に別のキューの処理に移るのではなく、少しの間待った方が性能が上がります。しかし、fsync()時のアンプラグでは、呼び出しプロセスはsync完了待ちで寝ています。次のリクエストは決して来ないことが分かっているのだから、待つのは無駄です。

3つ目は、リクエスト構造体のメモリ管理についてです。従来はreadとwriteの2つのメモリプールを使って管理していました。しかしこれだと、fsync()のwrite用requestが必要なとき、プールに空きがない場合、優先度の高いはずの同期リクエストが優先度の低い非同期リクエストを待つことになるので、意図どおりの順番で処理されません。別プロセスがreadを発行すると、read（同期リクエスト）はwrite（非同期リクエスト）よりも優先されるため、writeリクエストは長時間処理されない可能性があるからです。

これらの問題はブロックレイヤのメンテナ、Jens Axboeにより修正されました。

Index Linux Kernel Watch 5月版

Firefoxのプチフリーズ問題から始まった大論争 Page 1

それはFirefoxのプチフリーズ問題から始まった Page 2

お前のページを共有する、抵抗は無意味だ――KSM

-stableの進ちょく