Tokyo Server Side Swift meetup #8で発表してきた #tsssmeetup

tokyo-ss-swift.connpass.com

SwiftでSidekiqクローンを開発してるよ、という話をした。

speakerdeck.com

基本、SidekiqのJSONフォーマットに乗っ取って実装しているのでRuby版のクライアントとか管理画面が使えるようになっている。 しかしSwift4ベースで開発をしているのと、現状のレイテンシーに不満があるので開発中という感じ。オリジナルの作者自身が書いているSidekiqのCrystal版とベンチマーク結果を比較すると圧倒的に遅く、特にボトルネックとなっているのがRedisクライアント自身となっていて、現在Swiftで高速・使いやすいRedisクライアントを書くための勉強をしている。(crystal-redisは異常に早く、hiredis以上

github.com

Server Side Swiftに対する自分自身の意見を表明しておくと、現時点でも今後1、2年後でも実用的な状態になるのは無理だと思っている。 SwiftはOSS化されてまだ浅いしFoundationも含めて標準ライブラリが現状ではとても貧弱で(Foundationのバグ・未実装の問題もあるし、サーバーを書くためのIO周りが足りないというのもある)、 何かしらサーバーを書くためには自前でまともなTCPクライアント・サーバーを実装しなければならないような状況にある。

もちろん、Vapor・Kitura・Perfectなど既成のWebフレームワークなどは存在はするけど標準ライブラリだけで出来ること少ないせいで、 それぞれ自前の実装がオレオレ実装が横行していて整っていない感じがする。そういう意味で昨日の発表にもあったswift-server/httpには期待していて、 HTTPサーバーを書くための基礎的な部分をみんなで作ってくぞというプロジェクトなので、標準ライブラリにはないけどサーバー書くのに必要な処理を補完してくれると嬉しい。

GitHub - swift-server/http: Repository for the development of cross-platform HTTP APIs

ということでスライドにも書いたけど、基本的にServer Side Swiftは真面目に取り組むと苦労しかないので、 システムプログラミングとかミドルウェアの開発の勉強とかそういうモチベーションで自分はやっていってます。

転職留学夫婦

f:id:ainame:20170422165248j:plain

最終出社日翌日に同僚と行ったThe Stone Rosesのライブ会場の武道館です。

転職

以前ブログに書いた通り、海外で暮らす・働くための転職をします。 以下のような記事を書いて辞める宣言をしから予定通り半年と1ヶ月ほど働き、株式会社ミクシィを退職しました。 4/21が最終出社日で6/15に退職し、次の職場は翌日の6/16から働きます。

ainame.hateblo.jp

余談ですが最終出社日前日に自分が直近半年ぐらいでやったことなどを1人で話し続けるという勉強会を社内向けに開いてみ他のですが、評判良かったので良かったです。 他の人もやってみてほしい。もともと時間考えずにスライドを作り続けてて、当日はソースコード見せながら解説したり質問しあったりして結果的に3時間ぶっ続けで話していました。

留学

なんとか職が見つかったものの圧倒的に英語力は足りないと自覚しているので、 有休消化期間中にフィリピンの語学学校に留学してくる予定で4/27には旅立って6/2に帰る予定です。

夫婦

フィリピンの学校が終わる5/27からフィリピンで妻と合流して数日間遊んでから一緒に帰国します。 帰国した後は国内でついに同居する予定です。

今後

海外で働く前提での転職をするが、すぐさま海外で働くわけではなく、 普通に試用期間を経てビザ申請のような形となるため、しばらく日本で暮らしながら働きます。 全てことがうまく進めば実際には年末年始ぐらいには移住するかもです。

また前職で幅広く開発していたせいで、転職時の職種をどうして行くか迷っていたのですが、 しばらく何か一つに集中していきたいなと思っていてiOSエンジニアになることとしました(Swiftが毎日書けるのは良い)。 (海外のスマホシェア的にはAndroidも書いていかなければいけないと思うがAndroid力はまだ低い・・・。) 必要あらばサーバーサイドのコードも書いて行く所存です。

各位、お世話になりました。 今後ともよろしくお願いします。

飲みに行くぞ。

ビルド職人になるために覚えたコマンドメモ

ここ2年ぐらいffmpegとかopencvとかRuby + CUDAみたいなやつとか たまーにビルド職人になることがあって上手くコンパイルするために各種コマンドを使うことがあるのだけど、 使い方はおろか、普段あんまり使わないのでコマンド名すら忘れることが多々あるためコマンド名とか使い時を覚えている限りざっくりメモしとく。 あとはmanを読めば良い。

pkg-config

インストール済みのライブラリをコンパイルに利用するときに必要なコンパイルオプションを返してくれるやつ .pcファイルを元に返してくれる。 PKG_CONFIG_PATHを指定して利用したりする。

ldd

.so(Shared Object)ファイルが動的リンクで依存しているライブラリへの依存関係を表示してくれる。 何かをコンパイルした結果、共有ライブラリがリンクできているかを調べるのに使える。

$ ldd `which ffmpeg`
        linux-vdso.so.1 =>  (0x00007ffeaa3c0000)
        libavdevice.so.56 => /usr/local/lib/libavdevice.so.56 (0x00007fda9b4f4000)
        libavfilter.so.5 => /usr/local/lib/libavfilter.so.5 (0x00007fda9b1c5000)
        libavformat.so.56 => /usr/local/lib/libavformat.so.56 (0x00007fda9ae0a000)
        libavcodec.so.56 => /usr/local/lib/libavcodec.so.56 (0x00007fda996e2000)
        libpostproc.so.53 => /usr/local/lib/libpostproc.so.53 (0x00007fda994c4000)
        libswresample.so.1 => /usr/local/lib/libswresample.so.1 (0x00007fda992ac000)
        libswscale.so.3 => /usr/local/lib/libswscale.so.3 (0x00007fda99037000)
        libavutil.so.54 => /usr/local/lib/libavutil.so.54 (0x00007fda98ddc000)
        libpthread.so.0 => /lib64/libpthread.so.0 (0x00007fda98bbf000)
        libm.so.6 => /lib64/libm.so.6 (0x00007fda988bd000)
        libc.so.6 => /lib64/libc.so.6 (0x00007fda984fb000)
        libz.so.1 => /lib64/libz.so.1 (0x00007fda982e4000)
        libdl.so.2 => /lib64/libdl.so.2 (0x00007fda980e0000)
        liblzma.so.5 => /usr/lib64/liblzma.so.5 (0x00007fda97ebc000)
        /lib64/ld-linux-x86-64.so.2 (0x0000557f457af000)

ldconfig

ldがリンクするときに共有ライブラリ探すための情報をキャッシュするために使用する。 共有ライブラリをインストールした直後に期待通りに動かなかったら叩いてみたりする。 -pでキャッシュ済みのライブラリのリストを見られるのでリンク失敗するときにみる。

nm

.soとか.oとか実行バイナリとかのシンボル一覧を見るのに使う。 シンボル一覧見つつリンクがうまくいっているかどうか見る。 リンクが失敗してundefined symbolみたいなエラー出たときに何が足りないのか見極めるときに使う。

適当な間違ったコードを書いたときにprtfなんていう存在しない関数でオブジェクトファイルを作ったときに、 nmコマンドの表示結果がprtfに対してUとなっているときはこの時点ではprtfが実際には未定義状態であることを表す。 こういう情報を元にソースコード上にタイポがあってビルドうまくいかないとかそういうのを探した記憶がある。

$ cat a.c
int main () {
  prtf("aaaaaaaaa");
}
$ gcc -c a.c
$ nm a.o
0000000000000000 T main
                 U prtf

nm自体は多分もっといろいろ使えると思う。
nmコマンドでC/C++のシンボルテーブルを見る、C++の名前マングリング、"C"リンケージ、あるいはリンカに関するメモ - 百日半狂乱

objdump

Macでlddしたいときに最初から入ってた気がしたので代わりに利用できる。 実際にはotoolが良いらしい。

rpmbuild

名前の通り、RPMパッケージを作るときに使う。 オプションがややこしいので毎回ググりながら使っていたがモダンな日本語情報を見つけるのが難しいので、 自分が参考にしたページリストを貼る。

cmake

cmakeはmakeで使うMakefileの生成するためのメタなやつで、CMakeLists.txtに生成ロジックが書いてある。 make単体で使うときは再ビルドするときにmake cleanしてたけど、cmake使うときはビルド中のファイルが吐き出されるのは 自分が指定したディレクトリになるのでビルド結果を入れるディレクトリに移動してからコマンドを叩いてディレクトリごと消せば良い。

$ cd /path/to/src_root
$ mkdir build
$ cd build
$ cmake ..
$ make

tar

xxx.tar.gzじゃなくて xxx.tar.bz2 を解凍するときは tar -xvfz みたいな4つ入りオプションじゃなくて、 tar -xf のみで良いとのこと。-zgzip解凍用のオプションとのこと。よく忘れて困るのでメモ。

CUDA, cuDNN, nvcc

NVidiaGPUを使うためのSDK。そこそこ特殊環境なので油断するとハマる。 CUDAを使うときは、とにかく公式の手順通りにインストールすると入る。 各OSごとにインストール用のパッケージが用意されている。(ディープラーニング用のcuDNNは、NVidiaのサイトにアカウント登録しないとダウンロードできないので注意。) docs.nvidia.com

/usr/local/cuda-8.0/lib64以下に共有ライブラリが配置されたりして標準のパスだけではビルド通らないので注意。 -I-Lでパスを追加してコンパイルする。.cuファイルをビルドするときはg++とかの代わりにnvccを利用する。

mkmf

library mkmf (Ruby 2.4.0)

コマンドではないけどRubyのC拡張作る時に使う標準ライブラリ。 いわゆるextconf.rb。ext/以下にdependファイルをおくとextconf.rbで生成するMakefileの末尾にそのdependファイルが追記されるのでそこを駆使すると便利。 ruby ext/hoge/extconf.rbで実際にMakefileを生成してみてデバッグもできる。

xcode-select install

Macでなんかコンパイルするときに#include <..>の探索パスの一覧を出すコマンドが clang -x c -v -E /dev/nullで、これを見たときに /usr/local/include がない場合はヘッダーが見つからないエラーに陥る。 xcode-select installすると以下の様に正しく設定される。

$ clang -x c -v -E /dev/null
Apple LLVM version 8.0.0 (clang-800.0.42.1)
Target: x86_64-apple-darwin16.4.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin
 "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang" -cc1 -triple x86_64-apple-macosx10.12.0 -Wdeprecated-objc-isa-usage -Werror=deprecated-objc-isa-usage -E -disable-free -disable-llvm-verifier -discard-value-names -main-file-name null -mrelocation-model pic -pic-level 2 -mthread-model posix -mdisable-fp-elim -masm-verbose -munwind-tables -target-cpu penryn -target-linker-version 274.2 -v -dwarf-column-info -debugger-tuning=lldb -resource-dir /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/../lib/clang/8.0.0 -fdebug-compilation-dir /Users/namai/src/github.com/ruby-dlib/ruby-dlib -ferror-limit 19 -fmessage-length 185 -stack-protector 1 -fblocks -fobjc-runtime=macosx-10.12.0 -fencode-extended-block-signature -fmax-type-align=16 -fdiagnostics-show-option -fcolor-diagnostics -o - -x c /dev/null
clang -cc1 version 8.0.0 (clang-800.0.42.1) default target x86_64-apple-darwin16.4.0
#include "..." search starts here:
#include <...> search starts here:
 /usr/local/include
 /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/../lib/clang/8.0.0/include
 /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include
 /usr/include
 /System/Library/Frameworks (framework directory)
 /Library/Frameworks (framework directory)
End of search list.
# 1 "/dev/null"
# 1 "<built-in>" 1
# 1 "<built-in>" 3
# 330 "<built-in>" 3
# 1 "<command line>" 1
# 1 "<built-in>" 2
# 1 "/dev/null" 2

他に便利なのあれば教えてください。

新婚旅行をしてきた、現地集合・現地解散で。

2016年12月20日〜2017年1月5日まで、17日間の新婚旅行をしてきました。 以前記事に書いた通り(結婚した&妻を追って海外に行く - ainameの日記)、 現在海外に滞在している妻に会うために年末年始の休み+会社の福利厚生の結婚休暇(5日)+有給を活用してイギリスまで単身で乗り込んだ。

f:id:ainame:20170123000849j:plain:w400

一応去年結婚して以来、初めての長期旅行なので新婚旅行という事になるが、それにしても現地集合・現地解散で新婚旅行する人が世の中に一体どれだけいるのだろうか??

せっかくヨーロッパまで行くのだから単にイギリスだけを観光するんじゃなくて、 ヨーロッパの各都市をぶらぶら巡ってヨーロッパ生活の雰囲気を感じてこようという気持ちも持って臨んだ。 実際に行った都市は上のマップ通りで陸路・空路それぞれ活用して6カ国巡った。 3日1回ぐらいは移動していて移動日が入る感じになる。

今回初めてヨーロッパまで行った時の感想とちょっとしたtipsをまとめたい。

ヒースロー空港は怖い

新婚旅行の集合場所はイギリスのヒースロー空港だった。まず入国審査をしなければいけない。 ググったら分かるけど、ヒースロー空港は異常に入国審査が厳しい空港らしい。

f:id:ainame:20170124030504p:plain:w380

実際に強制送還までされた人がいるとか・・・。そんな感じで事前に散々脅されたので、 適当に「サイトシーイング」とか答えるだけでは全然ダメかと思ったかなりビビっていた。

今回の場合、既婚者で指輪を手につけているのに一人で観光しようとしていて怪しいし、 ロンドンでの宿泊先は妻の友人の友人宅(住所知らない)でランディングカードに滞在先の住所が書けなくて、 質問されたらなんて答えたらいいか、英語で正しく答えられるのか・・・??

あわわわわわってなって事前に心配していたとはいえなんも準備せずに空港まで着いてしまって、 いざ入国審査ってなると強制送還という最悪のパターンが頭によぎって(3ヶ月ぶりに妻に会いに来たのに会えずに強制送還ってどんな罰ゲームなの!???うわー)めちゃくちゃ緊張してたし、 いきなり審査の列に並んだらいざという時に話す内容思い浮かばないと思って、 空港のWiFi使って小一時間まだ見ぬ妻とLINEして相談したりして喋る内容決まったぞ!!!ってなって覚悟を決めて列に並んだ。

んで実際、緊張しすぎてもはやなんて聞かれたかあんまり分かんなかったけど、とりあえず自分の第一声は「サイトシーイング」って言った気がする。 あーもうこれ以上質問しないでくれー頼むーうわーなんて思っていたら、なんとそのまま職員のおっちゃんはにこやかに微笑んで軽く通してくれて、 さっきまでの俺の心配を返せ!!って思ったけど、そのあと妻と再会できたときはただ単に久しぶりにあった以上の感動があった気がするよ。

そんな感じで無事合流できたのでした。

ヨーロッパでは英語が通じる

当たり前かもしれないけど、ヨーロッパの人々はかなり英語喋れる人が多いので英語が話せれば困ることはなさそうだった。 とはいえ、全員が喋れるわけではなくフランスとかイタリアではお店の店員さんとかで英語が苦手って感じの人がたまに居て、 返答に困って英語喋れる上司を呼びつけて対応してもらうなんてのもあり、日本人が英語喋れなくてあたふたしているのと似ていてなんだか親近感が湧いた。

今回の旅では、英語がペラペラ喋れる妻が一緒にいるものの厳しい教育方針によって、 飲食店や店での買い物をする際には全て自分が無理やり頑張って話して事を進めて行った。 とはいえ、事前準備をして居たわけではないので必要なフレーズは妻から随時聞き出して学んでいった。

お会計の時に「Can I get the bill?」ってひたすら言ってた。これによって「Can I 〜〜?」的なフレーズの使い方がすごいしっくりくるようになった。 同様に「Do you have〜〜?」的なフレーズもそこそこ使った。「Do you have free WiFi?」とか「Do you have any recommendation?」とか。 そんな感じで本当に初歩的な英会話を体験する良い機会になった。(WWDCでサンフランシスコ行ったときはもっと適当な英語でごまかしていた)

観光しているのは日本人だけでは無い

これも当たり前だけど、観光しているのは自分たちだけじゃなく他のアジア人だってそうだし他のヨーロッパの人が自分の国以外に観光していることも普通にある。 街中歩いていると、アジア人以外は現地の人と観光客の違いはパッと分からなかったけど(ロンドンはとにかく人が多かったので比率で言えば観光客の割合少なかった多分)、 アムステルダム、パリ、ミラノあたりは、あからさまに観光客が結構居たりした。(カメラ持ってるとかね。)

面白かったエピソードが一つあって、ベルギーからフランスへ移動する時に高速バスに乗ったのだけど、 途中休憩のためにバスが停車する際に車内アナウンスがあったのだけどフランス語でしかアナウンスされなくて、 何時まで外ぶらつけるのか全然分からなくて困って、隣の席の何人か分からないけどカップルに英語で聞いて見たら、 「俺たちもフランス語はわからな¯\(ツ)/¯」みたいな感じでさらに困る。他にもフランス語話せない人は結構居て困って居たけど、 そんな感じでワタワタして居たら、¯\(ツ)/¯のカップルの後ろの人たちがフランス語分かるらしくて、英語で時間教えてくれて事なきを得た。

敵の敵はまた味方というか、旅行中現地の言葉が分からないのは日本人だけじゃなくてヨーロッパの人も結構わかってないんだなーという学びがあった。 旅行者同士、お互いの言葉わからないこともあるけど、困ったら困ったもの同士助け合おうみたいな気持ちが生まれた。

イギリスの飯は不味く無いしフランスの飯は不味かった

これは、イギリスに住むかもって周りに言った結果、散々イギリスの飯はまずいまずいって脅されたので、 必要以上にイギリスのご飯にビビっていたのだが、結果的には店を選べば美味いもの食えるし問題ないって分かった。 特にDishoomというインド料理屋は事前に調べていて、実際に行って見たらとても美味くてビックリした。 今まで食べたインドカレーの中で一番美味いし店内がめちゃくちゃオシャレだった。

逆にフランスで歩き疲れてお腹すいてどこでも良いから早くご飯食べようってなって入った適当なカフェのボロネーゼは、 給食のソフト麺のようなグダグダに茹でられたスパゲティでソースも水っけが多くて味薄いし酷かった。 ググってみるとフランス人はみんなグダグダになるまで茹でているみたいなので、スパゲティは2度と注文したくないと思った・・・。 (フランスで食べたものだと、ここの店のラビオリはとても美味しかった。)

という具合で、国ごとに個性はあるけど、店や食べるべき料理の選び方を間違わなければよっぽど不味くて辛い思いをすることはないと思った。 しかし外食は全般的に高いので、住み始めたら気軽には出来なさそうだなーと思った。(どの国もレストランで普通に二人で飲み食いすると6、7千円ぐらい)

ところで、チップに関してはヨーロッパはアメリカほど強気に要求される所はほとんどなく、 会計の時にサービス料数%が既に組み込まれている所があったり、クレカリーダーにクレカを差した時に額を入力させられる時があったら、 適当に料金の10%程度入れておけばやり取りは済んだので、心配はあまりない。

博物館・美術館すごい

ヨーロッパの国の文化・歴史はとてもすごい。今回、妻の趣味もあって美術館とか博物館などを結構多めに周ってとても文化的な観光をした。 グリニッジ天文台は結構変わり種的な感じだけど、エンジニアとしては普段は手を焼くことの多い時間の取り扱いにも、 そこに至るまでに色々みんな苦労して、正確な時間が測れて使えてみんなと共有できるようになったんだなと分かり面白かった。

  • グリニッジ天文台(正確な時間の測り方が安全な航海を実現させて植民地拡大を助けたという話が面白かった)
  • 大英博物館(無料・巨大・展示品もレア度高い・何度も通いたい)
  • アムステルダム国立美術館レンブラントって名前聞いたことあるな、ぐらいの気持ちで見た。夜警かっこいい。)
  • ゴッホ美術館(ゴッホも最初からあの画風を勝ち取ったわけじゃなくていろんな作品からインスパイアされていってあの作品が生まれたという)
  • アムステルダムセックスミュージアム(名前の通りの施設だけど、19世紀のゲイの写真みたいな時代背景を考えると写真が残っていること自体すごいのまであったとか)
  • 最後の晩餐が観れる教会(異常に厳密に壁画が守られていて同時に25人しか壁画のスペースに滞在できない)
  • ダビンチ科学技術博物館(ダビンチの天才さが感じられたついでに、他の展示も結構面白かった。)

