2011年2月アーカイブ

Cache::Memcached(::Fast)を使う上でベストプラクティスをまとめたモジュールを書いてみた。名前は、Cache::Memcached::IronPlate。おのみち焼き。

githubにあります。ドキュメントが日本語だけです: https://github.com/kazeburo/Cache-Memcached-IronPlate

つかいかた

use Cache::Memcached::IronPlate;
use Cache::Memcached::Fast;

my $memd = Cache::Memcached::IronPlate->new(
    cache => Cache::Memcached::Fast->new(...).
);
$memd->get
$memd->get_multi
$memd->set
$memd->add
$memd->replace
$memd->append
$memd->prepend
$memd->incr
$memd->counter
$memd->decr
$memd->delete

インスタンス作成時にCache::Memcache(::Fast)などのオブジェクトを渡します。ほかにもいくつかオプションがありますが、機能の説明をしながら紹介します。

自動キーフィルタ

memcached injectionを防ぐためマルチバイトや制御コードがkeyに含まれている場合、それらをURI Escapeして利用します

$memd->get("key hoge\n") => get("key%20hoge%0A")

キーが250文字以上の場合は、Digest::MD5でhash値を作り利用します。またオブジェクトの場合はシリアライズしてkeyとして用います。

キャッシュ分散

設定情報など、比較的変化が少なく多くのページで読まれるキャッシュは複製して多数のサーバに分散することで負荷の集中、memcachedサーバ障害時の影響を小さくすることができます。分散するkeyには「:dist」を付加します

$memd->set("mypref:dist")

内部的には、:dist:${num} などと1から複製する数の値をkeyに追加して、memcachedクライアントで分散されるようにします。分散される個数はデフォルト20です。変更するにはインスタンス作成時に distribution_num を設定します

my $memd = Cache::Memcached::IronPlate->new(
    distribution_num => 30
);

このキャッシュ分散の機能は、setとget、get_multi、deleteにのみ有効です。他のメソッドに対して:distが付いたキーを渡すと例外となります

キャッシュ複製

Cache::Memcached::Fastのようにrehashしないクライアントでは、特定のmemcachedサーバに接続ができない状態になると特定のキーの保存ができす、セッション管理に用いていた場合では、特定のユーザのみがログインができないなどの影響がおこります。 keyの名前に「:dup」を付与すると、キャッシュ分散と同じように自動的にキャッシュを複製します。

内部的にもキャッシュ分散と同じように、:dup:${num} とキーに数値を追加して、複製、分散されるようにします。キャッシュ分散と異なるのは、キャッシュ取得時に複製したデータを全て取得し、取得できたデータが過半数に達した場合のみ、データを返す事です。

${num}はデフォルト3です。インスタンス作成時に duplication_num を設定します

my $memd = Cache::Memcached::IronPlate->new(
    duplication_num => 30
);

キャッシュ複製では大きなキャッシュデータに利用した場合、通信量に影響がでるので注意してください。この機能も、setとget、get_multi、deleteにのみ有効です。他のメソッドに対して:dupが付いたキーを渡すと例外となります

カウンター

memcached の increment は指定した値がない場合では動作しません。counterは自動で初期値を保存します。

get_multiの分割処理

get_mulitiに1,000個以上のkeyを渡した場合は、タイムアウトを防ぐため内部的に1000個ごとに分割して処理をします

以上のような機能が使えます。お試しくださいませ。

やや大袈裟な名前ですが「memcachedにおけるキャッシュシステムの Thundering Herd 問題への対策案」とか「キャッシュシステムの Thundering Herd 問題への対策案。その2 排他制御」で書いていたコードをモジュールにした

github: https://github.com/kazeburo/Cache-Isolator

機能としては、平行動作数を制御できるget_or_setと、一定の確率で少し早く有効期限が切れるキャッシュの保存、取得、削除あたりがあげられます

使い方ですが、まず、get_or_setの例。

