Rubyでの非同期処理について

こんにちはhelloです。ISUCONで課題に上がっていたRubyでの非同期処理についてまとめてみたいと思います。

まず記事に関連しそうなワードについて整理したいと思います。

非同期処理について

  • 非同期処理とは、あるタスクが実行している際に他のタスクを実行できる方式です。

平行処理について

  • ある時点で一つの仕事しかしないが複数の仕事を切り替えて行う事。

並列処理について

  • ある時点で複数の仕事をしていること。

GIL(グローバルインタプリンタロック)

  • インタプリンタのスレッドによって保持されるスレッドセーフ出ないコードを他のスレッドと共有してしまうことを防ぐための排他ロック。

IO

  • DiskIO
  • NetworkIO
  • 同期ブロッキング
  • 同期ノンブロッキング
  • 非同期ブロッキング
    • システムコールでio処理を登録しレスポンスを待つ
    • 登録された何かが終わったらレスポンスを行う
    • epollが該当しO(1)で返される。
  • 非同期ノンブロッキング
  • マルチプロセスモデル
    • クライアントからの接続ごとにプロセスをフォークして処理を行う。
    • メモリ空間が子プロセス毎に独立している。
      • プロセス間でのメモリの直接参照が不可能。
  • マルチスレッド
    • 接続ごとにスレッドを生成する
    • メモリ空間を各スレッド間で共有可能
    • メモリ空間の切り替えがないため、メモリ消費量やコンテキストスイッチが抑えられる。
  • preforkモデル
  • workerモデル
    • マルチスレッドとマルチスレッドを合わせたもの。
    • プロセスが複数のスレッドをたち上げる。スレッドが一つのクライアントの処理を行う。
    • リクエストとスレッドが1対1
  • イベント駆動モデル
    • nginxやnodejsで利用されている
    • 1プロセスは1スレッドのみを利用する。
    • シングルスレッドのプロセスのためメモリ空間を共有可能
    • クライアントアクセスが増えてもプロセス・スレッド数は増えないため
    • メモリ消費量・コンテキストスイッチコストが小さい

非同期処理について整理していたつもりがすごい量になってしまいましたね...

Rubyでの非同期処理について

Delayedjob

  • queueをデータベースに用意し、定期的にポーリングする。
  • よくrailsで使われるやつですね。特に難しい事はないので割愛します。

    Rubyでのマルチプロセス

    Process.forkをすることによりプロセスを作成する事が出来ます。

port = #listenさせたいport
socket = TCPServer.new(port)
loop do
  read_socket = socket.accept
  fork do
    #何かしらの処理
  end
end

のような形になると思います。
gemとして有名なものとしてparallelがあります。Parallel.mapでそれぞれのプロセスで実行した結果をまとめる事ができます。
注意点としてあげるならメモリが共有されないのでparallelのようなgemを使用する際でも意識して下さい。結果を扱いたいときはParallel#mapなどを使ってください。

Rubyでのマルチスレッド

Thread.newをしてください

port = #listenさせたいport
socket = TCPServer.new(port)
loop do
  read_socket = socket.accept
  Thread.new do
    #何かしらの処理
  end
end
  • 基本的なRuby(CRuby)ではGVLがかかっているので、他の言語でいう並列処理を再現できていないです。
  • JRubyやRubinusでは並列処理をサポートしているのでPumaといった有名なwebサーバーはそれらを推奨しています。

