attach プラグインの md5 計算、mimetypeのチェックによる負荷の問題†
- ページ: BugTrack2
- 投稿者: ryu1k
- 優先順位: 普通
- 状態: 完了
- カテゴリー: 本体新機能
- 投稿日: 2007-07-29 (日) 22:16:25
- バージョン: 1.4.7
- リリース予定バージョン: 1.5.1
- cvs:plugin/attach.inc.php (r1.87)
- 「添付ファイルの一覧」を表示させる場面において、必ず「添付ファイルに対するMD5値の算出(※重たい処理)」を行う作りになっていました。これをMD5が必要な場面でのみMD5を算出する様に修正しました。結果として、以下の一覧処理に関する不要な負荷が無くなりました。
- ページの下部に表示される、そのページに添付されたファイルの一覧
- 添付ファイルの一覧 (そのページ/全ページ)
メッセージ†
attach プラグインはファイル情報を取得すると自動的に md5 でのハッシュ計算を行います。そのため、添付ファイルのサイズ、数によっては、添付ファイルの情報を含むページの表示に時間を要し、その間 cpu を消費し続けます。これは、使い勝手についての問題、および、システム運用上の負荷の問題となっています。問題が発生する条件は限られますが、少ない副作用で実現可能であるならば対処を行ってもよいように思います。
問題点の補足 2007/7/30 追記
md5 を計算する際に OS のディスクキャッシュが追い出され、環境によっては他のアプリケーションに影響を与えます(正式にはバッファキャッシュとかページキャッシュとか?)
仕様についての評価†
簡潔に言えば md5 sum の表示は必要か? ということになると思います。
評価の前提
- 現在は常に自動で計算され、詳細ページで表示されています
- 詳細ページが表示された場合のみ md5 を計算することが可能です (ぃぉぃぉさんの方法をとった場合)
- 計算負荷などに対する定量的な評価は今のところ行われていません (必要?)
- md5 計算に伴うディスクキャッシュの追出しは、環境によっては他のアプリケーションの操作性を低下させます
コメント
- たいしたコストでなく現行の仕様を保持できるならば現状維持が良いのではないか、と思います。*1 ディスクキャッシュが追出されることを考えると、ぃぉぃぉさんの方法を採用して、かつ、ハッシュ計算をキャッシュしたばあいであれば、デフォルト有効でよいように思います。各ページごとであってもハッシュ計算が行われる場合、クローラがアクセスしてきたような場合にディスクキャッシュの追出しを招くように思います。なお、ファイル実体もダウンロードするようなクローラの場合、たいていの場合ディスクキャッシュは結局追い出されてしまいます。 -- ryu1k
- お疲れ様です。ユーザーのニーズが不明確というのであれば、それを明確にするためのアクションを取ればいいので無問題です。というより、そうならないように/そうだったら明確にしながらやらないと迷走しちゃいます。 -- henoheno
- 穏当な仕様変更であればためらわずおこなう。それによって、全体のシンプルさが保たれるのであればなおさら、といったところでしょうか。 -- ryu1k
- ニーズと現状をなるべく正確に認識するように努め、うまい方法が見つかったら、今度は互換性を維持できないか考えたりする、というあれこれを繰り返して、シンプルかつスマートな結果であると周りに証明できるものを結果的に手に入れられたのなら、それは行動で語ってると言えるのではないでしょうか -- henoheno
- 問題を切り分けるための条件についてコメントします。閲覧時、またはWebクローラーによりMD5の負荷がサーバーにかかってしまうデメリットの大部分は、単純な(HTTPの)GETメソッドによるものでしょうから、MD5ハッシュを計算するきっかけをPOSTメソッドに(専用のボタンを押した時に動作する様に)置き換えてしまえば、MD5が叩かれる可能性を大雑把に削減できると思います。JavaScriptや他の手段を利用した POSTメソッド によるアクセスが入る可能性は否定しません。 -- henoheno
- もう一つ。MD5ハッシュを知りたいと思う主体が誰であるのかがあいまいですが、ほとんどの場合「環境・条件の整っていない(管理者か、それに準じる)メンバー」であると考えています。そのため「MD5を計算させる機能を常時有効にする」機能は、一般的にメリットよりデメリットの方が大きいと思います。 -- henoheno
- ※条件の整っている一般ユーザーならば、MD5ハッシュを知りたいと思うことはありません。そもそも転送中のエラーが起きません。 --
- ※条件の整っている管理者ならば、MD5値を別の場所であらかじめ算出する事ができ、MD5値を別のページに、普通のコンテンツの一部として示しておく事ができます。この場合、「Webサイト上でMD5を動的に演算させる機能」を実装する必要がそもそもありません。 --
- ※PukiWikiに一般的に添付されるファイルは、通常数MB(極端な例で数十MB程度)だと思いますが、いずれにせよダウンロードし直せる規模なので、MD5ハッシュが必要になるケースはほとんどありません。MD5を気にするほど重要なデータであれば、管理者が事前に算出しておくべきです。それはファイルをアップロードするより前に算出しておかねばなりません。そこまで気にするなら、アップロード中にファイルが壊れる(?)事も考慮すべきでしょうから。 --
- といった事から、「Webサーバー上でMD5計算をしたい管理者」のニーズと、「その必要がない(負荷が増えるのは嫌だ)管理者」のニーズを両立できるといいのではないかと思いましたので、「MD5値を計算するかどうか」という場面で管理者パスワードを尋ねるようにしておけば、とりあえずクリアできるんじゃないかと思います。 -- henoheno
- もう一つ。「添付ファイルに対するMD5計算」を一般ユーザーに許可しちゃうと、そのURIがDoSに脆弱になると思います。なのでこれは無くしたいと思っています。(MD5値のキャッシュは、この切り口に対して有効ですね :) ) -- henoheno
- 自分が思いつくmd5の利用用途はトラブル時の調査です。(必要かどうかの意見ではなく、md5を利用するとしたら、という話です) -- ぃぉぃぉ
- ローカルでは見れるのにアップロードした画像が見れないとか、DLした圧縮ファイルが解凍エラーになるとか、サーバーのデータが悪いのかそれともpukiwikiが出力するときに壊れているのか、それともローカルの環境が悪いのか、判断の材料にできます。 -- ぃぉぃぉ
- この場合はキャッシュされたmd5だと都合が悪いので、キャッシュを実装するなら、キャッシュ更新手段も用意していてほしいです。 -- ぃぉぃぉ
- 更新より、チェックの方がいいかな。md5ハッシュは変化しないはずなので、変化していないか確認する、と。 -- ぃぉぃぉ
- ふむふむ。Webサーバー上でmd5を叩け(て、かつencodeされたファイルが操作でき)る人には関係ないかもしれない。 -- henoheno
- また、自動広告挿入機能などが添付ファイルを動的に破壊してしまう件については対応できないかもしれない。決定打ではなくあくまでも判断の材料という事ですね。 -- henoheno
修正案†
- md5 の自動計算を無効化する
- md5 の計算結果をキャッシュする
- 上記 2 者に加えて現行の方式を加えた 3 方式を選択できるようにする
修正案の評価†
現行 ( md5 を常に計算 )†
利点
欠点
- 添付ファイルの数やサイズによって、CPU 負荷の問題が発生します
md5 を計算しない†
利点
欠点
- 現行の attach plugin とは仕様が異なってしまいます
コメント
- (ここにあった話題は、修正案の分類の話題とは関係ないので下に移しましたよ) -- henoheno
md5 をキャッシュする (添付してある実装の場合)†
利点
- 仕様を維持しつつある程度の負荷軽減が実現できます
- ファイル実体をメモリに読み込むことがなくなるので、他のファイルをバッファから追い出すことがなくなります。(体感的な快適さから言えば、こちらの効果も大きいです。)
欠点
- 実装が複雑です。
- 負荷が完全になくなるわけではありません
- PukiWiki の正式なインターフェイス以外でタイムスタンプ保持したままファイルを変更した場合など、ごくまれに md5 が正しくない状況が生じえます
上記 2 者または 3 者を選択できるようにする†
利点
- 互換性をとるも、負荷の軽減をとるもユーザーの自由になります
欠点
コメント
- 三択でよければ、私の方で(CVS版に対する)パッチを作成してみてもよいと考えています。 -- ryu1k
修正案の実現†
md5 の計算結果をキャッシュする†
パッチ*2を以下に示します。
ファイル名が非常に長いなどでハッシュファイルを作成できない場合、毎回計算が実行されます。
Index: attach.inc.php
===================================================================
--- attach.inc.php (revision 39)
+++ attach.inc.php (revision 41)
@@ -47,6 +47,9 @@
// mime-typeを記述したページ
define('PLUGIN_ATTACH_CONFIG_PAGE_MIME', 'plugin/attach/mime-type');
+// use md5 cache mechanizm
+define('PLUGIN_ATTACH_HASH_CACHE', TRUE); // FALSE or TRUE
+
//-------- convert
function plugin_attach_convert()
{
@@ -449,9 +452,67 @@
$this->logname = $this->basename . '.log';
$this->exist = file_exists($this->filename);
$this->time = $this->exist ? filemtime($this->filename) - LOCALZONE : 0;
- $this->md5hash = $this->exist ? md5_file($this->filename) : '';
+
+ $this->hashCacheFile =
+ CACHE_DIR . encode($page) . '_' . encode($this->file) .
+ ($age ? '.' . $age : '') . '.attach_hash';
+
+ if ( PLUGIN_ATTACH_HASH_CACHE ) {
+ $this->md5hash = $this->getHashCache();
+ } else {
+ $this->md5hash = $this->exist ? md5_file($this->filename) : '';
+ }
}
+ function getHashCache()
+ {
+ // cache file will be stored in cache/*.attach_md5 file.
+ // Format: 3 int and 1 string splited by \t
+ // cache version : int (0)
+ // size : int (byte)
+ // stamp : int (mtime)
+ // md5sum : stringn (hex text)
+
+ if ( ! $this->exist ) { return ''; }
+
+ $cache_version = 0;
+
+ // open target file.
+ $fpf = fopen( $this->filename, "r" );
+ if( ! $fpf ) { return ''; } // It's strange...
+ $fresh = fstat( $fpf ); // get stat info
+
+ // open hash cache file
+ $fpc = fopen( $this->hashCacheFile, "r" );
+ if( $fpc ) { // cache existing.
+ $cached = fscanf($fpc, "%d\t%d\t%d\t%s\n");
+ if ( $cached[0] == $cache_version &&
+ $cached[1] == $fresh['size'] &&
+ $cached[2] == $fresh['mtime'] ) { // fresh and fine cache. use it.
+ fclose( $fpc );
+ fclose( $fpf );
+ return $cached[3];
+ // return "cache:" . $cached[3];
+ }
+ fclose( $fpc ); // close cache file once.
+ }
+
+ $md5 = md5_file($this->filename);
+
+ // Need to create cache
+ $fpc = fopen( $this->hashCacheFile, "w" );
+ if ( ! $fpc ) { fclose( $fpf ); return $md5; }
+
+ // generate cache file
+ fwrite($fpc,
+ sprintf("%d\t%d\t%d\t%s\n",
+ $cache_version, $fresh['size'], $fresh['mtime'], $md5 ));
+ fclose( $fpc );
+ fclose( $fpf );
+ return $md5;
+ // return "gen:" . $md5;
+ }
+
// ファイル情報取得
function getstatus()
{
@@ -617,7 +678,10 @@
}
}
- // バックアップ
+ // remove hash cache.
+ if ( PLUGIN_ATTACH_HASH_CACHE ) { @unlink($this->hashCacheFile); }
+
+ // バックアップ
if ($this->age ||
(PLUGIN_ATTACH_DELETE_ADMIN_ONLY && PLUGIN_ATTACH_DELETE_ADMIN_NOBACKUP)) {
@unlink($this->filename);
- あ。そういえば <添付ファイル名.log> って以前からあるな。これもファイル名の長さの問題を抱えている気がするけれど、仮にhashをここに格納できれば、ファイル数が爆発する問題も緩和される気がする。 -- henoheno
コメント†
- ここで聞く話なのかいまいちわかりませんので、適切な場所があれば誘導してもらえると助かります。attach plugin がファイル一覧を表示すると毎回 md5 を計算しているようで、大きめの添付ファイルが多いサイトではクローラの類がくると CPU 負荷がひどいことになってしまいます。タイムスタンプとファイルサイズをキーにして、 md5 をキャッシュするような仕組みを作ろうかと思っているのですが、すでに存在していたりするでしょうか。結構メジャーな要望だと思うのですが、ちょっと探した範囲では見つけられませんでした。 -- ryu1k
- たいした規模でもないのでとりあえず作ってしまいました。 -- ryu1k
- こんにちは。この件、md5を計算させる意味がほとんど無い(必要な場面では、各自別途提示するよね)という観点から、挙動を止めてしまう(実行させるとしても、条件を設ける)のが良いのだろうと思っています。上記の案では結果をキャッシュさせておられるようですが、残念ながら拡張子をつけてしまっているため、添付ファイルのファイル名がシステムの限界ギリギリであった場合に、拡張子をつけたファイル名がシステムの限界を超えてしまい、キャッシュを保存できなくなる可能性があります。 -- henoheno
- 拡張子が付けられないという件はattachが生成するファイル名がそもそも拡張子を含んでいない、というデザイン上の問題(管理し辛く応用が利かない方を選んでしまった)が影響しています。そのために、添付ファイルだけを操作するようなバッチ処理が書き辛かったり、.cvsignoreが効かなかったりもします -- henoheno
- よろしかったらBugTrackに持っていって、どのようなデザイン上の問題があるのかをまとめてしまって下さい。 -- henoheno
- attach プラグインのデザイン全般に関する議論は今のところ私には荷が重過ぎるように思いますので、とりあえず今回のハッシュ計算負荷の問題について dev:BugTrack2/264 に移動しました。 -- ryu1k
- ( ここまで、pukiwiki:雑談/16 からコピーしました。 ) -- ryu1k
- ファイル名の限界に関する問題についての確認ですが、ファイルが添付できないことではなく、添付ができてキャッシュファイルの生成ができない場合があるという、非対称性が問題ということでよいでしょうか。そういった場合には、毎回ハッシュ計算を行い md5 を表示できるようにしました。過去との互換性や、キャッシュ実装の容易さを考えると、場合によって毎回ハッシュ計算が行われてしまうというのは適当な妥協点だと思います。 -- ryu1k
- 拡張子を追加する事によりファイルが作成できないことがある件を回避されたとの事。上手いですね。キャッシュ実装については、
苦労して、ファイルを多数作ったとしても思ったほど効果が出ないかもしれない(※とあちらに書かれている)、という部分が難ですね ぃぉぃぉさんがツボを見つけたので、意味合いが少し変化したかな -- henoheno
- お疲れ様です。こちらの件、そもそものニーズの部分から掘り起こしをお願いします。どのテーマでもこれを忘れると空回りが続く可能性があります。今回の件については不要論や、必要なのはどのシチュエーションであるからどうすれば良い、といった妥協点が理由つきで出てくるはずです。上の修正案にはとりあえず「条件つきで許可する」案が(条件も)抜けている様です -- henoheno
- (MD5を計算する処理は) attach_info以外では使われていないようなので、attach_infoの時だけmd5を計算するようにしては?
それ以外では計算しなくても、attachプラグインの外部仕様に変更はないので、普段は計算しなくても良いのでは。 -- ぃぉぃぉ
- 修正案。詳細表示以外では十分効果があるかと。詳細表示の場合は1個だけだから我慢ということで。
- function attach_info($err = '')
$obj = & new AttachFile($refer, $file, $age);
+$obj->md5hash = $obj->exist ? md5_file($obj->filename) : '';
- class AttachFileのfunction AttachFile($page, $file, $age = 0)
-$this->md5hash = $this->exist ? md5_file($this->filename) : '';
- あ。確かに今現在の外部仕様変わりませんね。賛成です。キャッシュとか面倒なことをしなくても、コレで必要十分な気がします。 -- ryu1k
- ハッシュのキャッシュを無理にでも生き残らせようとしているかのようで、かっこ悪くていやなのですが、ディスクキャッシュの追い出しを予防する意味ではハッシュのキャッシュは有効なように思います。 -- ryu1k
- ぃぉぃぉさんの指摘されている件、コンストラクタで必ず md5演算をかけてしまっているというのは確かに今回の発端となった件の肝の部分なんだろうと思います。MD5値を計算するその処理はAttachFileのメソッドにすればよいのでせう。 -- henoheno
- cvs:plugin/attach.inc.php (r1.87) この部分は明らかな修正点なのでとりあえず回収させていただきました。メソッド名はryu1さんからもらいました。これで、添付ファイルを一覧する時の無用な負荷は減ったはずです。試してみて下さい。 --
- ・・・えーと、PukiWikiはデフォルトで添付ファイルの一覧をページ下部に表示させているわけですが、その部分が重い件は、実はこの部分も噛んでる?? ・・・・・・・・・ああ噛んでる噛んでる・・・ -- henoheno
- とりあえず簡単にr1.87の結果報告をしておきます。r1.87 をパッチとして取得して適用しました。少なくともキャッシュ版と同程度まで軽くなっています。(当然ですね) -- ryu1k
- 確認ありがとうございます。今回掘り出された問題の一つ(一覧表示時の負荷)は「そもそも無駄な処理をしていた」という点に原因がある事がわかりました。その無駄を取り外したので、「MD5の結果をキャッシュするアイデア」が活躍する場面は「とあるファイルのMD5を計算するシチュエーションでの負荷(今の「詳細」画面)」に限定されます。 -- henoheno
- もう一つ、重たい処理を発見しました。 -- ぃぉぃぉ
$this->type = attach_mime_content_type($this->filename);
これも、このgethash()の様にgettype()としてやるのが良いです。そして$this->typeの呼び出し箇所2カ所を$this->gettype()とします。
- それもですけど、attach_info() とAttachFile::info() から呼ばれるAttachFile::toString() 、attach_freeze() とAttachFile::freeze() 、attach_open() とAttachFile::open() 、とそれぞれ2箇所でAttachFile::getstatus() を呼んでいるのもあまり意味がないのでは?いくらキャッシュが効くとはいえ、2度も同じファイルのサイズを得ようとするのは・・・・・
AttachFile::toString() はAttachFiles::toString() 等からも呼び出されるので、AttachFile::getstatus() を動かせないでしょうから、attach_info() を
$obj = & new AttachFile($refer, $file, $age);
- return $obj->getstatus() ?
+ return $this->exist ?
$obj->info($err) :
array('msg'=>$_attach_messages['err_notfound']);
とするのが無難でしょうか? --
- MD5速度改善のご対応を誠にありがとうございました。
ぃぉぃぉさんのご指摘通り、attach_mime_content_type()内で処理される、下記の処理が、行われているため、大きなサイズのファイルを添付したとき、
数秒程度の負荷がかかるようです。
$size = @getimagesize($filename);
この関数は、どんなファイルであっても画像ファイルを前提にかなりの部分を読み込んでしまい、オーバーヘッドになっているようです。
画像拡張子を持たないファイルで、中身だけ画像であるファイルはまず使われないかと思います。
逆にjpgを装った、不正なファイルはあるかもしれないので、
画像拡張子を持ったファイルのみ中身をチェックするようにするのはいかがでしょうか。
実際には下記のコードを使用してみました。
... ここまで同じです。
if (! file_exists($filename)) return $type;
$matches = array();
if (! preg_match('/_((?:[0-9A-F]{2})+)(?:\.\d+)?$/', $filename, $matches))
return $type;
$filename_decode = decode($matches[1]);
$psinfo0 = pathinfo(decode($filename_decode));
$ext0 = $psinfo0['extension'];
if( preg_match( '/gif|png|jpg|jpeg/i', $ext0 ) ) {
$size = @getimagesize($filename);
if (is_array($size)) {
switch ($size[2]) {
case 1: return 'image/gif';
case 2: return 'image/jpeg';
case 3: return 'image/png';
case 4: return 'application/x-shockwave-flash';
}
}
}
// mime-type一覧表を取得
- if (preg_match("/\.$ext$/i", $filename)) return $_type;
+ if (preg_match("/\.$ext$/i", $filename_decode)) return $_type;
適用した結果、巨大ファイル(pdfなど)を添付した時でも、2秒程度の遅れがなく表示できるようになりました。
コード自体に改善点はあるかもしれませんが、参考になれば幸いです。 -- Thanks
- md5について commit:2b92bec1fe17dc0c690cc66df6f7d5ed64f9c60b にてパッチが取り込まれていますので完了とします -- umorigu
- mime-typeの問題を見落としていました。画像拡張子を持たないファイルに対してgetimagesize()が呼び出されている問題を commit:edfd4a4717f9ad5a8a15a8f97bae340fb5f0cbc3 で修正しました。Windows 8.1, SSD, PHP5.6で試すと、30MBのPDFファイルで 修正前:1秒、修正後:0.5秒、程の違いがありあした。結果的に完了です -- umorigu