2010年12月アーカイブ

こうやって並べてみると、転職したことで課題ががらりと変わったのがわかるような気がする今年のエントリまとめ。

1月に書いた2つのエントリー。mixiアプリのバックエンドで大変だったころ。

データベースサーバを複数台構成とか2010年代には流行らない

アプリケーションがマルチスレッドでもマルチコアCPUを活かせない件

んで、5月にミクシィ退社。退職しましたエントリニーにブクマいっぱい。これがブクマ数一番多いとかじゃなくてよかった。

株式会社ミクシィを退職しました

退職直前まで作っていたリソース監視ツールをオープンソースにて公開。転職後のライブドアにてさらにブラッシュアップしていく事になります。

CloudForecastっていうリソース監視のツール/フレームワーク作った

6月にライブドアにJOIN。もうちょっと有休消化すればよかったなといまさら反省。

株式会社ライブドアに入社しました

入社後にCloudForecastの環境を整えながら、Plackアプリケーションのステータスをどうやってとるかで次のエントリー。7月末にCPAN公開。社内でも使っています

StarmanやStarletでmod_statusっぽい情報を得る簡易版Plack::Middleware::ServerStatus

8月、9月はmixiの大規模障害により、memcachedがアツかった。

CloudForecastでmemcachedのコネクション数をモニタリング

技術評論社のgihyo.jpにてmemcachedの(再)連載をします

Re: @kazuho: handlersocket plugin や mycached を使えば memcached は不要か、それとも使うべきケースがあるか。考察せよ [10点]

Apacheの設定を見直しながら、(Min|Max)SpareServersの動きをまとめたのが次。今年のブクマ数1位。今でもアクセスが多い。

プロのサーバ管理者がApacheのStartServers, (Min|Max)SpareServers, MaxClientsを同じにする理由

9月10月は勉強会・イベントで講演をさせて頂いた。主にmemcachedとCloudForecast。

gumiStudy#2 で memcached の運用について喋ってきた

Shibuya.pm#14 で memcachedの運用について発表しました

YAPC::Asia 2010 Tokyo で CloudForecast について喋ってきた

Log::MinimalとScope::Containerは運用から欲しくなって書いたモジュール。データベースの接続管理とエラーログの吐き出し方について考えた。Scope::Containerは某Blogサービスに導入済み。Log::MInimalは布教がまだまだ足りない

運用におけるエラーログの重要性もしくはLog::Minimalってモジュール書いた話

Contextの生成・破棄を任意のタイミングで制御可能にする Scope::Container

ログレベルの使い分け方/コミュニケーション手段としてのログ

CloudForecastの改善をいろいろやっていましたが、中でも大きかった変更は複数サーバの表示。社内のエンジニアから要望で作ってみたら便利すぎて鼻血でた。

CloudForecastに複数サーバ一括表示とデータ取得状態の確認機能付きました

ORMではないDBフレームワークを作りたいなぁという思いで調査中でてきたのが、DBIx::Printf。使っている方いるのかな。

LIKE節におけるエスケープ文字とDBIx::PrintfもしくはDBIx::Printf::Named

某Blogサービスのデータベースの負荷対策をしていく中で一つの案。このあと実際にこのようなコードをサービスに投入し、大きな効果がありました。

memcachedにおけるキャッシュシステムの Thundering Herd 問題への対策案

来年もよろしくお願いします。

Plack::Middleware::Log::Minimal v0.01ではLog::Minimalのログ出力方法などの設定を一切できませんでしたが、v0.02でいくつか設定ができるようになりました。Log::Minimal v0.04でサポートした自動シリアライズも使えます。

まず、自動シリアライズの機能。

builder {
    enable "Plack::Middleware::Log::Minimal", autodump => 1;
    sub {
        my $env = shift;
        warnf("warning message");
        ["200",[ 'Content-Type' => 'text/plain' ],["OK"]];
    };
};

このように、autodumpを設定すると、Log::Minimal::AUTODUMPが有効になります。

二つ目は、xaicronさんにpatchもらった、LOG_LEVEL。

