2011年6月アーカイブ

弊社では毎週水曜日はノーエンジニアデーなので、最近はMacbook AirとWIMAX持って外で仕事しています。意外と快適ですが、ここで書くのはサーバの使い方の話です。

ときおり、次のような状況に遭遇することがあります。

  • 開発環境して使っているけど、セットアップをどのように行ったか残っていないので、新サーバへ移動できない
  • 本番環境だけど、セットアップをどのように行ったかわ(ry
  • デプロイ元/管理ツールサーバとして使っているので古いサーバだけど捨てることができない
  • DBがどこから参照されているか管理できていないので、サーバの入れ替えが困難
  • コードがどこから参照が把握できていないので、容易にサーバ構成の変更ができない

椅子^H^H

一度設置したサーバの移動なんてなかなかすることないと思う人はいるかもしれないけど、サーバが何の警告もなしに突然壊れて入れ替える必要がでてくるのはもちろん、インフラ技術が進歩している今、古い環境を残し続けるより電力あたりの性能がいいサーバに入れ替えをしたり、スケーラビリティを求めてデータセンターからクラウドへ移行、クラウドから別のクラウドへの引っ越し、クラウドからデータセンターへ移動しながら、インフラを積極的に最適化を行って行くのが当たり前になっている(なっていくと思う)

個人の開発環境でもより快適な、効率の良い環境を求めて、デスクトップ、ラップトップ、サーバ、クラウドとWorkdirを移動するよね。

サービスの最適化を積極的に行っていくためにも、Webアプリケーションの実行環境、開発環境もいつでも引っ越しができるように、移動がしやすいようにしておきたいものです。そのために以下のような点を気にしてもらえるといいと思います

dotfilesをVCSを使って管理する

エンジニアは自分の環境をカスタマイズする人間ですが、カスタマイズの内容をすべて覚えていたりはしないでしょう。シェルやエディタの設定ファイルはすべてVCSを使って管理していくのがお勧めです。また、本番環境は複数人のエンジニアが触る可能性があるので、カスタマイズを行わなくても最低限の作業ができるようして置きましょう。

実行可能な形式でセットアップのメモを作る

サーバの設定をドキュメントで残すことは必ず行われると信じてますが、意味のあるドキュメントとするためにも、ドキュメント自体をシェルスクリプトにしたり、コピペで実行できるものにしておきたいものです。ドキュメントに『「Foo」と「Bar」をインストールする』と書かれるより、

$ sudo yum -y install Foo Bar

と書いてあれば、そのまま実行でき、ドキュメントの維持のモチベーションにも繋がります

大規模サーバ環境ではPuppetやChefなどを使って構成管理したり再利用可能なフレームワークを利用しますが、それはそれ。小規模なものであれば、設定をシェルスクリプトにして、sshでリモート実行という手でも自動化は十分なはずです。もちろん、作成したメモはVCSやwikiで管理しましょう。Word?Excel?何それおいしいの?

自動化の際に一つ難しいのが、設定ファイルの一部だけを書き換えるという処理です。sedやperlを使って部分的に書き換えるのも手ですが、設定ファイル自体をVCSで管理して、設定スクリプトにてsymlinkを張ってしまうのも方法もありです。

シェルスクリプトにする際のコツですが、

CURRENT=$(cd $(dirname $0) && pwd)

とすると、スクリプトがあるディレクトリがとれるので。シェルスクリプトと設定ファイルを同じディレクトリにおいて

mv /etc/init.d/httpd.conf /etc/init.d/httpd.conf.bak
ln -s $CURRENT/httpd.conf /etc/init.d/httpd.conf

と簡単に書くことができます。絶対パスへの依存は極力なくしていくことが大事

同じスクリプトを繰り返し実行しても問題ないように

if [ ! -L /etc/init.d/httpd.conf ]; then
    mv /etc/init.d/httpd.conf /etc/init.d/httpd.conf.bak
    ln -s $CURRENT/httpd.conf /etc/init.d/httpd.conf
fi

としておくと安全ですね

サービス間の連携は疎結合で

異なるサービスを連携させる際には、互いの関係を疎に保ちましょう。ルールのないコード参照/データベースへの接続はダメゼッタイ!

もしAというサービスがBというサービスから使われる場合、Aに公開用のインターフェイスを設け、Bはそれを利用します。Aを構成変更する場合インターフェイスがこれまで通り動作することだけを保証すればよくなるので問題が減ります。もしインターフェイスの仕様を変更したい場合は、新旧両方のインターフェイスを稼働させ、Bからの(もしかするとCもDも)古いインターフェイスへの接続がなくなったことを確認して、閉じると問題なく仕様変更が可能となります

coservice.png

長時間かかる処理は小さい単位にjobを分割して実行する

実行に数時間かかるようなバッチがある場合、そのバッチが動いている最中はサーバをいじることができません。もしその間に障害があったとして、復旧後に処理を最初からやり直すとするとまた数時間処理が続くことになります。大変ですね

長時間かかるバッチは、処理をretryが可能な小さい単位に分割し、JobQueueを使って全体をつなげるとリスクを減らすことができます。

batch-jobqueue.png

長時間に渡るバッチやデーモンプロセスでは安全にプロセスを停止でき、不整合なく処理を再開できるようにしたいものです。

オペレーションエンジニアに相談する

サーバの使い方で困ったらオペレーションエンジニアに相談しましょう。彼らの経験を元にアーキテクチャを考えたり、セットアップの自動化やサーバ移転作業などいろいろ手伝ってくれるはずです!

2005年12月から運用していました Femo ですが、このたび誠に勝手ながら今月末 2011年6月30日(木)をもちましてサービス終了をさせて頂きました。これまで利用して頂いた方には大変ご迷惑をおかけしますが、ご理解、ご協力をお願いいたします。

femo.jpg

たまーにメモを残すために使ってるLIMILICを自宅サーバからAmazon EC2のマイクロインスタンスに移しました。

limilic.jpg

ボタンのCSSとpaging以外の見た目に変化はありません

移動する際に、Catalyst(Moose以前)とDBIx::Class、Template-Toolkitという今となるとやや分厚い組み合わせでできていたアプリケーションを、自作フレームワーク、DBIx::Sunny::Schema、Xslateという構成に書き直しました。

書き換えあたり、DBIx::Sunny::Schemaのfilter機能をフル活用してORMとの機能差を埋めています。例えば記事のデータを引いてきたときに、更新時間をDateTimeオブジェクトにしたり、ユーザ情報やコメント数を別のクエリを発行して取得するなどをfilterを使って実装してあります

sub inflate_article {
    my ($row, $self) = @_;
    $row->{created_on} = $DTFMT->parse_datetime($row->{created_on});
    $row->{created_on}->set_time_zone("Asia/Tokyo");

    $row->{user} = $self->retrieve_user( id => $row->{user_id} );
    $row->{total_comments} = $self->select_one(q{SELECT COUNT(*) FROM comments WHERE article_id = ?}, $row->{id});

    $row->{can_modify} = sub {
        my $user = shift;
        return if ! $user;
        ...
    };
}

__PACKAGE__->select_row(
    'retrieve_article',
    id => 'Uint',
    q{SELECT * FROM articles WHERE id=?},
    \&inflate_article
);

__PACKAGE__->select_all(
    'recent_articles',
    'offset' => { isa => 'Uint', default => 0 },
    q{SELECT * FROM articles WHERE acl_view_mode = 1 ORDER BY id DESC LIMIT ?,11},
    \&inflate_article
);

クエリの後ろにCodeRefを渡すと取って来たデータに対してそれを実行してくれます。DBから複数行取って来たときはDBIx::Sunny::Scheme側でloop回してくれるのでコードがシンプルにできます。

上のcan_modifyのようにCodeRefを混ぜ込むことで、オブジェクト要らずになるんじゃねと思ったのですが、Xslateでそのまま動かなかったのでちょっと悩んだ。

例えば、

my $mysub = sub { time };
say $tx->render_string('Time is <: $mysub() :>', {mysub => $mysub});
my $myhash = {
    mysub => sub { time }
};
say $tx->render_string('Time is <: $myhash.mysub() :>', {myhash => $myhash});

実行するとこうなる

Time is 1308722386    
Text::Xslate: Undefined method mysub called for HASH(0x100878450) (<string>:1) at hoge.pl line 17
----------------------------------------------------------------------------
Time is <: $myhash.mysub() :>
----------------------------------------------------------------------------
Time is

myhashが単なるhashなので、method呼び出せないということなんだろう。

$tx->render_string('
<: my $mysub = $myhash.mysub :>
Time is <: $mysub() :>
', {myhash => $myhash});

とりあえず、こんな感じで今回は逃げておいたんだけど、普通に括弧つけるだけで呼び出せたらいいなと思いました!

追記 20110622

$myhash.mysub()を許すと簡単に組み込みメソッドが増やせなり、それが嫌なので敢えて今のようになってます!ちなみに($myhash.mysub)()でも行けます! / LIMILICをEC2に移してCatalyst+DBIx:… http://htn.to/4yBeTQless than a minute ago via Hatena Favorite Retweet Reply

とのコメント頂きました。修正します!

サーバが重かった原因は別だったんだけど、某サービスでメールを受信してプログラムを起動する際のコストが大きいんじゃないかという話をしたので、以下のようなことを考えてみた。

qmailを使っている場合、届いたメールをプログラムで処理したい場合は .qmail に

| /path/to/program.pl

と書きます。標準入力にメールの内容が渡され、環境変数等を通して表書き発送者アドレス等が取得できます。postfix でも master.cf にtransportを追加し、mapファイルを変更すると大体同じことができます。

参考: RailsとPostfixで受信メールを処理する方法

ただし、この方法だとメールの受信の度にプログラムを起動(exec)するコストが高くなりがちです。特にデータベースに接続したり、アプリケーションのライブラリを読み込んで行くと起動の負荷が上がって行ってしまいます。そこでメールから起動するコマンドをアプリケーションから切り離して軽量に実装し、常駐デーモン(アプリケーションサーバ)にHTTPやキューを通して渡すということを行うと思います。mod_perlに渡すとか昔つくりました。

ただ、この軽量のプログラムもまだ重いし、エラー処理が面倒だなぁと思っていたので、Maildir をキューとして使って、直接jobqueueで処理してしまう方法を考えたみた。

この方法では、まず任意のディレクトリにメールを配送します。.qmail でも

/path/to/dir/maildir/

とディレクトリ指定できます。最後のスラッシュがないとmailbox形式になってしまうので注意。postfixでもvirutalhost機能を利用すれば同じ設定できると思われます。メール はMaildir 内の new サブディレクトリに保存されます。

受信したメールを処理するjobqueue workerにはこのnewディレクトリを監視し、新着のメールがあれば都度処理を行うループを実装します。これでメール受信の度のforkの必要がなくなるので負荷の削減にも繋がりそうです。またメールの処理中にエラーが起きた時はファイルをそのままにしておくだけであとで再実行できるのが楽です

今回作ったサンプルには、前々回のエントリーで書いていたように、workerは複数プロセス起動し、MaxRequestPerChild機構もいれてみました。実装はgistにて

複数のプロセスで1つのディレクトリを読むので、flockして競合をさけています。またLinuxだとInotify2を使ってディレクトリを監視するので、新着のメールがあった場合瞬時で処理を開始することができます。上の例では何も処理はしていませんが、実際使うなら67行目のtry-catch内にアプリケーションロジックを書くことになると思います。

Maildirでは通常メールを処理するとき、newディレクトリからcurディレクトリにrenameして処理を行うことになってますが、今回は目的が1つなので無視しますた^^;

twitterに思いつきを書いていたところ

@kazeburo qpsmtpdで受けてjob queueに突っ込むとかやってますless than a minute ago via YoruFukurou Favorite Retweet Reply

というご意見を頂きました。qpsmtpdは以前から気になってるけどMTAを入れ替える踏ん切りが付かないところ。実際の運用例があれば参考になるなぁー♡

@satoh_fumiyasu さん参考となる情報ありがとうございました

Log::Minimal v0.08 で2つの機能を追加しました。1つは例外を投げるメソッド、もう1つはメッセージの色付けです。

Plack::Middlewareも同時にアップデートしています

例外を投げるメソッド追加

Log::Minimalの今までの機能は基本的にwarnを吐くものでしたが、新規に追加したメソッドはdieします。

use Log::Minimal;
croakf("Flying Saucer");

これは

2011-06-10T17:41:59 [ERROR] Flying Saucer at sample.pl line 23

というメッセージを吐いてdieします。もちろんcroakffもあります。dieする際のメッセージ内容を変更するには、$Log::Minimal::DIEを変更します

local $Log::Minimal::DIE = sub {
    my ( $time, $type, $message, $trace) = @_;
    die "Died - $message at $trace\n";
};
croakf("Flying Chairs");

ログの色付け

もうひとつ、Plack::Middleware::Log::Minimal でログの色付けをサポートしていましたが、ターミナルでも色付けしたいとhirose31さんから要望があったので、これをLog::Minimal本体に移植しました。

use Log::Minimal;

$ENV{LM_DEBUG}=1;
$Log::Minimal::COLOR=1;

debugf("foo debug");
infof("bar info");
warnf("foo warn");
critf("bar critical");
croakf("Flying Saucer -On The Road");

$Log::Minimal::COLORを真にして、実行すると

log-minimal-color.png

となります。Plack::Middlewareのほうもこっちを利用するように変更してます。

Webアプリケーション内で処理を直列に実行せずにJob Queueに回して非同期に実行することが多くなって来て久しいと思いますが、そのおすすめ構成と気をつけることについてつらつらと。

1) 既存のデータベースをキューとして使う構成例

jobqueue1.png

1つ目はMySQLなどのデータベースをキューとして用いる例。既にアプリケーションで利用しているデータベースにキュー用のテーブルを作成して利用します。データベースを利用したキュー管理の仕組みとしてJonkQudoTheSchwartzなどがPerlでは有名どころです。 依存するミドルウェアが増えないので最もシンプルな構成になると思います。

上記の図ではWorkerはアプリケーション内で実行することで冗長性を確保しますが、キューを格納するデータベースはSPOFになります。しかし、、データベースに障害があった場合キューだけでなくすべてのサービスが停止すると思われますので、取り上げて問題となることはないでしょう。

性能についてですが、Workerがキューを取得する間隔がどうしても1秒やそれ以上になるので、キューを登録してから実行されるまでのレイテンシが大きく、負荷が大きくなるとキューが滞留してしまうことがあるかもしれません。新規にサービスを開始する際はまず依存が少ないこの構成をとって、キューの数を監視しつつ次のアクションを考えるのがおすすめです

2) 専用のメッセージキューを導入する構成例

