Rubyで並列処理をやっていく #AdventCalendar

mixiグループアドベントカレンダー2016 1日目です。

今回は、自分が今まで利用したRubyでの並列処理を書くためのgemとか知見を紹介します。

機運

先日のRubyKaigi 2016で、Ruby3ではGuildという新しい並列処理のモデル*1が、導入されるというセッションがあったり、concurrent-rubyというgemの開発が流行り初めて居たりと、Ruby界隈でも何となく並列処理がブームきているように感じます。

マルチプロセス/スレッド

しかしRubyで並列処理するのは言語の仕様としてそれなりに制限があり、他の言語のようにThreadをバンバン立ててマルチコアで計算!爆速化!!みたいなのは難しいです。 というのも、Ruby1.9からネイティブスレッドは導入されたものの多くのC拡張を使ったgemのスレッドセーフ性が問題となるため、GIL(Global interpreter lock)と呼ばれる仕組みが存在しており、RubyやC拡張の処理自体が1つのプロセス上で同時に実行されることがありません。(逆にいうとGILのおかげでスレッドセーフ性について何も考えずにRubyが書けている!)

それでも、並列に処理を実行したいときは普通にRubyでアプリケーションを開発していると訪れて、何とかやっていく必要があります。 その場合、以下のような実現方法が考えられます。

  • forkしてマルチプロセス化(PerlとかPHPではこっちが多い)
    • メモリ消費大 (Copy on Writeという仕組みがあるので一定は共有される)
    • スレッドセーフ性考えなくて良くてシンプル
    • Rubyでの計算処理でもCPUのコア数使いきれる
  • マルチスレッド化 + IO多重化
    • メモリ消費小
    • バグなく動かすためスレッドセーフ性が必要
    • Rubyでの計算処理ではCPU使いきれないのでIO処理と組み合わせる

並列処理を書くのは難しい

上述のようなプリミティブな実現方法があったとしても、自分で一から全て正しく書くのはとてもハードルが高いことです。 他の言語でもThreadを直接使うよりを抽象化されたモデル(Future, Actor, async/awaitなど)を利用することが多いです。 Rubyの場合は用途に特化した便利な実装がすでにあるのでこの辺を使う所から始めると良いと個人的に思うので紹介と、これまで得た知見を共有します。

  • Parallel
  • Sidekiq

最初に触れたconcurrent-rubyを使うと他の言語で利用で利用されているような非同期処理の抽象化モデルが利用できますが今回は省略します。

Parallel

ループ処理をめちゃくちゃ簡単に並列化できるライブラリ。 プロセスモデル・スレッドモデル両方採用できます。

サンプルコード。デフォで、マシンのCPU数を調べてその数だけマルチプロセスを起動して処理してくれます。 記法も簡単で、Parallel.mapの引数に配列を渡すだけ。各do〜end内の処理が複数のプロセスやスレッドで処理が行われるようになります。 mapを利用すればRubyArray#mapのように処理した結果を配列で受け取ることも可能です(もちろん引数で渡した配列の順番も保持してくれます)。

# 2 CPUs -> work in 2 processes (a,b + c)
results = Parallel.map(['a','b','c']) do |one_letter|
  expensive_calculation(one_letter)
end

README.md#usage

こういうとき使ってる

  • サーバー1台で実行する程度のスクリプト(集計とか)の処理を早くしたい時
    • 仕様クラスみたいなものを作ってテーブル全件調べて対象データのみ抽出とか
    • 1台で実行すれば処理のアウトプット先を1箇所にまとめるのが簡単
  • 並列に画像などのデータをダウンロード・アップロードする
class FooTargetUserSpecification
  def satisfied_by?(user)
    # ...
    # 重めの判定ロジック
  end

  def satisfied_users(&block)
    User.includes(:some_associations).find_in_batches do |gruop|
      Parallel.each(group, in_processes: 4) do |user|
        @reconnected ||= User.connection.reconnect! || true
        block(user) if satisfied_by?(user)
      end
      User.connection.reconnect!
    end
  end
end

file = File.open('target_users.csv', 'rw+')
spec = FooTargetUserSpecification.new
spec.satisfied_users do |user|
  file.puts(user.id)
