put_lastmodified() の負荷軽減(案)†
- ページ: BugTrack2
- 投稿者: 0
- 優先順位: 低
- 状態: 提案
- カテゴリー: その他
- 投稿日: 2006-01-15 (日) 04:37:34
- バージョン:
(PukiWiki 1.4.7)
- 関数 lastmodified_add() を新規に作成しました。
- put_lastmodified() が情報を格納するキャッシュファイル recent.dat について、必要最小限の件数だけ情報を保存する様になりました。
- 従来は常に全ページのページ名とその時刻を保存していたため、ページ数に比例した書き込み負荷が発生していました。
メッセージ†
official の WebTrack/59(←削除予定) から移動してきました。
ページ更新時の負荷の大部分が put_lastmodified() によるもの*1なので、その負荷軽減の案です。
official ではページ数が 2450 ページ弱*2あるので、更新時に get_filetime($page) が 2450 回呼び出されます。タイムスタンプを取得すべきは 2450 分の 1 ページのため、その他は recent.dat に記録されたタイムスタンプを使いまわすことができれば処理時間の短縮が可能になります。
ページ数が 1000 単位になった場合、処理時間は大きく変わるはずです。
以下に案として挙げておきます。
diff -ur /org/file.php /dev/file.php
--- /org/file.php
+++ /dev/file.php
@@ -259,20 +259,68 @@
// Update RecentChanges
function put_lastmodified()
{
- global $maxshow, $whatsnew, $non_list, $autolink;
+ global $maxshow, $whatsnew, $non_list, $autolink, $vars;
if (PKWK_READONLY) return; // Do nothing
- $pages = get_existpages();
+ $term = 60 * 60;// 1時間以上経過していれば作り直す
+
+ $recentFile = CACHE_DIR . 'recent.dat';
+ $recentTimefile = $recentFile . 'time';
+
+ $timeFlag = (file_exists($recentTimefile) && time() - $term < filemtime($recentTimefile));
+ $RecentFlag = (file_exists($recentFile) && $timeFlag);
+
$recent_pages = array();
$non_list_pattern = '/' . $non_list . '/';
- foreach($pages as $page)
- if ($page != $whatsnew && ! preg_match($non_list_pattern, $page))
- $recent_pages[$page] = get_filetime($page);
+ if ($RecentFlag) {// recent.dat を使用する.
+ $varsPage = (isset($vars['page'])) ? $vars['page'] : '';// 空になる可能性は?
+
+ $fp = @fopen($recentFile, 'r');
+
+ // 不整合を防ぐため (排他的ロック && ノーブロック)
+ $wouldblock = true;
+ if ($fp && flock($fp, LOCK_EX + LOCK_NB, $wouldblock) && ! $wouldblock) {
+ $lines = file($recentFile);
+ $lines = str_replace("\n", '', $lines);
+ foreach ($lines as $line) {
+ if (empty($line)) {
+ break;// ないはず.
+ }
+ list($getTime, $getPage) = explode("\t", $line);
+ if ($getPage != $whatsnew && ! preg_match($non_list_pattern, $getPage)) {
+ $recent_pages[$getPage] = $getTime;
+ }
+ }
+ if (is_page($varsPage) && ! preg_match($non_list_pattern, $varsPage)) {
+ $recent_pages[$varsPage] = get_filetime($varsPage);
+ } elseif (isset($recent_pages[$varsPage])) {
+ unset($recent_pages[$varsPage]);
+ }
+ } else {
+ $RecentFlag = false;
+ }
+ }
+
+ // 通常の処理 (排他的ロック中に遭遇した場合も諦めてこちら)
+ // ロックで弾かれた場合、処理が追いつくことはないはず・・・
+ if (! $RecentFlag) {
+ $pages = get_existpages();
+ foreach($pages as $page) {
+ if ($page != $whatsnew && ! preg_match($non_list_pattern, $page)) {
+ $recent_pages[$page] = get_filetime($page);
+ }
+ }
+ @fopen($recentTimefile, 'w');// ファイルを作るだけ
+ }
// Sort decending order of last-modification date
arsort($recent_pages, SORT_NUMERIC);
+ if (isset($fp)) {
+ flock($fp, LOCK_UN);
+ }
+
// Create recent.dat (for recent.inc.php)
$fp = fopen(CACHE_DIR . 'recent.dat', 'w') or
die_message('Cannot write cache file ' .
ブラウザ複数窓, sleep() などで実験はしましたが、実用に耐えうるか(整合性が保てるか)が良く分からないので、上記処理は 1 時間に 1 度は現在と同じ処理をするようにしています。更新処理を行なった後 1 時間は軽減される、といった感じです。
コメント†
- 確かにお試しは軽いですねー。うちのサイト(約2000ページ&増量中)も更新に5秒以上かかるので更新が軽くなると嬉しいです。(サイト不具合のような新機能のような・・・これはきっとBugTrackじゃないですか?) -- かい
- 関連 dev:BugTrack/763 負荷対策のまとめ -- teanan
- BugTrack に移動させた方が良いですかねぇ・・・? (^^; -- 0
- パフォーマンス改善ネタとしてBugTrackにあげて頂いたほうがいいと思います :) -- teanan
ここまでが WebTrack/59 でのコメントです。
recent.datの内部構造、想定している本来の利用方法について†
- 各種コメントありがとうございます。本日見たところでは「recent.dat を get_filetimeのキャッシュにする」のは無理ではないかと思います(下記)。ただお蔭様でrecent.dat周りに改善点がいろいろある事が判って来ました。上で挙げられている改善案も、何か良いエッセンスがあれば参考にさせていただきたいと思います。 -- henoheno
- (1) recent.datの内部構造は参照側(recentやrss)に特化されており、内部は時刻の新しい順にソートされている前提がある。また $non_listにあたるページなどは追記されない事になっている(等々、例外事項がある)。この時点でキャッシュに使うのはちょっと厳しい。キャッシュに使う入れ物ならソートなんて余計(オーバーヘッド)だし例外処理も余計。
- (2) get_filetime()の結果をページ数分抱える意義が本当にあるのか、PHP自身のキャッシュを無視する意味があるのか、それは純テキストファイルでやるべきなのか。性能のためと言うならば結構シビアな話題です。
- (3) 1を踏まえて考えるに、recent.dat は必要最小限の行数だけ用意するべきで、数千ページある時に数千行書き込むべきじゃない。つまり書き込む瞬間の処理がページ数に影響されるはずがない。(でも今はそうじゃない)
- (4) 3を踏まえて考えるに、新たにページが更新された時には、「(RecentChangesに数十ないし百行程度表示したいなら)数十ないし百行程度の情報を一項目分だけ更新する処理」に専念できるはず。つまり更新時の作業量がページ数に影響されるはずがない。(でも今はそうじゃない)
- (5) 4の状態まで来ると、RecentChangesを更新するのに全ページをスキャンする必要がそもそも無くなるはず。全ページをスキャンするのは recent.dat の更新時刻とその時の時刻に相応の開きがあった時など、ごく最小限でいい。(でも今はそうじゃない)
- (6) これはデータ構造と、そのためのアルゴリズムの話であってキャッシュの話じゃないはず。キャッシュ(あるいは、キャッシュのキャッシュ)は基本的にオーバーヘッドになるし、本当にすべき事を見失いがちになるのでご用心。類似の話題は多分 BugTrack2/83。
- cvs:lib/file.php (1.51): BugTrack2/151: Cut unused lines from recent.dat
- 上記(3)を実装しました。$maxshow で設定している (RecentChangesのための)項目数を越える行は recent.dat に保存されません。誰も使わないので。影響範囲としては、仮にRSSやrecentプラグインでその値以上の項目数を指定した場合、recent.datに存在するぶん以上はどうやっても表示されません。この点についてはどちらも、$maxshowより遥かに少ない項目数で利用される事が期待されている機能なので特に問題ないでしょう(そんなニーズがあるなら事例を添えて理由を教えて下さい)。 -- henoheno
- 上記(4)~(6)は、recent.datを新規に構築する (5) を発生させるべき状況をもう少し明確にしたいところ。 -- henoheno
- 仮に時間としましょう。6時間経ったら更新させるとして・・・仮に一日に一回書き込むか、書き込まないかという低頻度の編集ニーズがあるWikiだと、その書き手にとっては常に「重たいWiki」のまま変らないですね。つまりこれでは対外向けコンテンツや社内向けコンテンツを提供するためのWikiの管理者グループを楽にできません。 -- henoheno
- recent.dat が存在しない場合
や、その項目数が設定と異なる時は強制更新すべき。管理者が設定を変える以外は普通起きない状態です。(global変数を悪意あるコードから書き換える状況は除く) 素早く更新するコードが上手くできており、内容がvalidであれば、特に強制更新しなくとも問題ないはず(素早く終わる事ができるはず) -- henoheno
- (3)について。BugTrack2/45のrecentの1機能、特定ページ以下のみ表示するケースはどうでしょう。recent.datのみで判断できないだけで無駄にはならない&本体未取込機能ですが、一応そういうニーズもあるという事で。 -- にぶんのに
- いつもありがとうございます。「現状の仕様において、recent.datに保存する情報は $maxshow の件数のみで事足りる」という事は上で示した通りです。それを踏まえてきちんと実装する事が、本来期待されてる最速の状態をもたらすはずです。 -- henoheno
- で、「特定の文字列でフィルタする」とか「特定の期間で考える」ニーズがある場合、「件数($maxshow)」という概念から考え直す必要があるでしょう。例えば「ここ一週間の間に更新されたhogeで始まるページを知りたい」というニーズがあった時、recent.datには「ここ一週間の更新情報」全てを事前に記録するべきです。重要なのは、全ページのデータを収める必要は(件数ベースの実装と同様に)無いという事です。また、期間ベースでデータを蓄積するのですから、大量にpostされた場合に、recent.datが恐ろしく大きく成長する可能性が生まれます。反面、だからこそある期間の変化を確実に知ることができる余地が生まれます。 -- henoheno
- 件数ベースでストアしている情報を強引にフィルタする場合、結果が0件に近づく可能性が高くなります。つまり購読者が意図していないかもしれない、妙なRSSを出力しやすくなります。期間ベースであっても、フィルタする内容によっては同様です。つまり利用者は「購読している物はあくまでもフィルタの結果である」という事を意識する必要があります。 -- henoheno
- このへんは設計(デザイン)の話でしょうね :) -- henoheno
- 件数ベースで記録している現状は、$maxshow で指定された件数より少し大目にrecent.datに記録する様にして、(頻発するページ削除により)それを越える件数が削られたことを検知したタイミングでのみディレクトリを走査すると良いでしょう。 -- henoheno
- 例えば 10件 余分に recent.dat に記録する様にしたサイトに、ページを110個連続で削除するスクリプトが働いたとして、recent.datの再生成が発生するのは10回(今までは110回)です。 ※個々の削除と同じタイミングで、別の誰かがrecent.datに載っていないページを110回編集/追加したならば、recent.datの件数は減らないので再生成は発生しない -- henoheno
- 更新時の処理のみ無理やり実装。ページ削除に対するケアも可能だし、cvs:lib/file.phpの他の部分もあわせて叩き直す余地がかなりあります。また今回作っている部分も見直しが足りないので、作業は当分続くでしょうけれど、これで一つ山を越えました。AutoLinkの更新処理が「ディレクトリの全チェック」を必要とするかのように作られている部分について、本当に必要かどうかが現時点では見えないので(今回のrecent.datのようにできるかどうかまで見ていないので)、とりあえず $autolink = 0 の時だけ動作する様にしてあります。 -- henoheno
- 質問箱を見るに、コンテンツの引越などの理由でwiki/*.txtの直接入れ替えをする方も一定存在するみたいなので、 linksプラグインの様に、put_lastmodifiedを呼び出してrecentキャッシュを明示的に最新化する機構が必要ですかね…。や、リリースまでにですが。 -- にぶんのに
- 今この瞬間のCVS版の実装で答えるならば、ページを適当に作ってそれを「削除」すれば、今まで通りにディレクトリを走査しますので強制更新が行われます。この半端な状態でも大部分のニーズは満たすので、タイミング的に今回(1.4.7)このままかもしれません。余談ですが管理用プラグインについては色々数とバリエーションがありすぎるので、何かまとめるいい案が欲しいです。 -- henoheno
- その他、recent.datを削除することで、再作成を強制することができるでしょう。 -- henoheno
- 削除のアクションは、RecentDeletedなどの挙動もフォローしなければいけないでしょう。 -- henoheno
- 削除時の処理で、lastmodified_add() に RecentDeleted のページ名を渡せばいい予感が。駄目かな? -- henoheno
- OK。cvs:lib/file.php (r1.67) -- henoheno
- これで、ページの削除に関しても、あらかじめ多めに取っておいた「遊び」の範囲内で、recent.dat が素早く(Wikiのページ数に依存しない速度で)更新されます。 -- henoheno
- 特にないなら、この見出し部分の話題は終了です。(あれば)質疑応答や、上にあるトピックに他にもお宝がないか検討しましょう。 -- henoheno
- 関連: BugTrack2/196, BugTrack2/273 --
コメント: バッファ操作?†
コメント†
- BugTrack2/341 --
- 5000件程度のデータ挿入をすると直っていないのがわかる --
- それだけ1度で変更するなら、lastmodified_add()を5000回じゃなくて、最後にまとめてput_lastmodified()したほうがましなのでは・・・。(AutoLinkとかも有効にしているのなら、なおさら)
似たような問題を抱えてそうなのは、ページ名変更の話題があるBugTrack2/196かな --
- 挿入後 データを編集して、保存すればわかる。やりもしないで答えないように。 --
- BugTrack2/56, BugTrack2/80 --