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
// 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で使われてるだけあって、さすがに良く出来ています。ドキュメントが無いので印象的に損している感じ。他にも色々便利な機能が有りそうなので暇なときにソースをチラ見しています。
久々にてけとーな文章書いたので時間かかった・・・。
- by
- at 23:16

comments