builder {
    enable "Plack::Middleware::Log::Minimal",
        loglevel => 'WARN';
    sub {
        my $env = shift;
        infof("information message");  #出力されない
        warnf("warning message"); 
        ["200",[ 'Content-Type' => 'text/plain' ],["OK"]];
    };
};

DEBUG,INFO,WARN,CRITICALとNONEが指定できます。NONEを指定すると何も出力されません。

最後は、formatter。ログを出力する前のhook的に使えます。

enable 'Log::Minimal',
    formatter => sub {
        my ($env, $time, $type, $message, $trace, $raw_message) = @_;
        sprintf "%s [%s] [%s] %s at %s\n", $time, $type, $env->{REQUEST_URI}, $message, $trace;
    });

formatterにはcoderefを指定します。PSGIのenv、時間、ログレベル、色付きメッセージ、トレース情報、色なしメッセージがcoderefに渡されるので、自由にカスタマイズできます。もし、multilogによるロギングをしていて、時間情報が必要ないなら、

formatter => sub {
    my ($env, $time, $type, $message, $trace, $raw_message) = @_;
    # [type] [uri] [pid] message at trace
    sprintf "[%s] [%s] [%s] %s at %s\n", $type, $env->{REQUEST_URI}, $$, $raw_message, $trace;
});

のようにして消してしまえば良さそうです。この辺は環境変数のPLACK_ENVなどをみて、切り替えるとよさげかもしれません。出力方法を弄るだけではなく、ここでsyslogに飛ばすなどのこともできると思います。

先月にData::Dumperサポートした時には自動でオブジェクトやリファンレスをDumpしていませんでしたが、0.04で自動で行うようなオプションを設けました。

普通にログメッセージにリファレンスを渡すと

warnf({ foo => bar}); # HASH(0x100804ed0)
warnf("foo: %s",{ foo => bar}); # foo: HASH(0x100804ed0)

とrefaddrが返るだけですが、AUTODUMPフラグを有効にすると

local $Log::Minimal::AUTODUMP = 1;
warnf({foo=>'bar'}); # {'foo'=>'bar'}
warnf("dump is %s", {foo=>'bar'}); #dump is {'foo'=>'bar'}

と、Data::Dumperでシリアライズして出力します。

前回悩んでいたところの、演算子をoverloadしているオブジェクトが渡された場合ですが、sprintfのコンテキストに応じて動きを変えることで解決しました。ストリングコンテキストでは文字列化→数値化の順に試し、両方存在しない場合はdumpし、数値コンテキストでは、数値化→文字列化の順に実行し、どちらもなければそのままsprintfに投げます。

まず、リファレンスを渡した際ですが、

local $Log::Minimal::AUTODUMP = 1;
my $hashref = {foo=>'bar'};
warnf("%s : %d", $hashref, $hashref);
# => 2010-12-29T10:38:47 [WARN] {'foo' => 'bar'} : 22768536 at -e line 1

この様に、ストリングコンテキストの時はDumpされ、数値コンテキストでは、refaddrとなります。演算子オーバーロードがないオブジェクトも同様の動きです

次に、文字列変換が定義されているオブジェクトの場合、

local $Log::Minimal::AUTODUMP = 1;
my $uri = URI->new("https://blog.nomadscafe.jp/");
warnf("%s : %d", $uri, $uri);
# => Argument "https://blog.nomadscafe.jp/" isn't numeric in sprintf at /usr/.../Log/Minimal.pm line 96.
# => 2010-12-29T10:41:38 [WARN] https://blog.nomadscafe.jp/:0 at -e line 1

ストリングコンテキストはそのまま、stringfyされ出力されます、数値コンテキストでは数値化が定義されていないので、文字列化してsprintfに渡します。最終的には数値ではないので、warningが吐かれます。

最後に、数値変換ができる場合、

package Num;
use overload '0+' => sub { ${$_[0]} };
sub new {
    my ($class, $value) = @_;
    bless \$value, $class;
}

package main;

local $Log::Minimal::AUTODUMP = 1;
my $num = Num->new(654321);
warnf("%s : %d", $num, $num);
# => 2010-12-29T10:51:38 [WARN] 654321 : 654321 at /tmp/hoge.pl line 12

