Thriftが便利すぎる

ID: 36
creation date: 2009/09/24 19:55
modification date: 2009/09/24 19:55
owner: shunya

9/29 追記

Thriftのperlクライアントをデフォルトのままで使うとパフォーマンスがあまりよくないようです.高速に使いたい場合は以下のエントリーを参照してください.

http://blog.broomie.net/index.cgi?id=38

----

ちょっと前に「thriftって便利らしいよー」って話を聞いていたのだけれども、なかなか手をつけられずにいたらはてなブックーマークで使われているらしいという噂を聞いたり、Thriftを使って俺俺Key-Value Storeを作ったのように、TXを使ったThriftの紹介などが出てきたりしたのでそろそろ自分でも試したいなあと思い、試しました。で、先に結論を言っておくとThrift、とても気に入りました。とても簡単に処理用の専用サーバをたてることができて、かつ簡単にクライアントから処理要求が送れます。ボクは今まではRESTFulな感じでhttpでこのタスクをやっていたのですが、RESTFulな専用サーバをたてるのは結構開発コストがかかるんですよね。その点で、Thriftは開発コストはとても落ちると思うのでとても気に入っています。なんといって言語バインディングを自動で生成してくれるのは本当に開発コストが落ちますね。

Thriftを使うモチベーション

文書の自動分類とかを分類器にさせる場合、分類をリアルタイムに高速にやりたい場合があります。そんなときは分類器サーバはいくつかに分散されていることが望ましいし、ウェブアプリケーションから簡単に分類ができるようにしたいですね。そのために上記で述べたように、僕はRESTFulな環境をよく構築してhttp経由で処理していたりしていたんですね。適当ではあるのですが、以下にその概要図を示します。

image:1:1253789684-REST.png

この場合だとクライアントはperlスクリプトで書かれていて、http経由で分類の要求をするためにLWPなどを使います。これはこれで、高速だし使い勝手もなかなかよいのだけど、サーバーサイドのCGIプログラミングは結構チューニングが必要だったり、クライアントプログラムは言語によって複数実装しなければならなかったりと、開発コストが結構かかります。

そこで、Thriftを使うと以下の図のようになるんですね。

image:2:1253789694-THRIFT.png

何が楽になったかというと、サーバサイドも、クライアントサイドも基本的にはThriftがプログラムを生成してくれるんですね。特に通信部分を実装する際にRESTの場合だとCGIなどを自分で書かなくてはいけなかった部分を、ThriftではThriftで定義されたプロトコルを使うように自動的にプログラムを生成してくれるのがありがたいです。かつ、クライアントのバインディングも図に示したように多様に対応してくれるんですね。クライアントプログラムは対応している言語が多ければ多いほど利便性は上がると思いますし、これはたまらんです。バインディングのある、ないは喧嘩の種にもなっちゃいますしね!

Thriftの簡単な使い方

で、何がすごいって本当にすごく簡単にできちゃうんですね。例として以下にThriftを使った簡単な「足し算」と「引き算」のサーバを作る例をしめします。実は簡単な使い方は他のブログで懇切丁寧に説明されているので、わかりやすかった説明を引用させていただきながら説明します。間違えなどがあったらごめんなさい!

Thrifのインストール

  • ThriftのインストールはUbuntuであれば以下のエントリーの内容に従えばできます。

http://d.hatena.ne.jp/conceal-rs/20081116/1226838725

  • fedoraでも試しましたが、基本的にはC++の開発環境が入っていれば問題なくコンパイルできました。
  • 自分の環境ではruby-devが入っていないと、怒られました。yum search ruby-devで簡単に探せます。

インストールが完了したら、「足し算、引き算」プログラムの作り方を順に説明します。

thriftファイルの作成

  • thriftファイルにはサーバーサイドのプログラムに定義される関数のインターフェースのようなものを書きます。これをもとにサーバのスケルトンをThriftが生成するようです。
  • 今回はパッケージ名をTinyCalcと名付けます
  • 定義する関数は2つだけ、sum(足し算), subtract(引き算)
  • TinyCalc.thriftと名付けます

TinyCalc.thrift

#!/usr/bin/thrift

service TinyCalc
{
        double sum(1: double param1, 2: double param2)
        double subtract(1:double param1, 2:double param2)
}
  • 引数には引数番号を指定します。書式は例に示した通りです。
  • 今回使っている型はdoubleで指定しましたが、voidやstring, map, boolなども使えるようです。
    • なぜかintは定義されていないと怒られました。なぜ定義していないのかわかりません。

10/25 追記

  • インターフェースで使えるデータ型は以下のものらしいです