end
file.close

知見

  • マルチプロセスで実行するとメモリ食うのでこういうバッチスクリプトをCronで回す場合はちゃんと安定して実行できるかどうか本番データと同じ規模で検証必要
  • 実行内容によってスレッドベースでやると良いのかプロセスベースでやると良いのかは考える(メモリ効率・スレッドセーフ性など)
  • ActiveRecordと組み合わせて使う場合には、connection_pool周りで問題が起きるのでgemのREADME.mdに書いてある再接続処理使うと良い

Sidekiq

いわゆる非同期Jobキューの実装として有名。Sidekiqのプロセスを起動しておいて、RedisにJobを積むと積んだそばから Sidekiqのプロセスが随時ワーカーを起動してJobを消化していってくれます。

スレッドモデルで並列化をしているので、IO処理などでブロッキングされる場合に効果が出るので、 アプリのPush通知を送ったり、メールを送ったり、DBのレコードを更新したりに向いてます。 同様のgemにResqueというものがあるが、あちらはプロセスモデルで実現しているのでメモリを余分に食ったりする。 Sidekiqでもプロセスモデルで実行した方が良い重い計算処理を行いたい場合は並列数1で複数のプロセスを立てれば良いです。

作者の努力によってバージョンが上がるごとにスループットがめちゃ上がったり、gem自身の依存関係が減ったり、エンタープライズ向けの機能も用意されていて徳が高いです。

こういうとき使ってる

  • HTTPリクエスト内では処理しりきれない遅い処理を非同期に実行するJobキュー
    • Push/メール通知
    • 遅延させてUpdateクエリを発行させたい時
    • 動画のエンコード処理
  • 細かい大量の処理を一気にスケールアウトさせて処理したい時
    • 新たなサムネイルを事前作成し、キャッシュを温めたい時
  • S3からDBに登録された画像をダウンロード -> 解析処理 -> DBに永続化
class UserFooWorker
  include Sidekiq::Worker

  sidekiq_options(queue: :default, retry: 3)

  def perform(user_id)
    user = User.find_by(id: user_id)
    return unless user # userが取得できなかったら終了しとく

    user.do_something
  end
end

# 呼び出し側
# perform_asyncを呼ぶとRedisにJobが積まれる
UserFooWorker.perform_async(user.id)

知見

  • Workerのコードを書くときはとにかく冪等制!冪等制!冪等制!と3回ぐらい唱えてからコードを書いてそして読み直す
    • 2回以上同じ処理を実行しても良い処理を書く
    • 1度実行した処理かどうかをチェックできるようにする
    • 仕様として2回実行されてしまうのを一部許容する
  • Workerの要件によって適切にretry回数・条件・間隔を設定する
    • 1度失敗して再度実行したら成功するのかどうか?
      • 通信系はエラーになりやすいので何回かリトライさせる
      • 存在しないデータへの処理は2度と成功しない場合が多い
    • exponential back-offによって1週間後とかに再実行されて嬉しいの???
    • Worker内のコードはできるだけシンプルにしてどこでエラーが発生していつリトライされるのかが分かりやすくなるよう心がける
  • リリースが失敗した場合のリカバリー手段を考えておく
    • 作りが甘くてぬるぽバグとかで大量にリトライJobを出してしまってretryもすぐ消化してしまったとき
      • バグを修正したのちに影響範囲分を再度積み直すとか
  • 1 Workerの粒度を小さくする
  • Queueを意識する
    • リクエスト時に非同期で行うJobを積むQueueと、バッチ処理的に一気にJobを積む際のQueueは分ける(非同期側が詰まる)
    • CloudWatchなどにQueueのサイズをメトリクスとしてPutしておく(AutoScalingに利用できる)
  • Redisを意識する
    • backtraceオプションは便利だが、有効にしたJobを大量に積んで全部こけるとほとんど同様のbacktraceがストレージに書き込まれてものすごい容量を食うので注意
    • XXX.perform_asyncを1万回ループするより、Sidekiq::Client.bulk_pushのAPIを利用して一括で詰むことで負荷もかけずに速く詰める↓のように
