Go の MySQL ドライバの効率の良い使い方

10/5 に ISUCON 3 の予選に Go 言語で参戦していました。

とりあえずレポートは会社のブログに書いたので、 Go 言語で go-sql-driver/mysql を使って MySQL を使う時に知っておくと良い点をまとめておきます。

ちなみに MySQL ドライバにはもうひとつ MyMySQL というものがあり、 まだ試していませんが、 MyMySQL の方が落とし穴が少なそうな気がします。

sql.Open() が返す DB オブジェクトはコネクションプールをしてくれる

なので、自前で DB オブジェクトを使いまわしてコネクションプールを実装しても意味は無いです。 DB.SetMaxIdleConn() で、使い終わってもクローズしないコネクションの数を設定できます。 デフォルトだと使い終わったコネクションを閉じてしまうので、 DB オブジェクト自体をプールしても コネクションプールは実現できていません。

最大接続数を制限したい場合は、最大接続数分のバッファを持ったチャンネルを作っておいて、 DBを使う前に dbMutex <- true して終わったら <- dbMutex するとかで良いと思います。

接続開始から一定時間経ったコネクションを自動で閉じるなど、もっと細かい制御がしたい場合は、 自前で実装する必要があります。 その場合は database/sql をやめて自前で Driver 叩いたほうが楽だと思います。

rows.Close() の前に Commit するとエラーになる

rows.Next() は1 Row を読み込み、最後に EOF を読み込むと false を返します。

例えば1行だけのレスポンスを返すクエリを実行し、 rows.Next() を一回実行しただけで Tx.Commit() を 呼び出すと、コミットパケットを投げて、次のOKパケットを受け取るつもりで、残ってたEOFパケットを 読み込んでしまいます。

これ実装難しいところなんですよね。コネクションと相互作用のあるオブジェクトを複数 (この場合は Rows と Tx) 作るとき、片方のオブジェクトからした操作によって、もう片方のオブジェクトのオブジェクトの中途半端な状態を 適切に処理しないといけない。

プレースホルダを使って Query/Exec できない

プレースホルダありの Conn.Query()ErrSkip を返し、 database.sql は代わりに Conn.Prepare()Stmt.Query() を実行します。 Exec も同様です。

昔会社のブログにも書いたのですが、 1回のクエリのためだけに Prepare & Execute すると、通信回数が多くなってパフォーマンス的に よくありません。

なので、文字列を適切にエスケープしつつ、 fmt.Sprintf() でクエリ文字列を作ってしまいましょう。 Go を使うということは文字コードは当然 utf8 を選ぶでしょうから、エスケープするのはそれほど難しくありません。 ちなみに、 'sql_mode="NO_BACKSLASH_ESCAPES"' すると、シングルクォートを2重にするだけで エスケープできるのでオススメです。

https://github.com/methane/isucon3-qual-go/blob/master/app.go

func sql_escape(s string) string {
    return strings.Replace(s, "'", "''", -1)
}
...
    result, err := DB.Exec(
        fmt.Sprintf("INSERT INTO memos (user, content, is_private, created_at) VALUES (%d, '%s', %d, '%s')",
        user.Id, sql_escape(r.FormValue("content")), isPrivate, now))
このブログに乗せているコードは引用を除き CC0 1.0 で提供します。