%sの場合に、文字列化がないので数値化して利用し、%dでは数値化が使われます。結果同じ値が返されています。

説明すると面倒ですが、使う分には問題なく使えると思います。引き続きご意見募集中。

前のエントリーで書いた Thundering Herd 問題への対策案 で、重いクエリを排他制御すればいいのではないかというご意見も頂いたので、それをmemcachedで実現するようなモジュールを書いてみた。

下のモジュールではmemcachedのaddを使って制御します。addが成功したときだけ渡されたコールバックを実行し、ロックを得ることができない場合はaddに失敗するので、その場合はsleepして処理をやりなおす。他の排他制御するモジュールと違い、キャッシュ専用なので、排他制御の前にキャッシュにgetを行い、sleep中に既にキャッシュができていないかを確認するようになっている。

コードはなんの確認もしてないのであしからず

package Cache::ExclusiveControl;

use Try::Tiny;
use Time::HiRes;
use Class::Accessor::Lite (
    rw  => [ qw(cache poll_time max_wait parallel) ],
);

sub new {
    my $class = shift;
    my %args = (
        poll_time => 0.1,
        max_wait => 10,
        parallel => 1,
        @_;
    );
    bless \%args, $class;
}

sub get_or_set {
    my ($self, $key, $cb, $expires ) = @_;

    my $value;
    my $loop = 0;

    while  ( 1 ) {
        my $lockkey = $key . "::lock::" . int(rand($self->parallel));
        $value = $memcached->get($key);
        last if $value;

        my $locked = $memcached->add($lockkey, 1, $self->max_wait );
        if ( $locked ) {
            try {
                $value = $cb->();
                $memcached->set( $key, $value, $expires );
            }
            catch {
                die $_;
            }
            finally {
                $memcached->delete( $lockkey );
            };
        }

        $loop++;     
        die "timeout" if $loop >= $self->max_wait / $self->poll_time;
        Time::HiRes::sleep( $self->poll_time );
    }

    return $value;
}

1;

package main;

my $c_ec = Cache::ExclusiveControl->new(
    max_wait => 10, #最大10秒待つ。10秒超えたらdie
    poll_time => 0.05, #lockを確認する間隔
    parallel => 1, #callbackが並行動作できる数
    cache => Cache::Memcached::Fast->new(...)
);

# get_or_set( key, callback, expire );
my $value = $c_ec->get_or_set(
    'cache_may_thunder',
    sub {
        my $sth = $dbh->prepare("selet super heavy sql");
        $sth->execute(..);
        my $row = $sth->fetchrow_hashref;
        return $row;
    },
    3600, #expirer
);

おそらく、addをサポートするキャッシュシステム全てで使えるはず。あと、Cache::Memcached::Semaphore というモジュールはあるのだが、これは排他制御の部分しか提供してくれない。

block動作をするような排他制御システムを使う場合は、lockを獲得した後すぐに重いクエリを動かすのではなく、まずキャッシュへgetを行うのが必須だと思われます。

キャッシュシステムの Thundering Herd 問題とは、

通常、キャッシュに格納されるデータは、それぞれ単一の生存時間をもっています。問題は、頻繁にアクセスされるキャッシュデータがエクスパイアした際に発生します。データがエクスパイヤした瞬間から、並行に走る複数のアプリケーションロジックがミスヒットを検知し、いずれかのプロセスがキャッシュデータを格納するまでの間、同一のリクエストが多数、バックエンドに飛んでしまうのです。

という問題。クエリが重かったりするとそれだけでシステムに致命的な負荷を与えてしまい、キャッシュがあるにも関わらずキャッシュが切れたタイミング全体が停止することも考えられます。memcachedでこの問題に対応するため、次のような手段を考えてみました。

まず、保存時に通常のキャッシュと、それよりも指定した秒数Expiresが短いキャッシュを2つmemcachedに対してsetします。getする時はランダムに指定した確率でexpiresが短いキャッシュを見に行きます。これで一定の確率でキャッシュが早く切れるので一斉にバックグラウンドへリクエストが飛ぶことをさけることができます。Expiresの秒数や確率はシステムによって適正値がかなり異なると思われます。キャッシュを削除するときは両方削除するのをお忘れなく。

