2011年12月アーカイブ

祝オープンソース化。

STF 分散オブジェクトストレージシステム
http://labs.edge.jp/stf/

ライブドアのサービスで主に画像管理用に使っているSTFがオープンソースで公開されています。

Perl/PSGI、Q4M、MySQL、Apacheという、Webアプリケーションエンジニアにとってとてもなじみやすい構成を取っており、実際運用もしやすくなっています。

ただひとつ気になるのはMySQLのデータのデカさ。3億オブジェクト/10億エンティティを保存した段階でのMySQLのデータサイズは、約220GBにもなります。これを潤沢にメモリを積み、SSDを4本RAID10にしたサーバにて運用しております。

データの取り回しも大変で、データのダンプに数時間、リストアに数十時間、レプリケーションが追いつくのにまた数時間と移設作業を行うのにまるまる一週間かかるような感じです。とってもカジュアルではありませんね。

テーブル構成

STFの主なテーブルは以下の4つです。

  • bucket
  • object
  • entity
  • storage

bucketはオジェクトをまとめる単位。objectは外から見えるファイル、entityはファイルの実体でサーバに分散配置されます。そのサーバについて定義するのがstorageテーブルです。

すべてのスキーマはソースコードの中に含まれるので以下から取得可能です。

https://github.com/stf-storage/stf/blob/master/misc/stf.sql

このテーブルのうち、データサイズが大きくなるのはobjectとentityの2つ。

CREATE TABLE object (
       id BIGINT NOT NULL PRIMARY KEY,
       bucket_id BIGINT NOT NULL,
       name VARCHAR(255) NOT NULL,
       internal_name VARCHAR(128) NOT NULL,
       size INT NOT NULL DEFAULT 0,
       num_replica INT NOT NULL DEFAULT 1,
       status TINYINT DEFAULT 1 NOT NULL,
       created\_at INT NOT NULL,
       updated\_at TIMESTAMP,
       UNIQUE KEY(bucket_id, name),
       UNIQUE KEY(internal_name)
) ENGINE=InnoDB;

CREATE TABLE entity (
       object_id BIGINT NOT NULL,
       storage_id INT NOT NULL,
       status TINYINT DEFAULT 1 NOT NULL,
       created_at INT NOT NULL,
       updated_at TIMESTAMP,
       PRIMARY KEY id (object_id, storage_id),
       KEY(object_id, status),
       KEY(storage_id),
       FOREIGN KEY(storage_id) REFERENCES storage(id) ON DELETE RESTRICT
) ENGINE=InnoDB;

データサイズを小さく保つための工夫

まず object テーブルをみて気になるのが、ユニーク属性を保つインデックスが3つあるということ。

  • PRIMARY KEY
  • UNIQUE KEY(bucket_id, name)
  • UNIQUE KEY(internal_name)

PRIMARY KEYはBIGINTで時間とDispatcherサーバに割り当てられたIDから計算されるユニークな数値です。2つは目システムの外から見えるバケットとファイル名のユニーク制限。そして3つ目がSTF内部でのファイル名です。

nameもinternal_nameもVARCHARとなるので、インデックスのサイズも大きくなりそうです。そこで利用できるのがMurmurHash等を使いより小さいデータにインデックスを張る方法。

データ分散とインデックス最適化のためのハッシュ関数の利用 - Perl Advent Calendar Japan 2011 Hacker Track
http://perl-users.jp/articles/advent-calendar/2011/hacker/11

MurmurHashは32bitの数値となるのでハッシュ値が衝突する可能性が割と高いと思われるので、実際に使う場合はhash値をにユニークインデックスを張らず、通常のインデックスとし、データ追加時にトランザクションを使ってデータが被らないようにする必要があります。

internal_nameについては、本当にこの値が必要かどうか考えることが必要そうです。ここまで既にテーブル上に2つのユニークな値があるので、そっちを活用すればインデックスだけではなくカラム自体の削除もできそうです。例えば、PRIMARY KEYのBIGINTを内部のファイル名にしても良いはずです。

entityテーブルについては、2点ほど気になる点があります。1つはcreated_atとupdated_atのカラム、もう一つがKEY(object_id, status)のインデックスです。

MySQLのドキュメントに各データタイプにおける必要保存領域が書かれています。

10.5. データタイプが必要とする記憶容量
http://dev.mysql.com/doc/refman/5.1/ja/storage-requirements.html

created_atとupdated_atはそれぞれINTとTIMESTAMPなので、4byte+4byte=8byte必要となります。10億レコードあれば8GB分のデータにもなります。消すと若干節約できますね。

KEY(object_id, status)のインデックスも基本必要ないはずです。object_idを指定した段階で結果はオブジェクトの複製数(せいぜい3から5)にしぼらるので、statusをインデックスに含めなくても高速に検索可能です。PRIMARY KEYが(object_id, storage_id)なのでこちらで十分に事足ります。

