mrubyを今更ながらhelloworld

今更ではありますが、mrubyのhello worldを書いてみましたよ
あらら、ほんとうにrubyがcから動かせちゃいましたね、と 

% ./build.sh
% ls
a.out  build.sh  hello.c  mruby
% ./a.out
Executing Ruby code from C!
"hello world!"

ビルドしたコードは
https://github.com/mruby/mruby/wiki/Hello-World
こちらから拝借したもの

私が書いたものとビルドしたソース一式はとりあえず
https://github.com/doublefree/helloworld/tree/master/mruby
へおいときました。build.shをたたけば動くはず。いや、Makeファイルとか書かなくてごめんなさい。 

LRUキャッシュとしてのRedis

RedisはList機能など、いろいろ高機能っぽいのでちょっとリッチなLRUキャッシュとして使ってみようと考え始めて見た

memoryがしきい値を超えたときの説明は
http://siguniang.wordpress.com/2012/03/19/redis%E3%81%AEmaxmemory-policy/ 
に詳しく書いてあるのでそちらを参照

ここで候補に挙がるのは以下の2つ

  • volatile-lru
  • allkeys-lru

ちょっとソース追っかけながら見てみる。
メモリの解放処理とかをやってるのはこの辺
http://doublefree.org:8080/source/xref/redis-2.4.13/src/redis.c#1541 

1586            if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_LRU ||
1587                server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_RANDOM)
1588            {
1589                dict = server.db[j].dict;
1590            } else {
1591                dict = server.db[j].expires;
1592            }
1593            if (dictSize(dict) == 0) continue

 expiresを追っかけてみると
http://doublefree.org:8080/source/xref/redis-2.4.13/src/redis.h#300 

300    dict *expires;              /* Timeout of keys with a timeout set */

http://doublefree.org:8080/source/xref/redis-2.4.13/src/db.c#448 

448void setExpire(redisDb *db, robj *key, time_t when) {
449    dictEntry *de;
450
451    /* Reuse the sds from the main dict in the expire dict */
452    de = dictFind(db->dict,key->ptr);
453    redisAssert(de != NULL);
454    dictReplace(db->expires,dictGetEntryKey(de),(void*)when);
455}
456

どうもexpire時間がセットされているkeyは通常のdictionary(dict)とは別のdictionary(expires)にもsetされるみたい。
ちなみに、replaceDictは

160static int dictReplace(dict *ht, void *key, void *val) {
161    dictEntry *entry, auxentry;
162
163    /* Try to add the element. If the key 164 * does not exists dictAdd will suceed. */
165    if (dictAdd(ht, key, val) == DICT_OK)
166        return 1;
167    /* It already exists, get the entry */
168    entry = dictFind(ht, key);
169    /* Free the old value and set the new one */
170    /* Set the new value and free the old one. Note that it is important 171 * to do that in this order, as the value may just be exactly the same 172 * as the previous one. In this context, think to reference counting, 173 * you want to increment (set), and then decrement (free), and not the 174 * reverse. */
175    auxentry = *entry;
176    dictSetHashVal(ht, entry, val);
177    dictFreeEntryVal(ht, &auxentry);
178    return 0;
179} 

keyがあれば上書きして、なければaddするだけ。dictからexpiresにkeyを移している訳じゃない

expireに関するredisのdocumentを参照してみると
http://redis.io/commands/expire
ふむ。expireというコマンドを秒数指定で既存のkeyに打つのか。 
実際にsetExpireが呼ばれているのも以下のコードから分かるようにexpireコマンドから
http://doublefree.org:8080/source/xref/redis-2.4.13/src/db.c#528 

528void expireGenericCommand(redisClient *c, robj *key, robj *param, long offset) {
529    dictEntry *de;
530    long seconds;
531
532    if (getLongFromObjectOrReply(c, param, &seconds, NULL) != REDIS_OK) return;
533
534    seconds -= offset;
535
536    de = dictFind(c->db->dict,key->ptr);
537    if (de == NULL) {
538        addReply(c,shared.czero);
539        return;
540    }
541    /* EXPIRE with negative TTL, or EXPIREAT with a timestamp into the past 542 * should never be executed as a DEL when load the AOF or in the context 543 * of a slave instance. 544 * 545 * Instead we take the other branch of the IF statement setting an expire 546 * (possibly in the past) and wait for an explicit DEL from the master. */
547    if (seconds <= 0 && !server.loading && !server.masterhost) {
548        robj *aux;
549
550        redisAssert(dbDelete(c->db,key));
551        server.dirty++;
552
553        /* Replicate/AOF this as an explicit DEL. */
554        aux = createStringObject("DEL",3);
555        rewriteClientCommandVector(c,2,aux,key);
556        decrRefCount(aux);
557        signalModifiedKey(c->db,key);
558        addReply(c, shared.cone);
559        return;
560    } else {
561        time_t when = time(NULL)+seconds;
562        setExpire(c->db,key,when);
563        addReply(c,shared.cone);
564        signalModifiedKey(c->db,key);
565        server.dirty++;
566        return;
567    }
568}
569
570void expireCommand(redisClient *c) {
571    expireGenericCommand(c,c->argv[1],c->argv[2],0);
572}
573
574void expireatCommand(redisClient *c) {
575    expireGenericCommand(c,c->argv[1],c->argv[2],time(NULL));
576} 

 なんか、volatile-lruが求めているものに近いなぁと思ったけど、これだとallkeys-lruの方が近い気がする。