イタリアのアサシンクリード感が異常

アサシンクリードを遊んだことがある人は、一度は感じたことがあるはずの「あ、この壁登れそう」感。 アサシンクリード2遊んだ経験があるのでイタリアではフィレンツェとか行ったらもっと楽しかったかもしれないけど、 今回はミラノだったのでゲーム中に登場した建物がなかったけど、ミラノのスフォルツェスコ城はアサクリの中でよく現れる、 衛兵が見回っている城って感じの建築様式?設計?ですごい感動した。

ちょうど実写版アサシンクリードが、ヨーロッパでは先に公開されていていろんな箇所でポスターを見かけた。 特にイタリアではポスターの扱いもかなりデカかったよ。さすがイタリア。

アプリのオフライン機能が便利

今回、旅行のためにWiFiを事前準備したり、現地でSIMを購入せずに旅行した。 これは準備している時に妻がそんなの無くても(駅とか空港とかカフェでWiFi使えるから)大丈夫大丈夫って主張してきたからで、 しばらくどうしようか悩んで海外旅行によく行く人に聞いたりしたけど、結局郷に入っては郷に従への気持ちで妻のやり方についていくことにした。

インターネットがないと呼吸できない人種の人間としては最初はすごい不安だったけど、 WiFiルーターなりSIMカード持ってたとしても遅くて品質の悪い3G回線だったら逆にイライラするだけだなと思って、 割り切って考えたら、持ってないのは不便かもってのは置いといてまぁ良いかって気持ちになった。