ここまでぐだぐだと書いた内容を入れ込むとスキーマはこんな感じかな

CREATE TABLE object (
       id BIGINT NOT NULL PRIMARY KEY,
       bucket_id BIGINT NOT NULL,
       name VARCHAR(255) NOT NULL,
       name_hash INT UNSIGNED NOT NULL,
       size INT NOT NULL DEFAULT 0,
       num_replica INT NOT NULL DEFAULT 1,
       status TINYINT DEFAULT 1 NOT NULL,
       created_at INT NOT NULL,
       updated_at TIMESTAMP,
       KEY(bucket_id, name_hash)
) ENGINE=InnoDB;

CREATE TABLE entity (
       object_id BIGINT NOT NULL,
       storage_id INT NOT NULL,
       status TINYINT DEFAULT 1 NOT NULL,
       PRIMARY KEY id (object_id, storage_id),
       KEY(storage_id),
       FOREIGN KEY(storage_id) REFERENCES storage(id) ON DELETE RESTRICT
) ENGINE=InnoDB;

データサイズがどれくらいになるかはやってみないとわからないけど、確実に小さくはなって、データのハンドリングが楽になると思うので、仕事が始まったらできるところからやってみたいです > 誰か

去年に引き続き、今年も書いてみる。

今年を振り返って公私共に影響が大きかったのは、やはり3/11の東北関東大震災。幸い家族には影響なく過ごしていますが、まだまだ被災された方は大変な思いをされていると思います。今自分ができることを少しだけでもつづけて行きたいと思います

当日は交通手段がすべてストップしてしまったので、新宿から横浜まで歩いて帰ってきました。

3.11 新宿から横浜まで歩いて帰ってきた

このあと自宅作業期間や、ノーエンジニアデーがあり、リモートで仕事できる環境が整いました。最近は朝はカフェで仕事しています。

山の8月/9月

今年のブログ記事を振り返って行くと、9月だけまったく記事がない。9月に何をやっていたかというと、一つは沖縄へ旅行行ってたことと、もう一つは大規模メンテナンス。

[完了報告][9/14] メンテナンスのお知らせ

この裏側では記事のデータベースを一気に移動する作業を行っていました。この作業とその後の対応で生まれたのがmysql40dump。Kansai.pmでも少し紹介しましたが、12月に公開版を用意し記事として書きました。

レプリケーション作成を簡単にする mysql40dump という mysqldump の wrapper を作った話

この9月の前の8月もわりとイベントが多い月でした。8/27 に開催したISUCONはその一つ。かなりもりあがりました

チート対策とhttp_loadに仕掛けた罠の話 #isucon

後日こんなGoogleでサジェストがされたようです。

kazeburo-wana.png

ひどい><

まとめてみたシリーズ

8月までは自分の考えを記事としてまとめてみた系が多い。ブックマース数もそれなり伸びた。

PSGIアプリケーションをリバースプロキシ下で使う際の静的コンテンツの配信方法について

HTTPコンテンツ圧縮とPlack

HTTPコンテンツ圧縮はどのレイヤーで行うのがいいか

Webアプリケーションエンジニアに知っていて欲しいインフラの知識

Webアプリケーションエンジニアはノマドであれ(特定のサーバに依存しない方法)

Webアプリケーションにおける Job Queue システムの構成例と Worker を作る際に気をつけること

私家版省サーバ運用2011またはWebシステムのコンポーネントの配置について

memcached を使ったアプリケーションの設計について

ディレクターやエンジニアが運用エンジニアにインフラの相談をする際に持って来て欲しい5つのこと

今こそ見直すApacheの設定

新規サービスだったり、既存サービスの設計・サーバ設定をする際に、こんなところに気をつけて欲しいなというのを書き出していったってのが実際のところです。記事を書くだけはなく、仕事ではエンジニアがサーバの設定を迷わずにできるツールを用意して、使ってもらったりしています。

作った話

新しく作ったモジュール・システムもいくつかあります。

SQLを書くプロジェクトのためのDBIx::Sunnyってのを書いている

DBIx::Sunnyはこのあと若干の方針転換があり、Sunny.pmとSchema.pmに分かれました。俺俺的に便利でこの後に書いたプロジェクトではほぼ必ず採用しています。

キャッシュシステムの Thundering Herd 問題対策モジュール Cache::Isolatorというのを書いた

CPANにもリリース済み。某Blogサービスでもこのモジュールではないけど同じ仕組みを導入して重い処理を捌いている。

OrePANとcpanmでCPANの部分ミラーを作ってCPANモジュールを管理する

新規サーバ用にperlbrewで新しめのperlとよく使うperlモジュールを最初から入れるという仕事があり、その結果として生まれたのがOrePAN(へのpatch)。最近では他の会社さんでも使っているそうな

