valid,invalid

関心を持てる事柄について

ISUCON10 予選敗退した

ISUCON10にソロチームBPM200で参加し、最終スコア770で予選敗退しました。通過スコアには大きく届きませんでした。

戦略

ソロなので時間は絶対に足りない、という前提のもと予めフォーカスするポイントはある程度決めておきました。

大方針としてはなるべく1台で勝負する、ということ。複数台構成に持っていくのは自分のスキル的に時間がかかりそうだし、過去問で素振りしてたとき、問題によってはアプリケーションレイヤでの改善に絞ってもそれなりに伸ばせると思ったため。今回もそうだと良いな〜と思ってましたが…現実は非常。

また、普段使わない解析ツールをインストールしたりログ設定を行ったりするのにかける時間もないため、それなりに手慣れたNewRelicでボトルネックの推移を見ながらチューニング対象を決めていく、というのも予定していました。

やったこと

環境準備

ssh設定確認したりgit initしたりマニュアルを読んだりマシンスペックを確認したり。

bot対策の概念を知ったけどボトルネックなのかどうかわからないので初手では手を出さないことに。

NewRelic導入

素振っていたとおり初手でNewRelicを入れました。導入後、最初に回せたベンチマーカーで出たスコアが280ぐらい。

ベンチマーカー流しつつtopコマンド眺めているとDB, CPU律速で、アプリケーションとしては物件検索と椅子検索がmost time consumingとNewRelicから理解しました。

f:id:ohbarye:20200920145442p:plain
NewRelic便利

インデックス追加

初期実装ではインデックス皆無なので物件検索と椅子検索のコードを読みながらインデックスを追加しました。複雑なパターンすべてに対応はできないだろうと思いながらも、Nginxのアクセスログを見ると単一条件のクエリがそこそこあり、スコアがちょっとだけ伸びました。

このときMySQL5.7の降順インデックス周りの挙動を知らず、EXPLAINしてfilesortになっているのはなんでだろう…とか思ってました。

範囲検索からID検索へ(失敗)

物件検索原因は範囲検索に対して有効ではないアプローチと考え、rentやwidthのidで検索できるように実装を変更。

generated columnにするとかadd columnして一括updateするとかすれば良かったのですが、何を血迷ったか、rent, width, heightのマスタテーブルと中間テーブルを作り、initialize処理の中でレコードをインサートしまくるという方針で実装してしまいました。

その実装に合わせて検索や物件追加のエンドポイントのコードも直す大掛かりな作業になったのですが案の定initializeでタイムアウトする結果になり…泣く泣くrevert。

ぜんぜんISUCON向きの実装ではないし今思えばいろいろツッコミどころがあるのですがここに3時間ぐらい使ってしまったのがこれ以降の思考に大きく影響を与えることに。

Bulk insert

検索周りで数時間溶けた失敗が心に来たので少しでも稼げそうなところを見つけては直してくことにしました。

物件と椅子のN+1インサートはまとめてやることに。このエンドポイントはボトルネックでもなんでもないということを知りつつ…!

物件検索結果をメモリキャッシュ

Nginxのアクセスログからだいたい同じクエリが飛んでくることはわかっていたので、検索条件で結果をキャッシュすることにしました。

これはちょっと効きました。

なぞって検索のN+1なくす(失敗)

最後の2時間ぐらいでもまだ物件検索と椅子検索が支配的ではあったもののなぞって検索が上位に追い上げてきたので見てみることに。わかりやすいN+1があったものの位置情報まわりで何してるかを理解するためにMySQLのドキュメントを読むのにそこそこ時間を使ってしまいました。

f:id:ohbarye:20200920154953p:plain
nazotte

POINT型カラム追加を試したり、クエリをマージしてN+1を直したはずがベンチマーカーが失敗するようになり、デバグを途中で断念…。

ここも成果ゼロにも関わらず2時間ぐらい溶けてました。

low_priced API用のindex追加

安い椅子と物件を検索するAPI (low_priced) もそこそこ時間を食っていてindexが効いてないことを見つけたので追加。

bot対策(失敗)

残り時間がなく、すぐに点数が伸ばせそうなものとしてbot対策があったことを思い出す。Nginxの設定をググりながら入れてみる。

が、スコアがさほど伸びていないため最後の時点でもbotからのアクセスは来てなかったはず…。明らかに優先度判断ミスっていました。

その他

最後の1時間で細々とした仕上げをしていました。

  • RACK_ENV=productionを設定
  • NewRelicをオフにする
  • Nginxなどの各種ログをオフにする
  • 再起動して問題ないか確認

反省

明らかに点数を稼げる大型の問題には気付いたものの、物件検索の改善アプローチを間違えて時間を溶かしてしまってからは明らかに焦ってしまい、冷静な優先度判断や時間配分ができずに終わってしまった…という感じです。小さい点数稼ぎはできてもブレイクスルーがなかった。

そうなってしまった原因はMySQL周りの力不足。generated column、降順インデックスだけでなく位置検索・空間インデックスあたり(この辺は他の参加者も苦戦していそうでしたが)の知識がなく誤ったアプローチをしてしまったり、DB分割はアイデアすら出てこなかったな〜。

感想

結果は惨敗でしたが自分の力不足を強く感じることができたうえめっちゃ学びがあったので参加できてよかったです。

噛みごたえのある良問を作ってくださったyosuke_furukawaさん、過去にない規模のチーム数にも関わらず捌き切ってくださった運営の皆様、スポンサーその他関わった大勢の方に大感謝です。

来年も楽しみにしています。

ISUCON9 予選の過去問で予選突破スコアを出すまで練習

ISUCON9 予選の過去問でNew Relicを使う - valid,invalidの続きです。

この記事は1台のVM上でベンチマーカーすべて含むstandalone構成のまま、予選突破スコアを超える12260点を出すまでにやったことのメモ。(ISUCON 9の予選突破スコアは9650点: 参照)過去問のため解説は公式ですでに公開されているので深くは触れません。

以下、(点数) やったこと

