Gマイナー志向

とくに意味はありません

ISUCON9予選で総合4位になり本選進出を決めました

ISUCON9予選1日目に「いんふらえんじにあー as Code」として参加し、1日目に3位(1位が棄権したため2位)で予選通過を勝ち取りました。 予選通過はISUCON4以来、5年ぶりです。なお、総合順位は4位だった模様です。

メンバー紹介

チーム名 いんふらえんじにあー as Code

あいこん なまえ やくわり
f:id:tmatsuu:20190909215614p:plain:w100 netmarkjp 司令塔
f:id:tmatsuu:20190909215620j:plain:w100 ishikawa84g レギュレーションやコードやログやDiscordを見る情報官
f:id:tmatsuu:20190909215748j:plain:w100 matsuu バリバリ実装する前衛

最終構成

nginx --+-- app(go) --+-- mysql 1台目
        |             |
        +-- app(go) --+         2台目
        |             |
        +-- app(go) --+         3台目

2台目と3台目は /login のアクセスのみ振り分け(bcryptのハッシュ処理のため) nginxとmysqlは初期バージョンのまま変更せず

スコアの遷移

f:id:tmatsuu:20190909220031p:plain
ISUCON9予選スコア遷移@いんふらえんじにあー as Code

事前準備

去年のISUCON8予選で敗退した際に原因分析を行ったところ、自分自身に以下の課題があると感じていました。

  • 職業プログラマーではないこともあり実装速度が遅い
  • Go言語の基本機能をきちんと把握できておらずドキュメントを見ながら実装していた
  • 過去のISUCONの復習を深いところまでできていない

これらの課題改善のため1年かけて以下に取り組みました。

  • AtCoderにGo言語で挑戦して基礎に慣れつつ実装速度をあげる
  • HighLoad Cupの過去問を解いてGo言語のWebアプリ実装に慣れる
  • ISUCON過去問を限界までチューニングしてチューニング力を高める

自分ではHighLoad Cupへの挑戦が一番大きな成果だと思います。 HighLoad Cupはマニュアルを元にアプリケーションを実装するオンライン版ISUCONのような大会です。 2018年と2017年の過去問が公開されており、ていつでもベンチマークを実行できます。みんなやろうぜHighLoad Cup。 ただしHighLoad Cupのマニュアルはロシア語もしくは英語のみです。

HighLoad Cupについては前回のブログ記事にも書きましたのでご確認ください。

方針・戦略

詳しくはnetmarkjpのブログをご確認ください。

matsuuが当日やったこと

文章としてまとめるのが下手くそなので箇条書きで失礼します。 あと細かく記録取ってませんでした。ごめんなさい。

15000までの道のり

  • ログイン後に環境構築
    • nginx、app、mysqlのログ周り整備
    • kataribe、pt-query-digest(percona-toolkit)、dstatのインストール
    • net/http/pprofを使ったプロファイル環境整備
  • nginxチューニング
    • error_logをerrorからinfoに
    • keepalive_requestsを1000000に
    • http2有効化
    • upstreamを使ってバックエンドとHTTP/1.1とkeepalive有効化
    • 静的ファイルをnginxから直接配信する設定に
  • mysqlチューニング
    • itemsに (status), (created_at), (seller_id, status), (buyer_id) のインデックスを追加
  • categories テーブルが更新されていないことを確認し、アプリのインメモリに保存
  • /users/transactions のN+1の一部を解消(items取得時にUserSimple相当の内容も取得する)
  • UserSimple をアプリのインメモリにキャッシュ保存( sync.Map )
    • NumCellItemsは更新されるため、更新のたびにキャッシュを更新
  • デッドロックが多発するようになる
    • ログと実行されるSQLから POST /buyデッドロックが発生していることを確認
    • SET innodb_lock_wait_timeout = 1 を入れてデッドロックを早めに発生させる
      • デッドロックとなった場合は500エラーを返す実装になっていたが、 item is not for sale として403を返すように書換
    • innodb_lock_wait_timeout は1秒より短くすることができないので他の方法を考える
      • Redisで SETNX を使おうと実装を試みるもうまく機能せず断念
  • appからDBへの接続に失敗するエラーが多発
    • appからMySQLの同時接続数を最大4096まで引き上げ
  • パスワードのチェック(bcrypt実装)でCPU負荷が高くなっていた
    • 同じパスワードをそのまま保存してはいけないレギュレーションがあるため、複数サーバに負荷を分散させることに
    • isucariアプリをサーバの2台目、3台目に配置して/loginのみ2台目、3台目にアクセスを振るよう変更
  • configs をアプリのインメモリに保存
  • nginxでToo Many Open Filesのエラーが発生した
    • /etc/nginx/nginx.confworker_rlimit_nofile を追加
    • ついでに /etc/systemd/system/isucari.golang.serviceLimitNOFILE を追加
  • temp fileの生成を抑制するため /etc/nginx/nginx.confclient_body_buffer_size を追加
  • 1台目サーバのCPU負荷が落ち着いてきたので Campaign: 0 => Campaign: 2

16000までの道のり

  • GET /users/transactions.json (getTransactions)で transaction_evidencesshippings のN+1を解消
  • POST /buyデッドロック対策として sync.MapLoadOrStore() を使って楽観ロックもどきを実装した
  • 1台目サーバのCPU負荷が落ち着いてきたので Campaign: 2 => Campaign: 4

19000までの道のり

  • http.DefaultTransportのMaxIdleConnsPerHostを4096に引き上げ

26000までの道のり

  • 外部APIへのステータス確認(APIShipmentStatus)でStatusがdoneになったものをアプリのインメモリでキャッシュ、複数回アクセスが発生しないように

工夫した点

ここらへんを工夫しました

  • ベンチマークの事前処理、事後処理を行うシェルスクリプトを作成
    • 繰り返しやる作業はas codeだ!
  • ベンチマーク中にアプリやミドルウェアのログを tail -f などで眺めるの重要
    • パフォーマンスに直結するログがポロッと出てくることがある
  • 500エラーを返す場所が複数あるもののどの500エラーが発生したのかわからなかったのでlog.Print()を挟んで場所を特定
  • 複数ある SELECT * FROM items WHERE id = ? FOR UPDATE のうちどれが原因で詰まってるか分かりにくかった
    • SELECT /* postBuy */ * FROM items ... のように書き換えて SHOW PROCESSLIST で目視判別しやすく
  • Alibaba Cloudで事前に過去問を解いて練習した際にredis-serverが正常に起動しない問題に気づく
    • redis-serverはIPv6が利用できる前提になっているが、今回の環境ではIPv6が割り振られない
    • /etc/redis/redis.confbind から ::1 を削除
  • (いつもの)sshrcで個人別競技環境をさっとセットアップ
    • vim最新版やneovim最新版もインストール
  • サーバは基本的に1台目のみ操作、ベンチ直前にrsync転送とssh経由でのsystemd再起動を実施
  • ソースコードや設定の変更はサーバ上で直接vim/nvim
  • 同じ場所に集まって横並びに座る
    • やりとりは基本口頭で、文字情報の共有はSlackで

まとめ

若者に負けてらんねー!本選がんばるぞ!