Plack版 mod_deflate の Plack::Middleware::Deflater がバージョンアップしました

miyagawaさんのモジュールですが、Co-Maintainerにして頂いたので機能追加してアップデートしました。HTTP圧縮周りのバッドノウハウにも対応できます

GreenBuckets という Object Storage を作りました

いわゆる「僕の考える最強の分散オブジェクトストレージ」。実際は最強を目指しているわけではなくシンプルに使えるものを目指しました。作った当初はなんの予定もありませんでしたが、なんと11月にサービスで使われ始めました。

Kossy と DBIx::Sunny で作る nopaste

ISUCONの際に用意したWeb Application FrameworkをCPANにあげました。GrowthForecastを始め、いくつかのツールで使っています。

GrowthForecastというグラフ表示ツールで捗る話

Yappoさんのlivedoorへの参加は個人的にすごく影響のあったことの一つ。Yappo氏の形にしていく力はいつも勉強になる。GrowthForecastもYappo氏のアイディアとプロトタイプが元になってます。

簡単にグラフにして見える化できるのでGrowthForecastはほんと便利です。

喋ってきた話/書いた話

Webサーバ勉強会#3に参加してきました

Yokohama.pm#7 で喋って来たよ!

Software Design 2011年9月号に運用エンジニアに関する記事を寄稿しました

MySQL 4.0 Casual Talks Vol.2 で LT してきました

YAPC::Asia Tokyo 2011 で発表してきました。

Kansai.pm #14 に参加して発表しました

WEB+DB PRESS Vol.65 Perl Hackers Hubに寄稿しました

いろんな方にお世話になりました。

さよなライブドア。こんにちはNHN Japan

来年1/1にライブドアはNHN Japanに会社統合され、株式会社ライブドアという名称はなくなります。なじみのある(そして昔からのあこがれであった)会社名がなくなる若干の寂しさはありますが、引き続きBlogをはじめとするlivedoorブランドのメディア、サービスを運用やスケーラビリティといった立場から支えて行く事になるとおもうので、これからもよろしくお願いします。

livedoor.jpg

最終日の帰り際に入り口を見に行ったらもうロゴが外されていた。。

PerlMagick が OpenMP 有効だと高負荷になる件 :: drk7jp」の件

どうやら、Perlに限らずマルチプロセスでOpenMPが有効なImageMagickを動かすとパフォーマンスが悪くなるようです。

Enabling OpenMP for most algorithms creates 8 threads (1 per core). If your process creates 8 threads that’s a total of 64 threads and that is a whole lot of contention and possible misuse of your processor cache.

ImageMagick • View topic - Multi Process Contention?」より

ということで、small_lightが動いているサーバ1台でImageMagickをビルドし直してみました。

その結果が以下のグラフ。16:30過ぎにmakeしているため一旦CPU使用率があがりますが、その後は1%強で推移。

disable-openmp.png

すごく… 効果がありました.. ///

年があけたら全台入れ替えて、そしてサーバを減らそう

GrowthForecastや社内のサーバアラートビュアーで使っているWeb Application Framework Kossyの使い方。

KossyはCPANにリリースされているので、cpanm でインストールができます

$ cpanm Kossy

アプリケーションのひな形を作る kossy-setup というコマンドもインストールされるので、これを使います。今回のサンプルアプリケーションの名前は KoPaste としましょう。

$ kossy-setup KoPaste
mkdir lib/KoPaste
mkdir views
mkdir public
mkdir public/js
mkdir public/css
mkdir public/images
mkdir t
writing lib/KoPaste.pm
writing t/00_compile.t
writing lib/KoPaste/Web.pm
writing app.psgi
writing views/index.tx
writing views/base.tx
writing Makefile.PL
writing public/favicon.ico
writing public/js/bootstrap-alerts.js
writing public/css/bootstrap.min.css
writing public/js/jquery-1.7.1.min.js
writing public/js/bootstrap-modal.js

スケルトンのコードとcss/jsなどが展開されました。早速ディレクトリに移動し、plackupでサーバを起動してみます。

$ cd KoPaste
$ plackup -r app.psgi

すごく… bootstrapです.. ///

kossy-setup.png

ここからNoPasteに必要な機能をつくっていきましょう。今回作るアプリケーションは十分に小さいのでデータを操作する関数も一つのクラスにいれます。

KoPaste::Web が Dispatcherクラスなのでこれを編集します

use DBIx::Sunny;
use Digest::SHA;

sub dbh {
    my $self = shift;
    my $db = $self->root_dir .'/nopaste.db';
    $self->{_dbh} ||= DBIx::Sunny->connect("dbi:SQLite:dbname=$db",'','',{
        Callbacks => {
            connected => sub {
                my $conn = shift;
                $conn->do(<<EOF);
CREATE TABLE IF NOT EXISTS entry (
    id VARCHAR(255) NOT NULL PRIMARY KEY,
    nickname VARCHAR(255) NOT NULL,
    body TEXT,
    created_at DATETIME NOT NULL
);
EOF
                $conn->do(q{CREATE INDEX IF NOT EXISTS index_created_at ON entry ( created_at )});
                return;
            },
        },
    });
}

