koukiblog

たぶんweb系の話題

gorilla/sessionでメモリリークが起きる原因を調べた

はじめに

開発中のGoで書かれたアプリケーションがメモリリークを起こしていて、gorilla/sessions ( https://github.com/gorilla/sessions ) というライブラリをバージョンアップしたら解決した。

メモリリークを解決したPRはこれ。

github.com

gorilla/contextが原因らしく、Contextが存在するよりも前にContext相当のことをしていたライブラリなので、併用するとよくないことが起こりそうだなーとは思うものの、なぜメモリリークするのかが理解できなかったので調べてみた。

調べた結果

Go 1.7で追加されたContextとgorilla/contextの競合によりメモリリークが発生するようになったのかと思い、少し混乱したのだが、

はそれぞれ独立した事象だった。が、後者のContextの競合は、メモリリークが発生する可能性をより高めてしまっている。 具体的には、 gorilla/contextと http.request.WithContextを併用すると、メモリリークに対する既知の対応を入れた場合でも、gorilla/contextで保存した内容を削除できずにメモリリークが起きる。

メモリリーク

gorilla/session を使う場合は、リクエストの最後にClearを行わないとcontextに保存した内容が削除されず、メモリリークが起きる。 これはgorilla/contextの仕様であり、GoのContextとは関係ない問題だった。 gorilla/muxを利用する場合はライブラリが面倒を見てくれるので利用者は気にしなくて良くて、gorila/session または gorilla/contextを単独で利用している場合に限り、利用者側が気をつける必要がある。

gorilla/contextとGoのContextの競合

gorilla/contextは、http.Requestのポインタをkeyにしたmapにデータを保存している。

var (
    mutex sync.RWMutex
    data  = make(map[*http.Request]map[interface{}]interface{})
    datat = make(map[*http.Request]int64)
)

https://github.com/gorilla/context/blob/master/context.go#L13..L17

一方、http.Request.WithContextは、requestをshallow copyした上でcontextをセットするので、引数として渡したrequestと戻り値のhttp.requestのポインタが変わってしまう。

WithContextの定義はこれ。

func (r *Request) WithContext(ctx context.Context) *Request {
    if ctx == nil {
        panic("nil context")
    }
    r2 := new(Request)
    *r2 = *r
    r2.ctx = ctx
    r2.URL = cloneURL(r.URL)
    return r2
}

go/request.go at 3409ce39bfd7584523b7a8c150a310cea92d879d · golang/go · GitHub

WithContextを利用すると、gorilla/contextで保存する先のmapが変わってしまう。 コードにすると以下のようなことが起こる。

package main

import (
    "context"
    "fmt"
    "net/http"

    gcontext "github.com/gorilla/context"
)

func main() {
    r := new(http.Request)
    fmt.Printf("%v\n", &r) // => 0xc000010020

    gcontext.Set(r, "key", "value") 
    fmt.Printf("gorilla/context: %v\n", gcontext.Get(r, "key")) // => gorilla/context: value

    r = r.WithContext(context.Background())
    fmt.Printf("%v\n", &r) // => 0xc00009c010

    fmt.Printf("gorilla/context: %v\n", gcontext.Get(r, "key")) // => gorilla/context: <nil>
}

上記の例では省略したが、context.Clear(r) を行ってもセットした値を消すことはできない

ライブラリ側の解決策

gorilla/sessionが、1.6 以前のサポートを切ったので、gorilla/contextを利用しなくなった。 上記の回避策が不要になった。 なので、最新のgorilla/sessionを使えばメモリリークは発生しない。

再掲だが、PRはこちら

github.com

gorilla/contextを直接利用すると変わらずメモリリークの可能性はあるので注意

備考

調べたときに利用したページを自分の備忘のため残しておく

そもそもContextとは

ちゃんと理解できてなかったので改めて調べた。こちらのページに日本語訳があって助かった www.ymotongpoo.com

なぜgorilla/muxを利用するとメモリリークしないのか

gorilla/mux ( https://github.com/gorilla/mux ) の mux.go の旧バージョンを参照すると、ServeHTTPメソッドに下記の記述があった。

func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {

       ~~~~

    if !r.KeepContext {
        defer contextClear(req)
    }

       handler.ServeHTTP(w, req)
}

実行されているcontextClearの定義は gorila/context の context.Clear。muxではライブラリ側で、gorilla/contextのcontextClearが実行されていたことが確認できた

ちなみに、gorilla/sessions でメモリリークの回避策で紹介されていた ClearHandlerの定義はこちら

func ClearHandler(h http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer Clear(r)
        h.ServeHTTP(w, r)
    })
}

ほぼ同じ効果が期待できそう。

muxのGoのContextへの対応

前述の通り、メモリリークとGoのContextへの対応はまた別の問題。gorilla/muxでは、Goのバージョンによって挙動を変更していた。

Store vars and route in context.Context when go1.7+ is used by ejholmes · Pull Request #169 · gorilla/mux · GitHub

修正のきっかけはこれっぽい

mux.Vars breaks with Go 1.7 · Issue #168 · gorilla/mux · GitHub

メモリリークが報告されている様子

https://groups.google.com/forum/#!msg/gorilla-web/clJfCzenuWY/N_Xj9-5Lk6wJ