自己紹介

太田一樹。
東京の大学の情報科学科に通う大学生。moratorium満喫中。

お勧め書籍 [全部見る]

飾り

Search


Category Archives

Recent Entries

  1. 論文
  2. JJUG CCCでプレゼンします
  3. kzk's bookshelf
  4. En Google by Gulfweed
  5. PNUTS
  6. コメントスパム対策
  7. Hadoop + Luceneで分散インデクシング
  8. Hadoopの解析資料
  9. Cluster 2008
  10. SWoPP 2008

2008年02月13日

Thriftを利用したログからの復旧機能の実装

さてさて卒論発表終わりましたよ。寝まくり&飲んだくれ&焼肉。暇な人遊んで。

最近、多言語RPC機構が欲しくなる機会が多く、Thriftを使ってみたりしています。

まずは3つ目のサイボウズ開発本部アルバイトの山本さんの記事を読むと、thriftがどういうものなのかが分かると思います。簡単に言いますと、RPCのAPIを独自記法で定義したファイルを用意し、それをthriftコンパイラに入力として与えると、各言語用にRPC用のコードを自動生成してくれるというものです。

クライアント・サーバーコードを各言語で書けるのは当然として、マルチスレッド型・イベント型などサーバーの種類を選べる等々、色々気が利いていて良い感じです。

似たものとしてbinpacというのも有るらしいのですが、これは実装は公開されてないみたいです。Googleの人がfirst authorなので中で使われているのかもしれませんね。

ところで先日コードを読んでいたら、Thriftを使うとログからのサーバー状態復旧機能が簡単に作れる事に気づいたので書いてみます。White Paperにもこの機能について少し言及はしてあるのですが具体的なコードまでは書いてません。

トランザクション処理や分散システムにおいては、ステートが変わる操作をディスク上のログに残し、ぶっ壊れた時にそこから復旧するという方法がよく使われます。たとえばMySQLのバイナリログみたいなもんです。

これを自前のアプリケーションで実装しようとすると、実に面倒くさい。まずデータを適当にシリアライズして、ログフォーマット決めて、書き込み側のコード書いて、読み込み側のコード書いて、デシリアライズして。。。というのは結構やってられない作業です。

Thriftを使うと、これら一連の操作がさぼれます。Thriftは非常にレイヤー化された作りになっていて、Protocol LayerやTransport Layer等から構成されます(非常に適当な説明なのでコード読んでくださいね)。

通常のRPCだとTransport LayerにTCPの機能を抽象化したTFramedTransportクラスを使用するのですが、今回はここにTFileTransportクラスを使用します。これによりファイルをTransportに使う事が出来ます。つまり、ファイルをサーバーもしくはクライアントと見立ててRPCを行うことができます。

ファイルをサーバーと見立ててRPCを行った場合、RPCの呼び出し情報自体がファイルに書き込まれます。これはロギングに相当します。ファイルをクライアントと見立てた場合、ファイルにシリアライズされた呼び出しが行われます。これはログからの復旧に相当します。

サンプルとしてfunc(int v, string s, vector vs)という関数の呼び出しを記録し、記録されたログから同じように呼び出しを再度行うプログラムを書いてみました。まずthriftコンパイラに食わせるファイルです。

// redo.thrift
service Redo {
  void log(1:i32 val, 2:string str, 3:list<string> vstr);
}

このファイルを $ thrift -cpp redo.thrift 等として処理し、コードを自動生成します。次にログを書く側のコードです。

#include <string>
#include <iostream>

#include <protocol/TBinaryProtocol.h>
#include <transport/TTransportUtils.h>
#include <transport/TFileTransport.h>

#include "gen-cpp/Redo.h"

using namespace std;
using namespace facebook::thrift;
using namespace facebook::thrift::protocol;
using namespace facebook::thrift::transport;
using boost::shared_ptr;

shared_ptr<TFileTransport> out;

static void
init(void)
{
  out = shared_ptr<TFileTransport>(new TFileTransport("foo.log"));
  out->seekToEnd();
}

