ISUCON7本戦「Railsへの執着はもはや煩悩」で4位だった
ISUCON7本戦に「railsへの執着はもはや煩悩の域であり、開発者一同は瞑想したほうがいいと思います。」チーム (@cnosuke, @rkmathi, @k0kubun) で参加し、4位でした。
本戦の概要
予選より参加者は少ないと思うので軽く解説しておくと、クッキークリッカーを模したトラフィックのほとんどがWebSocketのアプリで、1万桁とかのスコアを計算する都合ほとんどのチームのボトルネックが最後までBigintの演算になるような問題でした。
僕らは15:00くらいに1位になり、その後はスコアをそれほど改善できず終わってしまいました。
方針
「@k0kubun 氏のチューニングした3倍速いRubyで優勝したらまじかっけーっす!」って話をした。 #isucon
— FUJI Goro (@__gfx__) 2017年11月25日
最近Ruby向けのJITコンパイラを開発している ので、それを使ってバーンとやろうと思ってましたが、これは開始前にgfxさんにバラされたのでやめました。
というのは嘘ですが *1 、WebSocketと聞いた時点で並列度が必要になるんじゃないかなあと思ってた *2 のと、アプリのCPU演算がボトルネックになってた *3 ので普通に使って速い言語としてGoを使うのを最初から視野にいれていました。
一方で、チーム名からもわかるように我々はRubyに最もなじみがあり計測のノウハウがあるのと、また普通にRubyのコードはGoのコードに比べ文字数が少なく見通しが良かったので、問題を把握するまでの期間Rubyで参戦し、後にGoに切り替えました。
最終形の構成概要
僕は開始2時間くらい寝起きでぽよってたので @cnosuke や @rkmathi が最初に考えた内容ですが、大体以下の感じになりました。 問題の性質上、WebSocket以外を受けるノード以外の構成はどこも同じだったと思います。
- appサーバ1 (ベンチマーク時この1台のみ指定)
- appサーバ2
- WebSocketを返すGo
- 同ホストのGo向けMySQL
- appサーバ3
- WebSocketを返すGo
- 同ホストのGo向けMySQL
- appサーバ4
- WebSocketを返すGo
- 同ホストのGo向けMySQL
やったこと
- rkmathi のブログ: ISUCON7「Railsへの執着はもはや煩悩(ry」で本戦4位だった - 明日から本気だす
- cnosuke のブログ: ISUCON7本戦で @k0kubun と @rkmathi とのチームで4位だった 「Railsはもはや煩悩(ryチーム」 - cnosuke's blog (′ʘ⌄ʘ‵)
僕は業務で一番Rubyを触っている時間が長そうで、またGoもちょっと前はよく書いてたので、主にアプリのコードをいじる担当でした。逆にインフラが絡む部分は他の人に任せています。
点 | やった人 | やったこと |
---|---|---|
7112 | rkmathi | Python初期スコア計測 |
5980 | rkmathi | Rubyに変更、初期スコア |
4621 | rkmathi | Goも一応確認、初期スコア (その後Rubyに戻す) |
- | cnosuke | サーバーやリポジトリのセットアップ |
- | k0kubun | NewRelic をいれたが、99% WebSocketであるということだけがわかり、相性が微妙だったので今回は使用をやめた |
- | k0kubun | 予選でも使用したStackProfミドルウェアを入れた。 calcStatus がボトルネックであることがわかる。 今回もRubyを使っている間結構役に立ったが、StackProf周りは別のところで解説しようと思っているので今回は説明を省略 |
- | k0kubun | m_item テーブルを定数としてメモリに持つようにし、m_item のクエリを全てなくす。あまりスコアは変わらなかった |
- | k0kubun | get_power, get_price のBigintの計算がボトルネックになっていたので、これをcountが0〜50の場合の結果をアプリ起動時に作るようにした。 そこそこスコアが上がった気がする |
8968 | cnosuke | 採番して 2,3,4 にWebSocketのリクエストを分散 |
- | rkmathi | 2,3,4に個別のMySQL用意 |
- | rkmathi | 再起動耐性のための設定 |
- | k0kubun | 50個のキャッシュを500個にしてみたら全然起動しなくなった。この時点でも150までのカウントを使っていたが、150とかにしてもメモリの使用量がモリモリ増えて速攻で2GB使い切ってしまったので、Pumaのプロセスを減らすかとか考えていた(preload_app も試すべきだった)が、GILあるしなあとかまあそういうことを考えるのが面倒なので僕はここでRubyをやめることにした。 |
- | rkmathi | innodb_buffer_pool_sizeなど、MySQLの設定とか |
- | cnosuke | WebSocket以外を返しているノードへのWebSocketへの負荷減らし |
- | rkmathi | サーバーで動いているのをRubyからGoに切り替え |
- | k0kubun | Rubyにやった変更(m_item, power/priceのキャッシュ)を全てGoに移植 |
- | k0kubun | power/price のキャッシュを 50 → 80に調整。 このパラメータをいじるだけでスコアが上がることを発見するが、起動時間が指数関数的に述びていくのでこのへんで断念したが、このあたりからcnosukeが起動時間を縮めるためこれのキャッシュをシリアライズするのを着手していた |
- | rkmathi | 僕らが誰もGoのプロファイリング方法を知らないのでググり始める。rkmathiが速攻でpprofの使い方を理解し結果を共有してくれたのでとても助かった。始めて使ったけどtopとlistだけでかなり多くのことがわかる。あとなんかrkmathiがコールグラフをsvgにしていたが、これも便利だった |
- | cnosuke | WebSocketだけをさばくノードでnginxをなくしてGoで直接受けるように。 今回はあまり効果はなさそうだった |
- | k0kubun | やたら 値が1000の big.Int インスタンスを生成してるっぽかったので毎回同じものを使うように (効果なし) |
24227 | k0kubun | calcStatus内で 1000 * 13 のオーダーでやっているかけ算 + 比較を、13のループの外側で割り算 + ループ内比較 にすることでそこのかけ算のコストを 1/13 に。割とスコアが上がっていた |
- | k0kubun | calcStatus内で 13回やっているかけ算を1つの割り算にできるものをもう1つ発見し、適用。 ここは1000回ループの外なのでインパクトは小さそう |
- | k0kubun | 「1000 * 13回 のかけ算」 → 「1000回 の割り算」 に減らしたオーダーを、 「13回のかけ算」に減らした。これも効果があった記憶 |
- | k0kubun | この時点でpprofを見る限りでは big.Int の String()が90% くらい使っていたし、GoがCPUをほぼ使い切っていたので、僕は後半ずっとこれをどうにかできないかもがいていた。なんかfloat64に変換してLog10で桁数計算したり(これは変換した時点で精度が破滅)、なんかpprofのsvgの読み方をちょっと間違えてString()の後のItoaが重いと勘違いして、Exponentialインスタンスを作る時に整数への変換をスキップしたりしたが、無意味であった。これ以降僕はスコアを上げられていない |
- | rkmathi | puma のワーカー数いじり |
- | rkmathi | longtext -> varchar のalter |
- | cnosuke | power/priceのキャッシュをYAMLにシリアライズしてロードする奴を入れるが、バグのためrevert |
27328 | rkmathi | 再起動テストとかプロファイリングとか。我々はプロファイラを入れるとスコアが上がるジンクスを持っており、pprofを有効にした時最高スコアを更新した |
- | cnosuke | YAMLの奴をデバッグしていて、終了間際に原因がわかるが、ベンチマークを何度も回す余裕がなさそうなので、ここで変更を加えず何度かベンチを回して終了 |
やり残したこと
気付いていたのはaddingとかが過去になった奴をまとめておけそうなくらいですが、僕らの計測の限りではそこはボトルネックではなかったので着手しませんでした。ずっとボトルネックであった big.Int.String()
や、 price/power の計算結果キャッシュ数を増やすのをがんばっていたが両方失敗してこのスコアに留まった形です。
1位のMSAは、僕らはボトルネックだと思っていなかったあたりのオンメモリ化や過去の結果のマージを全て終わらせて高いスコアを出したようなので、計測は難しいですねという感じです。
我々は気付かず懇親会で知ったのは、(具体的な内容はそちらのチームのブログに任せますが) ソン・モテメン・マサヨシ チームのgoroutineを使ったものやGCのチューニングで、同じGoのアプリをいじってるにも関わらず僕らとは全く違う方向性で速くしていたようなので、その辺はGoへの慣れの差が出たような気がします。
感想
最後のISUCON本戦参加は学生枠で惨敗という感じだったのが、3年経って同じメンバーで社会人枠で健闘でき、成長した実感が得られたのがとても良かったです。
時の運次第では優勝に手が届きそうな感覚が得られたので、来年は優勝したいと思います。参加された方と運営の皆さま、お疲れ様でした & ありがとうございました!
ISUCON7予選2日目「Railsへの執着はもはや煩悩」で予選通過した
ISUCON7予選に「railsへの執着はもはや煩悩の域であり、開発者一同は瞑想したほうがいいと思います。」チーム (@cnosuke, @rkmathi, @k0kubun) で参加し、217,457点で予選通過だったようです。 正確な値は覚えてませんが、Best Scoreは25万くらいでした。
最終形の構成概要
- appサーバ1
- puma 16スレッド: 画像のアップロード/表示、雑多なリクエスト対応
- puma 2スレッド: GET /fetch だけ返す
- appサーバ2
- puma 16スレッド: 雑多なリクエスト対応 (画像はnginxがサーバ1に流す)
- puma 2スレッド: GET /fetch だけ返す
- DBサーバ
- MySQLがいるだけ
サーバ1, サーバ2をベンチマーク対象にしていました。この構成なのは GET /fetch がスコアにカウントされないため、それ以外にほとんどの時間を使えるようにするためでした。
やったこと
最終コード・他のメンバーのブログはこちら:
ISUCON7「Railsへの執着はもはや煩悩(ry」で予選通過した - 明日から本気だす
学生のころから何度も同じメンバーで出ているので、いつも通りチーム内で割と綺麗に並列に仕事ができました。
やったことと点数の推移の記録とかはやってないので、やったことだけを適当に列挙していきます*1。グラフからわかる通り多分序盤で意味のある改善が終了しており、終盤はあまりうまくいっていませんでした。
効果があった気がする奴を太字にしておく。
やった人 | やったこと |
---|---|
cnosuke | 公開鍵の準備とか |
rkmathi | リポジトリへの主要なコードの追加とか |
k0kubun | python→ruby変更、systemdの設定のリポジトリへの追加 |
k0kubun | NewRelicのアカウント管理、インストール。この時点では GET /fetch が支配的だったのを確認 |
k0kubun | sleepを消したり消さなかったりした後消す |
k0kubun | 適当にSELECTするカラムを消したり、messageテーブルにインデックスを貼ったり |
k0kubun | rack-lineprofを眺めるがほとんど参考にならなかった |
cnosuke | DBに入ってる画像をファイルにしてnginxから配信できるように変更 (ここで割と上位に来た)、多分Cache-Controlとかもこの辺でついてる |
k0kubun | GET /fetch を捌くpumaのプロセス (1スレッド)をまだ何もいない2つ目のサーバーに用意し、それ以外を元々いたpuma (16スレッド) に捌かせるようにした (これも結構上がり、後々もインパクトがあった) |
rkmathi | fetch用pumaのポートを別のサーバーからアクセスできるようにした |
k0kubun | (本来はruby実装のclose漏れによる)fdの枯渇をOSのfdを増やしたり nginxのworker_rlimit_nofile をいじったりしてどうにかしようとあがくがうまくいかない |
cnosuke | 2つ目のサーバーもfetchではないメインのpumaがレスポンスを返せるようにnginxを設定、画像を返すのを1つ目のサーバーに絞ったりとか |
k0kubun | close漏れに気付いて直す、多分workloadが上がってスコアが伸びる |
rkmathi | get_channel_list_info のループの余計な処理削り |
k0kubun | なんかsleepを0.2とかいれてみるがスコアが下がるのでなくす |
rkmathi | NewRelicで計測 |
cnosuke | MySQLの設定がうまく反映されてない奴とかの対応 |
k0kubun | GET /message のN+1つぶし、N=0で壊れるので直したり |
k0kubun | 自分のプロフィールの時に不要なクエリを減らす、そこのSELECT * のカラムも絞る。NewRelicのブレークダウンがなんかいつもと違って全く詳細になってなくてあまりインパクトのない変更を繰り返している |
cnosuke | MySQLがいるサーバーのメモリが使われるように設定を修正したり |
rkmathi | GET /history/:channel_id のN+1つぶし |
rkmathi | NewRelic見たり |
k0kubun | さっき自分がN+1で踏んだrkmathiのコードのバグとり |
k0kubun | GET /fetchのつぶしやすそうなN+1を1つつぶす |
k0kubun | fetchするpumaのスレッド数を適当にいじり2つにおちつく |
k0kubun | NewRelicのthread profilerを使うが、今回はびっくりするほど出力が見辛くはっきりいってほとんど役に立たなかった |
rkmathi | ランダムにする必要のないsaltを固定化 |
k0kubun | Mysql2::Client をリクエストごとに作るのをやめ、スレッドローカルに使いまわすようにする |
cnosuke | この辺でk0kubunがperf top を眺めてて、appサーバーがrubyよりlibzが支配的だったので、nginxのgzip_comp_levelをいじったりしている |
k0kubun | perfを見る限りではrubyはボトルネックではなさそうだったが、ERB回り遅いのではと言われたので僕がERBを速くしたruby 2.5に変更(特にスコアに変化はない) |
rkmathi | GET /register が静的なのでnginxだけで返すように変更 |
cnosuke | DBサーバーにもnginxをたて、そこからプロキシだけすることでglobalな帯域を有効に使えないか試したが、うまくいかなかった |
k0kubun | TD社内で書いた秘伝のstackprofミドルウェアを取り出し、何が遅いのか計測。rack-lineprofより今回はこっちの方が猛烈に見易かった。fetchでcounter cacheが効きそうなことに気付くが時間の都合でやらない |
cnosuke | なんかlibzまわりの関係でgzをいろいろやってる |
cnosuke | 画像を受けてるサーバーは1つだけだが、2つ目の方も最初からある画像は返せないか試していたが、うまくいかない |
rkmathi | 再起動テスト |
rkmathi | ログの出力を消したり |
k0kubun | rkmathiの変更で/registerがoctet-streamになってて動作確認がしにくかったので text/htmlにしてる |
cnosuke | bigintをintにしてた(のを↑のどこかでやってた)都合で壊れてるのがあって、initializeでauto incrementをリセットする変更をいれてる |
k0kubun | 余計なcreated_at, updated_atを作らないようにした |
あとどこかでtextになってるカラムをvarcharにしたりとかしてました。
心残り
appサーバ1, 2の帯域両方をどうにかして画像のリクエスト処理に使えるようにしたかった(パスのどこかが奇数か偶数かでどちらかに固定で送るとか)ですが、いろいろ考えたけど僕はあまりいい対応が思いつかなかったです。最後 POST /message がボトルネックだったのも、MyISAMにしたりMEMORYにしたりしましたが効果はなかったですね。Redisにするみたいな大きい工事はできませんでした。
GET /fetchのレスポンスは更新があるまで止めておくのが良い(ベンチマーカーがそういう風にスコアをつけてる)、というのを感想部屋で聞いたけど、これも全く気付きませんでした。
あと、奥の手としてJITを考えてましたが全くRubyにボトルネックが移せなかったのも残念ですね。
気持ち
上位のチームには相当離されていたので、どこに自分たちが見過していたブレークスルーがあったのか知るのが楽しみです。 本戦に出られるのは学生枠で出してもらっていた時ぶりですが、いい結果を残せるようがんばります。
*1:僕以外の人のタスクは僕が把握してるレベルしか書けないので多分かなり抜けててます
VMに手を加えずRubyを高速化するJITコンパイラ「YARV-MJIT」の話
先日のRubyKaigi 2017のLTではLLVMベースのCRuby向けJITコンパイラLLRBの話をしました。 5分はちょっとJITの話をするには短かかったですね。
LLRBをふまえた、Cのコード生成への軌道修正
さて、上記の資料にある通り、CRubyのJITにおいてはメインの高速化対象が既に存在するCのコードになるため、 開発の早い段階でパフォーマンスにインパクトを持てるとすればLLVM Passの順番を変えるくらいで、 LLVM IRを直接生成しても最適化上のメリットがほとんどないのでその部分はMJIT と同じくCのコードを生成するように変更したい、という話をした*1。
で、LLRBはC拡張として作るべくちょっと不思議な努力をいろいろやっており、 それらの設計はやってみた結果(コアに直接変更を加えるのに比べ)デメリットの方が大きいと思ったので、 LLRBの失敗を全て生かしつつ、今回YARV-MJITという奴を新しくスクラッチした。
YARV-MJITとは?
MJITはJITを実装する前にレジスタベースのRTL命令にVMの命令を全て置き換えた上でそれをベースにコンパイルしているが、 YARV-MJITはVMの命令セットは全てのそのままに、スタックベースのYARV命令をコンパイルする点が異なっている。
逆に言うとそれ以外は全てMJITのパクり、というかそのままコードを持ってきたフォークになっており、 「Copyright (C) 2017 Vladimir Makarov」になってるコードの方が多い。
Optcarrot ベンチマーク
まだ Optcarrot しかまともにベンチマークできてないのでこちらを。
- Intel 4.0GHz i7-4790K with 16GB memory under x86-64 Ubuntu 8コア で計測
- 以下の実装を用いた:
- v2 - Ruby MRI version 2.0.0
- v2.5 - Ruby MRI 2.5.0-preview1, YARV-MJITの元のベースに近い
- rtl - Vladimirの 最新の RTL MJIT (21bbbd3)
-j
なし - rtl-mjit - MJIT (
-j
) with GCC 5.4.0 with -O2 - rtl-mjit-cl - MJIT (
-j:l
) using LLVM Clang 3.8.0 with -O2 - yarv - 僕の YARV-MJIT の
-j
なしバージョン、v2.5と大体同じはずだが、JITが有効かのフラグのチェックのオーバーヘッドが入る - yarv-mjit - YARV-MJIT (
-j
) with GCC 5.4.0 with -O2 - yarv-mjit-cl - YARV-MJIT (
-j
) with LLVM Clang 3.8.0 with -O2
以下のように、MJITほどのパフォーマンスはまだ出せていないが、大体Ruby 2.0の1.59倍くらいは速くなる。
v2 | v2.5 | rtl | rtl-mjit | rtl-mjit-cl | yarv | yarv-mjit | yarv-mjit-cl | |
---|---|---|---|---|---|---|---|---|
FPS | 35.41 | 43.36 | 38.00 | 75.57 | 81.25 | 42.89 | 56.38 | 48.27 |
Speedup | 1.0 | 1.22 | 1.07 | 2.13 | 2.29 | 1.21 | 1.59 | 1.36 |
なおRailsでのベンチと解説をRails Developers Meetup 2017でやる予定。 そっちも多分内部に触れるけど、よりコアよりの話とかJITの実装の苦労話とかマイクロベンチマークの紹介はRubyConf 2017でやると思う。
何故YARV-MJITを作っているのか?
JITのためにVMの命令をレジスタベースにするメリットが何なのか確かめたい
ko1さんを含めコミッターの人たちは大体RTL命令への変更に概ね皆賛成っぽいんだけど、 同じオブジェクト指向言語でスタックベースのバイトコードとJITを持つJavaがRubyより十分に速い以上、 JITで高速化をするためにVMの命令をレジスタベースにする必要があるのか、ということにずっと疑問を持ち続けている。
VMやJITに馴染みがない人のために説明すると、 VMの命令セットをスタックベースからレジスタベースに変えるというのは大体以下のようなことと同じである:
あなたの会社の社員は全員はEmacsを使っているとする。巷では (anything.el や helm のような) ファジーファインダーでファイルを絞り込んで開くと編集効率が上がると言われているが、現在Emacsにはそのようなプラグインがまだないとする。
そこに突然、EmacsよりもVimの方が編集効率が高いと宣う熱狂的なVimmerが現れあなたは試験的に導入してみたが、 Emacsでできないことが突然発覚するリスクを背負う割にはそれほど編集効率が上がらなかった。 一方、そのVimmerはUnite.vimのようなプラグイン(Vimのanything.el的な奴)を開発し、それを使ってみたら開発効率が爆増し、 全社的にEmacsをやめてVimを導入することになった。…というような話なのである。
冷静に考えてエディタは変えずanything.el的な部分だけを作って使った方が安全に決まっているし、 Emacs上でファジーファインダーを開発してもVimの時と同じように開発効率が上がるはず、というのが僕の主張である。
まあ冗談*2はさておき、スタックvsレジスタでのJITにおける本質的な違いが何なのか技術的な興味があって続けている。
JIT基盤の変更とVM命令の全リプレースが同時に入るリスクを軽減したい
上記の考えとは全く別に、僕が開発しているようなLLRBとかに比べて、MJITのクオリティは本当に高い。
なのでRuby 3にMJITが入るのは僕も応援したいのだけど、その一方、
JITは-j
をつけるかどうかでオンオフが切り替えられるのに対し、
VMの命令セット全置き換えは切りようがないので、本番で動作しているアプリに投入するにはちょっとリスクが高すぎるように思っている。
特に弊社で動かしているような、一瞬でも止まるとお客様がかなり困るようなRailsアプリとかの場合。
一方現在のCRubyのコードベースでは、コマンドラインオプションで現在のYARV命令とRTL命令をスイッチする(かつRTLの時のみJITが使える)みたいな実装にするのは ちょっと厳しいんじゃないかとも思う。
そうすると、大きな2つの変更である「RTL」と「MJIT」に関して、オプショナルにできるMJIT側の基盤を先に入れてテストし、 後からRTL(とその命令のJITコンパイラ)を入れる方が安全にリリースできていいのでは、というのが僕の意見である。
MJITはよくできていて、JITコンパイラ側はVMの命令が呼び出す関数をインライン化していく作業がメインで、 特殊命令への変換やdeoptimize相当の処理の大部分はRTL命令側に実装されているので、実はJITコンパイラ部分だけ置き換えるのがそんなに大変じゃないと思っており、 マルチスレッドプログラミングが必要だったりポータビリティに難のあるオブジェクトのロードなどのMJITの基盤を先に導入できるメリットは結構あると思っている。
今後の展望
今年はもうなんだかんだ半年くらいJITを書き続けてるので、自分の満足の行くまで好きに続けようと思う。
Optcarrotで少なくとも60fpsは越えられるレベルになり、認識できるレベルのバグが修正され、JIT有効でもほとんどテストが通ったり、 実際のアプリケーションが問題なく動くことを確認できたら、 2.xの間の、3.0のRTL+MJITまでの繋ぎとしていかがですかというような提案ができたらいいなと思っている。
GraphQLは何に向いているか
今年GitHubがGraphQL APIを正式公開したあたりから、GraphQLが去年とかに比べちょっと流行り始めたように感じる。idobataがGraphQL APIを公開したり、Kibelaも公開APIをGraphQLで作ることを宣言している。
利用者側からすると使えるインターフェースの中から必要なものを調べて使うだけなのであまり考えることはないのだが、自分がAPIを提供する立場になると話は変わってくる。REST APIとGraphQL APIはどちらかがもう一方のスーパーセットという風にはなっておらず、どちらかを選択すると何かを捨てることになるので、要件に応じてどちらを選ぶのが総合的に幸せなのか考える必要がある。
以前趣味でGitHub連携のあるサービスを作っており、それを最近GraphQL API v4を使うように移行し、そこでついでにそのサービスのGraphQL APIを書いてみたりした結果GraphQLができること・できないことが少し見えてきたので、僕の現在の「GraphQLはREST APIに比べどういう用途に向いているか」についての考えをまとめておく。
REST APIと比較したGraphQL
この比較では、REST APIがJSON Schemaと同時に使われうることも想定して書く。理由は、GraphQLが解決している問題の一部をREST APIで実現するためにJSON Schemaが使われることがしばしばあり、逆にGraphQLでは元々それに近い機能がありまず使わないと思うので、現実世界の問題を解く上ではそれらをセットにして比較した方がいいと考えたため。
なお、対比する上で本質的な部分となる「仕様上の問題」と、時間が解決しうる(が現実世界では当然考慮が必要な)「現在のエコシステムの問題」は分けて記述する。エコシステムに関しては筆者の都合でサーバー側はgraphql-rubyを念頭に置いている。
GraphQLにしても解決できない問題
仕様上の問題
- GraphQLはありとあらゆるリソースをリクエスト一発で取得できる夢の技術ではない
- 正直触る前は大体そういうイメージだった
- 例えばページネーションなしに1種類のリソースを6000個取得しようとするとレスポンスに1分かかりリバースプロキシ(かunicorn, rack-timeout等)でタイムアウトになるREST APIが世の中には実在したのだけど、それをそのままGraphQLのクエリで再現したとして確実に同じ時間がかかる。つまりページネーションは確実に必要で、クライアント側でそのリソースに関してループを回す必要があり、そのリソースに関して何度かリクエストが必要になる。
- 単に1つのリソースをページネーションしないといけない場合だけでなく、N個あるリソースにそれぞれM個リソースがネストしていてぞれそれにorderが必要な場合、これは確実にN+1回クエリが必要なわけだけど、そうやって裏側で非効率なクエリが走るようなクエリを投げてしまうと一回のリクエストに時間がかかりすぎてタイムアウトするリスクがあるので、ある程度は分けないといけない。実際僕もGitHubのGrpahQL APIを叩いているとリトライをしても結構タイムアウトを見た。
- 後述するようにリクエスト数は減ることは多いが、必ず1回にできる銀の弾丸ではないという主張
現在のエコシステムの問題
- 現時点ではAPIクライアントを自動生成できるライブラリは限られており、アプリ側にいちいち長いクエリを書く必要がある
- GitHubがAPIをGraphQL化したモチベーションの1つに“We wanted to generate clients instead of manually supplying patches to our Octokit suite”というのがあるが、いまだにOctokitは自動生成されてない。
- もともとGtiHub API v3で採用されていたHypermedia APIにもAPIクライアントの実装の自動化というモチベーションが多少はあったんじゃないかと思っているが、それを活用するのは人類には難しすぎたんじゃないかという気がする。
- 一方でGraphQLだと自動生成に必要そうな情報がおおむねスキーマから取れるので、やれば割とうまくいくと思う
- awesome-graphqlを眺めると、npmのgraphql-auto-mutationは割とそれっぽい。
GraphQLにすると困る問題
仕様上の問題
クエリをパースしないとキャッシュの可否を判定できないため、HTTPキャッシュが難しいREST APIであれば、同じとみなせるGETリクエストをVarnish等でキャッシュすることが容易かつ効率的にできるが、GraphQLだとリクエストボディのJSONをパースし、その中に入っているqueryをパースし、そこにmutationがあるかどうかをチェックする必要がある。ワークアラウンドとしてquery fieldが生えてるエンドポイントとmutation fieldが生えてるものを分ける等が考えられるが、エンドポイントを分ける(REST APIに近づける)ほど当然キャッシュのコントロールがしやすくなるわけで、HTTPキャッシュがないと困る用途には向かないと思う。- 追記: id:yamitzky さんのコメントで知りましたが、
GET /graphql?query=...
といった形でのリクエストも仕様上想定しているため、これは誤解のようです。graphql-rubyのgeneratorだと生えないので勘違いしていました。
- HTTPのメソッドやステータスコードによる挙動の予測ができなくなる
- queryとmutationしかないということは、HTTPメソッドのGETかPOSTしかない状態に等しく、mutationの中でそれがリソースの追加・更新・削除のうちどれなのかを表現する方法は別に仕様レベルでは標準化されておらず、実装した本人以外から見たら挙動が予測しにくくなる。
- クエリの結果がエラーになっても大体
200 OK
が返ってくる。(OKとは)- 他にもGitHubの場合
502 Gad Gateway: This may be the result of a timeout, or it could be a GitHub bug
といったエラーが結構頻繁に返ってくるんだけど、timeoutの場合はリトライしたいしGitHub bugならリトライすべきではないのでこれはHTTPステータスコードで区別して表現してほしい。本当はどちらも(バグを含む何かが原因の)タイムアウトなのかなあ(そのうちサポートに確認する)。 - errorオブジェクトのルールをちゃんと決めて実装すれば解決できるけど、このあたりに標準的な仕様やガイドラインが存在しない結果そういうレスポンスを生んでしまうのは問題だと思う。
- 他にもGitHubの場合
- 必要なfieldを必ず明示しなければいけないので、自動生成しない限りはAPIクライアントを書くのに必要なコードの文字数・行数は増えそう
- 後述するようにIDEのGraphiQLのアシストがあるのでそこまで大変ではない
現在のエコシステムの問題
- GraphQL Proを使わないとモニタリングが難しい
- そこまで高くはない($900/year)ので仕事でやってるなら普通に金払えばいいんだけど、NewRelicとかで詳しくモニタリングしたかったらGraphQL::Proを使う必要がある
- graphql-rubyとかにはinstrumentationの仕組みがあるので、まあ困ったら自分で実装することは可能
- Railsで使ったらMVCのレールのうちVCから割と外れる
- N+1クエリの解決方法がいつもと違う感じになる
- 普通はActiveRecordでSQLのAST(Arel)を組み立て、そこにこのリソースをeager loadingするよという情報を埋め込むことで
ActiveRecord::Associations::Preloader
やActiveRecord::Associations::JoinDependency
になんとかさせるんだけど、これは使わなくなる - かわりに、こういう感じでクエリのトラバース中に必要なIDを集めておいて、最後にgraphql-batchのミドルウェアを通してまとめてクエリすることになる。ネストしたリソースをクエリしてくるのに必要な情報を、親のリソースの一覧を参照することで取ってこれる限りはどうにかできるので、REST APIで問題にならないようなリクエスト(しかできないようにスキーマを制限した場合)ならGraphQLでも問題にならないような気がしている。しかし、例えばコントローラーから使うことを想定してN+1を解決するpreloaderを独自に書いていた場合は使えなくなる可能性があると思う。
- graphql-batch相当のライブラリがない言語でやる場合、特にそれが静的型付け言語とかだとちょっと面倒かもしれない
- 普通はActiveRecordでSQLのAST(Arel)を組み立て、そこにこのリソースをeager loadingするよという情報を埋め込むことで
- graphql.jsを使う場合はFacebookのBSD+Patentsライセンスに同意する必要がある
どちらでも大差のない好みの問題
仕様上の問題
- 「REST APIのバージョン管理」 vs 「GraphQLの
@deprecated
」- GraphQLのサイトにEvolve your API without versionsと書かれているが、個人的にはあまりこれが優れている点だと感じない。
/v1
や/v2
みたいなバージョンを更新していくかわりにフィールドに@deprecated
をつけていくと更新の粒度は細かくできるが、例えばStringだったfieldを同じ名前でObjectにしようとすると、REST APIなら新しいバージョンを1つ生やせば済むが、GraphQLの場合は同じ名前空間でやらないといけないので1度別のfieldを用意してそちらに移行し元のfieldを直すという2ステップ必要になる。どっちが楽かはどう変更していきたいかによる。 - そもそも普通にアプリを書いているとRundeckみたいにAPIのバージョンをバシバシ上げる必要ってそんなに感じなくて、新たにfieldやエンドポイントを足すような後方互換性のある変更が多く、バージョンを上げるとしたらそれこそREST API→GraphQLくらい全体的に大きな変更がないとやらない気がしていて、もともとこれがそんなに問題ではない
- GraphQLのサイトにEvolve your API without versionsと書かれているが、個人的にはあまりこれが優れている点だと感じない。
現在のエコシステムの問題
- 「JSON Schema」 vs 「GraphQLの型」
- ドキュメントの自動生成のしやすさ
- ユーザーのパラメータ・レスポンスのプロパティにおける型安全性
- GitHub APIのGraphQL化のモチベーションにWe wanted assurances of type-safety for user-supplied parametersがあげられているが、これも別にJSON Schema書いてそれをユーザー入力のバリデーションに使えばいい話で、別に必ずしもGraphQLの仕様が解決する話ではなく、単にRESTの時にサボっていただけと見ることができる
GraphQLの方がより良く解決している問題
仕様上の問題
- いわゆるfields paramよりもインターフェースがより柔軟で記述力も高い
- fields paramというのは
?fields=id,name,..
みたいに返すfieldを指定するパラメータのことを言っている。cookpad/garageにこういうのがあるのだが、miyagawaさん曰くこれはgraphQL になる前の Facebook graph API のやつをまねたらしいので、ちゃんとGraphQLになったものがより洗練されてるのは頷ける話 - GraphQLのクエリは普通改行するが、スペース区切りでも書けるし、あまりネストしていないようなケースでそうした場合はリクエストする側のコードの見た目はどちらもあまり変わらない(見やすい)感じになる。
- fields paramというのは
- APIのリクエスト数やround trip timeを減らしやすくなる
- 余計なfieldのリクエストが減りやすい
- いちいち指定する面倒くささとのトレードオフになるが、仕様上使うfieldを記述することが強制されるので、(そもそも強制されていない、省略可能な)fields paramに比べたら使うfieldはちゃんとメンテされる傾向になりやすいと思う。
- GitHubのIt seemed like our responses simultaneously sent too much data and didn’t include data that consumers neededという問題意識は割とうまく解決されるんじゃないかという感じがする
- 各fieldをクライアントが使っていないことを明示できるので、fieldの利用状況を調べやすい
現在のエコシステムの問題
- クライアントキャッシュを実装するためのGlobally Unique IDsなどガイドラインが示されており、実際にRelayのようなそれを念頭に置き活用するフレームワークが存在する
- クエリのIDE的な機能を持つGraphiQLに型がちゃんと活用されている
- そのため、毎回必要なfieldを指定しないといけない割には、クエリを書くのはそこまで苦痛ではない
- 一方でGraphiQLはいろいろキーバインドが潰されてるので普通のテキストのエディットはとてもやりにくい
GraphQLではないと解決できない問題
- 思いつかなかった
まとめ
GraphQLを使う場合の前提条件として、HTTPキャッシュを使わないケース*1である必要があり、また現時点だとGraphQL Proに$900/yearを払うかAPIの詳細なモニタリングを諦める必要がある。
その上で、サーバー側に型の記述を強制しクライアント側にfieldの記述を強制することにより、以下の例のように双方が幸せになると判断した場合は好みに応じて使えばいいと思う。REST APIかGraphQLのどちらかを使わないとすごく困るという状況は上記の前提以外はあまりなさそう。
GraphQLが向いてそうなケース
CRuby向けのLLVMベースのJITコンパイラを書いている話
LLRBというRuby向けのメソッドJITコンパイラを書いている
RubyKaigi 2015の最後のキーノートで@evanphxが「LLVMでCRubyのコードをインライン化するメソッドJITを実装したら速いんじゃね」みたいな発表をしていたのを覚えているだろうか。
LLRBというのはまさにそれを実装しているプロジェクトであり、少なくとも現時点で「LLVMでCRubyのコードをインライン化するメソッドJIT」と言える状態まで実装でき、ものによっては効果が出る状態になったので公開した。
なんで書いてるの
言語を自分で実装するとその言語に関する理解が大分深まる、というのをHamlの実装とかCコンパイラとかで体験していて、僕が一番好きな言語はRubyなのでRubyでもそれをやっておきたい、というのがあった。また、Rubyは遅いと言われがちだが、どこに改善可能な点が眠っているのかにも興味がある。
また、ささださんがRuby3のゴールの一つにJust-in-Time compilationをあげていて、そのスライドで1つの選択肢として言われていた"Use LLVM"というパートを検証するものでもある。ただし、MatzがLLVMはDependency的に微妙と言っており、本体にこの方向性で入ることは多分なく、あくまで実験的なプロジェクトになる。
どのように動くか
READMEを書いてたら大分疲れたので、この記事はその日本語訳にしておく。
LLRBのビルドプロセスでは、CRubyの命令やそれに使われるいくつかの関数をLLVM bitcodeという形式にプリコンパイルしている。
________ _________ ______________ | | | | | | | CRuby | | CRuby | | CRuby | | C code |-->| LLVM IR |-->| LLVM bitcode | |________| |_________| |______________|
LLRBのJITのためのプロファイラが開始されると、Rubyの実行中にJITコンパイルがスケジューリングされる。 JITコンパイルの際、LLRBはYARV ISeqをLLVM IRにコンパイルするが、その際にプリコンパイルしておいたLLVM bitcodeをリンクするので、LLVM Passによる関数のインライン化やその後の最適化が行なえる。
最適化を行なった後、LLVM IRはLLVMのMCJITエンジンによって機械語に変換され、それを呼び出すためのCの関数ポインタとしてLLRBに返される。コンパイル対象のISeqに対し、後に説明する方法によって変化が加えられ、この関数ポインタが実行されるようになり最適化が完了する。
______ ______________ __________ ___________ _________ | | | | | | | | | | | YARV | | ISeq Control | | LLVM IR | | Optimized | | Machine | | ISeq |-->| Flow Graph |-*->| for ISeq |-*->| LLVM IR |-*->| code | |______| |______________| | |__________| | |___________| | |_________| | | | | Link | Optimize | JIT compile ______|_______ ___|____ __|____ | | | | | | | CRuby | | LLVM | | LLVM | | LLVM Bitcode | | Passes | | MCJIT | |______________| |________| |_______|
実際にパフォーマンスが向上するか?
基本的なインライン化は実装済なので、その効果を見ていく。
以下のようなruby/benchmark/bm_loop_whileloop.rbのコードについて考える。
def while_loop i = 0 while i<30_000_000 i += 1 end end
このメソッドのYARV ISeq (LLRBのコンパイル対象) はこうなっている。
> puts RubyVM::InstructionSequence.of(method(:while_loop)).disasm == disasm: #<ISeq:while_loop@(pry)>===================================== == catch table | catch type: break st: 0015 ed: 0035 sp: 0000 cont: 0035 | catch type: next st: 0015 ed: 0035 sp: 0000 cont: 0012 | catch type: redo st: 0015 ed: 0035 sp: 0000 cont: 0015 |------------------------------------------------------------------------ local table (size: 1, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1]) [ 1] i 0000 trace 8 ( 1) 0002 trace 1 ( 2) 0004 putobject_OP_INT2FIX_O_0_C_ 0005 setlocal_OP__WC__0 3 0007 trace 1 ( 3) 0009 jump 25 0011 putnil 0012 pop 0013 jump 25 0015 trace 1 ( 4) 0017 getlocal_OP__WC__0 3 0019 putobject_OP_INT2FIX_O_1_C_ 0020 opt_plus <callinfo!mid:+, argc:1, ARGS_SIMPLE>, <callcache> 0023 setlocal_OP__WC__0 3 0025 getlocal_OP__WC__0 3 ( 3) 0027 putobject 30000000 0029 opt_lt <callinfo!mid:<, argc:1, ARGS_SIMPLE>, <callcache> 0032 branchif 15 0034 putnil 0035 trace 16 ( 6) 0037 leave ( 4) => nil
LLRBのコンパイラは、これを以下のようなLLVM IRにコンパイルする。
define i64 @llrb_exec(i64, i64) { label_0: call void @llrb_insn_trace(i64 %0, i64 %1, i32 8, i64 52) call void @llrb_insn_trace(i64 %0, i64 %1, i32 1, i64 52) call void @llrb_insn_setlocal_level0(i64 %1, i64 3, i64 1) call void @llrb_insn_trace(i64 %0, i64 %1, i32 1, i64 52) br label %label_25 label_15: ; preds = %label_25 call void @llrb_insn_trace(i64 %0, i64 %1, i32 1, i64 52) %2 = call i64 @llrb_insn_getlocal_level0(i64 %1, i64 3) call void @llrb_set_pc(i64 %1, i64 94225474387824) %opt_plus = call i64 @llrb_insn_opt_plus(i64 %2, i64 3) call void @llrb_insn_setlocal_level0(i64 %1, i64 3, i64 %opt_plus) br label %label_25 label_25: ; preds = %label_15, %label_0 %3 = call i64 @llrb_insn_getlocal_level0(i64 %1, i64 3) call void @llrb_set_pc(i64 %1, i64 94225474387896) %opt_lt = call i64 @llrb_insn_opt_lt(i64 %3, i64 60000001) %RTEST_mask = and i64 %opt_lt, -9 %RTEST = icmp ne i64 %RTEST_mask, 0 br i1 %RTEST, label %label_15, label %label_34 label_34: ; preds = %label_25 call void @llrb_insn_trace(i64 %0, i64 %1, i32 16, i64 8) call void @llrb_set_pc(i64 %1, i64 94225474387960) call void @llrb_push_result(i64 %1, i64 8) ret i64 %1 }
上記のLLVM IRでcall
されている関数は全てLLVM bitcodeにプリコンパイルされており、LLRBはJITコンパイル時にそれをリンクする。そのため以下のようなインライン化と最適化がLLVMによって行なわれる。
define i64 @llrb_exec(i64, i64) #0 { ... land.lhs.true.i: ; preds = %label_25 %49 = load %struct.rb_vm_struct*, %struct.rb_vm_struct** @ruby_current_vm, align 8, !dbg !3471, !tbaa !3472 %arrayidx.i = getelementptr inbounds %struct.rb_vm_struct, %struct.rb_vm_struct* %49, i64 0, i32 39, i64 7, !dbg !3471 %50 = load i16, i16* %arrayidx.i, align 2, !dbg !3471, !tbaa !3473 %and2.i = and i16 %50, 1, !dbg !3471 %tobool6.i = icmp eq i16 %and2.i, 0, !dbg !3471 br i1 %tobool6.i, label %if.then.i, label %if.else11.i, !dbg !3475, !prof !3380 if.then.i: ; preds = %land.lhs.true.i call void @llvm.dbg.value(metadata i64 %48, i64 0, metadata !2680, metadata !3361) #7, !dbg !3477 call void @llvm.dbg.value(metadata i64 60000001, i64 0, metadata !2683, metadata !3361) #7, !dbg !3478 %cmp7.i = icmp slt i64 %48, 60000001, !dbg !3479 %..i = select i1 %cmp7.i, i64 20, i64 0, !dbg !3481 br label %llrb_insn_opt_lt.exit if.else11.i: ; preds = %land.lhs.true.i, %label_25 %call35.i = call i64 (i64, i64, i32, ...) @rb_funcall(i64 %48, i64 60, i32 1, i64 60000001) #7, !dbg !3483 br label %llrb_insn_opt_lt.exit, !dbg !3486 llrb_insn_opt_lt.exit: ; preds = %if.then.i, %if.else11.i %retval.1.i = phi i64 [ %..i, %if.then.i ], [ %call35.i, %if.else11.i ] %RTEST_mask = and i64 %retval.1.i, -9 %RTEST = icmp eq i64 %RTEST_mask, 0 ... }
いろいろインライン化されている上に僕の書いたCSSがクソなので読みづらいが、簡単に説明するとRubyVMの状態を取得し、<
が再定義されているかどうかをチェックし、再定義されていなければicmp slt
という命令が実行されるようになっている。インライン化されているので、llrb_insn_opt_lt
という本来の関数の呼び出しのオーバーヘッドもなくなっている。
これはYARVのinsns.defの定義をほとんどそのまま持ってきてインライン化しているだけなので、実装コストが低いというメリットがある。
この最適化で、以下のようなベンチマークで、
ruby = Class.new def ruby.script i = 0 while i< 30_000_000 i += 1 end end llrb = Class.new def llrb.script i = 0 while i< 30_000_000 i += 1 end end LLRB::JIT.compile(llrb, :script) Benchmark.ips do |x| x.report('Ruby') { ruby.script } x.report('LLRB') { llrb.script } x.compare! end
以下のようにパフォーマンスが改善することがわかる。
# x86_64 GNU/Linux, Intel(R) Core(TM) i7-4790K CPU @ 4.00GHz Calculating ------------------------------------- Ruby 2.449 (± 0.0%) i/s - 13.000 in 5.308125s LLRB 8.533 (± 0.0%) i/s - 43.000 in 5.040016s Comparison: LLRB: 8.5 i/s Ruby: 2.4 i/s - 3.48x slower
LLRBの設計はどうなっているか
C拡張として作成
どうやらYARVは最初C拡張として作られていたらしい。JITコンパイラをC拡張として作るのは大分無理があると思ってたけど、YARVがC拡張でできるならJITコンパイラもC拡張でできるのではないかと思ったのでそうしてみた。実際、CRubyのforkとして開発をするのに比べると、コアと疎結合になるだけでなく、bundlerとかbenchmark-ipsとかその辺のgemをカジュアルに使えるので開発はやりやすい。
しかし実際のところ、基本的な演算子のメソッドが再定義されてるかどうかを持つ変数のシンボルが当然exportされてないことに気付き、そこでCRubyに変更を加えず開発することは諦めた。ので、k0kubun/rubyのllrbブランチにしかインストールできない。それでも、シンボルのexport以外には何もしないという縛りを設けてあるので、今後CRubyのバージョンが上がっても追従はしやすい気がする。
YARVに手を加えない保守的なアプローチ
YARVは大分長い間運用されている信頼性のあるVMなので、仮にJITコンパイラを導入したとしても移行期間中はベースになるYARVがそのまま使える状態の方が安心できると思っている。なので、LLRBはYARV ISeqからコンパイルする方式を取り、またYARVのコア自体には一切変更を加えないようになっている。
じゃあそれでどうやってJITを達成しているかというと、YARVにはopt_call_c_function
という命令があり、「ネイティブコンパイルしたメソッドを起動。」という説明が書いてある。これをJITに使わない理由はない。
というわけで、LLRBは前述した方法でCの関数ポインタを取得してそれをopt_call_c_function
のオペランドとし、全てのメソッドを以下のようなISeqに変換する。
0000 opt_call_c_function 0002 leave
opt_call_c_function
はYARVの内部例外を扱える *1ので、この命令を使えば大抵のことは実装できるし、少なくとも僕がテストを書いた範囲ではthrowとかbreakとかreturnが動いている。
しかし、ISeqを書き変える方法だと考慮しないといけないことがある。それは、YARVが内部例外を補足するcatch tableがプログラムカウンターの位置に依存して内部例外をハンドルすることである。 プログラムカウンターが0か2にいればすむようにcatch table自体を書き変えると分岐できなくなるケースがありそうだったので、LLRBはcatch tableには手を加えず、逆にネイティブコードの中でプログラムカウンターを従来通り動かす方を選択した。
なので、プログラムカウンターがそのような位置にあっても適切にleave
できるよう、余った位置にleave
命令埋めをしている。これはshyouheiさんのDeoptimization Engineのnop埋めのパクリである。
正直それで問題が起きないのかはよくわかってないので他の人のツッコミ待ちだが、僕がテストを書いた範囲ではrescueとかensureが動いている。
サンプリングベースの軽量なプロファイラ
LLRBはプロファイラとしてサンプリングプロファイラを採用している。CPU時間で一定のインターバルおきにバックトレースのサンプリングを行ない、バックトレースの一番上に出現する頻度が高いものをコンパイル対象として選択する。
stackprofとかperfもサンプリングプロファイラであり、この手法自体は既に広く使われているため信頼性がある。
また、stackprofと同じくrb_postponed_job_register_one
APIを使う + GC実行中かどうかを判定していて、おそらくコンパイルしても安全と思われる瞬間にコンパイルが行なわれる。
低いコンパイルコスト
上述したように、CRubyのC関数はビルドプロセスでLLVM bitcodeにプリコンパイルされているので、LLRBがJITコンパイルを実行する際ランタイムでCの関数のパースやコンパイルを行なうオーバーヘッドは発生しない。 また、RubyのASTからコンパイルするわけではなく、ある程度コンパイルされたYARV ISeqからコンパイルを開始しているので、おそらくこれが一番コンパイルコストが低い手法になると思われる。
TODOs
- まだ全ての命令に対して最適化を実施できていないので、実際のアプリケーションで使うとパフォーマンスが劣化する。なので、まずはその対応から…
- ISeqのインライン化に対応したい
- まだコンパイルに対応してないYARV命令がいくつかある
expandarray
,reverse
,reput
,defineclass
,once
,opt_call_c_function
- コンパイル中に作るRubyのオブジェクトのうちGCを考慮できてない奴がある気がする
ビルド・使用方法
このへんを参照
気持ち
まだ大分未完成なんだけど、3か月くらい経ちモチベーションの維持が難しくなってきたので公開した。この辺で得られた知見をRubyKaigiとかで話せたらいいなと思っていたけど、まだ僕のCFPはacceptされていないので、まあ無理だったらRubyConfで会いましょう。
Ruby コミッターになりました
m_sekiさんとhsbtさんの推薦で、ERBのメンテナとしてRubyのコミット権をいただきました。
以下が初コミットです。
普段テンプレート言語Hamlの高速化やその更に高速な別実装Hamlitの実装をやっていてテンプレートエンジンの高速化に知見があり、ちょこちょこERBにも知見を還元したりしていたのですが、一昨日ふとERBの生成コードのiseqを眺めていた時に気付いてパッチを送った後、「入れるのめんどくさいし、ERBのコミッタやりますか」とお声がけいただいた形です。
というわけで、引き続き主に高速化の方面でERBのメンテナンスをやっていきますが、他にも以前僕がC拡張にしたHTMLエスケープとか、広い範囲でパフォーマンス改善をやれたらいいなと思います。
さっきのパッチに関連してto_sもメソッド呼び出しをバイパスできるようにしたら結構いろんなものが速くなるんじゃないかなと思ったんですが、ベンチを取ってみたところ効果が微妙だったので、コアの改善は難しいなあとか考えています。 *1
どうぞよろしくお願いいたします。
*1:前提として僕はメソッド探索をしなくなれば速くなると思っていて、メソッドキャッシュの影響はありそうなものの String#concat と String#to_s の呼び出しの間にどういう違いが発生するのかちゃんと調査していないのでわかっていない。
Treasure Data に入社しました
3月から Treasure Data で働いています。入社初日からタスクをアサインされ、RailsでAPIの開発をやりました。
なぜ Treasure Data に転職したのか
前職もやりたいことができて優秀な同僚に囲まれ文句ない環境だったのですが、アルバイト入社から数えるともう3年半が経っていたし、入社前にイメージしていたような仕事も大体経験できていました。
そのままいても良かったのですが、ある程度の間隔で新しいことに挑戦しないと成長は止まってしまうと思っているので職場ごと変えることも考え始め、以下のような観点から Treasure Data に転職することに決めました。
- エンジニアがユーザーになる仕事をしてみたい
- 僕は開発者が使うツールを作るのが好きで、技術を売っている会社の方がそういうものを作る機会が増えそう
- 正直あまりエンジニアリング以外に興味がないので、一般の人を対象にしたサービスを作っている会社より、エンジニアがユーザーな方が会社のビジネスに興味が持てる
- 前職だとあまりやらなそうなことに挑戦する選択肢が増えそう *1
- アメリカ (シリコンバレー) で働くチャンスを増やしたい*2
- 上記を踏まえて今後のキャリアを考えると、日本にいるよりはアメリカにいた方が楽しい人生が送れそう*3なので、チャンスがあれば移住したい
その他、転職先に求めていたこと
市場調査のため転職ドラフトに登録していた時にいろんな企業様から声をかけていただいたきとてもありがたかったのですが、上記に加え下記のようなことを考慮し決めさせていただきました。
- 自分が興味があったり得意な分野で尊敬できる人が多くいる
- 積極的にOSS活動や外部への発信をしている
- 流石に全部オープンにするべきとは思ってないけど、何かすごい成果を出せたら社外に自慢する機会がないとモチベーションが維持しにくいと思っていて、そうしている人が多い会社の方がきっと楽しく仕事をしてるんだろうなと思う
- 待遇が良い
- 技術的な成長だけが人生じゃないし、総合的に見て幸せになるにはやっぱりお金も必要
気持ち
偉そうなことをいっぱい書いていますが、まだまだとても経験の浅い身ですので、早く成果を出して同僚の方に認めていただけるようがんばります。