my $isolator = Cache::Isolator->new(
    cache => Cache::Memcached::Fast->new(...),
    concurrency => 4, # get_or_setのcallbackの最大平行動作数。デフォルト1
    interval => 0.01, #lockを確認するinterval
    timeout => 10, #lockする最長時間
    trial => 0, #lockを試行する最大回数
);

my $key   = 'query:XXXXXX';
# get_or_set( key, callback, expires)
$isolator->get_or_set(
    $key, 
    sub {
        get_from_db($key);
    },
    3600
);

次に早期有効期限の例。get/set/deleteは普通とかわらずに使えます。

my $isolator = Cache::Isolator->new(
    cache => Cache::Memcached::Fast->new(...),
    early_expires_ratio => 10, #1/10の確率で早くexpires
    expires_before => 10, # expiresする秒数
);

$isolator->get($key);
$isolator->set($key, $value, $expires);
$isolator->delete($key);

early_expiresでは、set時に2つキャッシュを作成し、片方のexpiresをexpires_beforeで指定した秒数早くして保存します。getするときは、early_expires_raitoの確率で早くexpiresするキャッシュを引き、通常のキャッシュよりも早い期限切れを表現しています。

この2つの機能は同時に使うことができるので、early_expiresをしつつ、get_or_setの平行動作数をしぼったりもできます。うまく組み合わせるとThundering Herd問題を回避できると思われます。

このモジュールではないけど、既に某blogサービスの一部の重いクエリに対してこの仕組みが導入されていたりします。かなりDBの負荷削減に効果ありました

HTTPコンテンツ圧縮はどのレイヤーで行うのがいいか」で書いたりしましたが、高トラフィック環境では、サーバ間の通信量も問題となるため、ApplicationサーバでもHTTP圧縮をかけたほう良さげです。Plackには既にPlack::Middleware::DeflaterというApacheのmod_deflate相当のMiddlewareがあるのですが、ちょいちょい弄っていたりするのでその紹介。

その前に、HTTPコンテンツ圧縮のおさらいです。HTTP圧縮はリクエストのAccept-Encodingにgzipやdeflateがあった場合に、コンテンツをgzip、deflateなどで圧縮し、レスポンスのContent-Encodingヘッダに圧縮に用いた方式を表記することで実現されます。さらに、クライアントサーバ間にキャッシュサーバが存在した場合に、Accept-Encodingを送ってきていないクライアントに対して圧縮済みのコンテンツを返してしまわないよう、Varyヘッダも必要となります。

Vary: Accept-Encoding

キャッシュサーバはVaryヘッダに記されたヘッダの内容ごとにキャッシュを分け、クライアントのリクエストに適したコンテンツを返します。

追記: 小悪魔元女子大生の奥さんが説明いらすと書いてくれた!
x2_49b8e74.jpg

仕様通りのHTTP圧縮であればこれで済みますが、実際のところブラウザ側のバグとも折り合いをつけて行かなければなりません。(だれも使っていないだろうけど)IE6ではHTML以外のコンテンツに圧縮をかけた場合、キャッシュが壊れるという問題があったり、(だれも使っていないだろうけど)Netscapeの古いバージョンではAccept-Encoding送ってくるのにそもそも圧縮をしたらうまく表示できないとかいろいろ問題があります。

そこでサーバ側でUser-Agentをみて圧縮をかけるかどうか判断し、VaryヘッダにUser-Agentも追加するのがよくある設定です

Vary: Accept-Encoding, User-Agent

Apacheのmod_deflateのドキュメントに書いてあるものがまさにそんな感じです。ただしIE6のバグについては対応していないようです

# Insert filter
SetOutputFilter DEFLATE

# Netscape 4.x has some problems...
BrowserMatch ^Mozilla/4 gzip-only-text/html

# Netscape 4.06-4.08 have some more problems
BrowserMatch ^Mozilla/4\.0[678] no-gzip

