はじめに
ISUCON では DB のデータを可能ならばオンメモリに載せるという定石があります。
以下のような記事もあり、オンメモリにこだわる戦略でも予選突破できるほどのテクニックです。
そのやり方について記事にしました。
やり方
ISUCON9 予選の問題を例にします。
どのデータをオンメモリに載せるか
INSERT や UPDATE などの書き込みがないテーブルをメモリに載せます。 書き込みがあるテーブルもメモリに載せることは不可能ではありませんが、排他制御が必要になり複雑になります。 また、DB ではリレーションについても考える必要があります。
ツールを使って、書き込みがないテーブルを探します。 一個一個見ていっては時間がかかりますし、見間違いも発生します。
$ go get -u github.com/kotaroooo0/isucontools/sqlstr
$ cat main.go | sqlstr
INSERT, UPDATE, DELETE のみを抽出し書き込みがあるテーブルが分かるので、同時に書き込みがないテーブルも分かります。
stdin.go:474:3 in postInitialize INSERT INTO `configs` (`name`, `val`) VALUES (?, ?) ON DUPLICATE KEY UPDATE `val` = VALUES(`val`) ============== 略 ============== stdin.go:1842:19 in postComplete UPDATE `shippings` SET `status` = ?, `updated_at` = ? WHERE `transaction_evidence_id` = ? stdin.go:1855:19 in postComplete UPDATE `transaction_evidences` SET `status` = ?, `updated_at` = ? WHERE `id` = ? stdin.go:2102:19 in postBump UPDATE `items` SET `created_at`=?, `updated_at`=? WHERE id=? stdin.go:2252:26 in postRegister INSERT INTO `users` (`account_name`, `hashed_password`, `address`) VALUES (?, ?, ?)
テーブル | configs |
users |
items |
transaction_evidences |
shippings |
categories |
---|---|---|---|---|---|---|
書き込み有り | ○ | ○ | ○ | ○ | ○ | × |
categories
はオンメモリに載せられそうです。
また、よくみるとconfigs
の INSERT はpostInitialize
内で行われているので最初しか呼び出しされません。
なので、configs
もオンメモリに載せられそうです。
どう実装するか
Go でcategories
テーブルをメモリに載せてみます。
1. グローバルで変数を宣言します。
var ( categoryMap = make(map[int]*Category) )
2. 初期化処理/initialize
でメモリに載せます。
categories := []*Category{} if err := dbx.Select(&categories, "SELECT * FROM categories"); err != nil { log.Print(err) outputErrorMsg(w, http.StatusInternalServerError, "db error") return } for _, c := range categories { categoryMap[c.ID] = c }
3. SQL を叩いて取得している部分をメモリに載せた Map から取得するように変えます。
func getCategoryByID(q sqlx.Queryer, categoryID int) (category Category, err error) { // 元々の実装 // err = sqlx.Get(q, &category, "SELECT * FROM `categories` WHERE `id` = ?", categoryID) // if category.ParentID != 0 { // parentCategory, err := getCategoryByID(q, category.ParentID) // if err != nil { // return category, err // } // category.ParentCategoryName = parentCategory.CategoryName // } category = *categoryMap[categoryID] category.ParentCategoryName = categoryMap[category.ParentID].CategoryName return category, err }
以下の PR で実装しました。
categories
テーブル
https://github.com/kotaroooo0/isucon9q/pull/1/files
config
テーブル
https://github.com/kotaroooo0/isucon9q/pull/2/files
おわりに
今回は書き込みがないテーブルのオンメモリ化についてでしたが、書き込みがあるテーブルのオンメモリ化は Go ならsync.Mutex
を利用することで排他制御し実現できます。
どのような SQL が叩かれるかなど吟味しながら、用法用量を守ってオンメモリ化するのが良さそうです。
オンメモリにとにかく載せるというのは現実のアプリケーションではデータの永続化ができないためほぼ行われません。 また、小手先のテクニック感が強いです。 そのため運営側からすると期待されている改善策ではなく、オンメモリ化による高速化しにくい問題が出題される傾向にあるとなにかの記事で見ました。