着いてから最初は妻が空港とか宿に着くたびに次行く箇所の移動方法を調べてスクリーショット撮ってメモして移動するっていうのを、 やっていてなんとかなっていた。本当に困ったら現地の人に聞いて乗り換え方法聞けば確かになんとかなる。 途中から知ったのがGoogleマップやTripAdvisorのオフライン機能でこれはとても便利で、より快適に移動できるようになった。

これは事前にWiFi環境でダウンロードして準備しておくと良い。こんな感じ。

TripAdvisorで観光スポットを検索 -> Google MapでTripAdvisorからコピった店名・住所で検索 -> 地図通り移動する

ということが全部オフラインで簡単に出来るようになる。とても便利なのでみんな使えばいいと思う。

中国国際航空(AirChina)

今回、中国国際航空の乗り継ぎを駆使して5万でイギリスまで行った。

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

中国国際航空は不満点は2点あった。それ以外はそんなに悪くない。 せっかくiPhone 7plus買ったので機内でスマホは使いたい。次回は中国国際航空使わないかもしれない。

  • 機内モードだとしてもiPhoneとかスマホの利用はダメ(でもなぜかiPadはOKだとか)
    • 行きはNetflixダウンロードコンテンツをかなり溜め込んで臨んだけど怒られて全然見られず
    • 帰りはしょんぼりしながらKindleで漫画買いまくって読んでた(中間管理職トネガワ面白かった)
  • 乗り継ぎに使うハブ空港が北京国際空港で次の飛行機を待っている間、人間的なインターネットが出来なくて息苦しい(中国なので)
    • Twitter, Line, Facebook, mixi, instagramなど一通り全部ダメだった
    • モンストは遊べたのでひたすらモンストしていた

