2012-05-19
MessagePack-RPCサーバのはまりどころ
MessagePack-RPCを使っていて「こういうときどうするんだっけ?」というのがあり、しかもけっこうはまりどころがあるので以下にまとめることにする。
古橋さんの以下の記事が大変参考になっております。
Ruby で MessagePack-RPC - 古橋貞之の日記
また以下で紹介したコードはgistにもはっつけております。
authorNari’s gist: 2729482 — Gist
登場人物
簡易なMapReduceもどきを考えてみる。
あるサーバから別の複数サーバに対してリクエストを出し、なんらかのレスポンスをクライアント返すようなもの。
関連図
+---+
+-->|add|
+------+ +--+ | +---+
|client|<->|mr|<-+-->|add|
+------+ +--+ | +---+
+-->|add|
+---+
add.rb
require 'msgpack/rpc' class Add def add(str) return eval(str) end end if $0 == __FILE__ then $sv_loop = Cool.io::Loop.default server = MessagePack::RPC::Server.new($sv_loop) server.listen('0.0.0.0', ARGV.first, Add.new) server.run end
mr.rb
require 'msgpack/rpc' class MapReduce ADD1 = {host: '0.0.0.0', port: 10001} ADD2 = {host: '0.0.0.0', port: 10002} ADD3 = {host: '0.0.0.0', port: 10003} end if $0 == __FILE__ then $sv_loop = Cool.io::Loop.default server = MessagePack::RPC::Server.new($sv_loop) server.listen('0.0.0.0', ARGV.first, MapReduce.new) server.run end
mr.rbに対してメソッドをいろいろ追加していく。
それぞれ以下のように起動しておく。
$ ruby add.rb 10001 & $ ruby add.rb 10002 & $ ruby add.rb 10003 & $ ruby mr.rb 10000
client.rb
# $ ruby client.rb method num args.. require 'msgpack/rpc' concurrency = ARGV.shift.to_i method = ARGV.shift th = [] concurrency.times do th << Thread.start do MessagePack::RPC::Client.open('0.0.0.0', 10000) do |c| c.call(method, *ARGV) end end end th.map(&:join)
同時に呼び出す人数,呼び出すメソッド名,メソッドへの引数.. をARGVにとる。
同期呼び出しで実現
#mr.rb def mr_leak(add1, add2, add3) clis = [MessagePack::RPC::Client.new(ADD1[:host], ADD1[:port])] clis << MessagePack::RPC::Client.new(ADD2[:host], ADD2[:port]) clis << MessagePack::RPC::Client.new(ADD3[:host], ADD3[:port]) r = clis[0].call(:add, add1) r += clis[1].call(:add, add2) r += clis[2].call(:add, add3) return r end
ふつうに考えたらこうなるし、ちゃんと結果も帰ってくる。
が、ひとつ忘れているのはclientのclose処理。
mr.rbはサーバであり長く生きるプロセスなので、clientをちゃんとcloseしないとコネクションがリークしてしたままになってしまう。
def mr_close(add1, add2, add3) # ...略... clis.map(&:close) return r end
同期呼び出しの場合はopenが使える。
#mr.rb def mr_close_with_open(add1, add2, add3) r = 0 MessagePack::RPC::Client.open(ADD1[:host], ADD1[:port]) do |cli| r += cli.call(:add, add1) end MessagePack::RPC::Client.open(ADD2[:host], ADD2[:port]) do |cli| r += cli.call(:add, add2) end MessagePack::RPC::Client.open(ADD3[:host], ADD3[:port]) do |cli| r += cli.call(:add, add3) end return r end
% ruby client.rb 1 mr_close_with_open 1+2 2+3 3+4 15
ちゃんと計算結果が返る。
非同期呼び出しで実現
同期呼び出しの例だと、それぞれのaddサーバの計算をまってから、次のaddサーバにリクエストをおこなう。
ほぼ同時に呼び出したいので、非同期呼び出しを使う。
#mr.rb def mr_async(add1, add2, add3) clis = [MessagePack::RPC::Client.new(ADD1[:host], ADD1[:port])] clis << MessagePack::RPC::Client.new(ADD2[:host], ADD2[:port]) clis << MessagePack::RPC::Client.new(ADD3[:host], ADD3[:port]) r = 0 fs = [clis[0].callback(:add, add1){|res| r += res.get}] fs << clis[1].callback(:add, add2){|res| r += res.get} fs << clis[2].callback(:add, add3){|res| r += res.get} fs.map(&:join) clis.map(&:close) return r end
ここで大事なのは、callbackで得たfeatureをちゃんと最後にjoinする、という部分。
msgpack-rpcのクライアントは何も指定しないとイベントループを新規作成するので、そのループをちゃんと走らせないとレスポンスを受け取れないわけだ。
mrサーバをスケールさせる(サーバのイベントループを使った非同期呼び出し)
mrサーバをスケールさせたいとする。addサーバ1の処理に2秒かかる処理があるとしよう。
それを3人が同時に呼び出ぶと...6秒もかかってしまった。
% time ruby client.rb 3 mr_async 'sleep(2);1+2' 2+3 3+4 15 15 15 ... 6.302 total
これを解決するために、まずaddサーバをスレッドを使って並行処理できるようにする。
# add.rb def con_add(str) as = MessagePack::RPC::AsyncResult.new Thread.start do as.result(eval(str)) end return as end
次に、mrサーバでjoinしている部分を削除したい。
そのためにサーバ側のイベントループを使った非同期呼び出しをおこなってみよう。
require "securerandom" def mr_async_on_sv_loop(add1, add2, add3) uuid = SecureRandom.uuid cli1 = MessagePack::RPC::Client.new(ADD1[:host], ADD1[:port], $sv_loop) cli2 = MessagePack::RPC::Client.new(ADD2[:host], ADD2[:port], $sv_loop) cli3 = MessagePack::RPC::Client.new(ADD3[:host], ADD3[:port], $sv_loop) @res ||= {} @res[uuid] = {res: 0, finished: 0} as = MessagePack::RPC::AsyncResult.new cb = ->(c){ ->(res){ @res[uuid][:res] += res.get @res[uuid][:finished] += 1 if @res[uuid][:finished] == 3 as.result(@res[uuid][:res]) @res.delete(uuid) end c.close } } cli1.call_async(:con_add, add1).attach_callback(cb.(cli1)) cli2.call_async(:con_add, add2).attach_callback(cb.(cli2)) cli3.call_async(:con_add, add3).attach_callback(cb.(cli3)) return as end
こうするとサーバ側のイベントループでaddサーバのレスポンスを待ち受けるようになる。
サーバ側のイベントループは自分へのリクエストも待ち受けているため、addサーバのレスポンスを受け取る前にリクエストがわりこんだりする。
% time ruby client.rb 3 mr_async_with_sv_loop 'sleep(2);1+2' 2+3 3+4 15 15 15 ... 2.359 total
このようにサーバ側のイベントループをなるべく使うようにすれば、わりとスケールする。
が、コールバック内の処理は逐次実行なので処理時間が短くなければイベントループが詰まってしまう。
そのため、コールバック内で非同期呼び出し&コールバック、の中でコールバック、といったコールバック地獄がおこる可能性がある。
もしくはDBへの接続はどうするかとか、時間がかかりそうな処理をどう対処していくかが問題になってくる。
mrサーバをスケールさせる(別スレッドで非同期呼び出し)
# mr.rb def mr_async_on_thread(add1, add2, add3) clis = [MessagePack::RPC::Client.new(ADD1[:host], ADD1[:port])] clis << MessagePack::RPC::Client.new(ADD2[:host], ADD2[:port]) clis << MessagePack::RPC::Client.new(ADD3[:host], ADD3[:port]) as = MessagePack::RPC::AsyncResult.new Thread.start(0, clis, add1, add2, add3) do |r, clis, add1, add2, add3| begin fs = [clis[0].callback(:con_add, add1){|res| r += res.get}] fs << clis[1].callback(:con_add, add2){|res| r += res.get} fs << clis[2].callback(:con_add, add3){|res| r += res.get} fs.map(&:join) as.result(r) ensure clis.map(&:close) end end return as end
% time ruby client.rb 3 mr_async_on_thread 'sleep(2);1+2' 2+3 3+4 15 15 15 ... 2.947 total
かなり処理がシンプルになったが、前の例と比べてスレッド生成のコストが少しかかってしまった。
処理が長時間におよぶ場合はスレッド生成のコストなど微々たるものなので、上記のスレッドの例を使える。
この例のように、2秒もかかる処理ならスレッドの例の方がよい。
また、addサーバが落ちていた場合はcallにタイムアウト分の時間がかかる可能性もあるので、スレッドの例でも問題ないだろう。
スレッドをある程度プールしておけばある程度性能は上がるだろうが、そこまでする必要があるのか、という気もする。
clientはスレッドを使っても共通化できる?
# mr.rb def mr_on_thread_with_shared_client(add1, add2, add3) @clis ||= [MessagePack::RPC::Client.new(ADD1[:host], ADD1[:port]), MessagePack::RPC::Client.new(ADD2[:host], ADD2[:port]), MessagePack::RPC::Client.new(ADD3[:host], ADD3[:port])] as = MessagePack::RPC::AsyncResult.new Thread.start(0, @clis, add1, add2, add3) do |r, clis, add1, add2, add3| begin r += clis[0].call(:con_add, add1) r += clis[1].call(:con_add, add2) r += clis[2].call(:con_add, add3) as.result(r) end end return as end
$ ruby client.rb 3 mr_on_thread_with_shared_client 'sleep(2);1+2' 2+3 3+4
上記のサーバは上記コマンドで高確率で以下のエラーを吐く。
ruby: loop.c:192: Coolio_Loop_run_once: Assertion `loop_data->ev_loop && !loop_data->events_received' failed. zsh: abort ruby mr.rb 10000
要するに「イベントループ実行中に再度ループ実行しようとしている」というエラーだ。
mr_on_thread_with_shared_clientはスレッドを作って並行にclientのcallを呼び出す可能性があり、MessagePack::RPC::Clientはこのような使い方をしてはならない。
スレッドを使う場合は、そのスレッド毎にクライアントを作り、スレットが死んだ時にきちんとcloseするようにしよう。
サーバのイベントループを使った非同期呼び出しの方法であれば並行に動くことはないため共通化可能だ。
def mr_async_on_sv_loop_with_shared_client(add1, add2, add3) uuid = SecureRandom.uuid @cli1 ||= MessagePack::RPC::Client.new(ADD1[:host], ADD1[:port], $sv_loop) @cli2 ||= MessagePack::RPC::Client.new(ADD2[:host], ADD2[:port], $sv_loop) @cli3 ||= MessagePack::RPC::Client.new(ADD3[:host], ADD3[:port], $sv_loop) @res ||= {} @res[uuid] = {res: 0, finished: 0} as = MessagePack::RPC::AsyncResult.new cb = ->(res){ begin @res[uuid][:res] += res.get @res[uuid][:finished] += 1 if @res[uuid][:finished] == 3 as.result(@res[uuid][:res]) @res.delete(uuid) end end } @cli1.call_async(:con_add, add1).attach_callback(cb) @cli2.call_async(:con_add, add2).attach_callback(cb) @cli3.call_async(:con_add, add3).attach_callback(cb) return as end
呼び出し先サーバが落ちていた場合の対処
MessagePack-RPCクライアントの接続時間タイムアウトはデフォルトで10秒。
1つのaddサーバが落ちていても、他の2つのaddサーバにリクエストを投げたい場合は、非同期呼び出しでjoinする(mr.rbのmr_async()を参照)
タイムアウトの例外はres.getでraiseされるのでそれをrescueし、よしなに処理する。
1つでもサーバが落ちていたら残りのサーバにリクエストさせない場合、同期呼び出しする(mr.rbのmr_close_with_open()を参照)。
これはcallの時点でタイムアウトの例外がraiseされる。
計算の途中経過がクライアント側で知りたい
MessagePack-RPCにはHTTP/1.1でいうところのChunked transfer encodingがないので、ちょびちょび結果を受け取ることができない。
処理にものすごく時間がかかる場合は計算の途中結果を随時知りたい場合もあるはず。
そのため、途中経過をポーリングするようなAPIを新しく設けてやる。
#mr.rb def mr_for_polling(add1, add2, add3) uuid = SecureRandom.uuid @res ||= {} @res[uuid] = {res: 0, finished: 0} clis = [MessagePack::RPC::Client.new(ADD1[:host], ADD1[:port])] clis << MessagePack::RPC::Client.new(ADD2[:host], ADD2[:port]) clis << MessagePack::RPC::Client.new(ADD3[:host], ADD3[:port]) Thread.start(0, clis, add1, add2, add3) do |r, clis, add1, add2, add3| begin @res[uuid][:res] += clis[0].call(:con_add, add1) @res[uuid][:finished] += 1 @res[uuid][:res] += clis[1].call(:con_add, add2) @res[uuid][:finished] += 1 @res[uuid][:res] += clis[2].call(:con_add, add3) @res[uuid][:finished] += 1 as.result(r) ensure clis.map(&:close) end end return uuid end def mr_for_polling_result(uuid) res = @res[uuid] if @res[uuid] && @res[uuid][:finished] == 3 @res.delete(uuid) end return res end
クライアント側では以下のようにポーリングする。
# $ ruby client.rb method num args.. require 'msgpack/rpc' concurrency = ARGV.shift.to_i method = ARGV.shift th = [] concurrency.times do th << Thread.start do MessagePack::RPC::Client.open('0.0.0.0', 10000) do |c| uuid = c.call(method, *ARGV) loop do r = c.call(method+"_result", uuid) p r break if r.nil? sleep 0.1 end end end end th.map(&:join)
% ruby client_for_polling.rb 1 mr_for_polling '1+2' 2+3 3+4 {"res"=>0, "finished"=>0} {"res"=>3, "finished"=>1} {"res"=>3, "finished"=>1} {"res"=>3, "finished"=>1} {"res"=>3, "finished"=>1} {"res"=>3, "finished"=>1} {"res"=>3, "finished"=>1} {"res"=>3, "finished"=>1} {"res"=>3, "finished"=>1} {"res"=>3, "finished"=>1} {"res"=>3, "finished"=>1} {"res"=>8, "finished"=>2} {"res"=>8, "finished"=>2} {"res"=>8, "finished"=>2} {"res"=>8, "finished"=>2} {"res"=>8, "finished"=>2} {"res"=>8, "finished"=>2} {"res"=>15, "finished"=>3} nil
Msgpack-RPCはこの辺が一番いけてないと思いますよね(チラチラ
MessagePack-PPCサーバ側の処理時間が見積もれない場合
サーバ側の処理時間が見積もれない場合は以下の2つの選択肢がある。
1.は接続先サーバがおかしい場合(停止しているとき)にも影響するので、なるべくこの方法はとりたくない。
したがって、2.をつかってやることになる。
あぁ、いけてませんねぇ。いけてない。
まとめ
- 同期・非同期呼び出しは、ほぼ同時に呼びたいか・呼びたくないかの違い
- サーバをスケールさせる方法は以下の2種類
- クライアントのcloseには気を配る
- クライアントを共通するさいの並行処理は注意
- MessagePack-PPCサーバの処理時間はなるべく短くするのが正解
- 処理時間が見積もれない場合
- タイムアウトを長くする
- ポーリングで結果を得る
- こちらの方がよさそげ
追記: サーバのイベントループとクライアントのイベントループを同じにすると同期呼び出しできない
あ、そうだ。これを書くの忘れてた。
サーバ側のイベントループとクライアントのイベントループを同じにすると同期呼び出しできない。
def mr_fail_on_sv_loop(add1, add2, add3) clis = [ADD1, ADD2, ADD3].each_with_object([]) do |add, clis| clis << MessagePack::RPC::Client.new(add[:host], add[:port], $sv_loop) end [add1, add2, add3].zip(clis).inject(0) do |r, (a, c)| r += c.call(:add, a) end end
# わかりやすいようにベタに書いてきたが、each_with_objectやzip,injectを使えばもっと綺麗に書ける。
実行するとmr.rbの方で以下のエラーを吐く。
ruby: loop.c:192: Coolio_Loop_run_once: Assertion `loop_data->ev_loop && !loop_data->events_received' failed. zsh: abort ruby mr.rb 10000
2012-05-17
東京で一ヶ月ほどお世話に
なっております。とりあえず、みなとRuby会議まではいる予定です。
ごはんを奢りたい、という人がいれば遠慮なく言ってもらって構いませんのですよ!(チラチラ
2012-03-30
kindlemailで時短や!
電子書籍をKindleで読むことが多くなってきました。
最近、手元にあるPDFを自分のKindle上に置く方法を探していて。
正直、いちいちPCとKindleを繋いでられないんですよ。
でも、AmazonさんにはPersonal Documentという便利なものがあってだな…。
Personal Documentというのは、Kindleごとに設定したメールアドレスに添付ファイル付きのメールを送ると、Kindleに自動的にダウンロードされる機能...
Kindle Personal Documentに追加された3つの新機能が便利すぎる - mteramotoの日記
そこで、おもむろにGmailを開いて、PDF添付!…するわけですが、そのたびにGmail開くのもなんか面倒だなぁと。
どうせならコマンド一発でやりたいですよね。
奥さん、そこでkindlemailですよ!!!1
djhworld/kindlemail ? GitHub
kindlemailとかいう今はもうメンテされていないgemのやーつがあります。
自分のKindleのPersonal Documentにコマンドラインで任意のファイルを送れるというもの。
内部的には自分のGmail accountからOAuth使ってメール送信するみたいですね。
$ gem i kindlemail
$ kindlemail --setup
でセットアップ。
Gmailのアドレスとか、自分のKindleのアドレスとか聞いてきますので、入力してあげましょう。
oauth_token,oauth_token_secretについては後で説明します。
ちょっとこれバグってて、設定し終わったら ~/.kindlemail/.email_conf の内容をちょっと修正してあげないといけないんでよね。
- email xxxxx@gmail.com + email: xxxxx@gmail.com
OAuthの設定
XoauthDotPyRunThrough - google-mail-xoauth-tools - A quick run-through of Xoauth with xoauth.py. - Google Mail Xoauth Tools - Google Project Hosting
をとりあえず読んでもらえばわかるのではないかと。anonymouseで権限をもらみたいですね。
xoauth.pyをテキトーに突っ込んで、動かせばいいでしょう。
※もちろん、いろいろちゃんと読んだほうがいいよ!
$ python xoauth.py --generate_oauth_token --user=xoauth@gmail.com oauth_token_secret: 41r18IyXjIvuyabS/NDyW6+m oauth_token: 4/nM2QAaunKUINb4RrXPC55F-mix_k oauth_callback_confirmed: true To authorize token, visit this url and follow the directions to generate a verification code: https://www.google.com/accounts/OAuthAuthorizeToken?oauth_token=4%2FnM2QAaunKUINb4RrXPC55F-mix_k Enter verification code: VFvY1j0R22BKuDtvN4gaAgZq oauth_token: 1/C1va1J7SjTd1-cddVmG2iqla2XmG8xRqX3UXuqt7Eps oauth_token_secret: sSCKc+z1xLlT+htfihqcAM3h
の、一番最後にでてきた、自分の oauth_token と oauth_token_secret を --setup の時に入れてあげましょう。
kindlemail実行
$ kindlemail my_book.mobi
とやるとコマンド一発で送れます。約2秒の時短や!
注意
Gmailは残念ながら25MBより大きい添付ファイルは送れません…。
2012-03-21
nari.age+=1
3.21の誕生日です。もう27歳ですね。
Amazon.co.jp: 中村 成洋: ウィッシュリスト
とりあえずwishlistをペタリ。
追記
下記のものをいただきました。ありがとうございます!!
- 作者: 蔡志忠,野末陳平,和田武司
- 出版社/メーカー: 講談社
- 発売日: 1994/12/13
- メディア: 文庫
- 購入: 10人 クリック: 25回
- この商品を含むブログ (7件) を見る
- 作者: アンドリューハント,デビッドトーマス,Andrew Hunt,David Thomas,村上雅章
- 出版社/メーカー: ピアソンエデュケーション
- 発売日: 2000/11
- メディア: 単行本
- 購入: 36人 クリック: 885回
- この商品を含むブログ (327件) を見る
一億人の英文法 ――すべての日本人に贈る「話すため」の英文法(東進ブックス)
- 作者: 大西泰斗,ポール・マクベイ
- 出版社/メーカー: ナガセ
- 発売日: 2011/09/09
- メディア: 単行本
- 購入: 7人 クリック: 39回
- この商品を含むブログ (20件) を見る
- 作者: グレッグ・イーガン,鷲尾直広,山岸 真
- 出版社/メーカー: 早川書房
- 発売日: 2011/09/22
- メディア: 文庫
- 購入: 5人 クリック: 104回
- この商品を含むブログ (55件) を見る
ゆっくり読もうとおもいます :D
2012-02-22
zshで困ってたことをボチボチ直した
暇ができたらやろうと思ってたことをやりました。
右側の表示(RPROMPT)が大変うざい
Gitのブランチ名をRPROMPTに表示する方法を改良してみた - Hello, world! - s21g をありがたく使っています。
これはgitが管理するディレクトリに入ると以下のように右っかわにブランチ名が出てくる便利なものなんですが、
nari@eve ~/ $ hoge [master]
これが不便なのはターミナルの実行手順をコピペするときで、右側の表示もコピーされてしまうため、なんか幅広の文字列がクリップボードに入ってしまいます。
結局、コピペした後に整形しないといけないと…。
で、面倒なので以下のような形に直しました。
nari@eve ~/:[master] $ hoge
すっきり!
BASE_PROMPT_LINE1="$BLUE$USER@$HOST $GREEN%~$DEFAULT" BASE_PROMPT_LINE2='%(!.#.$) ' PROMPT='${BASE_PROMPT_LINE1} ${BASE_PROMPT_LINE2}' # ... _update_prompt () { if [ "`git ls-files 2>/dev/null`" ]; then PROMPT='${BASE_PROMPT_LINE1}:[${GIT_CURRENT_BRANCH}] ${BASE_PROMPT_LINE2}' fi }
上記のように_update_rprompt()を少しだけ修正しています。
追記
コメント欄でvcs_infoが便利との情報をいただきました。たしかに!
setopt transient_rpromptも教えてもらいました。
もっと履歴情報欲しい
新山のbashrc - YouTube
新山さんの動画を見返していて、コマンドの履歴をもっと充実させたい欲がわきました。
自前で履歴吐けば色々と付加情報を与えられるので便利だな、と。
HISTSIZE=20000 SAVEHIST=20000 function i { grep $1 $MYHISTFILE } # 履歴関連 MYHISTFILE="$HOME/.zsh_my_history" function _log_history() { echo "`date '+%Y-%m-%d %H:%M:%S'` $HOST:$$ $PWD ($1) `history | tail -n 1`" >> $MYHISTFILE } function precmd() { local exit_status=$? _log_history $exit_status }
実行日付、ホスト名、位置、実行結果などが履歴で見れてよい感じです。
特に実行結果は便利で、失敗したコマンドを再度入力しなくてよくなりますね。
また、iコマンドで自前の履歴は検索できるようにしました。
古い履歴はiコマンドでみると思うので、HISTSIZE,SAVEHISTは小さめに設定しました。
今まで培ってきたzshの履歴があるので、それを自前の履歴に移行しておきます。
※~/.zshrcを修正する前に移行しておきましょう。
位置や実行結果などはわかりませんが、まぁしょうがないか…。
# -*- coding: utf-8 -*- require "time" require "pp" # histry -E 1 > /tmp/zsh_hist で吐き出したあとに実行 File.read("/tmp/zsh_hist").each_line do |l| begin # 2012-02-22 16:54:00 eve:28805 /home/nari (0) 115170 source ~/.zshrc s = l.gsub(/^\s+/, '').split(/\s+/) r = Time.parse(s[1..2].join(" ")).strftime("%Y-%m-%d %H:%M:%S") r << " __:__" r << " /__/__" r << " (_)" r << " #{s[0]}" r << " #{s[3..-1].join(' ')}" if not s[3..-1].nil? puts r rescue $stderr.puts l $stderr.puts s[2..3].join(" ") raise $! end end
2012-02-03
RubyのBitmap Marking GCによるメモリ使用量の改善(意訳)
そういえばBitmapMarking GCについて@m_stさんにInfoQで質問をうけました。
だいぶん残念な英語を返したのですが、向こうのインタビュイーさんの方でいろいろ修正していただいたみたいです。感謝感謝。
で、ちと意訳ですが以下に日本語訳も書いておきます。
ちょっとだけ補足したいこともあったので。
InfoQ Japanの方でも日本語訳していただいてます。ありがとうございます。CGの話も追加してもらったみたいですね。
RubyのBitmap Marking GCによるメモリ使用量の改善
原文
Narihiro Nakamuraによって書かれたGCの最大停止時間を短くするLazy Sweep Garbage Collector(参照:InfoQによるレポート) が、Ruby1.9.3には導入されている。
最近、Narihiroはcopy-on-wirte(CoW) friendlyなBitmapMarkingGC(以降、"bmap"と呼ぶ)を実装し、提案した。
これはRuby Enterprise Edition(REE)のcopy-on-write-friendly GCと似たものである。
POSIXのfork()システムコールでは、子プロセスとその親プロセスでメモリを共有しており、メモリ内の一部が変更された場合にその部分を子プロセスにコピーする方式により、メモリ使用量を削減している。
しかし、残念ながら現在のRubyのGCではこの仕組みはうまく動作しない。
Rubyはマーク・スイープGCを採用している。 このGCでは、はじめにすべての生きているオブジェクトを走査し、マークを行う。 マークではオブジェクトのヘッダフィールド上のFL_MARKフラグを立てる。 その後、すべてのオブジェクトをもう一度走査して、すべての死んでいるオブジェクトのマークを外す。 これの問題点は、CoWのセマンティクスを破壊することだ: マークするすべてのページがDirtyになってしまう.
InfoQはNarihiroのbmap実装が現在のLazySweepGCよりも改善されているのかを質問した。
Bitmap Markingアルゴリズムの良い点は以下のとおりです。
- ビットマップは、オブジェクトヘッダにマークビットを置くよりも、ビットを密集した領域に格納する - 高い局所性 - マークはオブジェクトを変更しない。そして、スイープは生きているオブジェクトを変更しない。 - CoW friendly, ダーティキャッシュラインが少ない - マークビットのクリアにmemset()が使える(訳注: マークビットが一気に消せる) - スイープがちょっぴり早い
CRubyの事情ではCoW firendlyがもっとも重要だと思います。 BitmapMarkingはLinuxでfork()を使っているようなプログラムでメモリ使用量を改善するものです。 で、CRubyでは本当の並列のパフォーマンスを得るためにfork()を使わないといけません。 さらに、CRubyはすでにfork()を使った多くのライブラリを持っています(Unicorn, Resqueとか)
(訳注: あんまLazySweepとは関係ないです、とも言いたかったんですけど…深刻な英語力不足が…)
InfoQ:
LazySweepGCはスループットを悪化させますよね。
話によるとbmapもまたスループットを少しだけ悪化させると聞いてます。
bmapは現在のGCを置き換えようとしていますか?
それともユーザ/開発者が設定で選択できるようなものですか?
私のプランは「Bitmap Marking GCをデフォルトのGCする」ものです。 あなたは「bmapは*少しだけ*遅くなる」と言いましたね。 それはたしかにそうですが、私はその速度低下はすべての人にとって許容範囲だと考えています。 なので、ユーザは設定のようなものをBitmapMarkingに対して必要としない、と考えています。
(訳注: あんまり速度低下ないから、標準にしても困らない。というより、ユーザに選択させる際には何らかの強いデメリット・メリットがある場合に限られると思っていて、今回の場合はデメリットが少ないから標準でいいんでは、と思った次第です)
InfoQ:
あなたはBitmap Markingを組み合わせたParallel Marking GCを作るつもりだとおっしゃってましたね。
大幅にGCが高速化するように思えますが、あなたはこれがどれくらい高速化するか(もしくはどれくらい速度低下するか)について何か考えをもっていますか?
実は、Bitmap Marking抜きのParallel Marking GCは作っています。 2コアのマシンで、いくらかのケースでは、総GC時間を40%くらい改善することできました。 Bitmap MarkingとParallel Marking GCを組み合わせると, たぶん少しだけ改善率は落ちるかもしれません。
すでにParallel Marking GCの詳細についてはRubyConfUS 2011で話しています(ビデオ[1]とスライド[2])。
MatzはすでにこのGCパッチをtrunkにコミットしている。そのため、これは次のリリース(たぶん2.0)に含まれるだろう。
(訳注: 細かい点ですが実際にはまつもとさんに許可をもらって、私がコミットしました)
2011-12-31
ゆく年、くる年
2011年。いい年でしたね。
2012年。いい年ですか?
早いものでもう5年目のブログ納めかぁ。
今年の目標は達成できたか
- 今の本を書き上げる
おぉ書き上げましたね -> 徹底解剖「G1GC」 アルゴリズム編 - 達人出版会
- もう1冊書き始める
おぉ書きはじめましたね -> no title
- CRubyGCの改善パッチを1つ
並列GCのパッチはありますね。出してないけど…。
ビットマップ作ってて、もうそろそろだそうかなぁ。
結局着手できなかったなぁ。
というかもうCでGC書きたくないんだよね路線を狙っている。
- 継続的な英語の勉強
よくがんばりました。
- 貯金・運用
貯金はできてるけど、運用はあんまりできてないなぁ。
一応やってるけど面倒なんだよねぇ。
- RubyKaigiへの参加・発表
達成!!
- RubyConfへの参加・発表
おぉ、達成。よくがんばったな。調子に乗って南米にも行ったし!!
- 楽しく健康に仕事する
個人的にはいろいろ楽になったなぁ。
今年も上のリスト以外にもよくがんばったというか、毎年えらいよねー。
来年の目標
- 今の本を書き上げる
- もう一冊書きはじめる
- CRubyGCの改善パッチ出したい
- 継続的な英語の勉強
- 『The Art of Multiprocessor Programming』『GC Handbook』読み切りたい
- PyPyのGC理解したい
- 自分で一個書いてみたい
- ytjitいじってみたい
- 暇でいたい
- 札幌RubyKaigiに行く
では、みなさん、よいお年を :D
(ブログ納め)
2011-12-28
「寄付が溜まったら本気出す方式」の考察
黙々と執筆する前に。
長めの記事になりましたが、今後同じ事をしたい人のために記録を残しておきます。
やったこと
目標金額決定
20万円という目標金額を立てました。
GC本の総収入から、執筆する予定のページ数分をみて、だいたいこれくらいかなーとざっくり計算しました(が、これはあまりよくないと思ってます)。
「寄付する人は何人くらいになるかなぁ」というのをG1GC本の販売数を見て予想しました。
寄付の受付場所。寄付の目標金額を設定したかったので何がいいかなぁと考えてました。
最初は READYFOR? (レディーフォー) | クラウドファンディング なども検討したのですが、審査とか期間は私には合わないのでやめました。サポートはいいなぁと思ったのですが。
結局、自由度は高いけどサポートのないPledgie — Helping you help others.にしました。
あとで気づくことになるのですが、Pledgieは3%手数料を取るようになっていて、githubとの連携も切れていました。
まぁそれはいいのですが、紹介ページで日本語が書けないのは辛かったです…。
準備したこと
https://github.com/authorNari/g1gc-impl-bookを用意しました。
issue登録できて、Web上で編集してpull request送れたりするのでだいぶん便利だなぁと。
最終的に読者の感想とかpull requestでもらえるようにしようかなぁ、とも考えてます。
- 公開用のWebサイト
no title
github側と差異がないようにしました。
WebサイトのトップページはREADME.mdと一緒で、サイト上の原稿はgithubのものを同期しています。
同期の方法はWebサイトをgithubで管理してpush時に自動的に同期する方法 - 古橋貞之の日記を参考にしました!
- 前半分の原稿
とりあえず試してみたかったので、かれこれ2年くらい持ってた書きかけの原稿に手を加えて公開しました。
ある程度書いてからじゃないと信憑性が薄いし、寄付する人もどういうものか想像できず、寄付しづらいのではないかと思います。
Web上のページはgithub上リポジトリと同期してるんですが、epubやpdfなどはそうしてません。
書きかけの途中のものをpdfで見てもなぁと思ったからで、例えば1章書けたとか、ある程度の区切りで公開していく予定です。
宣伝方法
- ブログ記事
意図を書いたりしました。
「ある程度寄付が溜まったら本気出す」方式の執筆を試してます! - I am Cruby!
- ついった
私のアカウントで途中からお礼を言うようにしました。
TL上で鬱陶しく感じる人もいるだろうなぁと思って最初は遠慮してたのですが、まず寄付した人に感謝の気持を伝えたかったので、その気持ちを優先しました。
アクセス数を見ると、お礼のついーとを見て本書の存在を知った人が多いようです。
- いいねボタンとかそういうやつ
をトップページにおきました。
データ
| 総人数 | 38名 |
|---|---|
| 総額 | ¥203,000 |
| 手数料(pledgie+paypal) | -¥15,878 |
| 手取り | ¥187,122 |
| 1人平均 | ¥5,342(!!!) |
| 1万円以上 | 8名 |
| 9999円〜5000円 | 5名 |
| 4999円〜3000円 | 7名 |
| 2999円〜1000円 | 14名 |
だいぶん平均がおかしなことになっています。紙の本よりもだいぶん高いですね。
多くの方は千円以上の値段を付けてくださったようです。ありがとうございますッ。
上位人の寄付金は本当にすごくって、特に@yamazさんは目標額の1/4以上あったような…。
ありがとうございます…。
スポンサーの皆様
no title
を見ると、かなりの方がGC本購買者であることがわかります(私には見えるッ)。
また、私のGCやRuby関連の活動で知り合った方々がほとんどです。
RubyKaigiやGC本執筆の影響が強いですね。感謝…。
日頃の行いを積み重ねることは大事だなぁと思いました。GCバンザイ!!
推移

1のあたりで公開しました。
2は「ある程度寄付が溜まったら本気出す」方式の執筆を試してます! - I am Cruby!を書いたタイミングです。
3はわたしが「xxさんに寄付いただきました!」のお礼ツイートをはじめたときです。
4はソーシャルスポンサーゲームが開始した時間です。
ソーシャルスポンサーゲームの熱狂ぶりが…(ごめんなさい)。
目標金額達成するまでもっと時間がかかると私は思っていました。
ま、まさか3日で集まるとは…。
KPT
Keep
- Twitter上でスポンサーになってくれた人にお礼する
- githubと連携した自動デプロイでWebページがすぐに更新できたのでよかった
- スポンサーリストのソート順(RubyKaigi流)
- 寄付しなくてもリストを見るだけでわりと楽しい
- 私はRubyKaigiのときそうだった
- ソーシャルスポンサーゲーム(!!)
- 寄付しなくてもリストを見るだけでわりと楽しい
- 途中でスポンサーリストにアイコン付けたのはよかった
- もっと楽しくなるから
Problem
- 目標金額の根拠が薄い
- スポンサーリストの脳内ソート問題がけっこうつらい
- 目標金額が高すぎるといつまでたっても執筆に取り掛かれない問題起こりそう
Try
2011-12-27
ある程度寄付が溜まったので本気出します
以下に書いてきた話の続きです。
no title
「ある程度寄付が溜まったら本気出す」方式の執筆を試してます! - I am Cruby!
今日、いただいた寄付の総額が目標金額に達しました。
正直まだまだ時間かかかるかなーと思っていたので、驚いています…。
掲題の通り、寄付がある程度溜まったので本気出します!
スポンサーのみなさまありがとうございます!!
みなさんの寄付金に恥じないようなコンテンツを作りたいと思います!!
no title
取り急ぎお礼まで。
2011-12-26
「ある程度寄付が溜まったら本気出す」方式の執筆を試してます!
「私が昨日公開した書籍は無料だと言ったな、実はあれは嘘だ。」
『徹底解剖「G1GC」実装編(執筆中)』無料公開 - I am Cruby!
no title
リンクをたどった方はお気づきかと思いますが、本書は寄付を募る形になっています。
「もし続きが読みたかったら寄付をいただけませんか」というやり方です。
そして、寄付者の名前を本に載せて、Contributeとして明記するようにしています。
なので、タダだと思う人はタダだし、そうじゃない人にはそうじゃないみたいな。
# 何をいっているのかわからないと思うが。
すでに寄付いただいた方もいらっしゃいます! 本当にありがとうございます!!!
no title
# こういうののお礼ってどうするのがいいのかなぁ。
# twitterで直接お礼するのもなんか違うのかなぁと思って…。
この方法は以下から影響をうけてやってみました。
- 「千人の忠実なファン」: 七左衛門のメモ帳の「大道芸人方式」
- 大江戸Ruby会議01 - Regional RubyKaigiの「お代は見てのお帰り」
- 日本Ruby会議2011(7月16日〜18日)の「個人スポンサー」
なぜこのような方式をとったかという意図ですけど…。
「とりあえずやってみるか」という実験的な試みなので私自身も暗中模索状態なんですよね。
とはいえ、説明不足感はあるので今のところで吐き出してみます。
技術書についていろいろ思うこと
きっとまとまらないとは思いますが、正直に思っていることを書いてみますね。
諸君、私はマニアックな本が好きだ
『徹底解剖「G1GC」 アルゴリズム編』発売!! - I am Cruby!の備考で書いたことです。
要約すると
- 私はマニアックな本が好きだ
- マニアックな本は需要がないので書いても(あまり)儲からない
- ただ書くのはすごく時間がかかる
- でも私はそんな本を沢山読んでみたい
となります。
著者に適切な労働の対価が入るだろうか?
ものすごく時間のかかる本の執筆に対し、それに見合う報酬は得られるでしょうか。
私が書いた今までの書籍では時給換算すると大分悲しい感じになります。
これはたぶん私だけじゃなくって、いろんな人がそうなんだと思うのですが。
いやー「コンテンツが悪いからだ。しょうがない」と言われればそれまでなんですけど(テヘヘ)。
レビュアのみなさんにも無償で質の高いレビューをしてもらっているのに、こちらからは何もできなかったしなぁ…とかもありますね。
とてもよい勉強になる
ここまで書くと私が金の亡者すぎて怖くなった方もいらっしゃるでしょう。
ですので、亡者っぽくないことも書いておくと、本を書くことはとても勉強になります。
日本語の勉強や、対象の技術について深く勉強できます。
これはお金では得られない貴重な経験ですし、やりたいと思う人は気軽に体験してもらいものだと思います。
しかも、本を売るとお金も貰えますしオイシイですよね、うっへっへ(あっ)。
本の値段って何だろうか?
ここまで考えると、「じゃあ本の値段を高くしてちゃんと報酬得たろうかい」と思うわけです。
でも、『ほん』の値段ってなんで決まるんでしょうか。
たぶん出版側からすると紙代とか人件費などの経費で決まると思います。
でも、読者側からするとそんな客観的な値段じゃなくて、主観的な値段なんですよね。
つまり「自分にとってこの本はXXXX円」と考えると思うんです。
とすると、読者が買う1冊の値段は読者が決めるのが自然な気がします。
ただ、それだと出版側に確実な報酬が約束されません。
それで出版側が欲しいと思う全体の報酬を定義して、1冊の本代は読者の人に決めてもらう方式がいいのかなぁと思いました。
まとめ
結局うまくまとまりませんでしたが(あぁ、なにがいいたいのか…)。
でも、上記のようなことをいろいろ考えた結果にとりあえずこういうことやってみました。
今回の実験的な試みの成功・失敗・停滞にかかわらず、すくなくとも何らかの結果は出ると思いますので、それを有用なデータとして使ってもらえると嬉しいです :D
こうならないようにがんばります。


