2011年5月アーカイブ

分散Object Storageの GreenBuckets ではストレージノードの実装を問わないので、こういうこともできると言う例

Kyoto Cabinet の Directry Hash DataBase を使うと、ファイルシステム上の1ファイルが1レコードとなるデータベースを作成することができます。通常のDBMでは数KBまでの小さいデータに性能が最適化されているのに対して、Directry Hash DataBaseでは数十KB〜のデータを扱いやすくなるということらしいです。

もちろん、Kyoto Tycoon からも使うことができるので、GreenBucketsのストレージノードとしても利用できます。

まず、ktserver でノードを立ち上げます。今回は試しに1つのktserverで複数のデータベースを担当させ、それぞれ1ノードとして扱います。

$ ktserver -li -port 8080 a.kcd b.kcd c.kcd
2011-05-29T00:42:04.418287+09:00: [SYSTEM]: ================ [START]: pid=77706
2011-05-29T00:42:04.424942+09:00: [SYSTEM]: opening a database: path=a.kcd
2011-05-29T00:42:04.426206+09:00: [SYSTEM]: opening a database: path=b.kcd
2011-05-29T00:42:04.427222+09:00: [SYSTEM]: opening a database: path=c.kcd
2011-05-29T00:42:04.428189+09:00: [SYSTEM]: starting the server: expr=:8080
2011-05-29T00:42:04.428292+09:00: [SYSTEM]: server socket opened: expr=:8080 timeout=30.0
2011-05-29T00:42:04.428329+09:00: [SYSTEM]: listening server socket started: fd=9

a.kcd、b.kcd、c.kcd というデータベースが作成されました。これをノードとしてGreenBucketsに登録します

mysql> select * from nodes;
+----+-----+-----------------------------+--------+-------+--------+
| id | gid | node                        | online | fresh | remote |
+----+-----+-----------------------------+--------+-------+--------+
|  1 |   1 | http://127.0.0.1:8080/a.kcd |      1 |     1 |      0 |
|  2 |   1 | http://127.0.0.1:8080/b.kcd |      1 |     1 |      0 |
|  3 |   1 | http://127.0.0.1:8080/c.kcd |      1 |     1 |      0 |
+----+-----+-----------------------------+--------+-------+--------+

わかりやすいですね。

Kyoto Tycoon では最初のディレクトリをデータベース名として扱うので、GreenBucketsのほうからディレクトリを含まないURIでノードにアクセスする必要があります。そのためには、GreenBucketsのconfigで

flat_dav => 1,

を指定してください。上の例のように既にデータベース名がノードに含まれている場合はこの必要はありません。

これで、GreenBucketsを起動して、

$ PLACK_ENV=development ./bin/greenbuckets jobqueue -c etc/config.pl
$ PLACK_ENV=development ./bin/greenbuckets dispatcher -c etc/config.pl

curlでアクセスしてみます。

$ curl -v -basic --user admin:admin -X PUT -d 'IloveKyoto' http://localhost:5000/test/test
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Date: Sat, 28 May 2011 15:50:24 GMT
< Server: Plack::Handler::Starlet
< Content-Type: text/html; charset=UTF-8
<
* Closing connection #0
OK

OKが返って来て、オブジェクトが保存されました。

この時のKyoto Tycoon側のログをみると、

2011-05-29T00:50:24: [INFO]: connected: expr=127.0.0.1:60567
2011-05-29T00:50:24: [INFO]: (127.0.0.1:60567): PUT /c.kcd/59/47/04b09ad074b6c9c58132c3dbfd81879f6294efbe9f1381ae295cf2ac HTTP/1.1: 201
2011-05-29T00:50:24: [INFO]: (127.0.0.1:60567): PUT /b.kcd/59/47/04b09ad074b6c9c58132c3dbfd81879f6294efbe9f1381ae295cf2ac HTTP/1.1: 201
2011-05-29T00:50:24: [INFO]: connected: expr=127.0.0.1:60574
2011-05-29T00:50:24: [INFO]: (127.0.0.1:60574): GET /c.kcd/59/47/04b09ad074b6c9c58132c3dbfd81879f6294efbe9f1381ae295cf2ac HTTP/1.1: 200
2011-05-29T00:50:24: [INFO]: connected: expr=127.0.0.1:60575
2011-05-29T00:50:24: [INFO]: (127.0.0.1:60575): PUT /a.kcd/59/47/04b09ad074b6c9c58132c3dbfd81879f6294efbe9f1381ae295cf2ac HTTP/1.1: 201
2011-05-29T00:50:37: [INFO]: connected: expr=127.0.0.1:60766