データベースとしてSQLiteを使って、Callbackで接続時にテーブルがなければ作ります。 $self->root_dir はapp.psgiが配置されているディレクトリが入っています。

がりっとエントリー操作用のメソッドも追加します

sub add_entry {
    my $self = shift;
    my (  $body, $nickname ) = @_;
    $body = '' if ! defined $body;
    $nickname = 'anonymous' if ! defined $nickname;
    my $id = substr Digest::SHA::sha1_hex($$ . join("\0", @_) . rand(1000) ), 0, 16;
    $self->dbh->query(
        q{INSERT INTO entry (id,nickname,body,created_at) VALUES ( ?, ?, ?, DATETIME('now') )},
        $id, $nickname, $body
    );
    return $id;
}

sub entry_list {
    my $self = shift;
    my $offset = shift;
    $offset = 0 if defined $offset;
    my $rows = $self->dbh->select_all(
        q{SELECT * FROM entry ORDER BY created_at DESC LIMIT ?,11},
        $offset
    );
    my $next;
    $next = pop @$rows if @$rows > 10;
    return $rows, $next;
}

dbh->query や dbh->select_all は DBIx::Sunnyが提供するショートカットメソッドです。

ここから Dispatcherの実装です。「/」にアクセスした際に実行されるコードは、スケルトンでは

get '/' => [qw/set_title/] => sub {
    my ( $self, $c )  = @_;
    $c->render('index.tx', { greeting => "Hello" });
};

このように書かれています。これをエントリーのリストが表示されるようにしましょう

get '/' => sub {
    my ( $self, $c )  = @_;
    my $result = $c->req->validator([
        'offset' => {
            default => 0,
            rule => [
                ['UINT','ivalid offset value'],
            ],
        },
    ]);
    $c->halt(403) if $result->has_error;
    my ($entries,$has_next) = $self->entry_list($result->valid('offset'));
    $c->render('index.tx', {
        offset => $result->valid('offset'),
        entries => $entries,
        has_next => $has_next,
    });
};

Dispatcherのメソッドには、2つの引数が渡されます。1つ目が KoPaste::Webのオブジェクト。2つ目がKossy::Connectionオブジェクトです。 KoPaste::Webオブジェクトはアプリケーション初期化時点からプロセスが死ぬまで有効なオブジェクトで、もちろんKoPaste::Webの他のメソッドの呼び出しに利用できます。Kossy::Connectionオブジェクトはリクエスト単位で生成されるオブジェクトとなります。

ここででてきたvalidatorはKossy内蔵のKossy::Validatorです。ページングする際のoffestについてバリデーションを定義しています。正の整数だけを受け取り、もしパラメータがないときには、0がデフォルトしています。

もしoffsetの値が正しくなければ、403 Forbiddenを返します。

$c->halt(403) if $result->has_error;

そして、上で定義したentry_listメソッドを叩き、一覧を取得。テンプレートに各値を渡します。

もう一つエントリーのポストを受け付けるエンドポイントを作りましょう。

post '/create' => sub {
    my ( $self, $c )  = @_;
    my $result = $c->req->validator([
        'body' => {
            rule => [
                ['NOT_NULL','empty body'],
            ],
        },
        'nickname' => {
            default => 'anonymous',
            rule => [
                ['NOT_NULL','empty nickname'],
            ],
        }
    ]);
    if ( $result->has_error ) {
        return $c->render_json({ error => 1, messages => $result->errors });
    }
    my $id = $self->add_entry(map {$result->valid($_)} qw/body nickname/);
    $c->render_json({ error => 0, location => $c->req->uri_for("/")->as_string });
};

POSTでデータを受け付け、JSONでレスポンスを返します。「/」と同じようにKossy::Validatorを使って入力値のチェックを行っています。JSONでレスポンスするには、$c->render_json が利用できますが、Amon2と同じ

  • 「GET リクエストである」かつ
  • 「Cookie ヘッダを送信している」かつ
  • 「/android/i にマッチする User-Agent ヘッダを付与している」かつ
  • 「X-Requested-With ヘッダを付与していない」

という条件に当てはまる場合、403エラーとなるので注意してください。

ここまででDispatcher側の変更はひとまず終了してテンプレートの編集に移ります。

テンプレートは、viewsディレクトリに配置し、Text::XslateのKolonシンタックスをサポートしています。index.txを開くと次のようになっています。

: cascade base
: around page_header -> {
<h1><: $c.stash.site_name :></h1>
: }

: around content -> {
<h2><: $greeting :></h2>
: }