データ型
インターフェース定義で利用できるデータ型は以下のとおりです。
    * 基本データ型
      bool, byte, i16, i32, i64, double, string
    * 構造体
    * コンテナ
      list, set, map
    * 例外

http://cydn.cybozu.co.jp/2007/06/thrift.html より引用

  • intはixxになっているんですね。

スケルトンの生成

  • 上記のthriftファイルを元にスケルトンを生成します。
thrift --gen rb --gen cpp --gen perl TinyCalc.thrift
  • 今回はperlとrubyのバインディングを生成するようにしました
  • サーバプログラムをc++で生成したいので--gen cppをしておきます

サーバプログラムを作成する

  • スケルトンが生成されたらgen-cpp gen-perl gen-rbという3つのディレクトリが生成されていますね
cd gen-cpp
ls
TinyCalc.cpp  TinyCalc.h  TinyCalc_constants.cpp  TinyCalc_constants.h  TinyCalc_server.skeleton.cpp  TinyCalc_types.cpp  TinyCalc_types.h
  • gen-cppには上記のようなスケルトンが生成されていると思います。
  • サーバプログラムを用意しましょう。
cp TinyCalc_server.skeleton.cpp TinyCalc_server.cpp
  • TinyCalc_server.cppがサーバサイドのインターフェースのプログラムとなります。これを修正します。
    • 以下の部分を修正します。
 double sum(const double param1, const double param2) {
    // Your implementation goes here                                                                                                                                
    printf("sum\n");
  }

  double subtract(const double param1, const double param2) {
    // Your implementation goes here                                                                                                                                                
    printf("subtract\n");
  }

  • ちゃんとthriftファイルに書いたように、sumとsubtractのスケルトンが生成されていますね!
    • 修正は関数の中身を実装するだけです。
double sum(const double param1, const double param2) {
    return param1 + param2;
  }

  double subtract(const double param1, const double param2) {
    return param1 - param2;
  }

これでサーバプログラムが完成です。

プログラムをコンパイルする

  • ちゃんとMakefileを書いたほうがいいのですが、今回はプログラムが小規模なので、「なんとなく日記」さんと同じようにコマンドを直接打ってコンパイルしてしまいます。
g++ -g TinyCalc_server.cpp TinyCalc.cpp -o TinyCal_server -lthrift -I/usr/local/include/thrift

これでサーバのインターフェースが完成です。

サーバを起動する

  • 作成したサーバを起動しましょう。単に実行するだけです。
./TinyCal_server

クライアントプログラムを書く

  • 今回はperlとrubyのバインディングを生成するように指定しました。
  • perlのクライアントプログラムを書く例を示します。
#!/usr/bin/perl

use strict;
use warnings;
use lib './gen-perl';
use Thrift;
use Thrift::BinaryProtocol;
use Thrift::Socket;
use TinyCalc;

# localhostの部分はサーバのipアドレスを入れれば他のクライアントサーバからでも呼べます
my $transport = Thrift::Socket->new('localhost', 9090);
my $tc = TinyCalcClient->new( Thrift::BinaryProtocol->new($transport) );

$transport->open();

my $rv = $tc->sum(1.0, 5.0);
print "$rv\n";
$rv = $tc->subtract(1.0, 5.0);
print "$rv\n";

$transport->close();

実行結果

perl test.pl
6
-4

ちゃんと動いていますね!

駆け足の説明となりましたが、このようにとても簡単にできるんですね。

Thriftの実用的な使い方

最初に説明したTxを使う例の紹介のように、言語処理などの重い処理にはとても有効だと思います。はてなブックマークでもブックマークの自動分類にThriftを使っているようです。そのほかにも形態素解析の専用のサーバを構築してクライアントサーバから呼んだり、クラスタリングなんかでも便利に使えますよね。

ここでは日本語文書を分かち書きにする例を示したいと思います。超簡単です。

準備

  • 分かち書きにするソフトは僕が作成したTinySegmenterxxを使います
  • これは工藤拓さんが作成した、Javascriptだけで書かれた日本語分かち書きソフトのC++で書いたものです。

http://code.google.com/p/tinysegmenterxx/

基本的な流れは上記で説明した、足し算引き算のものと一緒なので重要な部分だけ紹介します。

thirftファイルの作成

#!/usr/local/bin/thrift --gen cpp --gen perl --gen ruby

service JpSegmenter
{
  string segment(1:string input);
}

スケルトンの生成

thrift --gen rb --gen cpp --gen perl JpSegmenter.thrift

サーバプログラムの作成

JpSegmenter_server.cpp

// This autogenerated skeleton file illustrates how to build a server.                                                                                                              
// You should copy it to another filename to avoid overwriting it.                                                              