でも、allkeys-lruだと、
http://doublefree.org:8080/source/xref/redis-2.4.13/src/redis.c#1604

1604            else if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_LRU ||
1605                server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_LRU)
1606            {
1607                for (k = 0; k < server.maxmemory_samples; k++) {
1608                    sds thiskey;
1609                    long thisval;
1610                    robj *o;
1611
1612                    de = dictGetRandomKey(dict);
1613                    thiskey = dictGetEntryKey(de);
1614                    /* When policy is volatile-lru we need an additonal lookup
1615                     * to locate the real key, as dict is set to db->expires. */
1616                    if (server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_LRU)
1617                        de = dictFind(db->dict, thiskey);
1618                    o = dictGetEntryVal(de);
1619                    thisval = estimateObjectIdleTime(o);
1620
1621                    /* Higher idle time is better candidate for deletion */
1622                    if (bestkey == NULL || thisval > bestval) {
1623                        bestkey = thiskey;
1624                        bestval = thisval;
1625                    }
1626                }
1627            }
1628

maxmemory_samplesの数(defaultで3)だけrandomに抽出してそのなかでLRUが一番古い(大きい)keyを消去対象として選出している

これだと運が良くないと、結構新しいkeyがどんどんevictされていっちゃうので、memcached的なLRUと結構違う。keyの管理方法の問題だけど、これだとなかなか悩ましい。maxmemory_samplesの値を調整するともうちょっとまともにならないか、試してみようと思う。

 

 

 

Job Queue

Job Queueが欲しくて、いろいろ調べた。

要件としては、

  • persistent queue (not on memory, but storage)
  • low cost of maintenance
  • programming language independent

こんな感じのシンプルなJob Queue
FIFOでqueueを取り出せればいい。

という要件でいろいろ探したけれど、なかなかフィットするものがない。
その中でも一番良さそうだったのがMysqlのストレージエンジンであるQ4M。
プロトタイプを作って実験してみても、なんだか良さそう。

ただ、困ったことが一つ。
Q4Mはreplicationに対応していない、、、
完全なミラーリングがしたい訳じゃない(処理中のqueueくらい取りこぼしてもいい)んだけど、DBサーバすっとんですべてのqueueを失うような状況だけは避けたい。

https://groups.google.com/group/q4m-general/browse_thread/thread/32fa1c16bd149277/a4f12c3e269f3069?hl=ja&lnk=gst&q=replication#a4f12c3e269f3069
これを見ても、replicationするにはDRBDとかを使う必要があるらしい。
でも、サーバクラッシュするような状況で論理的に正しいファイルがDRBDのミラーリング先にできているのか不安っていうのと、heartbeatとか使ってMysqlのデータを動的に復旧させようって言うのはもろもろ無理があるというか大変すぎる気がする。

そもそものシンプルなJob Queueが欲しいというスタートから大きく逸脱している気がする。

DeNA, mixi等、大規模システムで導入されているのだから、何か運用のコツ等があるのだろうか。。

困った、、、

(続) Erlangでチャットを書いてみた