base.txをカスケードしています。base.txにHTMLの構造を書き、各テンプレートにはコンテンツ部分だけを記す事ができます。index.txを編集してエントリー一覧を表示します。一覧の下には投稿用のフォームも付けます

: around content -> {
: for $entries -> $entry {
<h2><a href="<: $c.req.uri_for('/'~$entry.id) :>"><: $c.req.uri_for('/'~$entry.id) :></a></h2>
<pre class="prettyprint linenums:1">
<: $entry.body :>
</pre>
<p><: $entry.created_at :> by <: $entry.nickname :></p>
: }

<div class="pagination">
<ul>
: if $offset > 0 {
<li><a href="<: $c.req.uri_for('/',[offset => $offset-10 >= 0 ? $offset - 10 : 0]) :>">Prev</a></li>
: }
: if $has_next {
<li><a href="<: $c.req.uri_for('/',[offset => $offset+10]) :>">Next</a></li>
: }
</ul>
</div>

<form id="create-form" method="post" action="<: $c.req.uri_for('/create') :>">
<h2>New Paste</h2>

<div class="alert-message error hide">
<p>System Error! <em>try again</em>.</p>
</div>

<fieldset>
<div class="clearfix">
<label>body</label>
<div class="input">
<textarea class="span9" name="body" rows="10"></textarea>
<span class="help-block">fill your code</span>
</div>
</div>

<div class="clearfix">
<label>nickname</label>
<div class="input">
<input class="xlarge" name="nickname" />
<span class="help-block">your name</span>
</div>
</div>

<div class="actions">
<input type="submit" class="btn primary" value="Submit" />
</div>
</fieldset>
</form>
: }

最後に、投稿処理用のJavaScriptをbase.txに追加します。

<script type="text/javascript">
$(function(){
  $('#create-form').submit(function () {
    create_form = this;
    $.ajax({
      type: 'POST',
      url: create_form.action,
      data: $(create_form).serialize(),
      success: function(data) {
        $(create_form).find('.alert-message.error').hide();
        if ( data.error == 0 ) {
            location.href = data.location;
        }
        else {
          $(create_form).find('div.clearfix').removeClass('error');
          $.each(data.messages, function (param,message) {
            $(create_form).find('[name="'+param+'"]').
               parents('div.clearfix').first().addClass('error');
          });
        }
      },
      error: function() {
        $(create_form).find('.alert-message.error').show();
      },
      dataType: 'json'
    });
    return false;
  });
});
</script>

スケルトンの状態でjQueryは既に読み込まれています。

ここまでできたら、再度アプリケーションを起動します

$ plackup -r app.psgi

ブラウザで確認すると

kossy-sample.png

のようにエントリーの投稿もできると思います。画面はさらにgoogle-code-prettifyも追加してエントリの整形も行っています。

このようにKossyを使うと、Webアプリケーションを素早く開発できます。Sinatra風のDispatcherのため、大規模の開発には向きませんが、管理画面や運用ツールなどに試していただけるといいかなと思います

みなさん mysqldump は好きですか? 自分はどっちでもありません。

MySQLでよくあるMaster-Slave構成を作る手順は以下のようになると思います

mysql40dump-3.png

  1. MasterからSlaveとなるサーバに一貫性を保った状態のコピーをし、そのデータのバイナリログのファイル・ポジションをメモ。
  2. SLAVEでデータをリストアし、Masterのホスト名、レプリケーションに使うユーザ名・パスワードとメモしたバイナリログのポジションをCHANGE MASTER文に渡し、START SLAVE

一貫性の取れたコピーを作成するためにmysqldumpやxtrabackup、LVMなどでのスナップショットが利用できますが、もっとも簡単な方法がmysqldumpだと思います。

mysqldumpで一貫性のあるデータをとり、その際のバイナリログポジションを記録するには

$ mysqldump --single-transaction --master-data

とします。single-transactionでバックアップをトランザクション中で行い、—master-data=2 でバイナリログのポジションをバックアップの先頭にコメントとして記録できます。(single-transactionでもInnoDB以外のテーブルがある場合は一貫性のあるデータがとれない可能性があります)

実際のmysqldumpが発行しているクエリは以下のようになってます。

FLUSH LOCAL TABLES
FLUSH TABLES WITH READ LOCK
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ
START TRANSACTION WITH CONSISTENT SNAPSHOT
SHOW MASTER STATUS
UNLOCK TABLES
DB選択
テーブルからデータの読み込み

(@tmtms のメモより)

まずREAD LOCKをかけ、つぎにトランザクション開始し、バイナリログのポジションを確保。その後READ LOCKを解放し、(トランザクションはそのまま)データのダンプを行っています。

このようにmysqldumpで一貫性のあるデータコピーとバイナリログの位置を取得できます。





