#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だったので、やるべきことを時間内にやれていれば本戦出場も狙えそうだ、という印象ですね。あとは手を早く、正確に動かせるように練習が必要。