と想定通りのアクセスがきています。まず、murmurhashを使ったsort順に、c.kcd(3)、b.kcd(2)にPUTし、キューに回し、キューは c.kcd(3) からオブジェクトを一旦取得し、a.kcd(1) にPUTしてレプリカを作成します。

と、GreenBucketsでKyoto Tycoonが利用できることが確認できました。もちろんKyoto Cabinetがサポートする他のデータベースでも問題なく使えますし、揮発性のある分散Object Storageも作れます^^。画像ストレージとして考えた場合、Directory Hash Database を選ぶことになると思うのですが、ファイルがすべて1つのディレクトリに保存されてしまうので、lsが遅いとか扱いにくいのがアレかもしれません。

合わせて読みたい

これまで GreenBuckets では、オブジェクトの読み込み時はノードがダウンした場合でも可用性が保たれていましたが、書き込みはオンライン状態にあるノード数がレプリカ数分ないと新規オブジェクトの追加ができませんでした。そのため可用性を保とうと思うと、レプリカ数が2では最低4台、レプリカ数3では最低6台のノードがないと1台でもノードがダウンした場合に書き込みができない状態になっていました。

これでは初期投資が大きくなりすぎるということで、最低2台(レプリカ数2)でも書き込みの冗長性が確保できるようにしてみた。

greenbuckets_recoveryqueue.png

左の図が、ノード1が障害でオフラインの状態。これまでだとノード1に書き込もうとして失敗するとすぐに保存エラーとクライアントに返していましたが、GreenBucketsを変更して右の図のように、ノード2と3に仮に保存して(1)、新しく設けた Recovery Queue に登録します(2)。Recovery Queueはキュー登録から10分以上経過したところで動きだし、ノード1、2、3のうちからオブジェクトを探し出し(3)、再度保存をかけます(4)。保存にしっぱいしたらキューを戻し10分毎に確認させます。

これでMySQLに障害がなければ保存時に必要なレプリカが確保できなくても、仮でオブジェクトを保存し、ノード復旧後にレプリカを作成するようになりました。

追加で、GreenBucketsに対していくつか変更していて、Queue数が取れるようになっていたり、GETリクエストの情報がノードまで届くようになっています。

Queue数は、JobQueueのstats経由で取得できます。

> telnet localhost 5101
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Uptime: 231
BusyWorkers: 0
IdleWorkers: 7
ObjectsMaxID: 200
BucketsMaxID: 1
Queue: 30
RecoveryQueue: 0

JobQueueはデフォルトで5101ポートにてstatusが取れるサーバを立ち上げますが、そこに情報を追加しました。Queue数は監視サーバに登録しておくといいでしょう。他にもObjectsテーブルの最大値もあるので、作成されたオブジェクト数をCloudForecastなどでグラフ化していくのも面白いかもしれません

あと、オブジェクト取得時のGETのQUERY_STRINGがノードまで届くようになっていたり、元のURIのPATHをuri_escapeし、X-GreenBuckets-Original-Pathヘッダに追加しています。ノード側でごにょごにょ処理をいれたい場合は便利なはず。

Scope::Container::DBI version 0.04 でデータベースへの接続が失敗したときに、自動で接続をやり直す機能を付けてみた。

http://search.cpan.org/dist/Scope-Container-DBI/

retry機能を使うには、connectの際のattributesにオプションを追加します。

my $dbh = Scope::Container::DBI->connect(
    'dbi:mysql:mydb;host=myhost', 'myuser', 'mypasswd',{
        RaiseError => 1,
        mysql_connect_timeout => 1, 
        ScopeContainerConnectRetry => 3
        ScopeContainerConnectRetrySleep => 100
    }
);

ScopeContainerConnectRetry がretryする回数、ScopeContainerConnectRetrySleep は再接続する際のインターバルで、単位はミリ秒です。デフォルトはsleepしません。

オライリーから日本語訳版が出版された「ウェブオペレーション」を読みました。

障害や負荷対策の苦労話はニヤニヤしつつ、アプリケーションエンジニアとの連携についてはやや耳が痛いところもあり、興味深く読ませて頂きました。

中でも、第八章「コミュニティ管理とウェブオペレーション」はとても短い章ですが、個人的にはグっときました。サービスを育てて行くために、オペレーションエンジニアは単に障害対応を行い、可用性やスケーラビリティを確保して行くだけではなく、ユーザコミュニティに大しても積極的に情報を開示して行く必要があり、エンジニアだけではなくカスタマーサポート、コミュティマネージャとの連携が重要となる。書かれているFrickrの機能ローンチのポリシーや障害への対応は参考になることが多そうです。