がががが、まぁよくある話で、MySQL 4.0のサーバに対して mysqldumpを実行すると

FLUSH TABLES WITH READ LOCK
BEGIN
DB選択
テーブルからデータの読み込み
COMMIT;
SHOW MASTER STATUS
UNLOCK TABLES;

と、なり、なんと最初から最後までREAD LOCKされ、dumpが終了するまでデータを更新できなくなってしまいます。数十GBになるデータを扱っている場合これでは運用が難しい。。。

Wrapper スクリプトの動作

そこで考えたのが2つのプロセスを組み合わせて、READ LOCKとトランザクションを発行する次の仕組み。

mysql40dump.014.png

mysqldumpをラップするperlスクリプトを起動すると、まずMySQLに対してREAD LOCKを行い全ての更新を止めます。そしてバイナリログの情報を取得。

次に、pipeを作成し、fork。forkした子プロセスでは標準出力をpipeに結びつけ、mysqldumpをsingle-transactionオプション付きでexec。single-transactionが有効なのでmysqldumpはトランザクションを開始してデータコピーを開始します。そのデータはpipeを通して親プロセスに送られます。

親プロセスでは、pipeからデータを読み込み、そのまま標準出力します。その際最初のCREATE TABLEを見つけたら、最初に行ったREAD LOCKを解放し、そのあとは mysqldumpが終了するまで動き続けます。

実際に最初のREAD LOCKからダンプを開始し、LOCKが解放されるまでは1秒も掛からないのでほぼ無停止だと言えるでしょう。これでMySQL 4.0でも一貫性のとれたデータダンプと、バイナリログのポジション取得が可能となります。

この仕組みを実装したのが mysql40dump というスクリプトで、既に実際にサービスのデータベースの複製・バックアップに使っています。次に紹介する機能が便利なので、4.0だけではなくMySQL 5.1系でも動作確認しています。

mysql40dump の追加機能

まず1つ目がSHOW MASTER STATUSの代わりにSLAVE STATUSを実行する機能。

mysql40dump-2.png

のようにSLAVEからSLAVEを作るときに使えます。MySQL 5.5から実装された mysqldump —dump-slave 相当の機能ですね。

もう一つが自身のIPアドレスを調べ自動でCHANGE MASTER文を構築する機能と、START SLAVEまで実行する機能。これでほぼ面倒なことなくレプリケーションの開始まで自動化ができるようになりました。この自動化のおかげて3つ4つ同時に異なるデータベースのコピーを行っていても全く苦にならず捗ります。

あと、MySQL 4.0でのバッドノウハウとしてダンプデータの先頭に

set FOREIGN_KEY_CHECKS=0

をいれます。これで何度泣いた事か。。

使い方

perlのスクリプトなのでインストールはcpanmでできます

$ cpanm https://github.com/downloads/kazeburo/mysql40dump/mysql40dump-0.03.tar.gz
$ mysql40dump -h

Master-Slave構成を取る場合

Masterにて

$ mysql40dump --master --repl --master-user user --master-password pass | gzip > dump.sql.gz

ファイルをコピーして Slaveにて

$ zcat dump.sql.gz | mysql

これでリストア後、レプリケーションの開始までやってくれます

Slaveを追加する場合

既存Slaveにて

$ mysql40dump --slave --repl --master-password pass | gzip > dump.sql.gz

ファイルをコピーして 新しいSlaveにて

$ zcat dump.sql.gz | mysql

これでMasterからのレプリケーションが開始されます。

簡単ですね!

次の課題はxtrabackupのwrapperかな

合わせて読みたい


図中のアイコンは、AWS Simple Icons for Architecture Diagrams を使いました

xaicronとネタが被ったようだけど気にしない><

livedoorでOperations EngineerやってるkazeburoだYo。最近livedoorからオープンソース化された3億ファイルを管理してるオブジェクトストレージ「STF」でも使ってるMessage QueueのQ4Mのインストール方法を紹介するよ! カジュアルだからインストールだけ!

知ってる人も多いと思うけどQ4Mはkazuhoさんによって開発されたMySQLのストレージエンジンとして実装されてるMessage Queue。livedoorではもちろん、mixiやDeNAをはじめソーシャルゲーム各社でも使われている。

Message Queueの説明や使い方はDIS_COMMENTでテーブルスペースフルの猫神様が書いてるので参考になるね!

Perl Hackers Hub 第10回 ジョブキューで後回し大作戦―TheSchwartz,Qudo,Q4M(1) http://gihyo.jp/dev/serial/01/perl-hackers-hub/001001

このQ4M。MySQLのストレージエンジンとして実装されてるので、インストールにするにはもちろんMySQLが必要だ。しかしどのバージョンでも良い訳ではなく、MySQL 5.1系だけが対応となってる。最新の安定版4.05.5には残念ながら対応してない。5.1を動かしていたとしても既にサービスで動いているMySQLに追加していれるもの勇気がいる。

