最初に
予選落ちなので勝者エントリを読みたい人はここで回れ右を推奨。
- 使用言語:Ruby
- 最終スコア:0
- 最高スコア:5822
モチベーション
ISUCONの出題範囲であれば(多少濃淡あるけど)一応1人で全部できると思ってたので腕試しのために1人チームで参加しました。
ISUCON歴
ISUCON6(2016年)とISUCON9(2019年)にそれぞれ当時の同僚と参加したので今回が3回目。(どっちも予選落ち)
事前準備
準備期間
6月中旬くらいから本番を想定した素振り(1セット8時間)をやってたのでだいたい2ヶ月くらいです。
7月は毎週末8時間素振りをやって、平日の夜に感想戦をやってました。
そしてISUCON開催週は有給で全部休んでひたすら毎日10〜18時で素振りをしてました。
やったこと
ひたすら過去問をやっていました。
古すぎると今と傾向が違うので比較的新し目のをときつつ、予選と本戦では問題の傾向が全然違う(予選は割とオーソドックスだけど本戦はトリッキーな問題が多い)ので予選を重点的にやりました。
解いた問題数だけでいうと下記。(計13セット)
- 6予選 x 1
- 7予選 x 2
- 8予選 x 2
- 9予選 x 2
- 9本戦 x 1
- 10予選 x 3
- 10本戦 x 1
- 11事前講習 x 1
7月の週末と8月第3週目に草が濃いのはISUCON素振りのためです。 *1
いやまぁ、2ヶ月間で100時間以上使っても予選突破できなかったのでもう少し素振りの質を上げるべきだったかなと後悔。
事前に用意したもの&やったこと
Sentryのbillingを有効化した
普段 https://sentry.io/ は無料枠で利用してるんですが、ISUCONの素振りをやったら一瞬で無料枠を使い切ったのでTeamプラン(月26ドル)にしました
SentryのSpike Protectionを無効化した
Sentryには突発的にエラーが送られてきた時にそれをブロックして課金を抑えるSpike Protectionという機能がデフォルトで有効になってます。
通常はこれは有効でいいんですがISUCONの場合この機能を有効にしてるとベンチマーク実行時のエラーがSentryに飛んでこないということがあったので無効にしました。
ちなみにSpike Protectionはプロジェクト単位ではなくorganization全体の設定なのでISUCONのチーム専用のorganizationを作ってそこで無効化するのがいいと思います。
デプロイスクリプト
素振りの度にリファクタリングしつつ、実際にISUCON11の予選で使った最終版は https://github.com/sue445/isucon11-qualify/blob/main/Rakefile になります
デプロイスクリプト解説
自分がRubyで参戦するし書き慣れてるのでRakefileを使いました。Makefileと違って動的にタスクを書きやすいのが特徴。
ここまで重厚になるとさすがにcapistranoでええんちゃう?って気持ちになるんですが、スニペットリポジトリからコピペするだけで使えるの便利なんすよね、、、
ローカルで rake
を叩くだけで git push
-> 本番全台で git pull
& 必要なデプロイ処理を全部実施しつつ、スコア記録用のissueに下記のようなコメントをつけるところまでやってます。(ベンチマーカーで出たスコアは手でissueに貼り付けてる)
実際にスコア記録用に使ったissueは https://github.com/sue445/isucon11-qualify/issues/1 なのでこれ見てもらうと当日の雰囲気は伝わると思います。
こだわりポイント
- 複数台に直列でデプロイすると遅いので
multitask
で並列デプロイできるようにしてる - サーバ全台にデプロイ後に
/initialize
を叩いて簡単な動作確認までやってる- ただ、今回のISUCONだとパラメータ無しで
/initialize
を叩けない仕様だったので/initialize_from_local
というエンドポイントをはやしてそいつをRakefileから叩いています
- ただ、今回のISUCONだとパラメータ無しで
/initialize
まで終わったら20210821-183139-sue445
のような日付ベースのtagを作ってpush- 最初はデプロイしたリビジョンだけissueにコメントするようにしてたんですが、それだと直前のデプロイからのcompareが取りづらいのでタイムスタンプベースのtagにしています
- 実際に直前のデプロイからのcompareリンクを作ってるのは https://github.com/sue445/isucon11-qualify/blob/main/Rakefile#L166-L171
- issueの最新のコメントに書かれてるリビジョンをparseしてもよかったんだけどこっちの方が手軽だったので採用
- 昔の癖でtagにデプロイした人の名前を入れたけど1人チームだったので全然ありがたみはなかったw
スニペット
https://github.com/sue445/isucon11-qualify/tree/main/ruby/config と https://github.com/sue445/isucon11-qualify/tree/main/infra が 開始直後にスニペットリポジトリからコピペした もので、configは必要に応じてrequireして使ってました。
過去問を解いてる過程であると便利だったユーティリティが https://github.com/sue445/isucon11-qualify/tree/main/ruby/config に集約されています。
スニペット系は元々個人esaに置いてたんですが、数が多くなってきたしテストコード書いてCIも回したくなってきたので専用のリポジトリを作りました。
その中からいくつかピックアップ
enable_monitoring.rb
アプリケーションコードを一切修正しなくてもファイル先頭でこれをrequireするだけでNewRelic, Sentry, Stackprofを有効化できる便利モンキーパッチ
NewRelicは便利なんだけどそれなりに重いのでどうしてもスコアが下がってしまいます。(過去問解いた感じだとNewRelicほどじゃないけどSentryもちょっと重かった)
設定が複数箇所にあると無効にするのが大変なので、ISUCON終了直前に一瞬で全部オフれるようにこのような構成になってます。
class Sinatra::Base
や class Mysql2::Client
をオープンクラスしてモンキーパッチ仕込んでるのが個人的なおすすめポイントです。
実際の終了直前対応は https://github.com/sue445/isucon11-qualify/commit/fd049a5834a03df9705c83c05ff2b3f86042e6ae になんですが、アプリだけでいうと enable_monitoring
のrequireをやめるだけですんでいます。
nr_mysql2_client.rb
https://github.com/newrelic/newrelic-ruby-agent は標準でmysql2をサポートしていません。
そこで下記エントリを参考に色々魔改造したのがnr_mysql2_clientです
元のソースからの差分は下記
- SQLのクエリに含まれてるテーブル名を正規表現で全部parseしつつ、joinやサブクエリなどで1つのSQL文で複数のテーブルが登場する場合には
reservations,users
のようにカンマ区切りで連結した文字列を返却- https://github.com/sue445/isucon11-qualify/blob/main/ruby/config/nr_mysql2_client.rb#L9-L25
- 競技リポジトリには含まれてないですがISUCONの過去問で出てきた実際のクエリを使ってテストコードを書いてるのでだいたいのケースは対応できてるはず
- 実際に実行されたSQLを
/tmp/sql.log
に出力- スロークエリログとるまでもないけど雑に見たいクエリ見たい時に便利
- mysql2経由ではなく
system "mysql ~"
のようにRubyのアプリから直接システムのmysqlコマンドを叩いた時にもNewRelicにメトリクスが送ることができるようにした- 具体的には with_newrelicのブロック の中で
system "mysql ~"
を実行 - 普通に考えたらアプリから
system "mysql ~"
を叩くなんてまずないんですがISUCON8の予選の過去問解いてた時に取得したレコード数が多すぎ *2 てpumaがメモリを食いすぎてサーバがOOMで死ぬということがあったのでそういうことも一応想定してました *3
- 具体的には with_newrelicのブロック の中で
最初の頃は
# Mysql2Client = Mysql2::Client Mysql2Client = NRMysql2Client def db Mysql2Client.new( host: @host, port: @port, username: @user, database: @db_name, password: @password, charset: 'utf8mb4', database_timezone: :local, cast_booleans: true, symbolize_keys: true, reconnect: true, ) end
のようにしてコメントで NRMysql2Client
を使うかどうか切り替えてたんですが、面倒くさくなってきたので最終的に前述の enable_monitoring.rb
で class Mysql2::Client
に直接モンキーパッチ仕込むようにしました
redis_methods.rb , memcached_methods.rb
Rails.cache.fetch
が好きすぎて with_redis
や with_memcached
のようなメソッドを作ってます。
Rubyだとmemcachedクライアントの dalli がバイナリプロトコルをサポートしてるのでRubyのオブジェクトをそのまま保存できるんですが、redisだとそれがない *4 のでちょっと苦労しました。
- Ruby組み込みのMarshal
- Ruby組み込みのto_json
- https://github.com/ohler55/oj
でそれぞれベンチマークをとった結果、ojが一番速かったのでredisにRubyのオブジェクトを保存する時はojを使うようにしました。
ベンチマークの結果は下記です。
nginx.conf
設定はいたって普通なんですが
include /home/isucon/webapp/infra/nginx/sites-enabled/*.conf;
のように /etc/nginx/nginx.conf
から直接 /home/isucon/webapp
配下のconfをincludeするのが個人的なマイブームです。
チェックリスト
ISUCON11の事前講習 で講師の人が「『nginxのworker_connectionsが1024になってるか確認』ぐらいの細かい粒度でチェックリストを作るのがいい」と言ってたのでそれにならってチェックリスト作りました。
個人esaにチェックリストのテンプレを作って過去問を解く時に利用し、ちょいちょい更新していったら最終的にこんな感じになりました。
サーバ構築用のItamae
過去問だとサーバ1台構成である程度動かせた後にAMIを作って2台目以降はそれベースで作っていたので、個人esaに構築手順のコマンドをメモってコピペで構築していました。
しかし直前に「当日はCloudFormationを使ってサーバを構築する」というアナウンスがありました。
運営の人に質問したところCloudFormation以外でサーバを作ってもいいけどサポート対象外だしあまり推奨しないという旨の回答をもらったので急遽構築手順をItamaeでコード化しました。
このItamaeでやってるのは主に下記です
- 各サーバ毎にhostnameの設定
- 必要そうなpackageのインストール
- 必要に応じてserviceの有効化と無効化
/etc/security/limits.conf
でsoft nofile
とhard nofile
を65536に設定/etc/newrelic-infra.yml
でenable_process_metrics
(起動してるプロセスのメトリクスを送信)を有効化/lib/systemd/system/mysql.service
でLimitNOFILE=65535
を設定- これをやらないと
max_connections
が増えなかったので脳死で設定
- これをやらないと
/home/isucon
でsshの秘密鍵を作ったり自分のdotfilesリポジトリからtigやtmuxの設定ファイルをダウンロード- 最新のRubyをインストール
過去問素振り用のTerraform
個人のAWSアカウントで素振りするために下記のようなTerraformを書きました。
Terraform 1.0.1, Terraform Provider for AWS 3.25.0で動作確認してますが多少古くても大丈夫なはず。
VPC, Subnet, SecurityGroup辺り全部入りなのでこれを既存のTerraformのリポジトリにコピペすればだいたい動くと思います。
過去問毎に想定スペックがまちまちだし前述の通り当初は1台ずつサーバ起動する想定だったのでEC2はTerraform化していません。
【おまけ】stackprof-webnavをRuby 3.0対応した
Ruby 3.0で素振りをしてた時にstackprof-webnavが動かなくて困ったのでいくつかパッチを投げました。
無事マージされてISUCON直前にRuby 3.0対応版がリリースされています。
余談ですがgemリリースが間に合わなかった場合を想定して自分のPR全部入りの状態で手元でビルドしてISUCON当日に gem install --local
して使う手順も一応用意してました
当日やったこと
予選落ちなので多くは語りません。
リポジトリは公開してるのでコードの意図を知りたい場合はブログのコメントやTwitterとかで聞いてください。
敗因は下記
- sinatraで返してる静的ファイルをnginxで返すのに苦労した
- 途中から急に
context deadline exceeded (Client.Timeout exceeded while awaiting headers)
が出始めて原因調査に時間を取られた- 原因はstackprofだったんですが、まさか初手で入れてるstackprofのせいで途中から動かなくなるとは思わなかった。(過去問の素振りでも一切問題なかったので余計にハマった)
1人チームで参加した感想
過去に複数人チームで参加した時と比べて下記のようなメリデメがありました
1人チームのメリット
- 他のメンバーとの共有作業が不要
- コードレビュー不要
- 普通のチームだとトピックブランチ作ってpushしてPR出してレビュー後にマージという流れになるが、1人チームだとレビューしようがないしブランチ作るのも面倒なので必然的にmainブランチでガンガンコミットしていくことになる
- 複数人での認識共有や意思疎通などのコミュニケーション作業が不要
- 他メンバーの予定を気にせずに好きな時間に好きなだけ素振りができる
- 実行環境を壊しても誰にも迷惑かけない
- ローカルでアプリを動かす必要がないのでガンガンデプロイしてガンガンベンチまわせる
1人チームのデメリット
- 1人
おまけ:素振りで使ったAWSの費用
ISUCON終わってAWSの個人アカウントの予算アラートの設定値も元に戻すのでその前に記念スクショ
*2:https://github.com/isucon/isucon8-qualify/blob/master/webapp/ruby/lib/torb/web.rb#L423-L459
*3: https://isucon.net/archives/52520045.html によるとmysql_use_resultって選択肢もあったんだけどmysql2でmysql_use_resultを使う方法がよく分からなかったのでアプリから SELECT ... INTO OUTFILE を使ってた
*4:redisに保存すると基本的に全部stringになる
*5:後述のstackprof-webnavで必要だった