#ISUCON 12 予選問題にJavaでトライして32000点までスコアを挙げた

普段はJavaを使っているので、Javaの参照実装を使ってISUCON 12予選を解いてみました。

実施環境・条件

  • 環境: AWS EC2
    • 競技環境: c5.large *3
    • ベンチマーカー: c5.xlarge
  • 条件
    • 制限時間は設けない
    • 実装言語はJava
    • ベンチマーカーの管理者向けログは極力見ない (./bench (args) | grep -v ADMIN)
    • ベンチマーカーの-reproduceフラグは指定しない (予選当日の挙動を再現しない)

リポジトリ・ベンチマーク記録

やったこと

  • Javaの参考実装に切り替えてベンチ: SCORE: 421 (+934 -513(55%)) ログ
    • Goと比べてスコアは低め。エラーもかなり出ている状況でした
  • docker剥がし: SCORE: 432 (+960 -528(55%)) ログ
  • ID採番をアプリ側で実施: SCORE: 426 (+1251 -825(66%)) ログ
    • visit_historyのSELECTのほうが重い状況だったので、この段階で採番を変えてもあまり伸びませんでしたね
  • sqlite3からMySQLへのデータ移行
    • 予選当日はやらなかった、データ移行に挑戦しました。最終的に、tenandId=1のplayer_score以外はすべてMySQL側に移行して、tenanId=1は特別扱いすることに。
    • player_scoreテーブルでは、必要なのは最新のレコードだけなので、最新のレコードだけをCSVに吐き出して移行しています (https://github.com/maruTA-bis5/isucon-practice/commit/f6043a09b0490b5a900709a72af5ee080dcff178)
      • CSVに吐き出し終わる前にコードを修正していたので、しばらくベンチがfailする状況が続いています
  • プレイヤーのスコアは最新のみ保持するようにし、バルクインサート化
  • public.pemはBeanの初期化時に1度だけ読み込んで、それを使い回す
  • visit_historyテーブルにインデックスを作成
  • 終了した大会の請求情報をcompetition_billingテーブル(新規)に保存するように
  • billing: 終了していない大会は集計せずに即座に返却する
  • ヒープメモリ割り当ての調整
    • ここで調整ミスって、EC2インスタンスのメモリを食いつぶしてしまったので、再起動しつつ3号機へ作業環境を移行しています。まだ1台構成ですね
  • デッドロック時にretryするように: SCORE: 4913 (+4913 0(0%)) ログ
  • App/DBでCPUを取り合っている状況なので、DBを1号機に移行: SCORE: 10215 (+10423 -208(2%)) ログ
  • competition_billingの更新を、競技終了後に2号機で行うように
  • 各種N+1の解消: SCORE: 31317 (+31956 -639(2%)) ログ
  • Nginxに若干負荷がかかっていたので、余裕のある2号機へ移動: SCORE: 33548 (+33886 -338(1%)) ログ
  • 各種監視・ログを止めて最終スコア: SCORE: 32551 (+38295 -5744(15%)) ログ

最終的な構成

  • 1号機: MySQL
  • 2号機: Nginx, App
  • 3号機: App

感想

  • flockの代わりにsynchronizedを使っている点がJava実装固有の差異でしたが、それ以外はGoと同様だったので、改めて実装面では言語による有利・不利が少なくなるようにされているんだと実感しました。
  • 最終スコアは32551だったので、やるべきことを時間内にやれていれば本戦出場も狙えそうだ、という印象ですね。あとは手を早く、正確に動かせるように練習が必要。

#ISUCON 12 オンライン予選に参加し最終スコア18494点で予選敗退しました

2022/07/23に開催されたISUCON11 予選にチーム「TF」で出場しました。 ISUCON (Iikanjini Speed Up CONtest) についてはこちら→ https://isucon.net

メンバー

前回と同じメンバーの2人チームでした。

  • maruTA (@maruTA_bis5) ←me
    • 普段はJava + PostgreSQLで仕事している人
    • ISUCON 11 SQLのtypoで2時間溶かした前科有り
    • 開発担当
  • yulis
    • AWSエンジニアだったはずだがオンプレミスも世話している人
    • 目grepが得意
    • 情報担当

使ったもの

  • 実装言語: golang
    • 今回はJavaの実装も提供されましたが、普段パフォーマンスを重視した開発をしていない()ので、今回はgolangで。
  • 各種監視: Grafana + Prometheus + Loki + Tempo + OpenTelemetry
    • 前回はNewRelicを使いましたが、業務で使っているツールに寄せる、という意図で選定
  • その他: ansible, kataribe, pt-query-digest

結果

最終スコア18494点、参考値一覧によると35位で予選敗退しました。

やったこと

リポジトリは https://github.com/maruTA-bis5/isucon12-qualify、ベンチマークのログは #4にあります。

スコア推移
  • アプリケーションをdocker composeでビルド・起動していたのを剥がした
    • network_mode: hostだったのでNWのオーバーヘッドはなかったみたいだが、ベンチマーク回す前のコンテナビルド時間が気になった
  • OpenTelemetryの計装
    • Tracesをapp -> otelcol -> Tempoに転送してGrafanaで確認できるように
  • ID採番の回数を減らせないか試行錯誤
    • MySQLのauto_increment_incrementをいじって回数を減らす、とか実装したがあまり改善していない気もする
    • 後から考えるとアプリ側で採番したほうが良かったか
  • ranking等時間がかかる集計をする際に、テナントDBのファイルを一時ファイルにコピーしておいて、そちらを参照する→整合性チェックを通らなかったのでrevert
  • visit_history(tenant_id, competition_id, player_id)にインデックス作成
  • スコア入稿時に最新スコアをMySQLに登録して、rankingでflockを取得しないように
    • 今回唯一のスコアの"崖"がこの修正。結局他の修正は微増する程度でしたなぁ
  • 最終的に1号機=アプリ、2号機=最新スコアDB、3号機=adminDBの構成

感想

SQLiteで面食らってしまったのもあり、後半までほとんどスコアが伸びませんでした。 前回はSQLのtypoに気づかずに時間を溶かしたが、今回はfailしたらすぐrevertする、エラーログをちゃんと見る、という2点を心がけて、スコアが出せない時間はそこまで続きませんでしたが、flockが重いことに気づいていながら取り切ることも出来ず。 最新スコアの切り出しをもう少し早く実施できていれば、あと4000点あれば予選突破できたことも考えると、非常に悔しいポイントでした。 瞬発力を上げるためにも、次回開催に向けて練習あるのみ。

今回、ベンチマーク・ポータルは非常に安定していたので集中してチューニングに取り組むことが出来ました。 運営の皆様、ありがとうございました。 次こそは予選突破・・・!

追記(2022/07/27 8:52 JST) ベンチマーク履歴のスクリーンショットを載せ忘れていたので追加