mintsu's blog

ISUCON10予選に参加してきました

2020-09-15 22:00:00
日記

ISUCON10予選に参加してきました

ISUCON10予選に参加しました。ISUCON10の予選でやったmysql,nginx,linuxのチューニング、アプリケーションの改善や感想について記載していきます。
結果としては予選敗退となってしましたが、最後の最後まで目星のついている改善箇所があり後ちょっと時間があったら予選突破もできていたのでは?という内容で非常に悔しい思いと同時に、時間ギリギリまで改善し、予選突破ラインに行けるかどうかという緊張と面白さがありました。ここ2年ほどいい成績が出ていなかったので今回はそれなりに成果が出せてよかったです。非常に楽しいISUCONでした。

チーム名は「motsu鍋」という名前のチームで参加し、私(@mintsu123)と@mogulla3 の二人のチームで参加しました。

目次

ISUCONってなんぞや?

ISUCON公式Blog

お題となるWebサービスを決められたレギュレーションの中で限界まで高速化を図るチューニングバトル、それがISUCONです。

「ISUCONは」という名前は いい感じにスピードアップコンテスト 略して ISUCON (Iikanjini Speed Up CONtest)となっています。

どのように競うかというと、 Webアプリケーションが入ったサーバが各参加チームに提供され、レギュレーションの範囲内で高速化します。
パフォーマンスチューニングの内容はレギュレーションの範囲内なら何でもあり。
今回のISUCON10予選では下記のように記載されています。

ISUCON10 予選レギュレーション : ISUCON公式Blog

許可される事項には、例として以下のような作業が含まれる。

  • 複数台あるサーバーの役割の変更
  • DBスキーマの変更やインデックスの作成・削除
  • データベースに利用するミドルウェアの変更
  • キャッシュ機構の追加、ジョブキュー機構の追加による遅延書き込み
  • 他の言語による再実装

パフォーマンスチューニング後、運営が用意したベンチマークを走らせてスコアがつけられます。
このスコアによって競われる競技となっています。

ちなみに今回のISUCON10は名前からもわかるように10回目のコンテストですね。

事前準備

チームメンバー全員1週間前まで何もしていないという状況でした。

  • 使う言語を決めていない
  • 戦略を決めていない

すでにだいぶ厳しい状態で、しかもチームメイトはISUCON前日の夜まで忙しく何もできないという状況でした。
私は時間が取れたのでこの一週間で対策をして、前日、当日にチームメイトに共有するという形になりました。

事前準備でやったこと

  • Goの勉強
    • メンバー全員Goはやったことがありませんでした。
    • なので一週間前から必死に勉強
    • go勉強
  • isucon9予選の過去問を解く
    • とりあえず予選突破ラインのスコアを出せるまで練習しました
  • isucon10で使うスクリプト郡作成。以下のスクリプトは事前に作成しておきました。
    • システムのOS情報、利用ミドルウェア、メモリサイズ、CPUコア数、主要なmysql変数等の情報を出力するスクリプト
    • アプリのコード、nginx,mysqlの設定を集約してgithubにpushするスクリプト
    • デプロイするスクリプト
    • slackに通知するスクリプト
    • pprofを計測して、pdf生成し、slackに投げるスクリプト
    • kataribeを実行して、pdf生成し、slackに投げるスクリプト
    • .gitconfig, .vimrc など設定をするスクリプト
    • カーネルパラメーターチューニング設定を追記するスクリプト
    • tmuxやjqなど使いそうなパッケージをインストールするスクリプト
    • 上記のスクリプトを実行、配置するスクリプト
  • 過去問でのチューニングのテンプレート化
    • 当日の朝に時間ができたので、ある程度過去問で行ったチューニングの共有や、この場合はこうするなどのテンプレート化をしました。
    • nginxのロードバランスの設定, mysqlの設定など
  • 大まかな戦略を立てる
    • 早いうちに複数台構成にする
      • 去年まではチューニングしてから複数台構成にするという方針でした、複数台構成にするまでに至らないことが多かったので方針を変更しました
    • DBスキーマの破壊的な変更は行わない
      • 去年まではスキーマ変更して一気にスコアを上げるみたいなことをやってましたがだいたい失敗してたのでやめました。

スクリプトは事前に作っておくことで今回初動はかなりスムーズに進めることができました。
Goの勉強でだいぶ時間が取られてしまい、本当はisucon8などの予選の過去問も解いておきたかったのですが、isucon9予選しか解けなかったです。

ちなみにGoの勉強に使ってたのはこちらの書籍で勉強しました。

Goの文法に関してはあまり学べないですがGoで動くWebアプリケーションの仕組みを知るには良い教材でした。