オペレーションエンジニアは、自分たちの技術が最終的に人とコミュニティに影響を与えるのだということを、忘れないでおきたいですね

まさしくその通りだし、そういう仕事をして行きたいと思う。@livedoorblog の中の人も頑張ってね!

ところで、最近、勉強会などで発表する場合は自称「オペレーションエンジニア」と自己紹介しています。なぜ、話題のインフラエンジニアではないのかというと、電気ガス水道のような「インフラ」のイメージがデータセンターへ行くことがまずない自分の仕事とあっていない気がしていたからです。そこで、twitterfacebookの採用ページを見てみると、「Operations」や「Technical Operations」という項目があり、業務範囲が似ているなぁと思ったのが自称しはじめた理由です。そんな最近の主な業務は

  • 監視ツールの作成や構築、運用
  • 監視からシステムの改善のサイクルを回す
  • アプリケーションエンジニアと協力しスケーラビリティの高いアプリケーションの構築を目指す
  • 運用しやすいアプリケーション構築方法の啓蒙
  • アプリケーション環境の整備

あたりとなっています。まぁ肩書きによって業務が規定されるわけでもないんだけどね。

/* カラム名を変更しています 20110524 */

@kamipo さんが正座して待っているのを思いだした。

GreenBucketsで、ノードがダウンした時の動作と復旧方法です。GreenBuckets自体の動作実績はないのであくまで想定です。ただ、mixiの画像クラスタの構成をまねているので復旧方法もほぼ同じかもです

まず、障害が起きて、復旧するまでの間を次の3段階にわけて対応を考えます

  • 障害が発生し、アラート検知、運用者が対応するまで
  • 運用者が対応を行い、一時復旧
  • データの整合性がとれ、完全復旧

■ 障害が発生し、アラート検知、運用者が対応するまで

さて、HDDが破損するなどしてサーバがダウンした場合、運用者が対応を行うまで、GreenBucketsは障害の影響をなるべく表に出さないよう、動作します

greenbuckets_recovery.png

上の2つの図はノード1がダウンした状態を示しています。オブジェクトを取得する際(左上)に、ノード1にアクセスしても正常にレスポンスが得られないので、同じグループの次のノードへアクセスします。(もちろんmurmurhashを使ってsortした順です) オブジェクトを保存する際(右上)は、ノード1へPUTが失敗するので、ノードを切り替えて保存します。もしこの際グループ1の他のノードにオブジェクトを保存していた場合は、JobQueueへ削除を依頼します。

下は3つめのノードがダウンしていた場合です、GreenBucketsはオンラインで2つのコピーを作成し、残りをJobQueueに依頼しますが、JobQueueでコピーに失敗した場合にもグループを変更してコピーし直します。コピーが正しくできたところで、MySQLにグループ変更を記録します。

このように、GreenBucketsはノードに障害があった場合でも、ノード数が足りていれば機能的には問題がなく動くようになっています。しかし、Retryを行うためにレスポンスが遅くなる可能性があるため、障害検知後、運用者が問題の起きたノードをサービスから切り離す必要があります。(監視ツールから自動で切り離すこともできるかもしれません)

■ 運用者が対応を行い、一時復旧するまで

運用者による対応が2つ目の段階です。

GreenBucketsのノード管理テーブルは以下のようになっています

mysql> select * from nodes;
 +----+-----+--------------------+--------+-------+--------+
| id | gid | node               | online | fresh | remote |
+----+-----+--------------------+--------+-------+--------+
|  1 |   1 | http://127.0.0.11/ |      1 |     1 |      0 |
|  2 |   1 | http://127.0.0.12/ |      1 |     1 |      0 |
|  3 |   1 | http://127.0.0.13/ |      1 |     1 |      0 |
|  4 |   2 | http://127.0.0.14/ |      1 |     1 |      0 |
|  5 |   2 | http://127.0.0.15/ |      1 |     1 |      0 |
|  6 |   2 | http://127.0.0.16/ |      1 |     1 |      0 |
|  7 |   3 | http://127.0.0.17/ |      1 |     1 |      0 |
|  8 |   3 | http://127.0.0.18/ |      1 |     1 |      0 |
|  9 |   3 | http://127.0.0.19/ |      1 |     1 |      0 |
+----+-----+--------------------+--------+-------+--------+

各ノードには、onlineとfreshという2つのフラグがあります(remoteは別の機会に)。これを用いてノードが有効かどうかを設定します。onlineがついていれば、読み出し可能であり、onlineとfreshの両方がついていれば新規オブジェクトの追加ができることを示しています。