jobqueue2.png

2つ目は専用のメッセージキューのミドルウェア(Message Broker)を導入する例になります。よく使われるソフトウェアはActiveMQRabbitMQQ4Mあたりでしょうか。新たなミドルウェアを導入する分、システムが複雑になります。

冗長性を確保するため、これらのミドルウェアは2カ所以上で起動しキューは分散して登録します。分散にあたっては単にランダムやランドロビンでサーバを選ぶのではなく、特定のキー(例えばuser_id)に因ってサーバが一意に決まる剰余による分散やConsistent-Hashingを使うと、問題が起きたときにどのサーバがキューを処理したのか追いやすくなります。Workerの作り方に因ってはキューの処理する順も制御できるかもしれません。このためWorkerはlocalhostのメッセージキューだけを参照します。

jobqueue3.png

サービスが大きくなり、キューが捌けないようになった際には上の図のようにメッセージキューとWorkerをセットにしてサーバを増やしていきます。キューを登録する側にて追加したサーバを使うよう設定をするだけでサーバを増やして行くことができるので運用もわかりやすくなります。

3) Workerを作る際に気をつけること

キューを処理するWorkerはPreforkモデルで作成することをお勧めします。サーバがマルチコアになっているのにアプリケーションがシングルプロセスではあまり意味がありませんし、1プロセスで動作していて、キューの処理に時間が掛かると残りのすべてのキューが遅延してしまいます。この上でSignalをハンドリングして、Workerのgraceful shutdownができるようにしましょう。安全にアプリケーションのアップデートや設定変更を行うのに必須です。PerlではParallel::Preforkを使うと、PreforkしつつSignalを受けWorkerを再起動するデーモンが書きやすくなります。