予選当日

今回大会の開催時間は12:20 ~ 21:00となりました。
タイムライン順でやったことを記載していきます。時間はなんとなく覚えてる範囲で書いてます。

初動 (12:20 ~ 13:00)

初動フェーズとして下記を実施

  • githubへのアプリケーション、ミドルウェアの設定ファイル、デプロイスクリプトのpush
  • vimrc,gitconfigなどの設置
  • 初回ベンチ
  • kataribeによる計測
  • pprofによる計測
  • マニュアルの読み込み
  • アプリの動作確認

サーバの設定やgithubのpushなどは私が担当し、マニュアルの読み込みやアプリの動作確認はmogulla3にやってもらい各自分担して行いました。
このあたりは事前にスクリプトを用意していたこともあり、初動はかなりスムーズに行ったと思っています。

この時点(初期実装)でのスコアは497点

マニュアル読み込みや、仕様確認しつつ、わかりやすい部分のところの改善 13:00 ~ 15:00

マニュアルを読み込んだり、アプリの動作確認をしつつ、 事前に用意したテンプレ化したものや、簡単にできそうな改善を実施。

  • mysql設定値変更
    • innodb_buffer_pool_size を増やす
    • innodb_doublewrite をオフに
    • innodb_flush_log_at_trx_commit = 0 に
    • max_connections を増やす
  • 静的ファイルのレスポンスにキャッシュコントロール付与
    • 効いていたかは不明だがとりあえず入れました
    • 304でもし返されば負荷や帯域の節約になるため
  • Botのアクセス制御
    • マニュアルに書いてあったのでngix.confに書いて実装しました
    • 後に効いてないことがわかり、これで結構時間が取られた。。
  • mysqlのインデックスを貼る
    • 更新系が少ないアプリケーションだったので、更新のコスト増加のことはあまり考えずindexはかなり貼った
      • isuumo.estate(rent, id)
      • isuumo.estate(popularity)
      • isuumo.estate(latitude);
      • isuumo.estate(longitude);
      • isuumo.chair(stock);
      • isuumo.chair(price, id);
      • isuumo.chair(popularity);
    • この時点でindex追加によるスコアに変更がなかったが、実はデプロイスクリプトで変更反映されていなくて後で気づいた。。
  • AppとDBサーバを分ける
    • DBサーバだけ分けるのは簡単にできるのでこの時点で実施。
    • ネットワークレイテンシによるスコア悪化が考えられるが、今回は最初から複数台構成にする予定だったのでそこは気にしませんでした。

このあたりまでは作業分担もうまくできて、割と順調に作業が進んでいたと思います。ミスっていることに気づいてないのを除いてはですが。。

この時点でのスコアは461 ~ 522 点程度.

コードリーディングと計測をしながら改善 (15:00 ~ 17時くらい)

コード見ると、更新系が今回かなり少ないことに気づいた。
nginxのProxy cacheやアプリケーション側でのキャッシュが絶対効くと思いつつ、ぱっと良い実装が思いつかず時間を消費。
結局最後までキャッシュは使わなかった。

MySQLのCPU負荷があきらかに高かったので、クエリ周りで改善箇所をいくつか見つけて対応

search/chairs, search/estates の改善

featuresの検索にlike文が使われていて重そうでした
具体的には下記のコード

	if c.QueryParam("features") != "" {
		for _, f := range strings.Split(c.QueryParam("features"), ",") {
			conditions = append(conditions, "features like concat('%', ?, '%')")
			params = append(params, f)
		}
	}

対応として下記の2つが浮かびました

  • ビットで管理する方法
  • MysqlのSet型で管理する方法

しかし、去年等こういうデータの持ち方変更で失敗してるので取り組むことができず、結局データの持ち方は変更に取り組むことはできませんでした。

データの内容的に,featureが4つ以上あるデータは存在しなかったので featureが4つ以上ある場合は件数ゼロで返すという対応で、特定の条件下で負荷を軽減するようにしました。
ちなみに、4つ以上のfeaturesを選択してリクエストが来るのかは確認せずに修正してしまったので、確認しておいたほうが良かったですね。

しかも最終的に、プルリクエスト上げ忘れてマージされていないということに、競技終了後気づきました。。。

なぞって検索のN+1の打ち切りを50件に

N+1クエリになっていました。
1 SELECT * FROM estate WHERE latitude <= ? AND latitude >= ? AND longitude <= ? AND longitude >= ? ORDER BY popularity DESC, id ASC
2 SELECT * FROM estate WHERE id = ? AND ST_Contains(ST_PolygonFromText(%s), ST_GeomFromText(%s))
1のリクエストでレコードを一気に取得し、2で更に絞り込み、最終的に50件までデータを返すというもの。