(1510) 初期状態

MySQL slow query log出力とNew Relicを設定したあとだったかも。

(1810) index追加

topコマンドでベンチマーカーの実行時の状況を見るとそれなりにmysqldに負荷がかかっていたのでまずはDBのチューニングから。

MySQL slow query logを出力し、average timeが遅いクエリを見つけてindexをいくつか貼った。

-- 01_schema.sql
CREATE INDEX created_at ON items (created_at);
CREATE INDEX seller_id ON items (seller_id);
CREATE INDEX buyer_id ON items (buyer_id);

このあとslow query logはオフにした。

(2010) shippment/status呼び出しをPOSTからGETへ

New Relicを見たところ、/users/transactions.jsonがメチャ重い。特にAPI呼び出し。

マニュアルを読んだらGETのAPIなのになぜかPOSTで呼び出したのが気になったので直して走らせたらなぜかちょっと上がったが誤差か。

(2620) categoriesをインメモリにしてfindのN+1地獄を回避

#POST /initialize時に全部読み込み、定数時間で取れるようHashでメモリに保存しておく。

(3210) items.map内のusers N+1を回避

categoriesはインメモリ化できたが、itemsに紐づくuserは厳しそう。loopの前にusersをガッと取得してループ内でlookupするようにした。joinでも良かったけど。

(7860) /user/transactions.jsonで外部APIの呼び出しは不要なので止める

DBにある値を表示すればそれで事足りた。ここで一気に伸びた。

(9160) indexを複合indexに修正した

WHERE buyer_id = ? ORDER BY created_at みたいにindex効いてるクエリのsortが大丈夫か?と思ってEXPLAIN見たらExtraにUsing filesortが出てた。複合インデックスにして潰せるようにした。

-- 01_schema.sql
CREATE INDEX created_at ON items (created_at);
CREATE INDEX seller_id ON items (seller_id, created_at);
CREATE INDEX buyer_id ON items (buyer_id, created_at);
CREATE INDEX category_id ON items (category_id, created_at);

ORDER BY created_atはかなりたくさんあったので結構スコア伸びた。

(7660) campaign=1にした

リクエストが爆増して全体的に耐えられなくなり、下がった。 APIをスケールアウトしたいがstandalone構成だと難しいので戻した。   {"pass":true,"score":7660,"campaign":1,"language":"ruby","messages":["GET /users/500.json: リクエストに失敗しました(タイムアウトしました)","POST /login: リクエストに失敗しました(タイムアウトしました)","POST /ship_done: リクエストに失敗しました (item_id: 50019)(タイムアウトしました)"]}

(9160) /user/transactions.jsonでN+1を解消

もっと伸びるはずが、対して変わらなかった。

(8960) /user/transactions.jsonでORクエリを解消

なぜか下がった。

New Relicを見るとこの時点でボトルネック/buyだったので誤差程度の修正にしかなっていないと判断。

(9160) /buyでuserをselect for updateする必要ない

ちょっと上がったけど誤差か。

(9460) /buyで外部リクエストを並列化

ちょっと伸びた。/buyボトルネックでなくなり、/items/:item_id.jsonに移ったことがNew Relicで確認できた。

(9660) /items/:item_id.jsonでseller, buyerをjoinで一発で取ってくるようにする

ようやく予選突破スコアまでいった 🎉

(9660) /items/:item_id.jsonでtransaction_evidence, shippingをjoinで一発で取ってくるようにする

変わらず。

(8760) campaign=1にした

耐えられないのが/loginだけになった。ログイン用の別サーバを用意するとかできたらもっといけそう。

とりあえず戻す。 {"pass":true,"score":8760,"campaign":1,"language":"ruby","messages":["POST /login: リクエストに失敗しました(タイムアウトしました)"]}

(12260) New Relicをオフにする

require 'newrelic_rpm'を消す。めっちゃ上がった!

所要時間

環境構築から数えたらたぶん15時間ぐらいかけていた(しかもぶっ通しではなく日をまたいでの細切れ)。なので本番でこのスコアが出せたかというと確実にNO…

学び

  • New RelicでボトルネックとなるAPIエンドポイントの移り変わりが確認できて便利
  • standalone構成でも地道にチューニング重ねれば予選突破スコアが出た
  • 計測系ツールをオフにするのを忘れてはいけない(伸びなくなったタイミングでオフるとちょっと伸びてボーナス感がある)

複数台構成にするとかNginxやMySQLの設定いじるとかのインフラ寄りは苦手なので、アプリケーション中心の改善でもこれだけ伸びる出題だと良いな〜

攻めの採用人事

元同僚と先日ビデオチャットしてるときに某社の某採用担当人事の方の話になり「あの方は良いですよ、"攻め"の採用人事ですから」という話になった。

その場では「たしかに"攻め"の人事は一緒に働きやすいですね〜」と話して「せやな」って感じで終わったのだが、このたび求職活動で色んな人事の方とやり取りする中で「あ〜この方は"攻め"側だろうな〜」と思うシーンがいくつかあった。


で、"攻め"ってなんなのかってことなのだが…

タスクベースで仕事をこなしていくのでなく成果にコミットする

ミッションや視座が文脈を意識せずにこなせるタスクレベルでなく数段高いところにあって、課題発見・解決の範囲が不確実性も難易度も高いところにある

返信が早いとか丁寧とか誠実とかそういった振る舞いのウラに「なぜそうすべきか」が通底している


「N人採用する」が達成したいことではなく「N人のどのような人をどのタイミングで採用すれば事業・組織への貢献が最大化するのか」を一緒に考えてくれ、事業部やエンジニアリング組織と同じゴールを見てるので話がスムーズ。

そういう方とは専門性が違ってもお互いをシンプルにリスペクトできるし、「採用人事のことをもっと知りたいんです〜」と聞けば本をおすすめしてくれたり時間を取って解説してくれたりする*1。一緒に働いていて気持ちが良い。*2

求職活動の応募者としてもやり取りの中で「"攻め"の採用人事だな〜」と確信できたり、他職種との信頼関係がしっかり構築できている様子が透けて見える会社は魅力的に映った。