また、ApacheのMaxRequestPerChildのような、一定数のキューを処理したら子プロセスが終了する仕組みが入っているとメモリを多く確保した場合でも、しばらくすれば解放されるので安心です。

Workerではデータベースへのコネクションなどのリソース管理も重要です。Workerプロセスが長い時間データベースへの接続を維持してしまうことで、データベースの最大接続数を使いきってしまったりリソースを無駄に使ってしまうこともあるので、注意が必要です。「1つのキューを処理 = Webサーバへの1リクエストに対する処理」のように捉えて制御することが重要です。

あ、あとログはちゃんと吐かないとsfujiwaraさんが○○にきます

小規模のサービスを如何にスモールスタートするか、そのために各コンポーネントをどうやって配置するのがいいのかという話。個人的な考えも含めて。 efficent_server_operarion.png

大まかな構成は昨年のnekokakさんのYAPC::Asiaでの発表、省サーバ運用と大体同じです。Web/Appに使うサーバ2台、データベース2台です。あとはLBが別にあればそれを、なかったらもう一台(組)必要となります。

Web/Appサーバには、Reverse Proxy、Application Serverがまず配置されます。あとは必要に応じてmemcached、Job Queueのworkerを動かします。ここまでのコンポーネントは2台のサーバ両方に配置し、Active-Activeで動作し冗長性がとれるよう構築します。cronについては、両方のサーバで動かしても問題がない状態が理想ですが、そうでない場合、Web/Appの1台目で動かすというルールを決めて運用します。DAVはおまけで、画像等アップロードする必要がある場合、ここにDAVサーバをたてGreenBucketsなどで管理します。このように、Web/Appサーバには2台以上で動かすことにより冗長性がとれるものを中心に配置していきます。