RubyのIOについて

  • 上記で述べた同期ブロッキングを行うシステムコールを呼ぶメソッドはIO#write,IO#readなどが該当します。
  • ではRubyでノンブロッキングIOを行うには何を使うかというとこちらのメソッドとなります。IO#read_nonblock,IO#read_nonblock
  • これらのメソッドはO_NONBLOCKに設定しread, writeを行いノンブロッキングIOを提供してくれています。
  • (IO#read_partialはread+selectによるものらしい)
  • 非同期ブロッキングを提供するepollを行うメソッドがありません。
    • 理由などは調べれなかった...すいません
    • OSによってkqeueだったりepollだったりするのが起因しているのだろうか
  • しかしeventmachinenio4rというgemではepollを実装しているようですね。

Rubyでのイベント駆動について

  • 有名なものは上記であげたeventmachineというgemです。
  • reactorパターンを用いています。
  • reactorパターンとは読み込み書き込みが可能になったタイミングでシステムコールselect(2)またはepoll(2)を行い用意できたソケットから処理を進めていくものです。
  • pumaでもreactorパターンを用いている様子

他のRubyでのノンブロッキングIOについて

  • Asyncというgemを用いたパターンです。
  • このgemではFiberを用いてブロッキングIOを非同期的に操作します。
require 'async'
def test
  async do |task|
    #IO待ち処理
  end
end

Async do
  test
  test
  test
end

のような形で非同期処理を行える。内部の処理見るとAsyncで括った処理にたいして渡したブロックからFiberを生成し、生成したFiberをイベントループで監視しyield実行させているみたいですね。

  • Asyncを用いて作られたFalconは性能もよいらしくとてもきになります。

感想

  • 非同期って知らなきゃいけないことが多くとても難しいですね...
  • Ruby3ではGUILD?などがくるようなのでそれも楽しみにしていきたいと思います。

つたない記事でしたが読んで下さりありがとうございました。

参考URL

ISUCON9予選に参加した話

こんにちはhelloooです。弊グループのアドベントカレンダーを埋めるネタが思いつかなかったので9月に参加したISUCONについての振り返りをしようかなと思います。

ISUCONとは

ISUCONは2011年から行われているコンテストで、課題となるアプリケーションを最も性能が出るように修正したチームが優勝というコンテストです。

チームについて

大学の友人たちと一緒に参加しました。言語はチームメンバーの2/3が慣れているRubyで参加しました。チーム構成はインフラ:1名,アプリ:2名で行いました。 実際に編集したコードはこちらです

取り組み

使用ツール

実施内容

N+1の解消


ベンチワークを回した結果GET /new_items.jsonが遅すぎたのでslow queryを消すことに。一つ一つ修正していくとスコアが上がっていた。
Seller_list buyer_listについても同様に修正def get_category_by_idの修正に合わせてfixを行う。
indexを張る作業も同時に行っていた。

1台構成を3台構成に変更


初期起動時は1台構成であり、web + app + dbがすべて一台で起動されている状態なので

  • 1台目:WEB+APP
  • 2台目:App +DB
  • 3台目:APP

の構成に変更した気がする。

nginx.conf・mysqld.confの設定修正


キャッシュ周りを主にやってもらった記憶がある...

わかっていたが対応できなかったもの


  • 外部API呼び出し
    • 明らかにボトルネックになっていることはわかっていた。
    • 「非同期処理にすればいけるでしょー」と思っていたが、他に影響あるか確認したりするコストが発生する上、Rubyでの非同期処理ってどうやるかをぱっと思い出せなかった。
      • Sideiq? Delayedjob? 言い訳ですがgoroutineで書きたいしGoですればよかった
      • ThreadとかFiberでもよかったのかなぁ
  • 1: db 2:app 3:app構成
    • 一度実施したが、画像のURLが違うというエラーが発生。画像アップロードを含むエンドポイント/sell/uploadを片方のAppに流すようにしたが解決せず。
      • 今でもなんでだめなのかいっちょんわからん

反省・所感

  • N+1については修正までのフローが去年に比べ早くなりスコアが上がりやすくなったと感じています。 ただindexについては感覚でやっていることがあり、もっと知識をつけるべきだなぁ
  • 方針についての判断を早くすべきでした。対応できなかったものについてどちらも中途半端に取り組み結果が出ずに終わりました。どちらに取り組むかを決めて動いていれば本戦のスコアに届く未来があったと思うと悲しみが深いです。
  • Rubyでの非同期処理について少し学んで起きましょうね!!!!!

終わりに

順位は161という形で終わりましたが去年よりも順位を上げているので成長はしているのかなぁと思います。 ISUCONは業務ともつながりが深く誰もが楽しんでもらえるコンテストですので社内開催をしても面白いイベント&技術力向上になると思いますねぇ....