gorilla/sessionでメモリリークが起きる原因を調べた
はじめに
開発中のGoで書かれたアプリケーションがメモリリークを起こしていて、gorilla/sessions ( https://github.com/gorilla/sessions ) というライブラリをバージョンアップしたら解決した。
メモリリークを解決したPRはこれ。
gorilla/contextが原因らしく、Contextが存在するよりも前にContext相当のことをしていたライブラリなので、併用するとよくないことが起こりそうだなーとは思うものの、なぜメモリリークするのかが理解できなかったので調べてみた。
調べた結果
Go 1.7で追加されたContextとgorilla/contextの競合によりメモリリークが発生するようになったのかと思い、少し混乱したのだが、
- メモリリークする
- gorilla/contextとGoの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はこちら
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のバージョンによって挙動を変更していた。
修正のきっかけはこれっぽい
mux.Vars breaks with Go 1.7 · Issue #168 · gorilla/mux · GitHub
メモリリークが報告されている様子
https://groups.google.com/forum/#!msg/gorilla-web/clJfCzenuWY/N_Xj9-5Lk6wJ