前回の日記 [ Erlangでチャットを書いてみた ( http://d.hatena.ne.jp/doublefree/20110718/1310970177 ) ]
で書いていた、Erlang チャットのやり残しのうちのひとつである

  • いなくなったユーザのログアウト処理

の部分を書き足したので
https://github.com/doublefree/brwchat/commit/2637ddfc024e51df43cea443787ba479e28eb04e
pushしておいた。

明示的なログアウト機能はつけてないけど、まぁこれで最低限のことはできたかな、とおもった。

一段落したのでOS X Lionにでも作業用Macをアップデートしてみようかと思ってる

Erlangでチャットを書いてみた

最近Erlangの入門書を読んだので、Erlangで何か書きたいなとおもっていました。
特に何も思いつかなかったので、ありきたりですが、チャットを書いてみました。

  • やりたかったこと
    • ブラウザだけでチャットしたいよね
    • 超高速ポーリングとか、やだよね(昔学生時代にそんなチャットを作ったな、、)
  • 基本的な構成
    • CGI (WWWサーバ)
      • yawsというerlangのwebサーバ上で動きます
      • リクエスト毎にerlangの1プロセスが割り当てられる感じ
    • バックエンド
      • yaws起動時に立ち上げられるプロセス
      • チャット関連リクエストをさばく担当のプロセスとデータハンドリング用の2プロセスを用意
        • 別に1プロセスでもよかったんだけど、チャットのデータ保持部分を手抜きしてetsでグリグリやっているので、そこはあとから変えられるように隠蔽しておきたかったのです
  • 動作の仕組み
    • ログイン、メッセージの書き込みは同期的(javascriptだから実際は非同期だけど、イメージの問題)にCGIに投げつけている
    • メッセージ取得部分は、ロングポーリングなイメージで、jsonpのリクエストをCGIサーバに投げて、CGIサーバが新着メッセージがあるとそのリクエストに返事する、という仕組み。Cometになっているか分からないけどそういうのをイメージして作った
    • http://ja.wikipedia.org/wiki/Comet <-- Comet
  • やり残していること
    • ロングポーリングっていってるけど完全に投げっぱなしなので、ある程度でタイムアウトもうけて再度リクエストを投げなおすように、クライアント/サーバの双方にそういう仕組みを入れたい
    • ログアウトとかないのでしばらく応答がなかったクライアントは強制的に退出扱いにするとか、そういう仕組みは入れないとサーバが死ぬ
    • 今回作ったものは1つのチャットルームでみんながしゃべる、というものだけど、erlang的にはおいしいところを味わいきれていない。
      • 多くのチャットルームがあって、それぞれにerlangのノードが割り当てられている
      • ノードの管理をするスーパーノードがいて、そいつに行き先のノードを聞いて、、、、っていうすてきなことができたらいいな、、、あはw
    • 奥さんのすてきな記事( http://labs.cybozu.co.jp/blog/kazuho/archives/2007/02/keeping_comet_alive.php )にあるような諸処の問題の検証と対策
      • 実際は、MacのChromeSafariでは動いたからまぁいっかな、とか思ってみたりしている

Erlang + yawsでSession(Cookie)ハンドリング

Erlangのhttpサーバであるyawsをいじって遊んでいます
おきまりのHttp関連の操作もどうやったらいいか分からずに
http://yaws.hyber.org/yman.yaws?page=yaws_api
この辺を見ながら試行錯誤。

とりあえずcookieを使用したsession操作をしたくて組んでみました。


out(Arg) ->
SessionName = "hogehoge",
% Session処理
C = (Arg#arg.headers)#headers.cookie,
{SessionCookie, SetCookie} = case yaws_api:find_cookie_val(SessionName, C) of
[] ->
Cookie = yaws_api:new_cookie_session({}),
CO = yaws_api:setcookie(SessionName, Cookie, "/"),
{Cookie, CO};
Cookie ->
case yaws_api:cookieval_to_opaque(Cookie) of
{ok, OP} ->
Cookie,
{Cookie, undefined};
{error, no_session} ->
CO = yaws_api:setcookie(SessionName, Cookie, "/"),
{Cookie, CO}
end
end,

case SetCookie of
undefined -> {html, "found cookie"};
_Else -> [SetCookie, {html, "cookie may be created"}]
end.

肝なのは最後の数行の


case SetCookie of
undefined -> {html, "found cookie"};
_Else -> [SetCookie, {html, "cookie may be created"}]
end.

この辺。つまりはSetCookieの変数部分の戻り値が、そのままheaderになっていて、html本文返す前にSet-Cookieヘッダ返さないといけないよ、という至極当たり前なことなのですが、この辺が隠蔽されていないフレームワークを使用するのが久々なのでちょっとはまった。

だって、このへん ( http://yaws.hyber.org/yman.yaws?page=yaws_api ) のマニュアルを見ても


setcookie(Name, Value, [Path, [ Expire, [Domain , [Secure]]]])
Sets a cookie to the browser.

って書いてあるだけで、ヘッダー相当の内容がこの関数から返ってくるとは思わなかった。
ちゃんと

case SetCookie of
undefined -> {html, "found cookie"};
_Else -> [SetCookie, {html, "cookie may be created"}]
end.

こう書いたら、

200 OK. .Server: Yaws 1. 90..Date: Sun, 1 9 Jun 20 11 14:53 :25 GMT.
.Content -Length: 22..Con tent-Type: text/ html
Set-Cookie : hogehoge=nonod e@nohost-3786894 771773034497;
path=/

こんな感じでSet-Cookieヘッダ返ってきて、めでたしめでたし。

うーーーん、2時間近くはまったなぁ

(続) apacheとnginxのベンチーマーク

apacheとnginxでベンチマーク
このエントリの続きとして、
apacheとnginxのベンチマークをさくらVPSでもやってみた。(静的ファイルのみ)

  • 環境
    • さくらVPS -> 512MBのプラン(月額980円のやつ)
    • OS -> Debian lenny
    • nginxのphp -> spawn-fcgi

自宅からサーバにたいしてabしても結果が変わらなかったので、自宅マシンがしょぼい、もしくはサーバの上流でNWしぼられてるんだと解釈して、サーバにログインして、localhostにむけてabした結果


Concurrency Level: 1000
Time taken for tests: 0.848 seconds
Complete requests: 3000
Failed requests: 0
Write errors: 0
Total transferred: 1030900 bytes
HTML transferred: 31720 bytes
Requests per second: 3536.44 [#/sec] (mean)
Time per request: 282.770 [ms] (mean)
Time per request: 0.283 [ms] (mean, across all concurrent requests)
Transfer rate: 1186.76 [Kbytes/sec] received

    • nginx


Concurrency Level: 1000
Time taken for tests: 0.297 seconds
Complete requests: 3000
Failed requests: 0
Write errors: 0
Total transferred: 666000 bytes
HTML transferred: 36000 bytes
Requests per second: 10100.43 [#/sec] (mean)
Time per request: 99.006 [ms] (mean)
Time per request: 0.099 [ms] (mean, across all concurrent requests)
Transfer rate: 2189.74 [Kbytes/sec] received

echoするだけのphp


Concurrency Level: 1000
Time taken for tests: 1.243 seconds
Complete requests: 3000
Failed requests: 0
Write errors: 0
Total transferred: 783360 bytes
HTML transferred: 12288 bytes
Requests per second: 2413.52 [#/sec] (mean)
Time per request: 414.332 [ms] (mean)
Time per request: 0.414 [ms] (mean, across all concurrent requests)
Transfer rate: 615.45 [Kbytes/sec] received

静的ファイルと比べて2/3ほどのレスポンス

    • nginx


Concurrency Level: 1000
Time taken for tests: 1.421 seconds
Complete requests: 3000
Failed requests: 0
Write errors: 0
Total transferred: 489000 bytes
HTML transferred: 12000 bytes
Requests per second: 2110.57 [#/sec] (mean)
Time per request: 473.806 [ms] (mean)
Time per request: 0.474 [ms] (mean, across all concurrent requests)
Transfer rate: 335.96 [Kbytes/sec] received

apacheと遜色ないレベル

400ミリsec sleepしたあとechoするphp


Concurrency Level: 1000
Time taken for tests: 8.999 seconds
Complete requests: 3000
Failed requests: 0
Write errors: 0
Total transferred: 960000 bytes
HTML transferred: 204000 bytes
Requests per second: 333.36 [#/sec] (mean)
Time per request: 2999.746 [ms] (mean)
Time per request: 3.000 [ms] (mean, across all concurrent requests)
Transfer rate: 104.18 [Kbytes/sec] received

    • nginx


Concurrency Level: 1000
Time taken for tests: 3.555 seconds
Complete requests: 3000
Failed requests: 801
(Connect: 0, Receive: 0, Length: 801, Exceptions: 0)
Write errors: 0
Non-2xx responses: 2199
Total transferred: 962472 bytes
HTML transferred: 478875 bytes
Requests per second: 843.87 [#/sec] (mean)
Time per request: 1185.013 [ms] (mean)
Time per request: 1.185 [ms] (mean, across all concurrent requests)
Transfer rate: 264.39 [Kbytes/sec] received

nginxがnon-blockingだからか、apacheと比べると数字がいい
failed requestが多いのが気になる

  • 感想
    • 静的ファイルを捌く能力だとapacheよりもnginxが良さそう。
    • サーバの回線が細いとWebサーバの能力使い切る前にNWが、、
    • 簡単なphpを動かすくらいだとapacheと性能変わらない
      • fast-cgiのプロセス数とかもいろいろチューニングは必要そう
    • 遅いphpを使用した場合の性能劣化がapacheよりも少ないので、ブロッキングする処理をするphpとかをたくさんhostするには良さそう
      • ただ、failed requestが多いのが気になる