#include "JpSegmenter.h"
#include <protocol/TBinaryProtocol.h>
#include <server/TSimpleServer.h>
#include <transport/TServerSocket.h>
#include <transport/TBufferTransports.h>

/* TinySegmenterxxのヘッダーファイル */
#include <TinySegmenterxx/tinysegmenterxx.hpp>


using namespace apache::thrift;
using namespace apache::thrift::protocol;
using namespace apache::thrift::transport;
using namespace apache::thrift::server;

using boost::shared_ptr;

class JpSegmenterHandler : virtual public JpSegmenterIf {
 public:
  JpSegmenterHandler() : sg(){
  }

/* 日本語文を分かち書きする関数 */
  void segment(std::string& _return, const std::string& input) {
    tinysegmenterxx::Segmentes segs;
    sg.segment(input, segs);
    for(unsigned int i = 0; i < segs.size(); i++){
      std::cout << segs[i] << std::endl;
      _return.append(segs[i]);
      _return.append("\n");
    }
  }

private:
  /* TinySegmenterxxのインスタンス */ 
  tinysegmenterxx::Segmenter sg; 

};

int main(int argc, char **argv) {
  int port = 9090;
  shared_ptr<JpSegmenterHandler> handler(new JpSegmenterHandler());
  shared_ptr<TProcessor> processor(new JpSegmenterProcessor(handler));
  shared_ptr<TServerTransport> serverTransport(new TServerSocket(port));
  shared_ptr<TTransportFactory> transportFactory(new TBufferedTransportFactory());
  shared_ptr<TProtocolFactory> protocolFactory(new TBinaryProtocolFactory());

  TSimpleServer server(processor, serverTransport, transportFactory, protocolFactory);
  server.serve();
  return 0;
}

スケルトンから修正したのは以下の三か所だけです

  • TinySegmenterxxのヘッダーファイルをインクルードする
    • 9/25 segment()の第一引数をconstにしたためプログラムを修正
/* TinySegmenterxxのヘッダーファイル */
#include <TinySegmenterxx/tinysegmenterxx.hpp>
  • インターフェースとなる関数を実装する
    • 日本語の文書を入力とし、分かち書きした結果を返す
/* 日本語文を分かち書きする関数 */
  void segment(std::string& _return, const std::string& input) {
    tinysegmenterxx::Segmentes segs;
    sg.segment(input, segs);
    for(unsigned int i = 0; i < segs.size(); i++){
      std::cout << segs[i] << std::endl;
      _return.append(segs[i]);
      _return.append("\n");
    }
  }
  • ちょっとthriftの癖のある書き方になっていますが、わからないでもないです
  • _returnという引数はthriftが勝手につけます(stringを返り値にした場合はこうなります)
  • で、返り値は勝手にvoidにされるので、_returnの中に計算結果を格納すれば、バインディングにはちゃんと_returnの内容が返るようになっているようです
  • TinySegmenterxxのインスタンス
private:
  /* TinySegmenterxxのインスタンス */ 
  tinysegmenterxx::Segmenter sg; 

プログラムのコンパイルと起動

g++ -g JpSegmenter_server.cpp JpSegmenter.cpp -o JpSegmenter_server -lthrift -I/usr/local/include/thrift
./JpSegmenter_server

クライアントプログラムの作成

  • ここでも例に従ってperlのクライアントライブラリの例を示します。
#!/usr/bin/env perl
use strict;
use warnings;
use lib './gen-perl';

use Thrift;
use Thrift::BinaryProtocol;
use Thrift::Socket;

use JpSegmenter;

my $transport = Thrift::Socket->new('localhost', 9090);
my $sg = JpSegmenterClient->new(Thrift::BinaryProtocol->new($transport));

$transport->open();

my $input = "東京特許許可局へ社会見学へ行ってきたよ.";
my $result = $sg->segment($input);
print "$result\n";

$transport->close();

このプログラムを実行すると以下のような結果が得られます。

perl test.pl 
東京
特許
許可
局
へ
社会
見学
へ
行っ
て
き
た
よ
.

ちゃんと分かち書きっぽくなっていますね!

すごい駆け足な説明になってしまいましたが、Thriftの便利さが伝わったでしょうか。僕個人としては便利すぎてちょっと興奮しています。本当にいろんなプログラムで使いたいですね。今後もThriftを使ってこの手のプログラムを書いたら紹介したいと思いますー。

Share Facebookでshareする
2 comments
shunya : tinysegmenterのプログラムを修正したため,エントリーの内容も少し修正しました. (2009/09/25 23:31)
happy.siro : 普通に複数のプログラミング言語でプログラム死体土岐にも便利な気がしたり (2010/04/23 11:43)
riddle for guest comment authorization:
Where is the capital city of Japan? ...