# MSIE masquerades as Netscape, but it is fine
BrowserMatch \bMSIE !no-gzip !gzip-only-text/html
# Don't compress images
SetEnvIfNoCase Request_URI \
    \.(?:gif|jpe?g|png)$ no-gzip dont-vary

# Make sure proxies don't deliver the wrong content
Header append Vary User-Agent env=!dont-vary

環境変数no-gzipが有効な場合、圧縮はされず、gzip-only-text/htmlが有効な場合はContent-Typeがtext/htmlの場合にだけdeflateが有効となります。Apacheの設定ではさらにgifやjpegは圧縮をしないようにno-gzipし、最後に画像以外にはVaryにUser-Agentを追加しています

IE6の件も含めると

# IE6はMozilla/4
BrowserMatch ^Mozilla/4 gzip-only-text/html
# Netscapehふるいの
BrowserMatch ^Mozilla/4\.0[678] no-gzip
# IE7,8はMozilla/4だけど問題がない。IE9はMozilla/5らしい
BrowserMatch \bMSIE\s(7|8) !no-gzip !gzip-only-text/html
Header append Vary User-Agent
AddOutputFilterByType DEFLATE text/html text/plain text/css text/xml text/javascript
AddOutputFilterByType DEFLATE application/javascript application/x-javascript

こんな感じに設定するのがいいんじゃないなぁと思っている。画像にもVary: UAつくけど

んで、Plack::Middleware::Deflater

CPANにあるバージョン0.03は圧縮とVary: Accept-Encodingの追加のみの機能しかないので、そこに圧縮をかけるContent-Typeの選択、Vary: User-Agentの付加、no-gzip, gzip-only-text/htmlと同じ動きをする環境変数の導入をしてみた。ソースコードはマージされてない部分があるので、僕のgithubのforkにて。

使う時はこんな感じ

enable sub {
    my $app = shift;
      sub {
          my $env = shift;
          my $ua = $env->{HTTP_USER_AGENT} || '';
          # Netscape has some problem
          $env->{"psgix.compress-only-text/html"} = 1 if $ua =~ m!^Mozilla/4!;
          # Netscape 4.06-4.08 have some more problems
          $env->{"psgix.no-compress"} = 1 if $ua =~ m!^Mozilla/4\.0[678]!;
          # MSIE (7|8) masquerades as Netscape, but it is fine
          if ( $ua =~ m!\bMSIE (?:7|8)! ) {
              $env->{"psgix.no-compress"} = 0;
              $env->{"psgix.compress-only-text/html"} = 0;
          }
          $app->($env);
      }
  };
  enable "Deflater",
      content_type => ['text/css','text/html','text/javascript','application/javascript'],
      vary_user_agent => 1;

Plack::Builder::Conditionalsの時に紹介してるけど、

builder {
    enable match_if browser(qr!^Mozilla/4!), 'ForceEnv',
        "psgix.compress-only-text/html" => 1;
    enable match_if browser(qr!^Mozilla/4\.0[678]!), 'ForceEnv',
        "psgix.no-compress" => 1;
    enable match_if browser(qr!\bMSIE (?:7|8)!), 'ForceEnv',
        "psgix.no-compress" => 0,
        "psgix.compress-only-text/html" => 0;
    enable "Deflater",
        content_type => ['text/html','text/css','text/plain','text/javascript','application/javascript'],
        vary_user_agent => 1;
    $app
}

こんな感じでも書けますね。簡単になったのかどうなのか分からないですが。

このBKくさい設定をデフォルトにしたPlack::Middleware::Deflater::Presetsとかあればいいのかしらne

たくさんのMySQLサーバを運用している際に、一意なserver-idを生成するルールを作っておくと急な負荷上昇でslaveサーバを追加しないと行けなくなった時に、masterや他のslaveのserver-idを調べてユニークなidを考える手間を減らす事ができます。

