🤖

🤖

:gijutsu_burogu:

ISUCON: DBのデータをメモリに載せて高速化する

はじめに

ISUCON では DB のデータを可能ならばオンメモリに載せるという定石があります。

以下のような記事もあり、オンメモリにこだわる戦略でも予選突破できるほどのテクニックです。

math314.hateblo.jp

そのやり方について記事にしました。

やり方

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 が叩かれるかなど吟味しながら、用法用量を守ってオンメモリ化するのが良さそうです。

オンメモリにとにかく載せるというのは現実のアプリケーションではデータの永続化ができないためほぼ行われません。 また、小手先のテクニック感が強いです。 そのため運営側からすると期待されている改善策ではなく、オンメモリ化による高速化しにくい問題が出題される傾向にあるとなにかの記事で見ました。

参考

github.com