データベースサーバは、MySQLのMaster-Backupの構成が基本です。backup側をslaveとして参照を行うことは可用性を下げるだけなので行いません。Kyoto Tycoonは主にsession管理用に使います。Web/Appサーバのmemcachedがスケールアウト/インによって追加削除されて行くのにたいして、Kyoto Tycoonは1台のmasterとbackupの構成をとってデータの永続化をします。Kyoto Tycoon以外でも永続性の必要なデータはここに載せて行くとWeb/AppとDBで運用上のメリハリがつきます。

サーバを増やす際にも、データベースなど永続性が重要なコンポーネントと、複数のサーバで動かすことにより冗長性が確保できるものはわけて配置しましょう。メリハリがついているとサーバ障害の際にどう対応すべきかがはっきりとし、復旧の速度もあがると思いまふ。もふもふ

クライアントからmemcachedを利用する際の、ベストプラクティスは以前書いているので、その前段階でmemcachedを含めたWebアプリケーションのアーキテクチャ(と一部クライアントの話)について今の個人的な考えをまとめてみます。Kyoto Tycoonを使ったキャッシュサーバでも基本は同じだと思います

1) 使わない

memcachedをアプリケーションに組み込むことで、プログラムがどうしても複雑になりがちです。データの削除や更新の際にキャッシュの更新を忘れると多くの問題が発生します。例えばユーザがニックネームやプロフィール写真を更新したのに画面上変わらないなどの現象が起こると、ユーザに対して不快な思いをさせてしまうでしょう。またデータベースが非同期のレプリケーションを行っている場合、masterに対してデータの変更をかけ、更新が反映される前にslaveから読み込んでしまい、キャッシュに保存してしまうと問題はさらに大きくなります。対応策としてexpires時間を短くしていることが散見されますが、本末転倒な気がします。

