Erlang で memcached を作ってみました。

# 見苦しい点が御座いましたらご指摘頂けると有り難いです。m(--)m

% ememcached.erl -module(ememcached). -export([start/0, ememcached/1, process_command/1]). start() -> register(ememcached, spawn(?MODULE, ememcached, [11211])). ememcached(Port) -> ets:new(item, [public, named_table]), {ok, Listen} = gen_tcp:listen( Port, [binary, {packet, line}, {active, false}, {reuseaddr, true}]), io:fwrite("< server listening ~p

", [Port]), ememcached_accept(Listen). ememcached_accept(Listen) -> {ok, Sock} = gen_tcp:accept(Listen), io:fwrite("<~p new client connection

", [Sock]), spawn(?MODULE, process_command, [Sock]), ememcached_accept(Listen). process_command(Sock) -> case gen_tcp:recv(Sock, 0) of {ok, Line} -> io:fwrite(">~p ~s", [Sock, Line]), Token = string:tokens(binary_to_list(Line), " \r

"), case Token of ["get", Key] -> process_get(Sock, Key); ["set", Key, Flags, Expire, Bytes] -> inet:setopts(Sock,[{packet, raw}]), process_set(Sock, Key, Flags, Expire, Bytes), inet:setopts(Sock,[{packet, line}]); ["delete", Key] -> process_delete(Sock, Key); ["quit"] -> gen_tcp:close(Sock); _ -> gen_tcp:send(Sock, "ERROR\r

") end, process_command(Sock); {error, closed} -> io:fwrite("<~p connection closed.

", [Sock]); Error -> io:fwrite("<~p error: ~p

", [Sock, Error]) end. process_get(Sock, Key) -> case ets:lookup(item, Key) of [{_, {Value, Expire}}] -> Diff = Expire - epoch(), if (Expire == 0) or (Diff > 0) -> gen_tcp:send(Sock, io_lib:format( "VALUE ~s 0 ~w\r

~s\r

END\r

", [Key, size(Value), Value])); true -> gen_tcp:send(Sock, "END\r

"), io:fwrite("EXPIRED: ~s

", [Key]), ets:delete(item, Key) end; [] -> gen_tcp:send(Sock, "END\r

") end. process_set(Sock, Key, _Flags, _Expire, Bytes) -> case gen_tcp:recv(Sock, list_to_integer(Bytes)) of {ok, Value} -> ets:insert(item, {Key, {Value, case list_to_integer(_Expire) of 0 -> 0; Expire -> epoch() + Expire end}}), gen_tcp:send(Sock, "STORED\r

"); {error, closed} -> ok; Error -> io:fwrite("Error: ~p

", [Error]) end, gen_tcp:recv(Sock, 2). process_delete(Sock, Key) -> case ets:lookup(item, Key) of [{_, _}] -> ets:delete(item, Key), gen_tcp:send(Sock, "DELETED\r

"); _ -> gen_tcp:send(Sock, "NOT_FOUND\r

") end. epoch() -> {Msec, Sec, _} = now(), Msec * 1000 + Sec.

実行方法は

% erlc ememcached.erl % erl -noshell -s ememcached start

で起動し、11211 を listen します。

僅かこれだけのコードで get と set と delete を行う Erlang 版 memcached が実装できました。 厳密なベンチマークがまだ出来ていませんがオリジナルの memcached と大差な いパフォーマンスが出ています。

続いて実装上のポイント幾つか紹介します。

ソケットオプション

ソケット通信のオプションにはデータを行単位で受信を行う line モードがあ ります。

memcache プロトコルにおいて、クライアントからのコマンドを受信する時に はこの line モードが非常に便利なのですが set コマンドを受け取った後は 改行を含むバイナリデータを受信する必要があります。

そこで通常は line モードでコマンドを受信しますが、set コマンドを受け取っ たら raw モードに切り替え、バイナリデータを受信したら line モードに戻す 様なコードになっています。

["set", Key, Flags, Expire, Bytes] -> inet:setopts(Sock,[{packet, raw}]), process_set(Sock, Key, Flags, Expire, Bytes), inet:setopts(Sock,[{packet, line}]);

ストレージシステム ets

ets はキーに対応するErlang のオブジェクトを格納し、探索を行うことが出 来るストレージシステムです。ets の場合メモリ上に保持されるため再起動す れば消えてしまいますが高速です。

# 前回付箋 Web アプリで使用した dets はディスクに保持されるため永続性 があります。

ets の使い方は以下の様にとても単純です。

% テーブルを作成 > ets:new(table, [public, named_table]). % キーと値を挿入 > ets:insert(table, {"key", "hello"}). true % キーに対応する値を取得 > ets:lookup(table, "key"). [{"key","hello"}]

今回はキャッシュのストレージとして ets を使用しましたが、 次回は Mnesia を使用して実装した ememcached を紹介したいと思います。 Mnesia を使用すると複数のサーバーでレプリケーションを行ったり、再起動 を行ってもデータが消えないよう永続化可能な memcached が実装できます。