# Jobを積む時はRedisに優しくするためにbluk_push使うと、一気に大量のJobを積めて良い
User.select(:id).find_in_batches do |group|
  args = group.map {|user| [user.id] }
  Sidekiq::Client.bluk_push('class' => UserFooWorker, 'args' => args, 'backtrace' => false, 'queue' => 'user_foo_worker_queue')
end

まとめ

Rubyでも簡単に取り入れられる並列処理の書き方について紹介しました。 既存処理を並列化して高速化出来ると気持ち良いので、ぜひ試して見てもらいたいです。 来年はconcurrent-rubyやRxRubyを試してみたい。

iPhone7 Plusを1ヶ月半ぐらい使ってみた感想

Apple StoreのオンラインでiPhone 7 plusのシルバー 256GBを購入して9月25日に届いて、1ヶ月半ぐらい使ってみての感想。 今後海外に行くことも考えてSIMフリー版を購入した。ちなみに以前はiPhone6のゴールド 64GBを利用していた。

良いところ

iOS10の良いところも混じっている

  • TouchIDの指紋認証が素早い&iOS 10のスマホを持ち上げたら画面が点灯するのが便利でロック解除のストレスがほぼフリー
  • ポートレート撮影が面白くて意外とおしゃれな写真になるので撮影が楽しい
  • 6と比べて画面サイズデカくてステレオ音声なので映像見るのが楽しいAbemaTVとかNetflix便利
  • CPUの性能が格段に上がったのを感じる。具体的にこのアプリ使ってる時、というよりはすべてのアプリの初期化処理が早く感じる。
  • メモリ1GB -> 3GBに増えてゲームアプリ -> Safariでちょっと検索やらLINEで返信など -> ゲームアプリに戻るという動作をする際にゲームアプリのメモリが解放されづらくなっててとても便利
  • ストレージ256GBはすごい(全然減らないのでいくらでも写真・動画の撮影できそう)ストレージの余裕は心の余裕
  • Apple Payは普通に便利

悪いところ

  • 値段が高い
  • 重い(慣れた)
  • 片手で持つのに不安(↓こういうの使っていてまずまず使い心地は良い)

Apple Payについて

iPhone 7系の一押し機能はやっぱりApple Payだと思う。 Visa系のカードとViewカードを持っていてQUICPaySuicaオートチャージが出来ている。 コンビニでの支払いは基本iPhone経由で行うようになったし、交通機関もすべてiPhone。 昼飯時にはチェーン店系ならQUICPayでいける(ココイチとか)けど、そんなに利用頻度は高くない。 たまにSuica支払いのできる店なら利用する程度。

近所のスーパーのマルマンストアとかマルエツプチでは電子マネー支払いができなくて現金だよりだけど、 系列によってはスーパーでも普通に使えそうなので次、国内で引っ越しを検討するときはその辺も考慮に入れたい。

Apple Pay経由でクレカを利用することでMoneyTreeにどんどん利用履歴が残るようになったけど、 何を購入したかまでは残らないのでちょっと不満がある。こういうログがPOSレジ側じゃなくて 消費者側にもセキュアに残せるようになったらとても便利だと思う。

というわけで生活の半分ぐらいは現金からiPhoneでの決済になった。 コンビニがすすめてくる既成の各種電子マネーは使える店舗が限られてしまうしポイント溜まるみたいシステム鬱陶しくて全く要らなかったけど、 交通系の電子マネーも含めて電子マネー系がApple Payに集約できるのはとても魅力的。メタ的な存在でかっこいい。

ApplePayのおかげでQUICPayという単語を知ったけど、レジにFelicaリーダーが置いてあるところは大体使えるイメージはある。 iDでは支払ったことないけど使えるかどうかイマイチわからん。

QUICPayの使えるお店|QUICPay 「iD」が使える主なお店|ドコモのiD

総合するとiPhone7 plusはとても満足度高い。

自宅でTOEICの問題を解いてみた