自分はプライベートIPアドレスの第三オクテットと、第四オクテットを使い

server-id = 第三オクテット*256 + 第四オクテット

としています。192.168.67.135 なら

server-id = 67*256 + 135 = 17287

です。/16 以下のネットワークならIDが被る心配ありません。セットアップツールなどで自動でmy.cnfに入れてしまうこともできると思います。

もし /16 ネットワークよりも大きなネットワークなら第一、第二オクテットも計算に含めればいいんじゃないでしょうか。server-idは、1から2^32-1の整数なのでIPv4なら被る心配ありません。

ちなみに、Tokyo Tyrant、Kyoto Tycoonのserver IDは2^16-1までなので上の第三、第四オクテットを使う方法が使えます。というかTokyo Tyrantのserver idが16bitに制限された時に平林さんに教えてもらった方法です^^

OrePANを作っていて一番難しいのはディストリビューションからモジュールのパッケージ名とバージョンを抜き出す部分。ExtUtils::MakeMakerModule::MetadataPAUSEのソースを参考にして書いているところ。

例えば、Log::Minimalであれば、パッケージを展開して

$ tar zxf Log-Minimal-0.04.tar.gz
$ cd Log-Minimal-0.04
$ find -name "*.pm" | grep "VERSION"

のような処理をします。(実際はperl)

これが一筋縄ではいかない作業でかなり苦労している

最初に引っかかったのがcommon::sense。これは*.pmファイルがない。実際にはsense.pm.PLというファイルがあり、実行することでperlのバージョンにあったモジュールを作りだす。

まずこれに対応するために、対象とするファイルを

\.pm(\.PL)$

この正規表現にする必要がある。また、sense.pm.PLではDATA以下にpackageとVERSIONがあるので、DATA以下もパースしなければならない。Module::Metadataではこれに対応できなかった。

次に実行しないとバージョンがわからないタイプ。Encodeでは

our $VERSION = sprintf "%d.%02d", q$Revision: 2.42 $ =~ /(\d+)/g;