初期実装では50件で打ち切る処理がなかったので、50件で打ち切るように修正しました。

この時点のスコア

この時点でスコアは614点となっていました。

3台構成に変更(17時〜18時)

相変わらずMySQLの負荷が高いため、MySQLをマスター1台、スレーブ2台という3台構成にしました。

レプリケーション接続は特にISUCONのために練習はしていなかったですが、
DB周りは普段からいじることが多く、こういった構成は親の顔より見た構成なので、すぐに対応できました。

Nginxは更新系はマスターに、参照系は3台にサーバに割り振るように設定しました。

ただNginxの設定のときにはミスをしてしまい、更新系のリクエストもスレーブに行ってしまいFAILする自体になりました、
この際、レプリケーション遅延で失敗してるのか、Nginxの設定でミスをしているのか原因の切り分けがうまくできずここで結構な時間を取られてしまいました。

原因切り分けのため更新時にはSleepを1秒入れるということをして、ようやくNginxが悪そうということで設定を修正しました。
そしてSleep1秒はあとですぐに調整できるからという理由で1秒のまま放置。

ここまでで構成としてはこのような感じ

構成

この時点でスコアは870点となりました

3台構成スコア

タイムアウトでFAILするようになる (18時 ~ 21時)

ここでログを確認するとBOTからのアクセスがあってNginxの設定が間違っていることに気づき修正して、正しくBOTをはじくようにしました。
この後あたりからベンチがタイムアウトして失敗するようになりました。

アプリケーションにchairとestatesのCSV入稿の機能があったのですが、初期実装でベンチを動かした際にはほとんど叩かれなかったのですが、リクエストがさばけるようになってくると、ベンチマークからCSV入稿のリクエストが来るようになっていたようです。
INDEXを多く貼っていたり、Sleepを入れている関係でCSV入稿のリクエストが来るとタイムアウトするようになってしまいました。
この時点でタイムアウトによりスコアが0点になってしまったため非常に焦りました

コードを見る中でCSV入稿がバルクインサートになっていないのでここは遅いというのはかなり前からわかっていたのですが、リクエストが今までほぼなかったので改善には手を付けていませんでした。

プリペアードステートメントを使いつつバルクインサートにするのは少し面倒そうなので、 SleepやINDEXを変えて対応してみるもタイムアウトは改善せず。
ここでかなり時間を浪費してしまいました。

だんだん時間がなくなって、バルクインサートをやるしかないということで、エスケープしなくても入稿が通ることを願って、文字列連結でそのままクエリを投げるようにmogulla3とペアプロ的に修正。
なんとestatesのCSV入稿はそのまま通ってしまった。

同様にchairのCSV入稿の方も修正を行う。
chairのほうはSQLのエラーが出てしまったので、エスケープが必要な文字列が必要な文字が含まれていたのかと思って確認するも見つからない。
終了まで残り10分というギリギリのところで、コードにtypoがあることに気づく。

修正してベンチを回してスコアは1573点

10分前スコア

残り5分しか時間がないのでもはやできることは少なくnginxのアクセスログと、pprofのコードを削除をしてベンチを回す。
Sleepなどまだ最終調整できるところがありましたがそれは時間が足りず。。。

最終スコア

最終スコアは1691

最終スコア

この時点で公開されていた20時点でのスコア順位では25位以内に入るスコアだったので、本戦出場ももしかしたら!と思っていましたが、そこまでには至りませんでした。

ISUCON10 オンライン予選 全てのチームのスコア(参考値) : ISUCON公式Blog
正確な順位は出ていないのでわからないですが、後日公開された「全てのチームのスコア(参考値)」ベースで見ると57番目という結果でした。

参加した感想

去年、一昨年と芳しくない結果だったので今年はなんとか良い結果を出したいと思っていました。
予選敗退とはなってしまったものの今回はそれなりにスコアは伸ばすことができたので良かったと思っています。

そしていいスコアを出していたからこそ、Sleep1秒が入ってしまったままになっていることなどが非常に悔やまれます。。

ただ、最後の最後は本当に時間ギリギリの戦いで、非常にワクワクする展開で楽しかったです。

反省点としては、凡ミスが多すぎました。。
nginxの設定ファイルやSQLにしても、ほとんど1文字ミスってるだけだったり、設定が反映されていないとか、プルリク漏れだったりそういう凡ミスが多すぎでした。
次回はこのあたりカバーする仕組み考えて取り組みたいところです。

一緒に戦ってくれたチームメンバーの@mogulla3ありがとうございました、お疲れ様でした!

そして運営の皆様、ありがとうございました!