個人的におすすめなのは「Webアプリケーションにおける Job Queue システムの構成例と Worker を作る際に気をつけること」というエントリでも書いたけど、既存のデータベースとは別に導入する方法。

jobqueue2.png

上の図だと、brokerがQ4Mにあたる。

さて、ここまで決まったら、あとは環境をあまりいじらずにQ4Mを入れるだけ。最近うちの会社では(STFでも)次のスクリプトを使ってQ4Mを既存のMySQLとは別に入れてるyo。

#!/bin/sh
set -e

MYVER=5.1.60
Q4MVER=0.9.5

CDIR=$(cd $(dirname $0) && pwd)
cd /usr/local/src

if [ ! -f mysql-$MYVER.tar.gz ]; then
    wget http://downloads.mysql.com/archives/mysql-5.1/mysql-$MYVER.tar.gz
fi

if [ -d mysql-$MYVER ]; then
    rm -rf mysql-$MYVER
fi
tar zxf mysql-$MYVER.tar.gz
cd mysql-$MYVER
# /usr/local/q4m 以下に導入
./configure \
    --prefix=/usr/local/q4m \
    --with-mysqld-ldflags="-static" \
    --with-client-ldflags="-static" \
    --enable-assembler \
    --enable-thread-safe-client \
    --with-charset=utf8 \
    --with-zlib-dir=bundled \
    --with-big-tables \
    --with-mysqld-user=nobody \
    --with-pic \
    --with-extra-charsets=all \
    --with-readline \
    --without-debug \
    --enable-shared \
    --with-fast-mutexes \
    --with-comment="Q4M" \
    --with-server-suffix="-q4m" \
    --with-unix-socket-path="/tmp/mysql_q4m.sock" \
    --with-tcp-port=13306 \
    --with-plugins=none \
    --without-plugin-daemon_example \
    --without-plugin-ftexample \
    --without-plugin-archive \
    --without-plugin-blackhole \
    --without-plugin-example \
    --without-plugin-federated \
    --without-plugin-innobase \
    --without-plugin-innodb_plugin \
    --without-docs \
    --without-man
make
make install

mkdir -p /usr/local/q4m/etc
cp $CDIR/my.cnf /usr/local/q4m/etc
cp $CDIR/q4m.init /etc/init.d/q4m
chmod 755 /etc/init.d/q4m
chkconfig --add q4m

/usr/local/q4m/bin/mysql_install_db --defaults-file=/usr/local/q4m/etc/my.cnf
chmod 755 /usr/local/q4m/var
/etc/init.d/q4m start

cd /usr/local/src
if [ ! -f q4m-$Q4MVER.tar.gz ]; then
    wget http://q4m.kazuhooku.com/dist/q4m-$Q4MVER.tar.gz
fi
if [ -d q4m-$Q4MVER ]; then
    rm -rf q4m-$Q4MVER
fi
tar zxf q4m-$Q4MVER.tar.gz
cd q4m-$Q4MVER
CPPFLAGS="-I/usr/local/q4m/include/mysql" CFLAGS="-L/usr/local/q4m/lib/mysql" ./configure \
    --with-mysql=/usr/local/src/mysql-$MYVER \
    --prefix=/usr/local/q4m
make
mkdir -p /usr/local/q4m/lib/mysql/plugin
cp src/.libs/libqueue_engine.so /usr/local/q4m/lib/mysql/plugin/
cat support-files/install.sql | /usr/local/q4m/bin/mysql -S /tmp/mysql_q4m.sock

my.cnfやinitスクリプトなどソースはすべてgithubで公開中。使う時は

$ git clone git://github.com/kazeburo/mysetup.git
$ cd mysetup/q4m
$ sudo sh ./setup.sh

するだけ!

これをと使うと、 /usr/local/q4m 以下にMySQLとQ4Mを入れて、mysqldのデーモンが起動する。データも /usr/local/q4m 以下に入る。ポートは 13306 がデフォルトだ。Q4M専用MySQLという位置付けなのでInnoDBは入らないのが注意点。

initスクリプトもインストールするので、

$ sudo /etc/init.d/q4m start|stop|restart

もできる。簡単。

$ /usr/local/q4m/bin/mysql
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 446907
Server version: 5.1.58-q4m-log Q4M

Copyright (c) 2000, 2010, Oracle and/or its affiliates. All rights reserved.
This software comes with ABSOLUTELY NO WARRANTY. This is free software,
    and you are welcome to modify and redistribute it under the GPL v2 license

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

ちなみにCentOSだけで確認しています(_ _)。Macの開発環境はxaicronのスクリプトを使えば簡単、サーバのセットアップは今回ば簡単。これでQ4Mを使わない理由はなくなったね!