ノードが落ちている場合、そのノードのonlineを0にすれば、該当ノードオブジェクトへの取得リクエストは停止されます。また、保存もできなくなるので、レプリカ数の3を満たさなくなり、ダウンしたノードが属するグループへオブジェクト追加は行われなくなります

mysql> BEGIN;
mysql> UPDATE nodes SET online = 0 WHERE id = 3;
+----+-----+--------------------+--------+-------+--------+
| id | gid | node               | online | fresh | remote |
+----+-----+--------------------+--------+-------+--------+
|  3 |   1 | http://127.0.0.13/ |      0 |     1 |      0 |
+----+-----+--------------------+--------+-------+--------+
mysql> COMMIT;

これで、ノードダウンへの一時対応は完了です。もし夜間に携帯電話に起こされたのであれば、再びベッドに戻って寝てしまっても大丈夫です。レプリカ数が3以上の場合はデータは2重化されているので、データ復旧の対応は次の営業日で問題がないはずです。

■ データの整合性がとれ、完全復旧するまで

最後にデータの復旧です。これには運用者がなれたツールを使います。

ダウンしたノードの代わりとなる新規のサーバを用意し、同じグループのノードからデータを単純にコピーします。各グループのノードには同じデータが格納されているので、これだけで復旧できます。

コピーする際はtar と ssh を組み合わせるのが個人的には好みです

127.0.0.13 $ tar cf - | ssh 新サーバ 'cd /path/to/htdocs && tar xf -'

ssh の代わりに、 nc を使うのもいいかもしれませんね。数百GBから数TBのコピーには数日掛かるかもしれませんので、screen等を使ってterminalが落ちても問題がないようにしておくのがおすすめです。

コピーが終わったら MySQL のノード管理テーブルをアップデートして完全復旧です

mysql> BEGIN;
mysql> UPDATE nodes SET node='http://新サーバ/', online = 1 WHERE id = 3;
+----+-----+--------------------+--------+-------+--------+
| id | gid | node               | online | fresh | remote |
+----+-----+--------------------+--------+-------+--------+
|  3 |   1 | http://新サーバ/    |      1 |     1 |      0 |
+----+-----+--------------------+--------+-------+--------+
mysql> COMMIT;

ディスクバッファがない状態でノードを戻すと、負荷が急上昇することが考えられます。その際は、freshを落とした上で(新しいオブジェクトがこない)、負荷をみつつonlineをつけたり、外したりを繰り返してみると対応できるかもしれません。ここはなんか考えたいところ。

最後にオブジェクトの削除について

GreenBucketsのオブジェクトの削除については、わりと緩やかに作ってあります。物理ファイルの削除は必ずJobQueueを経由しますが、ノードにDELETEアクセスし、もしなんらかの理由で失敗してもやりなおすことはありません。MySQLのデータが消されていること、削除されたMySQL上にしかないランダムな数値を使ってファイル名をhash化していることから、物理ファイルが残っていても、ほぼ到達することはできないからです。この仕様が許されない場所ではGreenBucketsはお勧めできないかもしれません。

ノードのremoteの値についてはまたいつか書きたいと思います。

個人的になんですが。

通常のMySQLで

SHOW /*!50000 ENGINE*/ innodb STATUS

を行うと、(5.5.10でも確認)

  • BACKGROUND THREAD
  • SEMAPHORES
  • TRANSACTIONS
  • FILE I/O
  • INSERT BUFFER AND ADAPTIVE HASH INDEX
  • LOG
  • BUFFER POOL AND MEMORY
  • ROW OPERATIONS

の順で出ていましたが、 XtraDB(5.5.11-rel20.2) では TRANSACTIONS が最後に出てきます。

解説ページ - http://www.percona.com/docs/wiki/percona-server:features:innodb_show_status

これが何がうれしいかというと、STATUSの結果は64KBでtruncateされるので、接続数が非常に多い場合、TRANSACTIONS の項目が巨大になり TRANSACTIONS以下の項目(ROW OPERATIONSなど) が見切れてしまっていた問題がありました。CloudForecast (Cactiのプラグインなどでも)では ROW OPERATIONS を参照するので接続数が多いデータベースでは情報が取れなくなってしまっていましたが、XtraDB ではその心配がありません

XtraDBでの出力サンプル

