How We Made GitHub Fast - GitHubデータのシリアライズおよびRPC(リモートプロシージャコール)としてBERT-RPCを選んだ様です。
Now that things have settled down from the move to Rackspace, I wanted to take some time to go over the architectural changes that we’ve made in order to bring you a speedier, more scalable GitHub.
...
For our data serialization and RPC protocol we are using BERT and BERT-RPC.
http://github.com/blog/530-how-we-made-github-fast
BERT and BERT-RPC 1.0 SpecificationBERT-RPCはBERT(Binary ERlang Term)という名前の通り、ERlangのterm_to_binary/1で生成されるフレキシブルなバイナリデータ交換の仕組みで、これをRPCに利用した物がBERT-RPCです。
BERT and BERT-RPC are an attempt to specify a flexible binary serialization and RPC protocol that are compatible with the philosophies of dynamic languages such as Ruby, Python, PERL, JavaScript, Erlang, Lua, etc. BERT aims to be as simple as possible while maintaining support for the advanced data types we have come to know and love. BERT-RPC is designed to work seamlessly within a dynamic/agile development workflow. The BERT-RPC philosophy is to eliminate extraneous type checking, IDL specification, and code generation. This frees the developer to actually get things done.
http://bert-rpc.org/
erlangで使える、integer,float,atom,tupple,bytelist,list,binaryをシリアライズして転送する事が出来ます。仕様書はerlangの拡張仕様ページに掲載されています。BERT
BERT (Binary ERlang Term) is a flexible binary data interchange format based on (and compatible with) Erlang's binary serialization format (as used by erlang:term_to_binary/1).
http://bert-rpc.org/
External Term Format詳しくはgithubのブログエントリでも説明されています。
The external term format is mainly used in the distribution mechanism of Erlang.
http://erlang.org/doc/apps/erts/erl_ext_dist.html
最近ではMessagePackやProtocolBuffer、Thriftなどバイナリフォーマットを使ったRPCが流行ですが、このErlangのバイナリフォーマットは無駄の無いシリアライズ形式になっているかと思います。
実際には型識別と、それに後続するデータで構成され、型によってはデータ部の前にデータ長が付加されます。
Erlangのシリアライズでは、Erlangのprotocolバージョン131から開始されますが、その前に全体長を付与した物がRPCで使われています。RPCの本体はtuppleと、その中のATOMで構成されています。
実装を見たい方は以下のrubyによる実装を見る事が出来ます。
mojombo's bert at master - GitHub
BERT (Binary ERlang Term) serialization library for Ruby.
http://github.com/mojombo/bert
mojombo's bertrpc at master - GitHubまたerlangによるシリアライズ実装、およびBERT-RPCを使ったRPCサーバの実装は以下から参照出来ます。
BERTRPC is a Ruby BERT-RPC client library.
http://github.com/mojombo/bertrpc
mojombo's erlectricity at master - GitHub
Erlectricity exposes Ruby to Erlang and vice versa
http://github.com/mojombo/erlectricity
mojombo's ernie at master - GitHub例えば上記githubブログへのリンク先で説明されている様に
Ernie is an Erlang/Ruby BERT-RPC Server.
http://github.com/mojombo/ernie
# calc.rb
require 'ernie'
mod(:calc) do
fun(:add) do |a, b|
a + b
end
end
というサーバのコードに対しては
require 'bertrpc'
svc = BERTRPC::Service.new('localhost', 9999)
svc.call.calc.add(1, 2)
# => 3
というクライアントのコードが書けるのですが、内部ではErlangでシリアライズされ、先頭にデータ長が付与された以下のイメージでデータ送信が行われます。
{call,calc,add,[1,2]}
そしてバイト列としては以下の様になります。
0000000: 0000 0021 8368 0464 0004 6361 6c6c 6400 ...!.h.d..calld.
0000010: 0463 616c 6364 0003 6164 646c 0000 0002 .calcd..addl....
0000020: 6101 6102 6a a.a.j
先頭4バイトにデータ長(0x21:33バイト)、バージョン(0x83:131)、tupple(0x68)、tuppleアイテム数(4)、atom ext(0x64)...と続きます。今日はこのBERT-RPCで動く足し算サーバををC言語から呼び出すサンプルを書いてみました。
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
// send BERT-RPC command
//
// request => {call,calc,add,[1,2]}
// result => {reply,3}
int
main(int argc, char *argv[]) {
int sock;
struct sockaddr_in serv_addr;
short nport = htons(9999);
struct hostent *host_ent;
char *host = (char*)strdup("127.0.0.1");
char *ptr;
FILE *sockfp;
ptr = strchr(host, ':');
if (ptr) {
*ptr++ = 0;
if (atoi(ptr) == 0) {
struct servent *serv_ent;
serv_ent = getservbyname(ptr, "tcp");
if (serv_ent)
nport = serv_ent->s_port;
} else {
nport = htons(atoi(ptr));
}
}
ptr = host;
if ((host_ent = gethostbyname(ptr)) == NULL) {
sock = -1;
return -1;
}
memset((char *)&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
memcpy(&serv_addr.sin_addr, host_ent->h_addr, host_ent->h_length);
serv_addr.sin_port = nport;
sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) {
return;
}
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) {
close(sock);
sock = -1;
return;
}
char n1;
short n2;
int n4;
// total length
n4 = htonl(33); // 4byte
write(sock, &n4, sizeof(n4)); // number of bytes
n1 = 131;
write(sock, &n1, sizeof(n1)); // version 131
// {call,module,function,arguments}
// ex: {call,calc,add,[1,2]}
n1 = 104;
write(sock, &n1, sizeof(n1)); // tupple
n1 = 4;
write(sock, &n1, sizeof(n1)); // number of tupple items
n1 = 100;
write(sock, &n1, sizeof(n1)); // atom
n2 = htons(4);
write(sock, &n2, sizeof(n2)); // number of atom items
// atom1
write(sock, "call", 4); // call
// atom2
n1 = 100;
write(sock, &n1, sizeof(n1)); // atom ext
n2 = htons(4); // 2byte
write(sock, &n2, sizeof(n2)); // atom length
write(sock, "calc", 4); // atom name
// atom3
n1 = 100;
write(sock, &n1, sizeof(n1)); // atom ext
n2 = htons(3); // 2byte
write(sock, &n2, sizeof(n2)); // number of atom items
write(sock, "add", 3); // atom name
// atom4
n1 = 108;
write(sock, &n1, sizeof(n1)); // list extension
n4 = htonl(2); // 4byte
write(sock, &n4, sizeof(n4)); // number of list elements
n1 = 97;
write(sock, &n1, sizeof(n1)); // small int
n1 = 1;
write(sock, &n1, sizeof(n1)); // int 1
n1 = 97;
write(sock, &n1, sizeof(n1)); // small int
n1 = 2;
write(sock, &n1, sizeof(n1)); // int 2
n1 = 106;
write(sock, &n1, 1); // nil ext(term of list extension)
char buf[256];
ptr = buf;
int x = 0, n, l;
l = recv(sock, buf, sizeof(buf), 0);
printf("recv size %d\n", l);
for (n = 0; n < l; n++) {
x = (x + 1) % 80;
printf("%02X ", (unsigned char)buf[n]);
if (x == 0) putchar('\n');
}
putchar('\n');
// [ 13bytes ] []
// 00 00 00 0D 83 68 02 64 00 05 72 65 70 6C 79 61 03
// length
n4 = (unsigned int) (
(*(ptr+0) << 24) +
(*(ptr+1) << 16) +
(*(ptr+2) << 8) +
(*(ptr+3) << 0));
printf("total length = %d\n", n4);
ptr+=4;
// version
n1 = *ptr;
printf("version = %d\n", (unsigned char)n1);
ptr++;
// small tupple extension
n1 = *ptr;
if (n1 == 104) printf("tupple extension\n");
ptr++;
// number of tupple items
n1 = *ptr;
printf("number of tupple items %d\n", n1);
ptr++;
// atom ext
n1 = *ptr;
if (n1 == 100) printf("atom extension\n");
ptr++;
// atom length
n2 = (unsigned short) (
(*(ptr+0) << 8) +
(*(ptr+1) << 0));
printf("atom length = %d\n", n2);
ptr+=2;
// atom name
char atom[200] = {0};
memcpy(atom, ptr, n2);
printf("atom name = %s\n", atom);
ptr+=n2;
// small int
n1 = *ptr;
if (n1 == 97)printf("small int\n");
ptr++;
n1 = *ptr;
printf("int %d\n", n1);
close(sock);
sock = -1;
return 0;
}
分かりやすい様にコメントに種別を書いていますが、実用するには構造化したり抽象化する必要があります。ただし言語によっては型が見合わない場合もあるので、擬似するコードを書かなければならない可能性もあります。まだrubyとjavascriptにしか実装がない様なので、どなたかチャレンジしてみてはいかがでしょうか。