先日海外に行くなどという記事を書きつつも、具体的にどう英語が出来るようになっていくと良いのかというイメージというかロードマップがなくて*1、 英語の勉強を先延ばしにしていた感があるけど、せっかく海外に行くまでにまだ時間があるので少しずつ勉強してみることにした。

推測するな計測せよという言葉がある通り、少なくとも今の自分の力量が分からないことには、勉強しようがないなと思ってTOEIC受けようと思った。 しかし、今から申し込んでも次に受けられるのが来年1月で、よくよく考えると別にTOEICの結果をどっかに提出しなきゃいけないという事情も無いので、 手っ取り早く本屋に行ってTOEICの公式問題集を買ってきて、帰宅後に解いてみた。

CDプレイヤーはないのでMacbook ProにBDドライブ繋いでCDをiTunesで取り込みヘッドホン繋いで聞けるようにし、 iPhoneのタイマーで時間を正確に図りつつズルとか無しで本番さながらの感じで解いた。 リスニング45分、リーディング75分。 問題形式とか時間配分とかそういうTOEIC向けの対策は全く覚えてなくて、 途中ゆっくり読んでしまったりしたけど残り4問残しぐらいで時間を終えた。

TOEICの最終的な点数は他の参加者とかの統計的な情報で決まるらしいので、 問題集だけでは正確な点数はわからないけど、参考スコアで495〜665点のレンジで中間が580点。 TOEICを最後に受験したのは学生の頃就活前の5、6年前ぐらいで、当時は500点満たなかった程度だったと思う。 今回は自分で思ってたよりは出来ていた気がするけど、海外で仕事で使うには全然足りない。最低限の生活するのは何とかなりそうかも。

400点~495点(英語で書かれた看板を見て理解できる) 500点~595点(英語で簡単な質問が理解できる) 600点~695点(英語のメモが理解できる、ゆっくりとしたスピードで話された場合の道順の説明が理解できる) 700点~795点(英語で書かれた社内文書や仕事の進め方について理解できる) 800~895点(Webから英語で書かれた情報の収集、同僚との議論を理解できる) 900点〜990点(英語で書かれた高度な専門書を理解できる、ネイティブの議論を理解できる) 出典 TOEIC受験前に知っておきたい!スコアと難易度の目安|大学生の困った解決マガジン

matome.naver.jp

結果を振り返ると、

リスニング

  • 単語は前より聞き取れることが多かったような気がするので1人が喋っているときは結構聞き取れた
  • 2人で会話するセクションではコンテキストが理解できず何の話をしているのかあまり想像つかなかった

リーディング

  • 時間配分気にしすぎた上に瞬時に回答を判断できなかったせいで前半の穴埋めがかなり適当に答えてしまった
  • 後半の長文問題(?)は時間かけて解いた分結構正解できてたけど時間かけすぎて全問解けてなかった

という感じだったので、これからしばらく単語・熟語やりつつ、 実際の英語での会話のやり取りを動画とか音声で学習してみようかと思った。

とりあえず今月は移動中とか昼休みに単語・熟語学ぶのを普通にやって、 家帰ったら毎日1時間ぐらいNetflixで英語字幕つけたり字幕なしで 繰り返し海外ドラマ(ゲットダウンが気になる)見続けて、日常会話を聞き取る練習してみたりする。

問題集はまだ一回分の試験が残っているので、1ヶ月後にもう一回解いてみて650〜700点ぐらいを目指したい。 仕事で使うには800点以上ないと厳しいとのことなので、少なくとも来年実際にTOEICを何回か受ける際には800点ぐらい取れていると嬉しい。 最終的にはどっかで会話も練習しないといけないけど、DMM英会話など利用する前に英語話せる妻との会話で練習するという手はありそう。 結婚は便利。

*1:日本企業で完全フルリモートするような状況になったとしたら仕事では家にこもって、英語は少しづつ現地で学べばいいんじゃないかとか、会社辞めたら何ちゃら島で1ヶ月ぐらい語学留学したら何とかなるんじゃないかという甘えた案もあった

Swiftでlibwebpラッパーを作っている

Swiftでlibwebpラッパーを作っている。