...
--------------
ROW OPERATIONS
--------------
1 queries inside InnoDB, 0 queries in queue
2 read views open inside InnoDB
---OLDEST VIEW---
Normal read view
Read view low limit trx n:o 24F00
Read view up limit trx id 24F00
Read view low limit trx id 24F00
Read view individually stored trx ids:
-----------------
Main thread process no. 17847, id 1300269376, state: waiting for server activity
Number of rows inserted 1176930773, updated 0, deleted 0, read 763239495
0.00 inserts/s, 0.00 updates/s, 0.00 deletes/s, 80200.83 reads/s
------------
TRANSACTIONS
------------
Trx id counter 4A4F2
Purge done for trx's n:o < 0 undo n:o < 0
History list length 7
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 0, not started, process no 17847, OS thread id 1353517376
MySQL thread id 261, query id 154572 localhost root
show engine innodb status
---TRANSACTION 4A4F1, ACTIVE 105 sec, process no 17847, OS thread id 1353251136 starting index read, thread declared inside InnoDB 339
mysql tables in use 2, locked 0
, holds adaptive hash latch
MySQL thread id 265, query id 154570 localhost root Sending data
SELECT xxxxxxxxxx ...
Trx read view will not see trx with id >= 4A4F2, sees < 4A4F2
----------------------------
END OF INNODB MONITOR OUTPUT
============================

こんな話もありましたね

この辺で書いていたHTTPコンテンツ圧縮を行うPlack::Middleware::Deflater の Co-Maintainer にして頂いたので早速アップデートしました。Plackで運用されているとおぼしきアプリケーションは圧縮掛かってないことが多いので、転送量削減、レスポンス速度向上のために検討してみるのはどうでしょう。
問題があれば教えてくださいませ

Version 0.04 で追加した機能は、

  • content_type 毎に圧縮を行うか切り替える機能
  • Vary ヘッダにUser-Agentを追加する
  • 環境変数 psgix.no-compress, psgix.compress-only-text/html の導入

の3点です。psgix.no-compress, psgix.compress-only-text/html はそれぞれ Apache mod_deflateの no-gzip と gzip-only-text/html と同じ機能です。

おそらく問題が少なく動くサンプルは以下

builder {
  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/html','text/css','text/javascript','application/javascript'],
      vary_user_agent => 1;
  sub { [200,['Content-Type','text/html'],["OK"]] }
};

毎度これをコピペするのはアレなんで、別途Middlewareがあるのがいいのかなと考え中。

あわせて読みたい

「最近 DBIx::Schema がよくバージョンあがりますね」と言われましたが、またあがりました。Version 0.10 で Yokohama.pm#7 の発表では「予定」としていた 問い合わせ結果の Filter と引数の Deflate をサポートしました。

まず、Filter ですが、methodを作成する際に最後にCodeRefを渡します。

my $DTFMT = DateTime::Format::Strptime->new(
    time_zone => 'UTC',
    pattern   => '%Y-%m-%d %H:%M:%S',
);
__PACKAGE__->select_all(
    'entry_list',
    'offset' => { isa => 'Uint', default => 0 },
     q{SELECT id,nick,body,ctime FROM entries ORDER BY ctime DESC LIMIT ?,11},
    sub {
        my $row = shift;
        $row->{ctime} = $DTFMT->parse_datetime($row->{ctime});
        $row->{ctime}->set_time_zone("Asia/Tokyo");
    }
);

上記の例のように select_all ならすべてのrowに対してfilterをかけ、select_row なら得られたrowだけにfilterを適用します。for文を書かなくてよくなるので便利ですね。select_one と query はこの機能をサポートしていないので注意。

次に、Deflate ですが、こちらは引数の定義の中に埋め込みます。

__PACKAGE__->query(
    'add_entry',
    id => 'Str',
    nick => { isa => 'Str', default => 'anonymouse' },
    body => 'Str',
    datetime => {
        isa => 'DateTime',
        default => sub { DateTime->now( time_zone=>'UTC' ) },
        deflater => sub { $DTFMT->format_datetime(shift) },
    },
    q{INSERT INTO entries ( id, nick, body, ctime ) values ( ?, ?, ?, ? )},
);

上の例は datetime のカラムに対して、DateTimeのオブジェクトだけを受け取り、デフォルトはUTCの現在時間という定義をし、deflater で DateTimeオブジェクトをDBに保存するための文字列化を行っています

時間系のカラムを扱う際はこれでコードを短くできるので、便利ではないでしょうか。

NoPasteのサンプルアプリは、これを使って書き直しているので、参考にどうぞ。

徒歩で帰れるのはYokohama.pmだけ!