現代的なハードウェア、最新のMySQLを利用し、適切なスキーマ設計・SQLであればMySQLは数百qps〜数千qpsのパフォーマンスを発揮します。もしこの数値でも足りないということであれば”“ピンポイント”“でmemcachedを導入することをお勧めします。よくあるORMのmemcached連携機能は使ってはいけません。

2) 消えては困るキャッシュを置かない

memcachedは揮発性のKVSで、再起動するとすべてのデータが失われるから消えては困るキャッシュを置いては行けない。というのはよく言われていることかもしれませんが、実は本質ではありません。memcachedの単体再起動はあまりすることではありませんし。

アプリケーションを効率よく運用していくために、スケールアウト(台数追加)、スケールアップ(性能の高いサーバに入れ替え)、スケールイン(台数削減)、スケールダウンなどサーバの入れ替えはいつでも行えるようにしてして置くことが必要です。memcachedのキャッシュの分散はクライアントのアルゴリズムにもよりますが、サーバの追加削除によってキャッシュミスが少なからず起こりえます。

消えては困るキャッシュの代表がセッションだと思います。このようなデータは別の永続化できるデータベースに(も)格納したり、Cache::Memcached::IronPlateのキャッシュ複製機能を利用し複数のmemcachedに格納するなどの対応策が考えられます。

3) リスクヘッジのためにmemcachedサーバはスケールアウトを行う

メモリの単価が下がったことで1台のサーバで16GB〜数十GBのメモリを積むことは珍しくなくなりました。memcachedも一つのインスタンスで数十GBをメモリを確保して使うことができます。しかし、スケールアップすることで1台のmemcachedサーバに障害があった場合、アクセスできなくなるキャッシュも膨大になるのでサービスへの影響が大きくなりがちです。適切なスケールアウトも行いましょう。

スケールアウトを行うことでサーバ群の障害発生可能性は上がります。Consistent-Hashingを利用したmemcachedクライアントライブラリでは、障害を検知して別のサーバへアクセスをし直すrehashの機能を持たないものがあります。その場合特定のキーだけキャッシュに保存できないので、サービスへの影響がでてくる可能性があります。そのようなキャッシュでもCache::Memcached::IronPlateのキャッシュ分散・複製機能が活用できるでしょう。メモリは余裕あるので複製しても問題はないはずです

様々なミドルウェアが出てくる中で、memcachedだけが唯一の選択肢ではなくなってきていると思います。しかし雨後のタケノコのようにでてくる多くのミドルウェアを同時に運用するのはなかなか骨の折れる作業です。冷静な技術の判断と適切なサイジングが重要なこのごろです。