Hatena::ブログ(Diary)

I am Cruby! RSSフィード Twitter

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. clientタイムアウトをとんでもなく長くして対応する
  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

これはサーバの待受ループとクライアントの待受ループを1つのイベントループでおこなおうとするから。

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

gemのやーつをインストールしてから…

$ 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をペタリ。

追記

下記のものをいただきました。ありがとうございます!!

マンガ 孔子の思想 (講談社プラスアルファ文庫)

マンガ 孔子の思想 (講談社プラスアルファ文庫)

プランク・ダイヴ (ハヤカワ文庫SF)

プランク・ダイヴ (ハヤカワ文庫SF)



ゆっくり読もうとおもいます :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()システムコールでは、子プロセスとその親プロセスでメモリを共有しており、メモリ内の一部が変更された場合にその部分を子プロセスにコピーする方式により、メモリ使用量を削減している。
しかし、残念ながら現在のRubyGCではこの仕組みはうまく動作しない

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

並列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でもらえるようにしようかなぁ、とも考えてます。

no title
github側と差異がないようにしました。
WebサイトトップページはREADME.mdと一緒で、サイト上の原稿はgithubのものを同期しています。
同期の方法はWebサイトをgithubで管理してpush時に自動的に同期する方法 - 古橋貞之の日記を参考にしました!

  • 前半分の原稿

とりあえず試してみたかったので、かれこれ2年くらい持ってた書きかけの原稿に手を加えて公開しました。
ある程度書いてからじゃないと信憑性が薄いし、寄付する人もどういうものか想像できず、寄付しづらいのではないかと思います。

Web上のページはgithubリポジトリと同期してるんですが、epubpdfなどはそうしてません。
書きかけの途中のものを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本購買者であることがわかります(私には見えるッ)。
また、私のGCRuby関連の活動で知り合った方々がほとんどです。
RubyKaigiやGC本執筆の影響が強いですね。感謝…。

日頃の行いを積み重ねることは大事だなぁと思いました。GCバンザイ!!

推移

f:id:authorNari:20111228103415p:image
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で直接お礼するのもなんか違うのかなぁと思って…。

この方法は以下から影響をうけてやってみました。

なぜこのような方式をとったかという意図ですけど…。
「とりあえずやってみるか」という実験的な試みなので私自身も暗中模索状態なんですよね。
とはいえ、説明不足感はあるので今のところで吐き出してみます。

技術書についていろいろ思うこと

きっとまとまらないとは思いますが、正直に思っていることを書いてみますね。

諸君、私はマニアックな本が好きだ

『徹底解剖「G1GC」 アルゴリズム編』発売!! - I am Cruby!の備考で書いたことです。
要約すると

  • 私はマニアックな本が好きだ
  • マニアックな本は需要がないので書いても(あまり)儲からない
  • ただ書くのはすごく時間がかかる
  • でも私はそんな本を沢山読んでみたい

となります。

著者に適切な労働の対価が入るだろうか?

ものすごく時間のかかる本の執筆に対し、それに見合う報酬は得られるでしょうか。
私が書いた今までの書籍では時給換算すると大分悲しい感じになります。
これはたぶん私だけじゃなくって、いろんな人がそうなんだと思うのですが。

いやー「コンテンツが悪いからだ。しょうがない」と言われればそれまでなんですけど(テヘヘ)。

レビュアのみなさんにも無償で質の高いレビューをしてもらっているのに、こちらからは何もできなかったしなぁ…とかもありますね。

とてもよい勉強になる

ここまで書くと私が金の亡者すぎて怖くなった方もいらっしゃるでしょう。
ですので、亡者っぽくないことも書いておくと、本を書くことはとても勉強になります。
日本語の勉強や、対象の技術について深く勉強できます。
これはお金では得られない貴重な経験ですし、やりたいと思う人は気軽に体験してもらいものだと思います。

しかも、本を売るとお金も貰えますしオイシイですよね、うっへっへ(あっ)。

本の値段って何だろうか?

ここまで考えると、「じゃあ本の値段を高くしてちゃんと報酬得たろうかい」と思うわけです。
でも、『ほん』の値段ってなんで決まるんでしょうか。

たぶん出版側からすると紙代とか人件費などの経費で決まると思います。

でも、読者側からするとそんな客観的な値段じゃなくて、主観的な値段なんですよね。
つまり「自分にとってこの本はXXXX円」と考えると思うんです。
とすると、読者が買う1冊の値段は読者が決めるのが自然な気がします。

ただ、それだと出版側に確実な報酬が約束されません。
それで出版側が欲しいと思う全体の報酬を定義して、1冊の本代は読者の人に決めてもらう方式がいいのかなぁと思いました。

まとめ

結局うまくまとまりませんでしたが(あぁ、なにがいいたいのか…)。
でも、上記のようなことをいろいろ考えた結果にとりあえずこういうことやってみました。

今回の実験的な試みの成功・失敗・停滞にかかわらず、すくなくとも何らかの結果は出ると思いますので、それを有用なデータとして使ってもらえると嬉しいです :D

こうならないようにがんばります。
f:id:authorNari:20111226184308g:image