ISUCON9 予選惜敗
yuki2006さんと2人で参加して最終スコア9560イスコインでした。
使用した言語はGo
個人的攻略ポイント
Nginxのアクセスログの集計結果から/users/transactions.jsonに時間がかかっていたことがわかったのでそこを改善する必要があると判断。実行しているSQLも重そうだが、取得したitemsをforループで回している中で配送サービスAPIにリクエストを投げているのがヤバそう。
とりあえず並行にリクエストを投げても問題なさそうだったのでsync.WaitGroupとgoroutineを使って並行化。スコアアップするもののまだ重い。
そもそも配送サービスAPIに問い合わせる必要があるのかよく考えてみる。APIからのレスポンスで使用しているのはstatusのプロパティでこれは配送サービス側が保持している配送状態を表す。 配送状態がどのように変化するかはアプリケーション仕様書に書いてある。
以下は仕様書から ISUCARI ステータス遷移表 を抜粋したもの
| WHO | items | transaction_evidences | shippings | |
|---|---|---|---|---|
| /sell (出品) | 出品者 | on_sale | - | - |
| /buy (購入) | 購入者 | trading | wait_shipping | initial |
| /ship (集荷予約) | 出品者 | ↓ | ↓ | wait_pickup |
| /ship_done (発送完了) | 出品者 | ↓ | wait_done | shipping or done |
| /complete (取引完了) | 購入者 | sold_out | done | done |
この表からいくつかわかることがある。
- 購入者が
/buyを叩いて完了した際に配送状態はinitialとなる - 出品者が
/shipを叩いて完了した際に配送状態はwait_pickupとなる - 出品者が
/ship_doneを叩いて完了した後の配送状態はshippingもしくはdoneのどちらかである - 購入者が
/completeを叩いて完了した際に配送状態はdoneで確定している
つまり/ship_done以外の場合には配送状態は一意に特定できる。そして、それと対応するように各エンドポイントの処理で配送状態をshippingsテーブルのstatusに保存している。
以上のことから、shippingsテーブルのstatusがshippingである場合のみそれが配送中なのか配送済みなのかは配送サービスAPIに問い合わせてみないとわからないのであって、それ以外の場合はDBに保存されている値を使えばいいことになる。
実際にAPIアクセスを削って、ついでに問い合わせた結果がdoneだった場合にDBの値を更新してみたところ、3700だったスコアが9000くらいに伸びました。
使ったツールなど
- rsync: ファイル転送
- tkuchiki/alp: Nginxのアクセスログのプロファイラ
- mysqldumpslow: MySQLのスローログ解析
- catatsuy/notify_slack: サーバ上での集計結果などをスラックに通知
- Go pprofのWebUI: GoのCPUボトルネック解析
- najeira/measure: Goの処理時間計測
手元のMacのGo 1.13でクロスコンパイルしてrsyncでサーバに実行ファイルを送るといった手段をとっていまして、もしサーバ上のGoのバージョンが古かったりしても気にする必要がなかったので割とよかった気がします。
また、今回の事前練習中にnotify_slackのことを知り使ってみたところ、journalctlでのログをそのままSlackに流し込んだりalpやmysqldumpslowでの集計結果をSlack上で共有するのが非常に簡単でとても便利でした。
感想
個人的に上記のISUCARI ステータス遷移表は運営側から与えられた大きなヒントだと思っていて、仕様をちゃんと理解すると不要な処理だとわかるといった問題がよくできているな、と感心しました。
ログイン処理や購入処理などもボトルネックになっていたのはわかっていましたが、ツールの使用方法を間違えて時間を消費したり/users/transactions.jsonに時間がかかってしまったりで他の部分の改善案を考える時間が足りませんでした。予選突破まであと少しと悔いが残る結果になってしまったので、来年リベンジできるよう精進しようと思います。