2011年1月アーカイブ

一度作成した正規表現を大文字小文字区別しないようにしたい。例えば

my $re = qr/^(get|head)$/;

とすると、これは

(?-xism:^(get|head)$)

となり、iオプションを付けてないので「-i」が含まれ、case-sensitiveな正規表現になる。これを後からiオプションを付けたいと思ったんだけど良い方法がなさそうなので、正規表現を正規表現で書き換えるという暴挙にでてみた。

$re =~ s/\(\?([^-]*)(\-[^i]*)i([^:]*?:)/?i$1$2$3/g;

これで、$re は

(?i-xsm:^(get|head)$)

と変更される。使う時は

$ENV{HTTP_METHOD} =~ m!$re!; (ここでiオプションは必要ない

こんな事しねぇよという突っ込みも含め、もっと良い方法はないのだろうか。。

追記:

lcでもいけるか

$re = lc($re);
lc($ENV{HTTP_METHOD) =~ m!$re!;

PSGIアプリケーションをリバースプロキシ下で使う際の静的コンテンツの配信は、リバースプロキシー側で行う事が多いと思うのですが、こうやるのが良いんじゃないかという案。

プロジェクトのトップディレクトリにhtdocsを作成し、その中にfavicon.ico、staticディレクトリをいれます

$ ls -l /path/to/myproj/htdocs
Total xxx
-rw-r--r--  1 user  user  1406 Jun 30  2010 favicon.ico
drwxr-xr-x  6 user  user   204 Jan 21 16:39 static
$ ls -l /path/to/myproj/htdocs/static
Total xxx
drwxr-xr-x   4 user  user   136 Jun 30  2010 css
drwxr-xr-x  11 user  user   374 Jan 21 16:39 js
drwxr-xr-x   4 user  user   136 Jul 22  2010 image

static下にcssやjs、画像を格納します。

psgiファイルではPlack::Middleware::Staticで下記の様に指定します。

use Plack::Builder;

builder {
    enable 'Plack::Middleware::Static',
        path => qr{^/(favicon\.ico$|static/)},
        root => $config->root_dir->subdir('htdocs')
    $app
};

htdocsディレクトリをrootディレクトリとして、/favicon.icoと/staticをマッピングします。htdocs以下に直接cssやjsディレクトリを置いてしまうとfavicon.icoやcrossdomain.xmlを置く場所がなくなるのでこっちのほうがおすすめかと。

本番環境では上位のリバースプロキシにてfaviconやstaticファイルを配信するので、Apacheであれば

<VirtualHost _default_:80>
    <Proxy balancer://myproj-apps>
        BalancerMember http://127.0.0.1:5000
        ...
    </Proxy>

    DocumentRoot /path/to/myproj/htdocs
    <Directory /path/to/myproj/htdocs>
        Order allow,deny
        Allow from all
    </Direcotry>

    RewriteEngine on
    RewriteCond REQUEST_URI ! ^/favicon\.ico$
    RewriteCond REQUEST_URI ! ^/static/
    RewriteRule /(.*)$ balancer://myproj-apps/$1 [P]
</VirtualHost>

などと、DocumentRootにhtdocsディレクトリを指定して、静的ファイル以外をアプリケーションサーバに振り分けるようRewriteCond/RewriteRuleを書きます。crossdomain.xmlなどが必要な場合はそれもくわえます。

ProxyPassで書きたい場合は、

ProxyPass /favicon.ico !
ProxyPass /static/ !
ProxyPass / balancer://myproj-apps/

となります。path の後ろに「!」を付けるとProxyしなくなります。

htdocs直下にファイルを置いた場合、設定ファイルの書き換えが必要になりますが、不要なものが置かれないようアプリケーション開発者やオペレーションエンジニアが確認できるのでいいと思っています。

rpmをつくりつつちょいApache期

2006年に書いていたpatchが当てる事ができなくなっていたので、直してみた。

mod_proxy_balancerで、接続ができなくなったサーバに対して一定間隔で再接続を試みるretryオプションがあります。これはretryで指定した秒数間隔でバックエンドに接続をします。

このpatchをあてることで、指定した回数だけ短い時間でretryを試みて、それを超えると通常のretry間隔にすることができます。何がうれしいかというと、バックエンドのサーバをDeployなどの理由で再起動した際になるべく高速に復帰しつつ、万が一障害で接続できなくなった時にサービスへの影響を最小限にできます。

<Proxy balancer://mycluster>
    BalancerMember http://192.168.67.10:5000 connectiontimeout=5 retry=60 quick_retry=2 quick_retry_max=10 
    BalancerMember http://192.168.67.11:5000 connectiontimeout=5 retry=60 quick_retry=2 quick_retry_max=10 
</Proxy>

図にするとこんな感じ。

quick_retry.png

patchはgithubに置いておいた。これまで作ったApacheへのpatchも一緒にあります。

https://github.com/kazeburo/apache-httpd-patch

実際のところ、balancer-managerにDeployツールがアクセスして自動でサーバのdrop/addを行うとかがいいと思うし、最近ではServer::Starter使ってhot-deployもできるので保険的な使い方しか使い道なさそうな2011年冬。

Apacheのmod_expiresは、既にレスポンスにExpiresヘッダが存在すると、それを変更しないので。そこで強制的に上書きできる様、patchをあててみた。

--- httpd-2.2.17.orig/modules/metadata/mod_expires.c    2008-11-12 04:59:22.000000000 +0900
+++ httpd-2.2.17/modules/metadata/mod_expires.c 2011-01-20 11:47:03.000000000 +0900
@@ -472,6 +472,11 @@
         expiry = apr_table_get(r->headers_out, "Expires");
         t = r->headers_out;
     }
+    if ( apr_table_get(r->subprocess_env, "override_expires") ) {
+        expiry = NULL;
+        apr_table_unset(t,"Expires");
+        apr_table_unset(t,"Cache-Control");
+    }
     if (expiry == NULL) {
         /*
          * No expiration has been set, so we can apply any managed by

override_expiresという環境変数をセットすると強制上書きモードになります

SetEnv override_expires 1
ExpiresActive On
ExpiresByType text/html "access plus 1 days"

これでReverseProxyで利用している時など、後ろからへっぽこなExpiresとCache-Controlが来たとき(例えば後ろがSquidだったとき)に上書きしてレスポンスすることができます。強制上書きモードでは、Cache-Controlのprivateやpublicなどのmax-age以外のオプションは消えてしまうので必要な場合はmod_headersモジュールなどで適宜追加するといいかと思います

nginxのHttpHeadersModuleでは、特にチェック無く上書きしてしまうようだ。RFC的にはExpiresを保持するのが正しいのかな

大規模なサイトでは、ORMではSQLレベルのチューニングがしにくいので(Class::DBIで苦労してる)、SQLを直接書くことが多いと思います。その際SQLをいろいろなモジュールに書いてしまうと、あとからALTERやパーティショニングしようとした時に悲劇となります。できるだけ1つのテーブル・データベースのSQLは1つのモジュールにおさめておく事がおすすめです。

DBIx::Sunnyはそのようなモジュールを書く時に使えそうなライブラリで、SQLを管理すると共にクエリ実行時に渡された値のバリデーションもできる様になっています。まだまだ実験中ですが、githubにソースコードあります。

使い方はこんな感じ。まずSQLを書くクラスをDBIx::Sunnyを継承して作ります。query、select_one、select_row、select_allってのがSQLを管理する用の暮らすメソッドとして提供されています。queryは更新系のクエリ用、select_oneは1行目1個目のカラムを取得、select_rowは1行目を取得、select_allは全ての行を取得するメソッドして作成されます。

package MyProj::Data;

use parent 'DBIx::Sunny';
use Mouse::Util::TypeConstraints;

subtype 'Natural'
    => as 'Int'
    => where { $_ > 0 };

subtype 'Uint'
    => as 'Int'
    => where { $_ >= 0 };

no Mouse::Util::TypeConstraints;

__PACKAGE__->query(
    'add_fuga',
    subject => 'Str',
    body => 'Str',
    'INSERT INTO fuga (subject,body) VALUES (?,?)'
);

__PACKAGE__->select_one(
    'count_fuga',
    'SELECT COUNT(*) FROM fuga'
);

__PACKAGE__->select_row(
    'get_data',
    id => 'Natural',
    'SELECT * FROM fuga WHERE id = ?'
);

__PACKAGE__->select_all(
    'get_data_list',
    offset => { isa => 'Uint', default => 0 },
    limit => { isa => 'Uint', default => 10 },
    'SELECT * FROM fuga ORDER BY id DESC LIMIT ?,?'
);

1;

4つのクラスメソッドには、作成するメソッド名、バリデーションルール、SQLの順に渡します。バリデーションルールはData::Validatorの形式となります。なので、Mouse::Util::TypeConstraintsで独自の型を作成して、使う事ができます。DSLっぽいインターフェイスもあれば便利そう。

使うときは、別途接続済みのDBIハンドラをSQLのクラスに渡します。

use MyProj::Data;
use DBI;

# RaiseErrorはおすすめ
my $dbh = DBI->connect('dbi:mysql(RaiseError=>1,PrintError=>0):test');
my $db = MyProj::Data->new(
    dbh => $dbh,
    readonly => 0 # 1にするとqueryメソッドがdieするようになる
);

$result = $db->add_fuga( subject => $subject, body => $body );
ok($result > 0);

# last_insert_idも取れる
my $last_insert_id = $db->last_insert_id;

# カウント
my $count = $db->count_fuga();

# ID指定で1行取得
my $row = $db->get_data( id => 1 );

my $row = $db->get_data( id => 'abc' ); #エラーになる
my $row = $db->get_data( id => 0 ); #エラーになる

# limit, offset付きで取得
my $rows = $db->get_data_list(); LIMIT 0,10
my $rows = $db->get_data_list(limit => 20); LIMIT 0,20
my $rows = $db->get_data_list(offset => 20); LIMIT 20,10

この他に、DBIx::TransactionManagerを利用したトランザクション機能もあります。

基本誰得モジュールなので自分で使ってみつつ、IN (…) とか bulk_insert とかどうしようかなと考え中。ご意見お待ちしています

おみくじで大大吉引きました。これで運を使い果たした気がします

DSC01391.jpg

今年も、様々なシステム上の課題に取り組んでいきたいと思います。