まとめ

他にも色々面白かったことはあるけど、書いていてだんだん大変になってきたので一旦この辺で打ち切りたい。

当初の趣旨としてヨーロッパ住めそうか見てくるってのは達成できたとは思っていて、結論としてはどの国でも住めそうだと思った。 どの国が一番良かったかと聞かれるとどの国も良かったし良さ・悪さはあったので1番っていうのは難しいけど、 やっぱり英語習得のことだけ考えたら目に入る文字全部が英語のイギリスが一番だなぁと思った。 (どの国も魅力はあるけど、逆に絶対にここに住みたいって決め手もない)

あとは、今回巡った場所は本当に一部のエリアでしかないので今後ヨーロッパに住んだら他の国とか都市とかも 気軽に旅行に行けるようになるのでいろんな場所に行って見たいなと思うようになったのが良かった。 (旅行前はあまり興味なかったけど、旅行後には興味が生まれた)

最後に、現地集合・現地解散というのは集合は嬉しさがあるけど、解散はとても寂しさが伴うのであんまりオススメしない。 でも、こんな感じの夫婦だからこそ今回のような面白旅が出来たと思うので妻には感謝。

おまけ

海外旅行行くときは、必ず冷蔵庫の扉が閉まっているのを確認してから出発しましょう。

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を試してみたい。