groupcache は Go で書かれた分散キャッシュライブラリで、複数プロセスでキャッシュを共有するプログラムが簡単に書けます。多くの用途で memcached を置き換えることを目指していて、Google のプロダクション環境で使われているようです。
個人的に groupcache の魅力は、
- キャッシュを複数のピアに分散する (sharding)
- キャッシュに無いデータを同時に大量にリクエストしても、キャッシュ生成処理は1度だけ実行され、他のリクエストには生成されたキャッシュを返す(厳密では無いが、だいたいの場合1度きり)
- 頻繁にリクエストされるデータがリモートにあった場合、ローカルにもキャッシュする
一方、注意しないといけない点はキャッシュのアップデートには対応していないことです。groupcache には expire や明示的な破棄は存在しません。あるキーに対する値は 常に同じ であることが要求されます。
サンプル
サンプルコードがほとんどなくて、groupcache の使い方が分かりにくかったのでサンプルを貼っておきます。このサンプルではピアと通信するのに http を使いますが、フロントとはポートを分けておきたかったので martini とは別で動かしています。
package main
import (
"encoding/json"
"github.com/codegangsta/martini"
"github.com/golang/groupcache"
"net/http"
"os"
"time"
)
func main() {
addr := os.Getenv("GROUPCACHE_ADDR")
peers := groupcache.NewHTTPPool("http://" + addr)
peers.Set("http://127.0.0.1:8000", "http://127.0.0.1:8001")
go http.ListenAndServe(addr, peers)
heavy := groupcache.NewGroup("heavy", 64<<20, groupcache.GetterFunc(heavyTask))
m := martini.Classic()
m.Get("/_stats", func() []byte {
v, err := json.Marshal(&heavy.Stats)
if err != nil {
panic(err)
}
return v
})
m.Get("/:key", func(params martini.Params) string {
var result string
if err := heavy.Get(nil, params["key"], groupcache.StringSink(&result)); err != nil {
panic(err)
}
return result
})
m.Run()
}
func heavyTask(ctx groupcache.Context, key string, dst groupcache.Sink) error {
time.Sleep(400 * time.Millisecond)
dst.SetString("Value of " + key)
return nil
}
実行
上記のコードを main.go
というファイル名で保存したら、端末を2つ開いて以下のように実行します。
$ GROUPCACHE_ADDR=127.0.0.1:8000 go run main.go
$ GROUPCACHE_ADDR=127.0.0.1:8001 PORT=3001 go run main.go
3つ目を開いて curl で試すと、キャッシュされているのが分かると思います。
$ curl localhost:3000/foo # 400ms
$ curl localhost:3001/foo # <1ms
また、http://localhost:3000/_stats
にアクセスすると統計情報が JSON で取れます。
課題
groupcache にはピアの自動更新機能はありません。そのため、うまく使うにはピアの管理がポイントになります。
orchestration ツールと組み合わせるか、memberlist や etcd と組み合わせて使うとよさそうです。