2カ所に書くので、もちろん、キャッシュの容量は2倍必要になります。

Perlで書くとこんな感じ

package EarlyExpiresCache;

use Class::Accessor::Lite (
    new => 1,
    rw  => [ qw(cache ratio early_expires) ],
);

sub set_ee {
    my ($self, $key, $value, $expires) = @_;
    $self->cache->set($key, $value, $expires);
    $self->cache->set($key . "::ee", $value, $expires - $self->early_expires);
}

sub get_ee {
    my ($self, $key) = @_;
    if ( int(rand($self->ratio)) == 0 ) {
        return $self->cache->get($key."::ee");
    }
    $self->cache->get($key) // $self->cache->get($key."::ee")

}

sub delete_ee {
    my ($self, $key) = @_;
    $self->cache->delete($key);
    $self->cache->delete($key."::ee");
}

package main;

my $eecache = EarlyExpiresCache->new(
    early_expires => 60, #sec
    ratio => 10, # 1/10
    cache => Cache::Memcached::Fast->new(...)
);

my $val = $eecache->get_ee('foo');
if ( !$val ) {
    .. heavy sql ..
    $eecache->set_ee('foo', $value, 3600);
}

*追記
せっかく2つ書いているので、get時に、通常のkeyで取れなかったら早くexpiresするkeyで取る様にした。これでデータが冗長化されるので、memcachedが落ちても(落としても)多少大丈夫
*/追記

memcachedの空間効率を気にする場合は、cacheの値にexpiresを入れ込むのもありですね。

某ブログサービスの某ブログが重いのでその対策になるかなぁと。

SQLのLIKE節では「%」と「_」の2文字をワイルドカードとして利用します。「%」「_ 」を通常の文字として使いたい場合、以下のようにエスケープを行います

% => like 'foobar\%%'
_ => like 'foobar\__'

ただし、デフォルトでbackslashにてエスケープが行われるのは、少し調べたところMySQLとPostgreSQLあたりで、SQLiteやOracleではエスケープされません。別途エスケープ文字を指定する必要があります。それがLIKE .. ESCAPEです

LIKE 'foobar\%%' ESCAPE '\'
LIKE 'foobar$%%' ESCAPE '$'

上のように、エスケープに利用する文字を指定できます。もし、エスケープをしないなら空文字にすれば良いそうです

LIKE '_foobar%' ESCAPE ''

あまりないとは思いますが、ユーザからの入力をLIKE節に使う際に、適切にエスケープしないと、前方一致だったのが、部分一致になってしまいインデックスが利用されずにDBに負荷がかかることが考えられます。その場合、

my $ESCAPE_CHAR = '\\';
my $param = $req->param('search');
$param =~ s![_%]!$ESCAPE_CHAR$&!g;
my $sth = $dbh->prepare("SELECT * FROM mytable WHERE subject like ? ESCAPE ?");
$sth->execute( $param . "%", $ESCAPE_CHAR );

のように書けば安全だと思われます。

kazuhoさんのDBIx::Printfだとこのエスケープ処理を自動でやってくれます。LIKE .. ESCAPEに対応してませんでしたが、patch書いて 0.08 で対応しました。

use DBIx::Printf;
my $sth = $dbh->prepare(
    $dbh->printf("SELECT * FROM mytable WHERE subject like %like(%s%%) ESCAPE '\$'", $req->param('search'))
);
$sth->execute();

%like(..) のところが適切にエスケープされた文字に変換されます。

さらに、named printfに対応したDBIx::Printf::Namedを今日リリースしました。DBIx::Printfのplaceholderを名前で指定できるようになります。

use DBIx::Printf;
my $sth = $dbh->prepare(
    $dbh->printf("SELECT * FROM mytable WHERE category = %(category)d AND subject like %like(%(search)s%%) ESCAPE '\$'",
        {
            category => $req->param('category'),
            search => $req->param('search')
        }
    )
);
$sth->execute();

%like(%(search)s%%) はキモイ。けど、便利になるかもしれないこともないかもしれない