最初のモチベーションとして、社内のデザイナーがWebP*1への変換ってどうするの?って言い始めた時に、 ちょっとググってみたところ、そんなに便利なMacGUIで変換できるアプリがないなーという話があって、 試しにMacアプリの練習がてら作ってみるかーというところだった。

何も考えずにlibwebpをSwiftのModuleとしてimportして実際に使うアプリを作ってみようとしてみたところ、 CのライブラリをSwiftで扱うのが意外と大変なので、一旦ライブラリとして外だししてみようと思い始めた。

ただ、自分で作るのだるいので誰かいいもの作ってないかと検索してみるとiOS/macOS/Swift周りのWebP界隈は現在こんな状況だった。

というわけで、以下のような要件でライブラリを作ってみることに。

  1. libwebpをラップしてエンコード・デコード出来る
  2. macOS/iOS/Linuxで使える
  3. Swift3ベースでC触ってる感がなくて済む

ということで色々試行錯誤してv0.0.1のタグを打って、もともと最初に作っていたMacアプリ側でcarthage経由でインストールできた。 まだエンコードしか詳細なAPIに対応してなくてデコードの方は適当。詳細なAPIに対応すると、デコード時にcrop/resizeが出来るらしい。 あと、macOS/iOS両方でビルド出来るようにしてあって多分Linuxでもビルドできる状態にも出来そうだけどまだ未対応。

github.com

macOSだけでよければbrewでインストールしたlibwebpにリンクさえしとけばとりあえず動くけど、 iOSでも動かそうとするとlibwebpをiOS端末のCPU向けにビルドしないといけなかったりして、 プロジェクト構成作る時点からすでに結構めんどかったりする。

ということで、その辺の詳細な知見をAdvent Calendarの時に書くのでよろしくお願いします。

qiita.com

イギリス行きの往復航空券をたった5万円でゲットする方法

結婚した&妻を追って海外に行く - ainameの日記

この記事を書いたら意外とイギリス行きの5万円程度の格安チケットの話に食いついている人がいたのでまとめておく。

そもそも俺自身チケットを買っただけで、普段全然旅行しないしもちろんイギリスにも行ったことない。 妻が学生の頃にバックパッカーをやってたりで格安にチケット買う手段に精通していたそうで教えてもらった。

といっても特殊なことは全くなくて、単に格安航空券を取り扱うサイトで検索してチケットを買う。たったそれだけのことだった。

他にも似たようなサイトを教えてもらったけど自分はskyticket(スマホ向けのUIはよくなかった)が一番使いやすかった。 時期によったり出発日によっては値段は上下するけど、適当に検索しても5万付近でチケットが取れる。

f:id:ainame:20161021004549p:plain:w600

ただし、注意したいのはこのような格安チケットは途中、どこかの国で乗り継ぎが必須だということだ。

行程1のように乗り継ぎ時間が45分しかないようなパターンだと、出発の便の離陸が少し遅れたり手続きに戸惑ったりすると乗り継ぎがミスる可能性がある(らしい)。 乗り継ぎ間隔は2時間ぐらいあればそこそこ安心できるとのこと。

f:id:ainame:20161021004730p:plain:w600

また、イギリスに行く際にロンドンに近いヒースロー空港に降り立つ場合に注意が必要らしい。

ヒースロー空港はとにかく入国審査が厳しく、普通に観光する場合にはまだ良いが妻のように、 インターンしに行く(=帰国用のチケットを買ってないのでいつ帰国するかわからない)みたいなパターンでは、 厳しい状況になる模様。どうすれば審査通るのかは各自ググってくれ。(俺は知らない。)

4travel.jp

せっかくヨーロッパに行くならイギリスだけで旅行を終える必要もないと思うので、 行きはヒースロー空港着でも帰りは別の国から出発しても良さそう。

その場合は「往復」ではなく「周遊」を選択して検索する。

f:id:ainame:20161021005825p:plain:w600

帰りはフランス・パリから帰国しても5万台。

f:id:ainame:20161021005938p:plain:w600

各航空会社の旅客機の乗り心地とかは知らんので検索したら良いと思う。

タダより高いものはない。

f:id:ainame:20161021011353p:plain:w200