2011/5/13 に開かれた「Yokohama.pm#7」で発表してきました。hirataraさんのまとめがいつもながらすばらしいです

自分の発表は、「Operation Oriented Web Applications」というなんかもっともらしいタイトルで最近作ってる

  • Log::Minimal
  • DBIx::Sunny
  • GreenBuckets

について話してきました。本当は Scope::Container も追加しようと思ったのですが、どう考えても時間ないのでカットしてます。それでも時間オーバーすみません><

資料はいつも通り slideshare


3つのモジュール/システムを紹介しながら、運用しやすい、運用からみて効率の良いアプリケーションとはというテーマをもってみました。ログ、SQL、効率的なスキーマ設計、ハードウェアの有効活用あたりが盛り込まれていますが、どうだったでしょうか。YAPC::Asia でももう少し追加+まとめて同じネタでやろうかなと考え中。

他にYokohama.pm#7で気になった発表では、

  • nekokak さんの HandlerSocket の話。スイートスポットが実際どんな場所で変更したことでどうサーバ負荷に変化があったのが知りたいなと思ったのと、セッション管理に使うあたりは実際やってみた感想など聴きたいです
  • TekuHubのAnyEventをつかった話は、サービスに投入した事例として興味深かった。成功されてサーバ環境を整えてもっと運用の深みにハマっていただきたい(良い意味で)。そしてYAPC::Asiaでも発表するといいんじゃないなぁーと勝手に思いました。

でしょうか。両方とも今後が気になります

今回の Yokohama.pm は会場費など livedoor から出して頂いていたようです。livedoor すばらしいですね!

2つのモジュールをアップデートしました。

これまでPlack::Middleware::Log::Minimal はログ出力時に テキスト文字列でもそのまま出力していましたが、これだとwarningがでてしまうので、指定した文字コードにencodeする機能をつけました。アプリケーション中で

use utf8; 
use Log::Miniamal;
use Plack::Builder;

my $app = sub {
    my $env = shift;
    warnf("にほんごの文言"); 
};

builder {
    enable 'Log::Minimal', autodump => 1, encoding => 'euc-jp';
    $app;
};

などとlatin-1範囲外のテキストを使った場合、自動でeuc-jpにencodeして出力します。encodingを指定しない場合デフォルトの utf8 でencodeします。

Log::Minimal はそれに対応するための変更です。Plack::Middleware::Log::Minimal v0.04を使う場合はアップデートが必要となります。

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

Object Storage とは何かというと、OSSではOpenStack Object StorageとかMogileFS 、Webサービスで使われているところでは mixi の ImageCluster とか livedoor の STF とか、比較にならないけどAmazon S3とかそういったたぐいのものです。しばしば画像のストレージなんかに使われていると思います。今回作ったのは GreenBuckets というもので、mixiのImageClusterの構成をまねしつつ、stfと同じようにバケット単位での操作を可能としています。なんですでにあるのに作ったのかというと、主に「つくってみかったから」ですね。一応目標として

  • シンプルだけど使える
  • cpanm でインストールが完了する
  • できるだけ少ない依存関係
  • 素のMySQLだけで動く
  • mapping のDBのインデックスサイズをできるだけ小さく保つ
  • 普通のサーバで数億ファイルまで管理可能を目標
  • Storageは既存のHTTPdを利用する
  • 運用のノウハウを生かす

あたりをあげました。現状動作実績なしなので注意。

ソースコード: https://github.com/kazeburo/GreenBuckets

そんな GreenBuckets の構成ですが

  • Dispatcher
  • JobQueue Worker
  • DAV Storage
  • MySQL

の4つになります。dispatcher と JobQueue Worker は Perlで構築され、それぞれデーモンとして動作します。DAV StorageはApache mod_dav や Nginx、Perlbal等の既成のHTTPdが利用できます。MySQLはmapping用のデータとJobQueueのQueue等を保存します。

それぞれの役割を図にすると以下のようになります。まず、画像を保存するときから

greenbuckets_put.png

画像を保存するときには、dispatcherに対して、PUTもしくはPOSTでデータを送ります。PUTだと同じファイル名のファイルに対し上書きができ、POSTだと上書きせずにエラーになります。(1)dispatcher はまず保存ができるノード(DAV Storage)をMySQLに問い合わせます。ノードは決められたレプリカ数のあらかじめ決められた組で構成されています。レプリカ数は2以上で設定可能です。このあたりはmixiのストレージと同じです。 次に(2)URL からバケット名を抜き出し、バケットの有効性を確認します。もしバケットがなければ自動で作成されます。 (3) ノードが決まったら、DAVプロトコルを利用してPUTします。自動でディレクトリを作成したりしないので、Apacheのmod_davだと最初に必要分を作成しておいたほうがいいかもしれません。内部のPATHは

