PyukiWiki 0.1.5にページキャッシュ機能をつけて高速化を図る

1.はじめに

このカモランドはPyukiWikiで稼働しているのだが,Wikiと言ってもページを作るのは作者なのでほとんどのページは内容が固定している.

しかしページを表示するために毎回CGIが動作してページ内容を生成しているというのが,何とももったいない.

これを何とかしようということで,Apacheのmod_cacheを試したりしたが,ブラウザから強制リロードをやられると,対処できない.*1

そこで,PyukiWiki側にもキャッシュ機能を実装することにした.

前提は以下の通り.

  • PyukiWiki 0.1.5
  • PyukiWikiを動かすOSは,LinuxなどUnix系 (キャッシュ保存にteeコマンドを使っているため)

2.キャッシュ機能の仕様

  1. wiki.cgiがページ表示時にレスポンスとして返却する内容を,同時にファイルにも保存する.(キャッシュファイル)
  2. 次回以降同じページの表示が要求されて,以下の条件を満たす場合は,既に保存したファイルの内容をそのまま返却する
    • dbmファイルのタイムスタンプがキャッシュファイルより新しくない.(前回のキャッシュ後にページに変更が入っていない)

blog20070429a.png
説明図

(番号)リクエストの性質ApacheのキャッシュPyukiWikiのキャッシュ表示速度
(1)通常(no-cache無し)のリクエスト維持する維持する最高速
(2)no-cacheつきのリクエスト更新A維持するやや高速
(3)対象ページが更新された直後のno-cacheつきリクエスト更新A更新B低速

更新A(Apacheキャッシュの更新)の発生条件

以下のどちらか
  • Apacheが保持しているキャッシュがExpires期限に達した (期限切れ)
  • ブラウザからのリクエストがno-cacheかつ,CGIレスポンスのLast-ModifiedヘッダがApacheが保持しているキャッシュのLast-Modifiedより新しい (強制更新)

更新B(PyukiWikiキャッシュの更新)の発生条件

以下のどちらか
  • dbmファイルのタイムスタンプがキャッシュファイルより新しい場合 (期限切れ)
  • 該当キャッシュファイルが無い場合 (強制更新)

なおApacheのmod_cacheを使っていない場合は,上述の(1)や「更新A」は無視してください.

3.プログラムの変更内容

PyukiWiki(0.1.5)のwiki.cgiについて,以下のようにdo_read()関数を改造する.

変更前

sub do_read {
	&skinex($::form{mypage}, &text_to_html($::database{$::form{mypage}}), 1);
}

変更後

sub do_read {
	my ($page) = $::form{mypage};

	my($dbm_lastmod) = (stat($::data_dir . "/" . &dbmname($page) . ".txt"))[9];
	my($cachefile) = $::cache_dir . "/" . &dbmname($page) . ".cac";
	my($cache_lastmod) = (stat($cachefile))[9];

	if ($dbm_lastmod > $cache_lastmod || $cache_lastmod == 0) {
		# キャッシュファイルを更新する
		open(CACHE, ">$cachefile") or die "Open failed: $!\n";
		flock(CACHE, 2) or die "Exclusive lock failed: $!\n";

		my($tempfile) = $::cache_dir . "/" . &dbmname($page) . ".tmp";
		open (STDOUT, "|tee $tempfile") or die "Tee failed: $!\n";

		&skinex($::form{mypage}, &text_to_html($::database{$::form{mypage}}), 1);

		close(STDOUT) or die "Close failed: $!\n";

		# 一時ファイルからキャッシュファイルに転送する
		open(CACHE_TEMP, "$tempfile") or die "Open failed: $!\n";
		while (<CACHE_TEMP>) {
			print CACHE $_;
		}
		close(CACHE);
		unlink($tempfile) or die "Unlink failed: $!\n";

	} else {
		# キャッシュファイルの内容をSTDOUTに転送する
		open(CACHE, "$cachefile") or die "Open failed: $!\n";
		flock(CACHE, 1) or die "Share lock failed: $!\n";
		while (<CACHE>) {
			print $_;
		}
		close(CACHE);
	}
}

この状態でアクセスすれば,$::cache_dir 配下にどんどんキャッシュファイルが作られてゆくでしょう.

mod_cacheと併用する場合について (2007.06.16 追記)

なお,mod_cacheでWiki(CGI)の高速化で行っているように,スキンなどで
  • Last-Modified:
  • Expires:

のヘッダを出力することで,Apacheのキャッシュ(mod_cache)を有効にしている場合は,上記の内容では足りない.

上記の内容だと,初回のページ作成時に付与されたヘッダ(Last-Modified,Expires)をそのまま返してしまうため,それ以降Apacheのキャッシュが効かなくなってしまう.(常に有効期限切れの扱いになる)

そのため,これらのヘッダについては,PyukiWikiのキャッシュファイルの内容そのままでなく,実行時に差し替えたものを返してやる必要があります.

カモランドでは,以下のように差し替え処理を入れています.上記のsub do_read修正の,else以下の箇所です.(なお,これを使うためには,事前にPyukiWikiのdate関数を修正しておく必要があります.)

	} else {
		# キャッシュファイルの内容をSTDOUTに転送する
		my($jst_diff) = ((localtime(time))[2] + (localtime(time))[3] * 24)
			- ((gmtime(time))[2] + (gmtime(time))[3] * 24);
		$jst_diff *= 3600;

		my($expire_period) = 86400 * 10; # 全ページ有効期限10日にしている
		my($http_lastmod) = &date("D, d M Y H:i:s", time - $jst_diff);
		my($http_expires) = &date("D, d M Y H:i:s", time - $jst_diff + $expire_period);

		my($in_httpheader) = 1;
		open(CACHE, "$cachefile") or die "Open failed: $!\n";
		flock(CACHE, 1) or die "Share lock failed: $!\n";

		while (<CACHE>) {
			if (/^$/) {
				$in_httpheader = 0;
			}
			if ($in_httpheader && /^Last-Modified:/) {
				print "Last-Modified: $http_lastmod GMT\n";

			} elsif ($in_httpheader && /^Expires:/) {
				print "Expires: $http_expires GMT\n";

			} else {
				print $_;
			}
		}
		close(CACHE);
	}

4.キャッシュの強制更新について

更新Bの発生条件で述べたように,PyukiWikiのキャッシュは対象ページが更新されて dbmファイルのタイムスタンプが新しくなったタイミングで更新される.

しかしWikiでは,他のページが更新されただけでも自ページが影響を受けて表示内容が変わることがよくある. (例えば,ls系プラグインを使っている場合とか)

そういうときにキャッシュを更新する方法は,現在のところ

  • ページ内容に変更が無くても更新してdbmファイルのタイムスタンプを新しくする
  • 対応するキャッシュファイルを手動で消す

という,いまいちなものしかありません.

今後の課題だが,「キャッシュ管理画面」というようなのがあって, そこで指定したページのキャッシュを更新できるというのが理想だ.

→管理画面を作成しました. PyukiWikiにキャッシュ機能をつける#2


*1 強制リロードはIEならCTRL+F5だが,FireFoxは何と普通にF5を押すだけでできてしまう :(


© 2024 KMIソフトウェア