static int
func(int a, const string& s, const vector<string>& vs)
{
  shared_ptr<TMemoryBuffer> b(new TMemoryBuffer());
  shared_ptr<TProtocol> p(new TBinaryProtocol(b));
  shared_ptr<RedoClient> c(new RedoClient(p));
  c->send_log(a, s, vs);
  uint8_t *buf; uint32_t size;
  b->getBuffer(&buf, &size);
  out->write(buf, size);
  return 0;
}

int
main(int argc, char **argv)
{
  cout << "==== DO ====" << endl;

  init();

  srand(time(NULL));
  for(unsigned int i=0; i<10; i++){
    int v = rand();
    string s;
    for(unsigned int j=0; j<(rand()%10); j++)
      s += 'a' + (rand()%26);
    vector<string> vs;
    for(unsigned int j=0; j<(rand()%10); j++){
      string str;
      for(unsigned int k=0; k<(rand()%10); k++)
        str += 'a' + (rand()%26);
      vs.push_back(str);
    }

    cout << v << ":" << s << ":";
    for(unsigned int i=0; i<vs.size(); i++)
      cout << vs[i] << ",";
    cout << endl;

    func(v, s, vs);
  }

  return 0;
}

main関数でランダムな入力を作って、funcという関数を呼び出しています。func関数の中ではthriftを利用してfoo.logに関数の引数情報をロギングする処理が書かれています。次に復旧側です。

#include <string>
#include <iostream>

#include <protocol/TBinaryProtocol.h>
#include <transport/TTransportUtils.h>
#include <transport/TFileTransport.h>

#include "gen-cpp/Redo.h"

using namespace std;
using namespace facebook::thrift;
using namespace facebook::thrift::protocol;
using namespace facebook::thrift::transport;
using boost::shared_ptr;

class RedoHandler : virtual public RedoIf
{
public:
  void log(const int32_t v, const string& s, const vector<string>& vs);
};

void
RedoHandler::log(const int32_t v, const string& s, const vector<string>& vs)
{
  cout << v << ":" << s << ":";
  for(unsigned int i=0; i<vs.size(); i++)
    cout << vs[i] << ",";
  cout << endl;
}

int
main(int argc, char **argv)
{
  cout << "=== REDO ===" << endl;

  shared_ptr<TFileTransport> t(new TFileTransport("foo.log"));
  shared_ptr<TProtocol> p(new TBinaryProtocol(t));
  shared_ptr<RedoHandler> handler(new RedoHandler);
  shared_ptr<RedoProcessor> processor(new RedoProcessor(handler));
  shared_ptr<TProtocolFactory> protocolFactory(new TBinaryProtocolFactory());

  TFileProcessor fileProcessor(processor, protocolFactory, t);
  uint32_t nEvents = 0; // unlimited
  bool isTail = false;
  fileProcessor.process(nEvents, isTail);
  return 0;
}

TFileProcessor.processによって、foo.logに書かれた1つ1つの呼び出し内容を再現しています。Transport層にはTFileTransportを指定しています。実行結果は次のようになります。

cloud% ./do
==== DO ====
906161766:ke:v,
1980049067:vn:v,
289453601:mg:m,v,vnbdl,
cloud% ./do
==== DO ====
1382118930:jkkcmwy:r,
406757158:nl:iklo,t,ks,
1254896440:guu:
cloud% ./redo
=== REDO ===
906161766:ke:v,
1980049067:vn:v,
289453601:mg:m,v,vnbdl,
1382118930:jkkcmwy:r,
406757158:nl:iklo,t,ks,
1254896440:guu:

doプログラムの内容をredoプログラムが忠実に再現できていることが分かります。ソースはここにおいておきます。自由に改変するなり配布するなりしてください。

ThriftはFacebookで使われてるだけあって、さすがに良く出来ています。ドキュメントが無いので印象的に損している感じ。他にも色々便利な機能が有りそうなので暇なときにソースをチラ見しています。

久々にてけとーな文章書いたので時間かかった・・・。


trackbacks

trackbackURL:

comments

comment form
comment form