http://{node}/\d{2}/\d{2}/sha224_hex( filename + random_id + バケットID)

で構成されます。\d{2} は filename の murmur_hash 値の下4桁です。オブジェクトの保存時にはすべてのノードにコピーするのではなく、2ノードのみにコピーし、残りをキューに任せます(5)。

この2ノードの選択方法は、ノードのIDと内部のPATHのhash値によって決められています。オブジェクトによって一意に決まる順序となります

my @url = sort {
    murmur_hash($a->{node_id} . $a->{path}) <=> murmur_hash($b->{node_id} . $b->{path})
} @...

オブジェクトの取得時もおなじルールを適用するので、キューによってコピーが遅延されたノードにデータを取りにいく可能性を小さくしています。また同じデータはかならず同じノードへアクセスするので、雀の涙ほどかもしれませんがディスクキャッシュの効率をあげることもできます。

(4) データのコピーが終わったら、MySQLにどのノードの組に格納したのかの情報を追加します。その際URLをキーにするのではなく、murmur_hash値を使います。

INSERT INTO objects (fid,bucket_id,rid,gid,filename) 
  VALUES( murmur_hash(filename), バケットID, random_id,ノードグループID , filename);

これはMySQL上のインデックスのサイズを小さく保つのが目的です。murmur_hash は 32bit の UNIT を返すので、DBもINT UNSIGNED(4byte)で済みます。indexはこの fid と bucket_id にのみ張ります。murmur_hash の値は衝突する可能性があるので、index は張りませんが、もとのファイル名も保存し、取得時はかならず確認します。

(5) (6) キューはデータが保存されたノードから一度ファイルを取得してコピーします。もしコピーに失敗したときは、別のノードグループに保存しなおし、MySQLをアップデートします。

次に取得時ですが

greenbuckets_get.png

dispatcherに対して普通にGETリクエストを送ります。(1)まずPUT時と同じようにバケットを調べます。(2)次にファイル名からデータが保存されているノードを問い合わせます。この際PUT時と同じく、murmur_hashを使います。

SELECT filename,node_id,.. FROM objects WHERE bucket_id = バケットID AND fid = murmur_hash(ファイル名)

取得できたfilenameとリクエストされたファイル名が一致するか確認します。一致したらそのノードのIDと内部PATHで一意に決められた順序でストレージにアクセスし、コンテンツをクライアントに返します。

■使い方

今のところの使い方。そのうちCPANにあげれるようにしたいけど今のところgithubから。試すにはMySQLとPerlが必要です。

$ git clone git://github.com/kazeburo/GreenBuckets.git
$ cd GreenBuckets
$ perl Makefile.PL
$ cpanm --install-deps . # PerlbalとTest::mysqldとかが入るので注意

まず、MySQLにデータベースを作成

$ mysqladmin create greenbuckets
$ ./bin/greenbuckets scheme | mysql greenbuckets

「$ greenbuckets scheme」とすることでスキーマが出力されるのでそれをパイプで流します。

とりあえず、ストレージについてはPerlbalを使ってみます。それぞれのノードは

http://localhost:8080/1/
http://localhost:8080/2/
http://localhost:8080/3/
http://localhost:8080/4/
http://localhost:8080/5/
http://localhost:8080/6/

とトップディレクトリを変更することで代用します。1..3がグループ1、4..6がグループ2とします。これをDBに登録

INSERT INTO `nodes` VALUES (1,1,'http://127.0.0.1:8080/1/',1,1);
INSERT INTO `nodes` VALUES (2,1,'http://127.0.0.1:8080/2/',1,1);
INSERT INTO `nodes` VALUES (3,1,'http://127.0.0.1:8080/3/',1,1);
INSERT INTO `nodes` VALUES (4,2,'http://127.0.0.1:8080/4/',1,1);
INSERT INTO `nodes` VALUES (5,2,'http://127.0.0.1:8080/5/',1,1);
INSERT INTO `nodes` VALUES (6,2,'http://127.0.0.1:8080/6/',1,1);
INSERT INTO `nodes` VALUES (7,3,'http://127.0.0.1:8080/7/',1,1);
INSERT INTO `nodes` VALUES (8,3,'http://127.0.0.1:8080/8/',1,1);
INSERT INTO `nodes` VALUES (9,3,'http://127.0.0.1:8080/9/',1,1);

そして Perlbal を起動します。confは以下のような感じ