書いてて思ったけど、誰も攻撃してはいないので適切な言葉は"攻め"なのか?と思ったが"攻め"には物事を無難にすませず、積極的な態度をとるの意もあるのだった。

つまり、自分の領分を決めて閉じこもっているのでなく職域を超えてゴール達成にあらゆる手段を使う、ってことだから"攻め"なんだな。*3

*1:余談: おすすめしてもらったうちの1冊はこれ。

人事と採用のセオリー

人事と採用のセオリー

*2:エンジニアカルチャーに踏み込んでプログラミングができるようになったり、SNSブランディング頑張ったりするのも大事だけど、上述のように仕事への向き合い方がしっかりしている事のほうが個人的にはより大事かな〜

*3:そういう意味では採用人事に限った話ではないけど、採用人事の話題をきっかけに思ったこと

ISUCON9 予選の過去問でNew Relicを使う

あとN+1回寝るとISUCONです。私はいま予選突破の夢を見つつNew Relicを素振っています。

本記事ではNew Relicの説明は特にしません。また、Rubyで実験しててちょっとハマったところを掘り下げたログが実はメインです。

(2020-07-22追記) 本記事を書いたときには気付いていなかったのですが同じことを公式のWebinarでも行うようでした。(7月30日開催|せっかく無料なのでISUCONでNew Relicをスッと使う方法RubyだけでなくGo, Pythonの設定の話やその他盛りだくさんなので期待!!

New Relic meets ISUCON

New Relicは業務で過去に使っていて大変便利だったのでISUCONもこれぐらい楽にメトリクス見てぇな〜と思っていたら、自腹契約して使うという道もあったのだということをISUCON9優勝の白金動物園mirakuiさんによる振り返りで学びました。

今回はたまたま別件で間違って Annual Subscription を自腹契約してしまった New Relic の個人ライセンスがあったので、それを使いました。分析できることは自前のスクリプトでやってるときとだいたい同じなんだけど、僕だけじゃなくてメンバーがベンチ結果分析に参加したり、同じ数字を見て語れるところが便利でした チーム白金動物園として ISUCON 9 予選を通過しました - 昼メシ物語

それはそれで良い話なのですが、なんと2020年のISUCON10ではNew Relic社がスポンサーについており、New Relic全機能を大会前後の期間中制限なく利用できるライセンスを参加者全員に提供するというすごいことになっています。自腹契約しなくても自作スクリプト頑張らなくても分析が始められます。(気に入ったらお勤め先でエンタープライズ契約しよう!)

New Relicアカウント取得

以下に従って進めるだけです。

ただし、過去に登録済のメールアドレスで登録するとメールが永遠に届かないことがある(自分がそうだった、昔いつか登録したのを忘れてた)のでその場合は別メールアドレスやエイリアスを使うなどして回避しましょう。

blog.newrelic.co.jp

isucon9-qualify環境構築

本題ではないのでさらっと。

やり方はなんでも良いのだけど僕は https://github.com/matsuu/vagrant-isucon を使い、Vagrantで環境構築してます。8以前の過去問に比べると言語バージョンとか諸々が比較的新しいのでVirtualBoxを起動しておいて、vagrant upすれば動きました。

$ git clone https://github.com/matsuu/vagrant-isucon
$ cd vagrant-isucon/isucon9-qualifier-standalone
$ vagrant up

ssh仮想マシンに繋いだら、最初はGolangのサービスが起動しているのでRubyに切り替える。

$ vagrant ssh

$ sudo systemctl stop    isucari.golang.service
$ sudo systemctl disable isucari.golang.service
$ sudo systemctl start   isucari.ruby.service
$ sudo systemctl enable  isucari.ruby.service

ベンチマークテストが完走すれば環境構築OK。

$ cd /home/isucon/isucari
$ bin/benchmarker
(ログ省略)
{"pass":true,"score":1410,"campaign":0,"language":"ruby","messages":[]}

New Relicの導入

まずは公式通り進めていきます。ログインするとapplicationの各言語ごとのsetup手順が出てきます。こんな感じのURLです (以下setup pageと呼ぶ)。 https://rpm.newrelic.com/accounts/xxx/applications/setup#ruby

さらにframework別の手順も Sinatra instrumentation | New Relic Documentation などに記されていますので一読すればOK。

Install gem

# /home/isucon/isucari/webapp/ruby/Gemfile
source 'https://rubygems.org'

+gem 'newrelic_rpm'
gem 'puma'
gem 'sinatra'
...
$ cd /home/isucon/isucari/webapp/ruby
$ /home/isucon/local/ruby/bin/bundle install

applicationからgemを読む

Railsと違って明示的にrequireする必要があります。Sinatra instrumentation | New Relic Documentation によるとSinatraの読み込み直後におく必要があるようです。

# /home/isucon/isucari/webapp/ruby/lib/isucari/web.rb
require 'json'
require 'securerandom'
require 'sinatra/base'
+require 'newrelic_rpm'
require 'mysql2'

設定ファイルを配置

newrelic.ymlを setup page からダウンロードし、application root (/home/isucon/isucari/webapp/ruby/)に置く。

その他

New Relic関連のlog

デフォルトだと /home/isucon/isucari/webapp/ruby/log/newrelic_agent.log にはログが吐かれます。設定がちゃんとできてるかな?と思ったらログに以下のような記述があるか見てみましょう。

[2020-07-22 11:58:45 +0000 vagrant (32128)] INFO : Reporting to: https://rpm.newrelic.com/accounts/:account_id/applications/:application_id
ライセンスキーの扱い

デフォルトではnewrelic.ymlに記載されているが、secret情報をgit historyに含めてしまうのは好ましくないです。 (git pushしてrepositoryをpublicにすると世界中の人がライセンスを利用できてしまう)

newrelic.ymlからlicense_key: xxxxxの行を消し、プロセス起動時に環境変数として与えるようにしておくと安心です。

NEW_RELIC_LICENSE_KEY=xxxxx

Ruby agent configuration | New Relic Documentation


ここまでは簡単ですね。ここからちょっと悩みました。

benchmarkerが失敗する

上述の手順でセットアップした直後にベンチマーカーを実行するとfailします。

$ bin/benchmarker
(ログ省略)
2020/07/21 08:58:07 main.go:129: cause error!
   {"pass":false,"score":0,"campaign":0,"language":"ruby","messages":["POST /login: got response status code 500; expected 401","GET /settings: got response status code 500; expected 200","GET /settings: got response status code 500; expected 200","GET /items/18697.json: got response status code 500; expected 200","POST /login: got response status code 500; expected 200","GET /items/50000.json: got response status code 500; expected 200","GET /upload/336bd3af5fbb247a55354184a3dfb35f.jpg: got response status code 500; expected 200","POST /items/edit: got response status code 500; expected 200 (item_id: 409)"]}

期待するレスポンスが返っていない。アプリケーションの挙動を変えてしまった。

省略したログからスタックトレースを抜粋して貼ったのが以下。

"status code: 500; body: RuntimeError: can't add a new key into hash during iteration
    /home/isucon/isucari/webapp/ruby/.bundle/ruby/2.6.0/gems/sinatra-2.0.7/lib/sinatra/base.rb:31:in `accept'
    /home/isucon/isucari/webapp/ruby/.bundle/ruby/2.6.0/gems/sinatra-2.0.7/lib/sinatra/base.rb:46:in `preferred_type'
    /home/isucon/isucari/webapp/ruby/.bundle/ruby/2.6.0/gems/sinatra-2.0.7/lib/sinatra/show_exceptions.rb:92:in `prefers_plain_text?'
    /home/isucon/isucari/webapp/ruby/.bundle/ruby/2.6.0/gems/sinatra-2.0.7/lib/sinatra/show_exceptions.rb:26:in `rescue in call'
    /home/isucon/isucari/webapp/ruby/.bundle/ruby/2.6.0/gems/sinatra-2.0.7/lib/sinatra/show_exceptions.rb:21:in `call'
    /home/isucon/isucari/webapp/ruby/.bundle/ruby/2.6.0/gems/newrelic_rpm-6.12.0.367/lib/new_relic/agent/instrumentation/middleware_tracing.rb:101:in `call'
    /home/isucon/isucari/webapp/ruby/.bundle/ruby/2.6.0/gems/sinatra-2.0.7/lib/sinatra/base.rb:194:in `call'
    /home/isucon/isucari/webapp/ruby/.bundle/ruby/2.6.0/gems/newrelic_rpm-6.12.0.367/lib/new_relic/agent/instrumentation/middleware_tracing.rb:101:in `call'
    /home/isucon/isucari/webapp/ruby/.bundle/ruby/2.6.0/gems/sinatra-2.0.7/lib/sinatra/base.rb:1950:in `call'
    /home/isucon/isucari/webapp/ruby/.bundle/ruby/2.6.0/gems/sinatra-2.0.7/lib/sinatra/base.rb:1502:in `block in call'
    /home/isucon/isucari/webapp/ruby/.bundle/ruby/2.6.0/gems/sinatra-2.0.7/lib/sinatra/base.rb:1729:in `synchronize'
    /home/isucon/isucari/webapp/ruby/.bundle/ruby/2.6.0/gems/sinatra-2.0.7/lib/sinatra/base.rb:1502:in `call'
    /home/isucon/isucari/webapp/ruby/.bundle/ruby/2.6.0/gems/newrelic_rpm-6.12.0.367/lib/new_relic/agent/instrumentation/middleware_tracing.rb:101:in `call'
    /home/isucon/isucari/webapp/ruby/.bundle/ruby/2.6.0/gems/rack-2.0.7/lib/rack/tempfile_reaper.rb:15:in `call'
    /home/isucon/isucari/webapp/ruby/.bundle/ruby/2.6.0/gems/rack-2.0.7/lib/rack/lint.rb:49:in `_call'
    /home/isucon/isucari/webapp/ruby/.bundle/ruby/2.6.0/gems/rack-2.0.7/lib/rack/lint.rb:37:in `call'
    /home/isucon/isucari/webapp/ruby/.bundle/ruby/2.6.0/gems/rack-2.0.7/lib/rack/show_exceptions.rb:23:in `call'
    /home/isucon/isucari/webapp/ruby/.bundle/ruby/2.6.0/gems/rack-2.0.7/lib/rack/common_logger.rb:33:in `call'
    /home/isucon/isucari/webapp/ruby/.bundle/ruby/2.6.0/gems/sinatra-2.0.7/lib/sinatra/base.rb:231:in `call'
    /home/isucon/isucari/webapp/ruby/.bundle/ruby/2.6.0/gems/rack-2.0.7/lib/rack/chunked.rb:54:in `call'
    /home/isucon/isucari/webapp/ruby/.bundle/ruby/2.6.0/gems/rack-2.0.7/lib/rack/content_length.rb:15:in `call'
    /home/isucon/isucari/webapp/ruby/.bundle/ruby/2.6.0/gems/puma-4.1.0/lib/puma/configuration.rb:228:in `call'
    /home/isucon/isucari/webapp/ruby/.bundle/ruby/2.6.0/gems/puma-4.1.0/lib/puma/server.rb:664:in `handle_request'
    /home/isucon/isucari/webapp/ruby/.bundle/ruby/2.6.0/gems/puma-4.1.0/lib/puma/server.rb:467:in `process_client'
    /home/isucon/isucari/webapp/ruby/.bundle/ruby/2.6.0/gems/puma-4.1.0/lib/puma/server.rb:328:in `block in run'
    /home/isucon/isucari/webapp/ruby/.bundle/ruby/2.6.0/gems/puma-4.1.0/lib/puma/thread_pool.rb:135:in `block in spawn_thread'"

RuntimeError: can't add a new key into hash during iteration

名前の通り、Hashをイテレーションで処理している破壊的な変更として新しいkeyを挿入しようとすると起きるRuntimeErrorとのこと。わかりやすいエラーメッセージだ!!

で、Sinatraの該当バージョンの該当コードを見るのですがイテレーションなんてしてないんですよねぇ…。

ググってみたところマルチスレッド環境下だと、別スレッドでイテレーションしてる最中に上記の破壊的なコードが実行されたりすると起きる雰囲気がある。

can't add a new key into hash during iterationが謎の状況で発生しているように見えるときは - 猫型の蓄音機は 1 分間に 45 回にゃあと鳴く

エラーが起きるエンドポイントに単発でリクエストを送ってもRuntimeErrorが再現しないのと、そもそもログを見ると色んなエンドポイントで起きているので単一のエンドポイントの問題でなくRack middlewareとかそのへんの問題な気がしてくる。

WEBrickに切り替える

マルチスレッドの問題なら非マルチスレッドのWeb server試すか〜と思って起動コマンドを変更してみます。

systemctlに登録してるserviceのコマンドを書き換えてWEBrickにしてみる。知らなかったけど、rackup -sオプションで何も指定しないとrack gem内の実装で書かれている優先度順に試行されるのだった(puma > falcon > webrick)。

# /etc/systemd/system/isucari.ruby.service
[Unit]
Description = isucon9 qualifier main application in ruby

[Service]
WorkingDirectory=/home/isucon/isucari/webapp/ruby
EnvironmentFile=/home/isucon/env.sh

-ExecStart = /home/isucon/local/ruby/bin/bundle exec rackup -p 8000
+ExecStart = /home/isucon/local/ruby/bin/bundle exec rackup -s WEBrick -p 8000

コマンドいじったのでdaemon-reloadしてからrestart、からのベンチマーカ実行すると…

$ sudo systemctl daemon-reload
$ sudo systemctl restart  isucari.ruby.service
$ bin/benchmarker
{"pass":true,"score":1410,"campaign":0,"language":"go","messages":[]}

pass!!

ヨッシャ、New Relicのwebで各種メトリクスや統計が見られるようになった!!

f:id:ohbarye:20200722020636p:plain
顧客が本当に欲しかったもの

余談: 初期化処理の#POST initializeまで記録されてて邪魔なので newrelic_ignore '/initialize' を書いておくと見やすくなるかも。

余談2: New Relicの力をフルに使うとTransaction traceなどの機能でクエリの内容やかかった時間が見られるようになりますが、 ISUCON9予選のRuby実装そのままではDatabaseへのクエリでかかっている時間は解析できません。クエリを実行するのにActiveRecordなどではなくMysql2::Clientを直接使っているためです。(New Relic agentがフックを仕込んでいない)

(追記2020-07-30) New RelicのセミナーでMySQL2::Clientを拡張してクエリを計測する方法の説明がありました。コードはこれ


とはいえpuma使えないって…そんなことあるか…?

ちなみにwebrickに切り替えたらベンチマークの結果も早速下がってます。

bundle exec rackup -> bundle exec puma

mtsmfmさんから「本当にマルチスレッドの問題なのか、threads数変えて試してみては?」との本質的な指摘をもらったので試してみる。

pumaのconfig file書くよりコマンドを書き換えたほうが早いのでまたisucari.ruby.serviceをいじる。

そういえばpumaコマンドじゃなくてrackupだったな…まずはpumaコマンドでちゃんと動くか見てみるか〜という感じでthreads数を変えずにコマンドだけ変更。

# /etc/systemd/system/isucari.ruby.service
-ExecStart = /home/isucon/local/ruby/bin/bundle exec rackup -s WEBrick -p 8000
+ExecStart = /home/isucon/local/ruby/bin/bundle exec puma -b tcp://0.0.0.0:8000

で、これで再起動したあとにベンチマーカーを試しに実行すると…なんとpassしました。

ええ… rackuppumaコマンド変えただけでSinatraの内部でRuntimeError起きるようになる、そんなことあるか…?

rackupとpumaの違い

挙動が違うからにはまぁ、何か違うのだろうということでrack/rack中心にコードリーディングしていく。

server (Puma::Serverインスタンス) をrunするところwrapped_appってなんや、何で包んどるんや。

server.run(wrapped_app, **options, &block)

その正体はこちらappmiddleware包み。

      def build_app(app)
        middleware[options[:environment]].reverse_each do |middleware|
          middleware = middleware.call(self) if middleware.respond_to?(:call)
          next unless middleware
          klass, *args = middleware
          app = klass.new(app, *args)
        end
        app
      end

      def wrapped_app
        @wrapped_app ||= build_app app
      end

結局素材はなんなのか、がわかるのがこちら

      def default_middleware_by_environment
        m = Hash.new {|h, k| h[k] = []}
        m["deployment"] = [
          [Rack::ContentLength],
          logging_middleware,
          [Rack::TempfileReaper]
        ]
        m["development"] = [
          [Rack::ContentLength],
          logging_middleware,
          [Rack::ShowExceptions],
          [Rack::Lint],
          [Rack::TempfileReaper]
        ]

        m
      end

      def middleware
        default_middleware_by_environment
      end

あ〜ここで完全に一部を理解しました(?)。

RACK_ENVもしくはrackup -E optionの値がdeploymentdevelopmentの場合 (デフォルトはdevelopment)、rackupコマンドで起動されるappにはいくつかのRack middlewareが足された状態になる、と。

上述のスタックトレースをよく見ると確かにこのmiddlewareを介して呼ばれてます。

rackup -E foo

じゃあこのmiddlewareが足されない状態なら大丈夫なのでは?ということで環境をfooにしてrackupします。deploymentdevelopment以外ならなんでも良いはず。

# /etc/systemd/system/isucari.ruby.service
-ExecStart = /home/isucon/local/ruby/bin/bundle exec puma -b tcp://0.0.0.0:8000
+ExecStart = /home/isucon/local/ruby/bin/bundle exec rackup -E foo -p 8000

これで再起動したあとにベンチマーカーを試しに実行すると…予想通りpassしました。

ウォーッ、rackupdevelopment環境で足してくるRack middlewaresと、マルチスレッドと、newrelic_rpmの組み合わせで再現する問題だということがわかった!!

わかったところで力尽きたのでこれをNew Relic forumかnewrelic_rpmにレポートして手を引きます。

余談: app_nameは環境に応じて変わる

newrelic.ymlの設定次第では環境 (= RACK_ENVまたは-E optionで指定する値) によってapp_nameが変わります。特定のapp_nameのpageだけをウォッチしていて「あれ〜データ来ないな〜」とならないよう注意。

デフォルトの設定だとこんな感じでした。

  • My Application (Development) => -E optionなし = デフォルトのdevelopment
  • My Application => development, staging 以外

2つのアプリケーションが一覧に出ているので見たいほうを見ましょう。

gyazo.com

まとめ

  • New Relicは便利
  • ISUCON9の過去問の構成のとき、Rubyの実装にNew Relicの設定を入れるだけだとベンチマークテストが失敗する
  • 以下のいずれかで、特定のRack middlewareとnewrelic_rpmの組み合わせを回避すれば成功する
    • WEBrickを使う (bundle exec rackup -s WEBrick)
    • environmentdevelopment以外にする (bundle exec rackup -E production etc.)
    • pumaコマンドで起動する (bundle exec puma)

まぁ、本番だったら時間も限られているのでこんな風にコードリーディングなんてせずに見つかったワークアラウンドのどれかで回避しているだろうとは思いますが、本番で予期しないことが起きると焦って一日が一瞬で終わるので利用したいツールの素振りを予めしておくのは大事ですね。

また、New Relicは特に設定せずとも突っ込むだけでアプリケーションのモニタリングがめちゃ捗りますが、デフォルトのアプリケーション実装だとスロークエリ分析までは届かなかったり、システム全体のボトルネック特定は難しいかもしれません(特定エンドポイントの外部APIリクエストが重い、とかはわかる)。topコマンドやMySQL slow query logなど古馴染みのツールとうまく併用して最大限役立てたいところです。

詳解 システム・パフォーマンス

詳解 システム・パフォーマンス

  • 作者:Brendan Gregg
  • 発売日: 2017/02/22
  • メディア: 単行本(ソフトカバー)

ブロックチェーンとDapp開発の基礎を学んだ

ブロックチェーンについては、2017年末の仮想通貨バブル時代にちょっと興味が湧いたものの投機に熱中していて技術的な理解は全然していなかった。

求職活動にて見知ったことなのだが、ここ数年でブロックチェーン技術を活用するスタートアップが台頭してきただけでなく、金融・不動産・保険業などの大手企業が実証実験に乗り出している。直近で業務として触るところまでいかなくても、将来的に利活用する製品やサービスに触れる機会があろう、ということで再訪してみた。

期間は2週間ほどで集中的に、おおよそ50時間ぐらいかけて基礎の基礎を学んだ。

学んだ成果

技術の根幹となる用語や概念や取り巻く環境を少しは理解することができた。

結果として、関連するニュースを読んで「わかる!」というポイントが見つかったり、「これはXXを理解しないとわからないのだろう」と、何がわからないかに気付ける機会ができてきた。

たとえば直近、Twitterアカウントの大規模な乗っ取りがあり、Bitcoinトランザクションを通じてメッセージを発したというニュースがあった。

Twitter不正投稿の首謀者、ビットコインブロックチェーンで「メッセージ」を発信

この記事を読み、

  • メッセージはどうやって埋め込んだのか?*1
  • この送金取引の結果はBitcoinブロックチェーン上にはどのように格納されているのか?*2
  • 不当に得たBitcoinを現金化するのは難しいと考えられるがそれはなぜか?*3

等々。こうした疑問に答えられたり、新たな疑問が湧いてくるようになったりした。


また、スマートコントラクトについて、ちょっとしたDappを書いてローカルのEthereumネットワークにデプロイしたり、Webブラウザで動作するフロントエンドのアプリケーションの実装をしてみた。

production useには程遠いまでも、「スマートコントラクトってよく聞くけど何?」というところから簡易な設計と実装を経て、スマートコントラクトとDappについて手触り感を伴うイメージを持つことができた。

やったこと

本当に何もわからなかったのでまずは体系的な理解から入ることにした。

本を読んで概観を掴む

まずは本2冊読んでみた。いずれも別記事にて感想を書いた。

おすすめはブロックチェーンのしくみと開発がこれ1冊でしっかりわかる教科書』感想

『スマートコントラクト本格入門―FinTechとブロックチェーンが作り出す近未来がわかる』感想

未読だが、評判を見たところ更に一歩先に理解を深めたければ『Mastering Bitcoin』や『Mastering Ethereum』あたりが良さそうだった。

ゲーム感覚でSolidityを学ぶ

実際にスマートコントラクトを自分で書いてみるため、専用のプログラミング言語であるSolidityを学ぶことにした。

CryptZombies*4なるチュートリアルがあるので"Beginner to Intermediate Smart Contracts"コースを全部やってみた。

cryptozombies.io

オンラインエディタでコードを編集しつつ、ステップバイステップで進められる。Solidity文法の基礎、スマートコントラクトにおける基礎概念がDappのコード上でどのように表現されるのか、WebブラウザからどのようにDappと連携するのかを学ぶことができた。

f:id:ohbarye:20200719000211p:plain
奇妙なゾンビ軍団を従えることができる

最終的にどういうものができるのかイメージがあまり湧かないまま進んでいく点と、(日本語版だと)Solidity versionがやや古いのが難点と感じたが、基礎をおさえる意味合いではさほど関係なさそうだった。

Solidityは静的型付け言語でJavaScript"風"のため書き始めるハードルはさほど高くなかった。とはいえ、作法やプラクティスみたいなところで悩むこともあったのだが公式ドキュメントEthereum Stack Exchangeである程度解消できた。

余談: 仮想マシン、EVM

Solidityで書かれたスマートコントラクトはEVMと呼ばれる仮想マシン上で実行される。EVMはEthereum上でスマートコントラクトを実行するためのEthereum独自の仮想マシンであり、Solidityのような高級言語コンパイル時にEVMバイトコードになる。

つまり、JVMや.NET等の先行するVMが知らしめたように、EVMバイトコードコンパイルさえできればどんな言語でスマートコントラクトを実装しても構わないということだ!

実際にSolidity以外にもEVMで動作するプログラミング言語はEthereum Wiki (https://eth.wiki/en/concepts/evm/ethereum-virtual-machine-(evm)-awesome-list) をチラッと見ただけでも7つあった。Solidityの次に有力なのはPython"風"の文法を持つVyperだろうか。

また、言語だけでなくEVM自体の実装も複数存在する。いくつかはすでに開発が停止しているようだが…。

この辺の動向を見守るのも面白そうだ。

Dappを書いてみる

ローカルでEthereumネットワークを立ち上げ、このネットワーク上にDaapをデプロイしてみた。

『スマートコントラクト本格入門―FinTechとブロックチェーンが作り出す近未来がわかる』にてgethを使ったネットワークの操作を学んだが、Ganacheを使うことでとても快適になった。

Web開発的にいえばテストデータ(seed data)が予め突っ込まれているデータベースがシュッと立ち上がり、実行されたUPDATE/INSERT(トランザクション)が全部記録として残り、さらにそれらをGUIで確認できる、みたいな感じ。

f:id:ohbarye:20200718235021p:plain
Ganache公式サイトより

CLIで操作できるものもあり、Docker imageも用意されている。慣れてきてGUI不要だったりDocker Composeでまとめて立ち上げたいならCLIを使えば良い。

また、Dapp開発のフレームワークとしてはTruffleを使ってみた。CLIでプロジェクトを立ち上げる(≒rails new)ことができ、デプロイやマイグレーション(≒rails migrate)、テストの実行を行うことができる。テストはSolidityでも書けるし、JavaScriptchaiやmocha風に書くこともできる!

Web開発との差分

静的型付け言語でプログラミングできてテストも書けてデプロイパイプラインを作れるCLIツールもあって…なんだかWeb開発とさほど変わらないのか?と思ったがそんなことはなかった。

Web開発と似てる部分もあるがだいぶ違うので「あ〜わかるわかる」と理解ったふりをせずに謙虚な姿勢で学ぶ必要がある

差分はたくさんあるので、その中でも特に印象深かった点を2つだけ書いてみる。

コスト意識

Dappではストレージに書き込んだり、コントラクトをインスタンス化したりするのにコストがかかる。ここでいうコストとはCPU・ストレージといった計算資源を使うことだけでなく、その操作の多寡に応じて実際にEthereumを消費するということ(Gasと呼ばれる)。Web appでいえばPOSTやPATCHリクエストを送るのにお金がかかる、というイメージ。

コントラクトを設計・実装する際には冪等性やメソッドの可視性といったWeb API的な観点のみならず、このメソッドはどれだけ高価なのかを意識することになる。そのうえ、ストレージに保存されるデータ量でコストが変わるため変数の型もしっかり考える必要がある。(1 byteですむようなデータにuint256など使ったらレビューで指摘されるのだろう)

デプロイしたらロールバックできない

スマートコントラクトは一度デプロイするとEthereum上で自律的に動き続ける。この実行環境は開発者が管理するサーバではなくP2Pネットワークのマシンたちなので誤ってデプロイしてしまったものを停止したりロールバックしたりはできない

修正後のコントラクトをデプロイする場合も別インスタンスで別アドレスを持つコントラクトアカウントが生まれることになるので旧バージョンは残り続ける。Web API的に言うと/v1/my_contractendpointにまずいところがあり直しても/v2/my_contractがデプロイされ、v1は生き続ける、みたいな。こわい。

さらに、コントラクトのコードはネットワークの参加者には公開されるため、機密情報・秘匿情報を含んではいけない。一度含んでしまったらこれも取り消せない。

モバイルアプリ開発でも同様の不可逆性はあるが、そちらには強制アップデートの仕組みもあるので、それ以上に厳しい。

脆弱性について学ぶ

上述の通りロールバックがきかないため、スマートコントラクトプログラミングでは慎重に脆弱性を潰していく必要がある。

セキュリティの指針やベストプラクティスをまとめたEthereum Smart Contract Best Practicesを読むのもためになった。

とりわけ面白かったのはReentrancyとして知られる脆弱性

Reentrancy

コントラクトAのコードを実行する際に外部のコントラクトBのメソッドを呼び出せる。その外部のコントラクトBが呼び出し元のコントラクトAを呼び出すこともできる。*5

たとえばBankAccountコントラクトが持つwithdrawメソッド内で残高の更新(ストレージへの書き込み)を完了する前に外部への送金を行うと、その送金を受け取った側へ処理フローが移る。そこからさらにwithdrawを呼ばれると、ストレージに記録されている残高が減ることなく資金が送金され続ける*6

実際に過去にこの脆弱性を突いた事件があり、"Ethereum Smart Contract Security Best Practices"にも記されているので、興味があれば掘り下げてみると面白いと思う。(少し探せば脆弱性があるコードを修正した実際のcommitまでたどりつける)

フロントエンドを書いてみる

Ethereumと連携するフロントエンドアプリケーションも書いてみた。Webブラウザで動作するやつ。

ローカルまたはリモートのEthereumネットワークと通信するためのweb3.jsというライブラリがあり、これを用いる。*7*8

web3.jsはあくまで外部とのインタフェース、言うなればaxiosなんかのようなAPI clientとしてのモジュールに過ぎないので、それ以外の部分の実装はReactでもVue.jsでもjQueryでもなんでも良い(CryptZombiesはjQueryを用いて説明している)。

Promiseベースのインタフェースなので馴染み深い。

かなりweb frontendぽい感じに見えてきたので、ここからちょっと違う話をする。

MetaMask

web3.jsから直接任意のEthereumに繋ぐこともできるが、Chrome ExtensionであるMetaMaskを介してネットワークに接続したり、アカウント情報を取得するのが乙なようだ。

MetaMaskはweb3.jsとEthereumの間にいるプロキシのようなやつで、接続先のネットワークやアカウントを管理できる便利Extension。web3.jsで送金処理を実行する場合にも一度MetaMaskが立ち上がって取引の内容を確認される。

www.youtube.com

同期/非同期

ブロックチェーン上のデータを参照するだけのcall(GETリクエストに相当)によるコントラクトのメソッド呼び出しでは同期的に値を返すことに何の問題もないが、トランザクションを生成することになるリクエストでは注意が必要。

ブロックチェーンで保存されるデータの変更や書き込みリクエストは即時に処理されるわけではない。ネットワークの承認によって初めてブロックチェーンにデータが格納されるためだ。なのでコントラクト側で発行するイベントをクライアント側でsubscribeして結果を受け取ったあとのコールバックを書いたりできる。

Web APIでも要求を受け付けたことだけを一旦レスポンスとして返し、裏側で非同期に処理が行われ、結果を取得するにはポーリングなりWebsocketなりで対処したりすることはある。その対応がわりと頻繁に必要になりそうだが、もっとうまいやり方はあるかもしれない。

その他

上記の他にも色んなリソースも見ていたが、とりわけ読み応えがあるのはこの辺。

あとはVitalik ButerinによるEthereumのWhite Paper!読んでも正直全てを理解できていないのだけど、人間って19歳でこんなこと考えられるんだ…と感嘆せずにはいられない。*9

おわりに

ブロックチェーン技術とその周辺は日々新しい情報が生まれる領域であり、これらの学習だけでは実務レベルにはまったく不十分ということがわかってきた。Web開発でいえば「Rails tutorialやりました」以前のところにいる。

しかしながらこういった基礎的な部分を押さえておくことで関連ニュースの情報処理効率は少なからず上昇するし、また、将来なんらかの形でブロックチェーンを活用した製品に利害関係者・消費者として接する機会が来るのはだいぶ確定的に思えてきた。その時に必要なリテラシーを備える意味でも、こういった学習を時たま行っていくのは上々の投資効果があると考える。

*1:メッセージはBitcoinアドレスの一部として埋め込まれている。通常、ビットコインアドレスは公開鍵から生成されるが、秘密鍵なしでもvalidな値("1"と混同しないように"l"を除外するとか)であればアドレスとして利用することはできる(ただしビットコインアドレスから公開鍵と秘密鍵を知ることはできないので誰もそのアドレスから送金できない)。

*2:データベースのようにレコードのmutableな値をupdateするのではなく、https://en.bitcoin.it/wiki/Transaction に従って取引ごとのOutputが記録されるのみ。残高を見る時はUTXOという仕組みで、Outputから未使用のものを集計している

*3:Bitcoin Tumblerと呼ばれる、不当に得たBitcoinとcleanなBitcoinを混ぜ合わせて洗浄するマネーロンダリングの手法があるが、全ての取引記録が改ざん不能な形で記録されている以上は追跡から逃れるのは難しい。過去に同様の手口で資金洗浄を試みたが失敗した例がある

*4:Ethereum上で稼働するゲームに、仮想子猫を売買できるCryptKittiesというものがあり、それをパロったやつ。

*5:あるAPIから外部のAPIを呼び出すというのはWeb appでもよくあることだが、Dappでは送金処理の際に外部のコントラクトのメソッドがコールバックとして呼び出されるのでけっこうカジュアルかつ頻繁に発生するようだ。

*6:コールスタックに上限があるので循環して無限ループが起きることはない

*7:ether.jsというのもあるが触っていない

*8:iOSAndroid用のintegrationもあるようだ

*9:年齢で人を評価するとかそういう次元でなく19歳の自分と比べて絶望する感覚

『スマートコントラクト本格入門―FinTechとブロックチェーンが作り出す近未来がわかる』読んだ

タイトル通りスマートコントラクトがメインなのだが1~2章はFintech業界やBitcoinの話題で、これは『ブロックチェーンのしくみと開発がこれ1冊でしっかりわかる教科書』と重複する部分もあったのでさらっと読み飛ばした。

個人的に面白かったのは3~5章で紹介される社会実装の実例。一般にブロックチェーンというとFintechばかりが想起される気がするが、生活・産業分野、Legaltech領域においても適用可能性が大いにあることと、そこでもたらされるのが既存業務の効率化だけでなくバリューチェーンの変革(中間業者を廃するなど)でもあることが示されていて、とても心が踊った。

数年後はどうかわからないが10年後にはこの辺の製品やサービスに触れる機会がありそう、そんな近未来感があった。


6~7章のスマートコントラクト開発の部分はやや古いのかも、と感じた。

geth (go-ethereum) を使ってローカルにEthereumネットワークを立てたりインタラクティブに操作するのは初手としては面白かったものの、truffleganacheを利用した場合に比べると開発体験がかなり劣るのでちょっとつらかった。

truffleは最後に少しだけ紹介されているが、 公式のチュートリアルのほうが手厚いうえに情報も新しいのでこちらを頼ったほうが良さそうだ。

www.trufflesuite.com

『ブロックチェーンのしくみと開発がこれ1冊でしっかりわかる教科書』読んだ

各トピックが数ページ単位でまとまっており、かつ、言葉だけではイメージしづらい技術的な解説についての図解がかなり多いのでとても読みやすい。

技術的な解説がどれだけ深いのかは読んでいる時は正直わからなかったが、前段で説明した内容に対する補足や解説が次節で語られるような順序になっており、読み進めるごとに理解が深まる実感を持てる構成であった。

また、公開鍵暗号や分散システムにおけるビザンチン将軍問題などWebシステムでも取り上げられる話題には馴染みがあった。(当然ながら)全く異界の技術でなくコンピュータとネットワークの上に構築される技術だというのもわかり、読書を通じてブロックチェーンへより強い興味が持てるようになった。