なまえは まだ ない

思いついたことをアウトプットします

ISUCON14延長戦の記録⑦ インメモリキャッシュに手を出す

ISUCON14の延長戦をやってます

以下の記事の続きです。

furusax0621.hatenablog.com

前回は細かいチューニングをしつつ、MySQLサーバーを別インスタンスに切り出しました。スコアは本戦当時の上位入賞に食い込める28,000点まで伸びています。

またしてもやることが見えなくなってきたので、講評を読みながら改善点を探ることにしました。

なお、最終的なコードは以下のリポジトリで公開しています。

github.com

通知エンドポイントの高速化

解説・講評を読んでいると2種類の通知エンドポイント /api/app/notification /api/chair/notification の高速化について言及されています。当日のマニュアルにもあるとおり、これらのエンドポイントはSSE(Server Sent Events)への実装差し替えが可能です。が、正直SSEについての知識も実装できる自信もなかったため、JSON APIのまた愚直にキャッシュしていくことにしました。

椅子の通知エンドポイントレスポンスをキャッシュする

椅子の通知エンドポイントのレスポンスをよく見てみると、椅子がライドにマッチングしている場合、その情報を返すようです。 返すべきライドの情報はいくつかありますが、ステータス以外の情報は固定であることからこの情報をインメモリキャッシュできそうだと考えました。

完全な差分は以下のPull Requestで確認できます。

github.com

恥ずかしながらRedisなどインメモリデータストアに関する知見が乏しいので、sync.RWMutexとmapで愚直に実装しました。mapは椅子のIDをキーに、椅子がマッチングされているライドの情報をもつマップとして宣言しています。mapを更新するタイミングは以前導入した chairs.is_free カラムと一緒です。マッチングしたタイミングでmapに追加し、通知エンドポイントで COMPLETED を椅子に通知したタイミングで削除すればOKです。

実装してから気付いたのですが、このmapを導入することによって椅子にライドが割り当てられてないときのレスポンスを高速に返せるようになりました。ちょうど以下の実装箇所です。

rideMapByChairIDMutex.RLock()
ride, ok := rideMapByChairID[chair.ID]
rideMapByChairIDMutex.RUnlock()

if !ok {
    writeJSON(w, http.StatusOK, &chairGetNotificationResponse{
        RetryAfterMs: 30,
    })
    return
}

https://github.com/furusax0621/isucon14-extend/blob/eda0047d9d96b8f1968eff0546d533f8a63cfe12/webapp/go/chair_handlers.go#L211-L220

map導入前は椅子のIDをキーに最新のライド情報を取得し、ライドが存在しないか完了していれば割り当てられてないと判断していました。 データベースアクセスが必要な上、ロジックの都合上トランザクションを作成する必要があるのでどうしても重くなってしまうようです。 これをデータベースアクセスなしで判断できるようになったのは、とても効果が大きかったようです。

まとめ・次回予告

2つある通知エンドポイントのひとつである椅子の通知エンドポイントを修正し、椅子毎のライド情報をキャッシュできるようにしました。 これを導入したところ、スコアが37,900点程度にまで伸びました。どんどん上がりますね。楽しくなってきた。

次回はもうひとつの通知エンドポイントのキャッシュをしてみます。つづく。