CREATE SERVICE static_server
  SET role           = web_server
  SET listen         = 0.0.0.0:8080
  SET docroot        = /tmp/greenbuckets
  SET dirindexing    = 1
  SET enable_delete  = 1
  SET enable_put     = 1
  SET min_put_directory = 0
ENABLE static_server

put/deleteを有効にし、自動でディレクトリも作成するように設定します

$ perlbal -c etc/perlbal.conf

つぎに greenbucketsのconfigをします

$ ./bin/greenbuckets config > config.pl

これで設定ファイルのテンプレートが吐き出されるので、DBのユーザ名、パスワード等を変更します。変更したら dispatcher と jobqueue を起動します。

$ ./bin/greenbuckets dispatcher -c config.pl
$ ./bin/greenbuckets jobqueue -c config.pl

それぞれ起動していればインストール完了です。デフォルトdispatcherが5000番、jobqueueはステータス取得用のデーモンが5101番で起動します

curlを使ってPUTしてみます

$ curl -basic --user admin:admin -X PUT -d "Mary Poppins" \
    http://localhost:5000/test/supercalifragilisticexpialidocious
OK

OKが返って来ました。実際保存されているか。確認します

$ find /tmp/greenbuckets -type f
/tmp/greenbuckets/1/88/85/d5e9c13ea8cc4fc218d54c6a3f55a663d52ea3f55f0d7c4ccca3e625
/tmp/greenbuckets/2/88/85/d5e9c13ea8cc4fc218d54c6a3f55a663d52ea3f55f0d7c4ccca3e625
/tmp/greenbuckets/3/88/85/d5e9c13ea8cc4fc218d54c6a3f55a663d52ea3f55f0d7c4ccca3e625

ノードグループ1の、ノード1、2、3に保存されているのが確認できました。次に実際に GET してみます

$ curl -v http://localhost:5000/test/supercalifragilisticexpialidocious
...
< HTTP/1.0 200 OK
< Date: Mon, 09 May 2011 14:49:53 GMT
< Server: Plack::Handler::Starlet
< Content-Type: text/plain
< Last-Modified: Mon, 09 May 2011 14:46:57 GMT
<
* Closing connection #0
Mary Poppins

ちゃんと保存したデータが得られました。ちゃんと動きそうです。ほっ。

今後の課題としては、CASをサポートしたいのと、遠隔地ノードを考えたいのと、実績と運用のドキュメントかな。もし社内外で興味のある方がいましたらご連絡ください。

miyagawaさんがjoinしてにわかにアツいdotcloudでnopasteなアプリを動かしてみたよ

元々、ここで紹介したモノで、dotcloud上で展開するにあたりいくつか変更している。主な変更点は

  • psgiをapp.psgiにrename。libへのパスも通す
  • 手軽に動かすためにSQLiteだったのでMySQLに変更
  • MySQLの設定はホームディレクトリ以下に config.pl を置いてそれを読む込む仕様に
  • サーバの時間がUTCなのでそれにあわせて変更
  • Makefile.PL に不足がないように
  • inc ディレクトリもgitで管理

です。

だれかが書いていたような気もするけど、dotcloud で MySQLを動かすには管理コマンドから

$ dotcloud deploy -t mysql mysql.kazeburo
$ dotcloud info kazeburo.mysql                        
cluster: wolverine
config:
    mysql_password: XXXXXXXXXXXXXXXXXX
created_at: 1304432444.8159039
name: kazeburo.mysql
namespace: kazeburo
ports:
-   name: ssh
    url: ssh://[email protected]:35XX
-   name: mysql
    url: mysql://root:[email protected]:35XX
state: running
type: mysql

deploy コマンドでインスタンスを作成したあと、info でパスワードを確認。ssh でログインして mysql に接続する

$ dotcloud ssh kazeburo.mysql
dotcloud@kazeburo-mysql:~$ mysql -u root -p

これで MySQL に接続ができるので DBやユーザの作成を行います。

> CREATE DATABASE nonopaste DEFAULT CHARSET=utf8;
> GRANT ALL ONnonopaste.* TO 'nonopaste'@'%' IDENTIFIED BY 'XXXXXXXX';

割とMySQLの設定がデフォルトそのまんまなので、その道の人から見ると「えー」なのかもしれませんが、アプリケーションのでプロイのベストプラクティスなど含めて、今後dotcloud上でアプリケーションをどうやってスケールさせていくのかとか、ドキュメントや事例が増えてくると思うのでそれに期待。OSCONのmiyagawaさんのセッションでも何かあるのかな?