【課題 6】毎リクエストの固定費を削る
読了目安 約2分
全リクエストが必ず通る認証処理と、開発用のままになっている設定を見直します。
この章の目次
ハンドラごとの重い処理を削ったので、次は全リクエストが必ず通る場所を見ます。
課題
認証処理のプロファイルです。 CPU 全体の 9.2 % をここで使っています。
ROUTINE ==== ...go.parseViewer
0 1.98s (flat, cum) 9.21% of Total
. 60ms 236: cookie, err := c.Request().Cookie(cookieName)
. 200ms 246: keysrc, err := os.ReadFile(keyFilename)
. 100ms 250: key, _, err := jwk.DecodePEM(keysrc)
. 700ms 255: token, err := jwt.Parse(
. 890ms 294: tenant, err := retrieveTenantRowFromHeader(c)該当するコードです。
keyFilename := getEnv("ISUCON_JWT_KEY_FILE", "../public.pem")
keysrc, err := os.ReadFile(keyFilename)
if err != nil {
return nil, fmt.Errorf("error os.ReadFile: keyFilename=%s: %w", keyFilename, err)
}
key, _, err := jwk.DecodePEM(keysrc)
if err != nil {
return nil, fmt.Errorf("error jwk.DecodePEM: %w", err)
}var tenant TenantRow
if err := adminDB.GetContext(
context.Background(), &tenant,
"SELECT * FROM tenant WHERE name = ?", tenantName,
); err != nil {
return nil, fmt.Errorf("failed to Select tenant: name=%s, %w", tenantName, err)
}サーバーの起動部分です。
e := echo.New()
e.Debug = true
e.Logger.SetLevel(log.DEBUG)
e.Use(middleware.Logger())
e.Use(middleware.Recover())リクエストごとに繰り返す必要がないものを 4 つ挙げてください。
解答例
1. 公開鍵の読み込みとパース
public.pem の中身はプロセスが動いている間ずっと同じです。
sync.Once で最初の 1 回だけにします。
var (
jwtKeyOnce sync.Once
jwtKey any
jwtKeyErr error
)
func jwtPublicKey() (any, error) {
jwtKeyOnce.Do(func() {
keyFilename := getEnv("ISUCON_JWT_KEY_FILE", "../public.pem")
keysrc, err := os.ReadFile(keyFilename)
if err != nil {
jwtKeyErr = fmt.Errorf("error os.ReadFile: keyFilename=%s: %w", keyFilename, err)
return
}
key, _, err := jwk.DecodePEM(keysrc)
if err != nil {
jwtKeyErr = fmt.Errorf("error jwk.DecodePEM: %w", err)
return
}
jwtKey = key
})
return jwtKey, jwtKeyErr
}2. e.Debug = true
true にすると、レスポンスの JSON を人が読みやすいように整形します。
第 6 章のプロファイルに出ていた encoding/json.appendIndent がこの処理です。
ベンチマーカーは見た目を気にしません。
e.Debug = false
e.Logger.SetLevel(log.ERROR)log.OFF まで下げると、errorResponseHandler が出す内部エラーのログも消えます。
落ちたときに原因を追えなくなるので、ERROR は残します。
3. middleware.Logger()
全リクエストのログを 1 行ずつ書き出します。 アクセスログは nginx 側でも取っているので、二重です。
// e.Use(middleware.Logger()) を消す
e.Use(middleware.Recover())4. テナントの引き直し
tenant テーブルは、行が追加されるだけで更新されません。
一度引いたらメモリに置いておけます。
var (
tenantCacheMu sync.RWMutex
tenantCache = map[string]*TenantRow{}
)
func lookupTenantCache(name string) (*TenantRow, bool) {
tenantCacheMu.RLock()
defer tenantCacheMu.RUnlock()
t, ok := tenantCache[name]
return t, ok
}/initialize はテナントを作り直すので、そのときにキャッシュも捨てます。
これを忘れると、消えたはずのテナントを返して失格します。
func initializeHandler(c echo.Context) error {
out, err := exec.Command(initializeScript).CombinedOutput()
if err != nil {
return fmt.Errorf("error exec.Command: %s %e", string(out), err)
}
resetTenantCache()
// ...
}おまけ: すでに手元にある値
ランキング取得には、こんなコードが残っています。
var tenant TenantRow
if err := adminDB.GetContext(ctx, &tenant, "SELECT * FROM tenant WHERE id = ?", v.tenantID); err != nil {
return fmt.Errorf("error Select tenant: id=%d, %w", v.tenantID, err)
}引いた tenant から使うのは tenant.ID だけです。
その値は引数の v.tenantID と同じものです。
クエリごと消せます。
効果
| スコア | |
|---|---|
| 取る量を減らした | 38522 |
| 公開鍵・Debug・Logger | 41693 |
| テナントの引き直しをやめた | 44139 |
1 回あたりは小さくても、全リクエストが通る場所は回数で効いてきます。