と、VERSION行が書かれている。このような場合はバージョン行を特定したあと、evalで実行してバージョンを得る必要がある。ExtUtils::MakeMakerだと

    if ( m{(?<!\\) ([\$*]) (([\w\:\']*) \bVERSION)\b .* =}x ) {
        my $eval = qq{
            package ExtUtils::MakeMaker::_version;
            no strict;
            BEGIN { eval {
                undef *version;
                require version;
                "version"->import;
            } }
            local $1$2;
            \$$2=undef;
            do {
                $_
            };
            \$$2;
        };
        local $^W = 0;
        $result = eval($eval);  ## no critic
}

こうやって実行している。OrePANではこのコードをコピーして使ってる

DBIもはまったものの一つ

# $Id: DBI.pm 14568 2010-12-14 15:23:58Z mjevans $
# vim: ts=8:sw=4:et
#
# Copyright (c) 1994-2010  Tim Bunce  Ireland
#
# See COPYRIGHT section in pod text below for usage and distribution rights.
#

require 5.008_001;

BEGIN {
$DBI::VERSION = "1.616"; # ==> ALSO update the version in the pod text below!
}

これがDBI.pmの頭からのコピー。package宣言より先にpackage名を含んだVERSIONが来ている。なんとなくVERSION宣言がpackage宣言よりあとにあることを期待してコードを書いていると裏切られる例。

どうやって対応したかというと、

while (<$fh>) {
    if ( m{^ \s* package \s+ (\w[\w\:\']*) (?: \s+ (v?[0-9._]+) \s*)? (?:\s+)?;  }x ) {
        push @pkgs, [$1, $2];
    }
    elsif ( m{(?<!\\) ([\$*]) (([\w\:\']*) \bVERSION)\b .* =}x ) {
        .. 実行する部分 ..
        push @pkgs, [$package, $version] if $package;
        $pkgs[-1]->[1] = $version;
    }
}

とりあえず見つけた順に配列に追加していって最後に判断する。package名がないVERSIONの場合はおそらくその前にpackage宣言があると仮定して、配列の一番最後のversionとして利用する。

DBIがそれなんだけど、1つのモジュール内で複数のpackage宣言がある場合、配列のなかから適したpackage名を選ぶ必要がある。そのために、OrePANではModule::Metadataを参考にファイル名と照らし合わせている

my $basename  = fileparse("$parsefile");
$basename =~ s/\..+$//;
my @candidates = sort { !$a->[1] <=> !$b->[1] }
    grep { $_->[0] =~ m/$basename$/  } @pkgs;
return @{$candidates[0]} if @candidates;

ぱっとみ分かりにくコードだけど、ファイル名と一致するモジュールをgrepして、バージョンがあるものを先に取り出している。そして見つかったものの最初を採用している。

ここまでやってきてまだ解決できないことがいくつかある。例えば Module::Setup::Flavor::PBP。コード中にバージョン名はなく、DATA以下のモジュールのテンプレート(フレーバ)に

package Module::Setup::Flavor::PBP;
use strict;
use warnings;
use base 'Module::Setup::Flavor::SelectVC';

1;
__DATA__
...
---
file: lib/____var-module_path-var____.pm
template: |
  package [% module %];

  use version; our $VERSION = qv('0.0.1');

このVERSION名を拾ってしまう。実際CPANでもそうなってる!

そしてこういうのも難しい(実際にみたわけではない

package Hoge;

use strict;
use warnings;

{
    package Hoge::Foo;
    our %foo = ();
}

our $VERSION = 0.01;

1;

こうなると構文を解析するか、実行してしまうしかないんじゃないかと思う。

かわった書き方をしたモジュールをCPANにアップロードした際、自分が指定したバージョンでインデックス登録されているか届くメールで確認したり、CPAN Searchで確認するのがいいかもしれない

これまでサービスで使うCPANモジュールの管理と言えばrpm/debパッケージが主流だと思いますが、最近ではperlbrewで使いたいバージョンのperlをインストールすることもあり、rpm/debでは対応できなくなってきています。

perlbrewのperlに対してプロジェクトが依存しているCPANモジュールを入れる際、

$ perlbrew switch perl-5.12.3
$ cd /path/to/MyProj
$ perl Makefile.PL
$ cpanm --installdeps .

などと cpanm —installdeps が使えます。

しかし、これではサーバを新規セットアップするたびに、最新のモジュールが入ることになりサーバ間でモジュールのバージョンの差異が生まれる可能性があります。

そこで対応策として考えられるのが開発サーバで—installdepsを実行し、perlbrewのディレクトリごと本番サーバにrsyncする方法です。しかしこの方法はOSのアーキテクチャ変更やメジャーバージョンの変更、ディストリビューションの変更に弱いという問題があります。もしアーキテクチャが変更された場合は本番サーバ、開発サーバの両方用意してperlbrewとCPANモジュールを入れ直す必要がありますが、その際に元の開発サーバと同じバージョンのモジュールが入る保証はありません。

そこで、プロジェクトが依存しているCPANモジュール(とそのCPANモジュールが依存しているモジュール)を全てローカルにダウンロードし、プロジェクトのディレクトリ内で外部依存が無いCPANの部分ミラーを作成し、cpanmでそのミラーだけからモジュールをインストールするという手段を考えました。

まず、DarkPAN(CPAN for intranet) の管理ツールであるOrePANをインストールします。変更がまだあるので自分のforkから

$ cpanm https://github.com/kazeburo/OrePAN/tarball/master

これで、orepan.pl と orepan_index.pl というのが入ります。このコマンドでローカルのCPANミラーのindexを生成できます。

次に、依存モジュールのdownloadだけを行うオプションを追加したcpanmを持ってきます。適当patchなので、これも自分のforkから

$ curl -LO https://github.com/kazeburo/cpanminus/raw/master/cpanm > cpanm
$ chmod +x ./cpanm

このcpanmを使って依存モジュールのダウンロードを行います

$ cwd
/path/to/MyProj
$ perl Makefile.PL
$./cpanm --dpan=./cpan --installdeps .
--> Working on .
Configuring /path/to/MyProj ... OK
==> Found dependencies: Router::Simple::Sinatraish, Tiffany, Module::Find
--> Working on Router::Simple::Sinatraish
Fetching http://search.cpan.org/CPAN/authors/id/T/TO/TOKUHIROM/Router-Simple-Sinatraish-0.02.tar.gz ... OK
Configuring Router-Simple-Sinatraish-0.02 ... OK
Copy to /path/to/MyProj/cpan/authors/id/T/TO/TOKUHIROM/Router-Simple-Sinatraish-0.02.tar.gz
--> Working on Tiffany
Fetching http://search.cpan.org/CPAN/authors/id/T/TO/TOKUHIROM/Tiffany-0.03.tar.gz ... OK
Configuring Tiffany-0.03 ... OK
Copy to /path/to/MyProj/cpan/authors/id/T/TO/TOKUHIROM/Tiffany-0.03.tar.gz
..

extlibにモジュールを導入する際は、-l や -L オプションも一緒に使います

$./cpanm -l extlib —dpan=./cpan —installdeps .

./cpanディレクトリにCPANと同じディレクトリ構造で依存モジュールのダウンロードしていきます。インストールはされません

$ find ./cpan -name "*.tar.gz" 
./cpan/authors/id/C/CR/CRENZ/Module-Find-0.10.tar.gz
./cpan/authors/id/T/TO/TOKUHIROM/Router-Simple-Sinatraish-0.02.tar.gz
./cpan/authors/id/T/TO/TOKUHIROM/Tiffany-0.03.tar.gz

全てのダウンロードが終わったら、今度はindexを作成します。先ほどインストールしたOrePANを使います

$ orepan_index.pl --repository ./cpan
...
2011-02-03T17:24:45 [INFO] Save cpan/modules/02packages.details.txt.gz at /...

これでcpan/modules以下にindexファイルができ、CPANミラーの準備ができました。このミラーのディレクトリはVCSでプロジェクトと同様に管理してしまうのがよさげです。

このミラーからモジュールを導入する場合は

$ cpanm --mirror file:///path/to/MyProj/cpan --mirror-only --installdeps .

このようにmirrorとmirror-onlyのオプションを使います。これで外部にモジュールを取りにいくことはなく、ローカルのみから依存モジュールを一括で導入可能です。

もし、依存モジュールが増えたり、依存モジュールのバージョンが新しくなった場合は Makefile.PL を更新しもう一度 cpanmを動かします

$ vim Makefile.PL
$ perl Makefile.PL
$ ./cpanm --dpan=./cpan --installdeps .

今度は、cpanmがcpan/modulesディレクトリ中の02packages.details.txt.gzを読み込んで必要なモジュールだけダウンロードします。依存モジュールのどこかで明示的に書かれていない限り勝手にバージョンがあがってしまう事はありません。

そしてindexを再生成します

$ rm -f ./cpan/modules/02packages.details.txt.gz
$ orepan_index.pl --repository ./cpan

モジュールを減らす場合はcpan/authorディレクトリから削除して、同じ様にindexを作り直せば完了です

この方法の良い点として、おまけ的ですが、search.cpan.orgが止まっていたり、グローバルにアクセスできない場所でも問題なく依存モジュールがインストール可能であること、ダウンロードの手間が無いのでインストールがちょっと高速になることも挙げられます。

いかがでしょうかNe。

Plack::Builderのenable_ifよりも簡単に、指定した条件下でmiddlewareを有効にするモジュール」で書いていた Plack::Builder::Conditionals をリリースしました。

ReverseProxyを使う場合は、接続元のIPアドレスを指定するのが必須になりますが、Conditionalsなら簡単に書けます

use Plack::Builder;
use Plack::Builder::Conditionals;

builder {
   enable match_if addr(['192.168.0.0/24','127.0.0.1']), "ReverseProxy";
  $app
};

今日出したバージョン0.02では、User-Agentをチェックするメソッドを追加してます。PlackでHTTPコンテンツ圧縮する場合は、これを利用して

builder {
    # 古いバージョンにはhtml以外に問題がある
    enable match_if browser(qr!^Mozilla/4!), 'ForceEnv',
        "psgix.compress-only-text/html" => 1;
    # Netscapeの4.0x とかもういねぇよ
    enable match_if browser(qr!^Mozilla/4\.0[678]!), 'ForceEnv',
        "psgix.no-compress" => 1;
    # IE7,8はMozilla/4だけど問題はないNe
    enable match_if browser(qr!\bMSIE (?:7|8)!), 'ForceEnv',
        "psgix.no-compress" => 0,
        "psgix.compress-only-text/html" => 0;
    enable "Deflater",
        content_type => ['text/html','text/css','text/plain','text/javascript','application/javascript'],
        vary_user_agent => 1;
    $app
}

ように設定できます。ただしpsgix.no-compressがサポートされているのはまだ僕のgithubのforkだけです。テストしてくれる方募集中

OrePANを弄っているときに、任意のスコープで一時的にchdirして作業後、スコープ外れれば自動で元のディレクトリに戻るモジュールあれば欲しいなと思ったので書いてみた。

use Cwd::Guard;
use File::Temp;
use Path::Class;

my $tmpdir = Path::Class::dir(File::Temp::tempdir());
my $pkgdir;
{
    my $guard = cwd_guard($tmpdir);
    # chdir to $tmpdir

    my($root, @others) = `tar ztf $tarfile`
        or die "Bad archive $tarfile";
    chomp $root;
    $root =~ s{^(.+?)/.*$}{$1};
    system "tar", "zxf", "$tarfile";
    die "Bad archive: $tarfile" if ! -d $root;
    $pkgdir = $tempdir->subdir($root);
}
# back to cwd

my $meta = $pkgdir->file('META.yml')->slurp;

上は、CPANのpkg.tar.gzのファイルをtmpdirで開いてそのpkg内のMETA.ymlを開くようなサンプル。cwd_guardで作業ディレクトリに一時的に移動し、tarの解凍が終わったら、元のディレクトリに戻る感じ。実際OrePANでやってます。

CPANには、既にFile::chdirとFile::pushdっていうほぼ同じ機能をもっているモジュールがあるんだけど、Cwd::Guardのほうが検索しやすいし、実装がシンプルなので(とgfxが言ってた)リリースしました。

Reverse ProxyとApplication Serverの2段構成でWebサービスを運用している場合、mod_deflateなどのHTTPコンテンツ圧縮をどちらでやるのがいいのだろうか

mod_deflatelayer.png

少し考えてみると、Reverse Proxyで一括してコンテンツ圧縮する場合は、圧縮に関する設定を1カ所でまとめられるという利点がある、ただし、Reverse ProxyとApplication Serverとの間は未圧縮で流れるため、この間で通信量が多くなる。

逆にApplication Serverでも圧縮した場合は、圧縮の設定を両方のサーバに書く必要があるが、サーバ間での通信量は減らすことができる。

おそらくWebサービスの規模が小さいうちは、サーバ間での通信量が気になることはないので前者の設定を一カ所にまとめるほうがよく、Application Serverの台数が10台〜20台程度になってラック内、ラック間の通信量が気になって来たらApplication Serverで圧縮を行うのが良いのかな。あと、gzipをかけるCPU負荷は対して大きくないけどApplication Serverのほうが台数増やしやすいので負荷分散も可能かなと。

他に明確にどっちがいい理由とかあるかなぁ。