Apache Arrow東京ミートアップ2018で「RubyとApache Arrow」というタイトルで発表してきました!!!

今日は Apache Arrow 東京ミートアップ2018というイベントに登壇してきました。

speee.connpass.com

おそらく今年最後の登壇になったかなと思います。
今日はRails Developers Meetup 2018 Day 4 Nouvelle Vagueがやっていて被ってなかったら行きたかったなぁ〜

さて今回は RubyApache Arrow という内容で話してきました。
資料は↓こちらになります。

speakerdeck.com

RubyApache Arrow を使う時に「今こんな現状」で「今後こうなると嬉しいな」という話をしました。
このブログでは「今後こうなると嬉しいな」で紹介したMySQLの実行結果を Apahce Arrow で返すところを紹介していきたいと思います。

Apache Arrow に関しては須藤さんの資料が参考になるかと思います。

slide.rabbit-shocker.org

MySQLの実行結果をApache Arrowで返すとは??

現状 RubyApache Arrow を扱う際には CSV であれば CSV Parser から高速で Arrow 形式に変換できるようになっています。
しかし MySQL に対象となるデータがある場合は一度なんらかのフォーマットでアウトプットすして変換したものを読み込む必要があります。例えばCSVとか
そのため MySQL から取り出したレコードをそのまま Arrow の形式に変換できると良さそうです。

どういうアプローチを検証してみたか

Rubyには mysql2 という libmysql を Ruby binding したgemがあります。
これは ActiveRecordMySQL を選択した際にも使用されています。

github.com

これを Apache Arrow 形式で取り出せるようにすれば高速でデータ処理ができるはずです。
実装イメージとしては↓こんな感じです。

f:id:hatappi1225:20181208232547p:plain

ポイントとしては mysql2 ではクエリを実行した後に MYSQL_RES を保持しているMysql2::ResultRuby 側にかえってくるので、これを再度 C, C++ のレイヤーにもっていき MYSQL_RES を取り出して Arrow 形式に変換しているところですね。
C, C++ のレイヤーで行うことで高速で処理できるというメリット以外にも C, C++ で行なっているので嬉しいのは Ruby だけではないところですね。
良い世界だ。

一番大変だったのは MYSQL_RES からレコードを取り出してArrow形式に変換するところです。
なぜかというと C++ で書いたのですが、今まで書いたことなかったので、 Arrow の実装などを見たり結構時間がかかってしまいました。
このあたりはGitterで須藤さんやむらけんさんに色々アドバイスをいただきました。
ありがとうございました。

検証は↓こんな感じでやりたかったので ActiveRecord に Arrow 形式で取り出せるような口を用意してあげる必要があります。

# pluck
User.limit(500000).pluck(:name).size

# AR
User.limit(500000).each_slice(3000){|r| p r.size }

# Apache Arrow
User.limit(500000).each_record_batch(size: 3000) { |r| puts r.n_rows }

今回は↓こんな感じで用意しました。。。。
もうちょっと良い感じにしたいけど良い案が浮かばなかった。

# Mysql2::Result を Arrow 形式に変換してくれる gem 
require 'mysql_arrow'

module Mysql2AdapterExtend
  def exec_query_and_mysql2_result(sql, name="SQL")
    execute_and_free(sql, name) do |result|
      if result
        # 本来は result.to_a してrowをArrayでわたしているところを Mysql2::Result をわたす  
        ActiveRecord::Result.new(result.fields, result)
      else
        ActiveRecord::Result.new([], [])
      end
    end
  end
end
ActiveRecord::ConnectionAdapters::Mysql2Adapter.send :include, Mysql2AdapterExtend

module RelationExtend
    def each_record_batch(size: 1000)
      conn = klass.connection
      sql = conn.to_sql(arel)
      # rows は Mysql2::ResultなのでArrow形式で取り出せる each_record_batch が生えている
      rows = conn.exec_query_and_mysql2_result(sql).rows
      rows.each_record_batch(size: size) do |r|
        yield r
      end
    end
end
ActiveRecord::Relation.send :include, RelationExtend

検証した結果

# pluck
User.limit(500000).pluck(:name).size

# AR
User.limit(500000).each_slice(3000){|r| p r.size }

# Apache Arrow
User.limit(500000).each_record_batch(size: 3000) { |r| puts r.n_rows }

ActiveRecord: 6.849s
pluck: 2.450s
Apache Arrow: 0.520s

こんな感じの結果になりました。
Apache Arrow 良いかんですね!!!

今後

検証してみて良さそうだったので継続して開発してリリースできる形にもっていきたいなと思っています。
直近は MYSQL_RES を Arrow 形式へ変更する処理を C++ で作って arrow リポジトリに取り込めないかを提案していきたいと思います 💪

最後に

今回は発表側として参加したのですが、Ruby 以外での言語とApache Arrow や Arrow を使ったシステムの話など普段聞けないような話をたくさん聞けてさらに、コード懇親会でコードがかけて良い感じだった。

あとは今回の発表資料を作るなかではじめて Ruby の binding という世界に触れられて楽しかった。