蛇足。最近rpmつくるの面倒でこんなscript書いて済ましていることが多い。そういうネタの一つでした。githubでそのうちいくつか公開していくかもしれません。以上。

追記 2012/06/22
公式ページを作りました。そちらも参考にしてくださいませ
GrowthForecast - Lightning fast Graphing / Visualization
http://kazeburo.github.com/GrowthForecast/

Kansai.pmのLTでも紹介したんだけど、APIを叩く事でグラフを更新するツールを書きました。話の発端としては「cloudforecastのグラフを外からAPIで更新したい」ということでしたが、cloudforecastではグラフの追加が重い処理になってしまうので、別のプロダクトとしています。

サーバの負荷などのメトリクスを収集し、グラフ化することで、システムに掛かっている負荷を把握し、パフォーマンスに影響がでるまえに対策をうったり、改善の結果を知る事ができますが、同じ事はシステムだけではなく提供しているサービスの動向を確認したり、サービスの改善の結果を知ることにも言えると思います。GrowthForecastは数値だけではくグラフでこのような変化を見やすくする為のツールとして利用することができます。

growth1.png

↑画面はこんな感じ。

API

APIはむちゃくちゃ簡単で、

http://example.com/api/:service_name/:section_name/:graph_name

というURLにPOSTでデータを送るだけです

もし、忍者取り合いっていうサービスのアイテムの中の手裏剣が売りたい数だったら

POST http://example.com/api/ninjyatoriai/items/syuriken_no_ureta_kazu

となります。utf8でればURIに日本語も使えます。送るパラメータは

パラメータ 説明 必須/オプション
number グラフに与える数値 必須
mode 登録済みの数値を number の値で加算する時には mode=count とします。
通常は number の数値で常に上書きます。
オプション

この2つとなります。

LWP::UserAgent を使うと以下のように書けます。

my $ua = LWP::UserAgent->new;
$ua->post('http://example.com/api/ninjyatoriai/items/syuriken_no_ureta_kazu', {
    number => 10,
});

送信したデータは一度SQLiteのDBに貯められ、5分毎にグラフ用のRRDデータを更新します。5分毎、30分毎、1時間毎など任意のタイミングでAPIを叩くとよいでしょう。

グラフ

データを投稿してから5分ぐらい待つとグラフが描かれます

以下は、2分毎に date +'%M' の結果をmode=countで送って描いたグラフ。上の2つは32時間と1週間のグラフです。modeがcountなので一方向に増えて行ってます。

graph2.png

GrowthForecastははグラフデータを更新する際に、前回との差分も取っているので、2行目のような差分グラフも表示できます。

差分グラフの表示、非表示グラフの色、単位などはWeb上のUIから変更可能です。

graph3.png

複合グラフ

もう一つ、GrowthForecastの機能で、複数のデータを1つのグラフにまとめて表示できる機能があります。

graph4.png

これは先ほどのdateを送っているグフラの差分データの上に別のデータを3つ重ねています。

複合グラフの編集もWeb UIからプレビュー表示付きで出来ます。

graph5.png

GrowthForecastの使い方として、エンジニアが手間をかけずにとりあえずあるサービスのデータを送るcronを設置しグラフ化、ディレクタや他のエンジニアがそれを確認しつつ、差分表示したり、グラフを重ねたりし、サービスが今どんな状態を知り、改善し、またその改善の結果を簡単に確認するというのがいいんじゃないかなと思います。弊社では既に導入されて使われています

GrowthForecastはYappo氏のアイディア+プロトタイプが元になってます。捗るツールの神ですね!

インストール方法

これまでRRDToolをインストールするのがなかなか困難でしたが、gfxがAlien::RRDToolを作ったのでだいぶ楽になりました。

$ wget http://search.cpan.org/CPAN/authors/id/G/GF/GFUJI/Alien-RRDtool-0.0*_**.tar.gz
$ cpanm Alien-RRDtool-0.0*_**.tar.gz

これでインストールできます。もちろんextlibに入れる事も可能です。pkg-config、gettext、glib、xml2、pango、cairoあたりのライブラリもあわせて必要になります。

あとはGrowthForecast本体をgithubからcloneしてinstalldeps

$ git clone git://github.com/kazeburo/GrowthForecast.git
$ cd GrowthForecast
$ cpanm --installdeps .

ここまでできたら、あとは起動

$ perl ./growthforecast.pl --port 5125

5125番ポートでStarletが起動し、定期的にRRDをアップデートするworkerも動きます。データはすべて、GrowthForecast/data以下に保存されます。

簡単ですね!

使ったソフトウェアなど

UI

  • jquery
  • bootstap

サーバ側

  • Plack
  • Starlet
  • Kossy
  • DBIx::Sunny
  • DBD::SQLite
  • Parallel::Prefork
  • Parallel::Scoreboard

KossyもCPANにあげたので、いつかもうちょっと詳しく書くつもりのよてい