Posted at

GoのMemcacheパッケージ比較

More than 3 years have passed since last update.


はじめに

Go言語で作るプログラムはほとんどの場合、そのままでも十分高速に稼働しますが、場合によってはキャッシュを使用して高速化を図る場合があります。

Goにはgocacheという強力なローカルキャッシュパッケージが存在しますが、gocacheだと下記の点でまれに問題が発生することがあります。

* ホスト・プログラム・サービスなどをまたいでキャッシュを参照させられない

* プログラムが終了するとキャッシュが消える

これらを回避するため、ローカルで済むキャッシュについてはgocacheでキャッシュしておき、それ以外のキャッシュはmemcacheredisなどに載せる、といった戦略を採ることが多いと思いますので、memcacheパッケージについて比較してみます。


Goのmemcacheパッケージ

Goのmemcacheパッケージは何があるのか、まずgithubで登録されているリポジトリを検索したところ、bradfitz/gomemcacheが一番スターを獲得していたので、こちらを試しに使ってみましょう。

(以下すべてgo 1.5.1です。)

パッケージの取り込み



$ go get github.com/bradfitz/gomemcache/memcache

実行サンプル


import (
"github.com/bradfitz/gomemcache/memcache"
)

func main() {
mc := memcache.New("10.0.0.1:11211", "10.0.0.2:11211", "10.0.0.3:11212")
mc.Set(&memcache.Item{Key: "foo", Value: []byte("my value")})

it, err := mc.Get("foo")
...
}

シンプルでわかりやすいですね。

せっかくなので、このパッケージのパフォーマンスがどれくらいなのかベンチマークを書いて計測します。

こういったベンチマークがすぐできるのがGoの強みでもあります。


mem_test.go

package main

import(
"testing"
"github.com/bradfitz/gomemcache/memcache"
)

func BenchmarkMemLib(b *testing.B){
b.ReportAllocs()
b.ResetTimer()
mc := memcache.New("127.0.0.1:11211")
for i:= 0; i < b.N; i++{
mc.Set(&memcache.Item{Key:"foo", Value:[]byte("unko")})
mc.Get("foo")
}
}


ベンチマークを実行します。

$ go test mem_test.go -bench=Bench

testing: warning: no tests to run
PASS
BenchmarkMemLib 20000 62516 ns/op 3169 B/op 60 allocs/op
ok command-line-arguments 1.881s

単純なSetとGetのベンチにもかかわらず、アロケートが多いような・・・

他のパッケージと比較してみます。

次にスターが多かったのがrainycape/memcacheです。

READMEにもbradfitz/gomemcacheを更に高速化するためにforkした、と書かれているので期待できそうです。

パッケージを取り込みます。

$ go get github.com/rainycape/memcache

実行サンプル

import (

"github.com/rainycape/memcache"
)

func main() {
mc, _ := memcache.New("10.0.0.1:11211", "10.0.0.2:11211", "10.0.0.3:11212")
mc.Set(&memcache.Item{Key: "foo", Value: []byte("my value")})

it, err := mc.Get("foo")
...
}

bradfitz/gomemcacheとほとんど一緒ですが、こちらはNew()の戻り値が多値になっています(memcacheサーバはどちらも複数指定できます)。

こちらも先ほどのベンチマークに追加して計測してみましょう。

BenchmarkMemLib2が追加ベンチマークです。


mem_test.go

package main

import(
"testing"
"github.com/bradfitz/gomemcache/memcache"
memcache2 "github.com/rainycape/memcache"
)

func BenchmarkMemLib(b *testing.B){
b.ReportAllocs()
b.ResetTimer()
mc := memcache.New("127.0.0.1:11211")
for i:= 0; i < b.N; i++{
mc.Set(&memcache.Item{Key:"foo", Value:[]byte("unko")})
mc.Get("foo")
}
}

func BenchmarkMemLib2(b *testing.B){
b.ReportAllocs()
b.ResetTimer()
mc,_ := memcache2.New("127.0.0.1:11211")
for i:= 0; i < b.N; i++{
mc.Set(&memcache2.Item{Key:"foo", Value:[]byte("unko")})
mc.Get("foo")
}
}


実行結果

$ go test mem_test.go -bench=Bench

testing: warning: no tests to run
PASS
BenchmarkMemLib 20000 63543 ns/op 3169 B/op 60 allocs/op
BenchmarkMemLib2 20000 64594 ns/op 208 B/op 7 allocs/op
ok command-line-arguments 3.854s

うーん、アロケーションは随分小さくなりましたが、処理速度は若干遅くなっています。

memcacheサーバへのアクセス以外の処理で負荷がかかってる可能性がありますね。。。


自作する

ということで、自分でも書いてみることにしました(Memcacheまわりの処理はvitesのソースコードが大変参考になります)。


mem.go

package main

import (
"bufio"
"fmt"
"net"
"strings"
"strconv"
)

type Memcache struct {
conn net.Conn
buffered bufio.ReadWriter
}

func Mem(addr string) (conn *Memcache, err error) {
nc, err := net.Dial("tcp", addr)
if err != nil {
return nil, err
}
return &Memcache{
conn: nc,
buffered: bufio.ReadWriter{
Reader: bufio.NewReader(nc),
Writer: bufio.NewWriter(nc),
},
}, err
}

func (mc *Memcache) get(key string) (result []byte, err error) {
_, err = mc.buffered.WriteString("get "+key+"\n")
if err == nil {
err = mc.buffered.Flush()
if err == nil {
for {
b,_,err := mc.buffered.ReadLine()
l := string(b)
if err == nil {
if strings.HasPrefix(l, "END") {
break
}
if strings.Contains(l, "ERROR") {
panic("ERROR")
}
if !strings.HasPrefix(l, "VALUE") {
result = append(result, l...)
result = append(result, '\n')
}
} else {
panic(err)
}
}
} else {
panic(err)
}
}
return result, err
}

func (mc *Memcache) set(key string, value []byte) (err error) {
_, err = mc.buffered.WriteString("set "+key+" 0 0 "+strconv.Itoa(len(value))+"\r\n")
if err == nil {
v := append(value,"\r\n"...)
_, err = mc.buffered.Write(v)
if err != nil {
panic(err)
}
err = mc.buffered.Flush()
if err == nil {
mc.buffered.ReadLine()

}
}
return err
}

func main() {
m, err := Mem("127.0.0.1:11211")
if err == nil {
err = m.set("foo",[]byte("unko"))
if err == nil {
res,merr := m.get("foo")
if merr == nil {
fmt.Printf("%s", res)
}
}
}
defer m.conn.Close()
}


ベンチマークを取ります。

BenchmarkMemOriginが自分で書いたMemcache接続プログラムの結果です。


mem_test.go

package main

import(
"testing"
"github.com/bradfitz/gomemcache/memcache"
memcache2 "github.com/rainycape/memcache"
)

func BenchmarkMemLib(b *testing.B){
b.ReportAllocs()
b.ResetTimer()
mc := memcache.New("127.0.0.1:11211")
for i:= 0; i < b.N; i++{
mc.Set(&memcache.Item{Key:"foo", Value:[]byte("unko")})
mc.Get("foo")
}
}

func BenchmarkMemLib2(b *testing.B){
b.ReportAllocs()
b.ResetTimer()
mc,_ := memcache2.New("127.0.0.1:11211")
for i:= 0; i < b.N; i++{
mc.Set(&memcache2.Item{Key:"foo", Value:[]byte("unko")})
mc.Get("foo")
}
}

func BenchmarkMemOrigin(b *testing.B){
b.ReportAllocs()
b.ResetTimer()
m, _ := Mem("127.0.0.1:11211")
defer m.conn.Close()
for i:= 0; i < b.N; i++{
m.set("foo",[]byte("unko"))
m.get("foo")
}
}


実行結果

$ go test mem.go mem_test.go -bench=Bench

testing: warning: no tests to run
PASS
BenchmarkMemLib 20000 62850 ns/op 3169 B/op 60 allocs/op
BenchmarkMemLib2 20000 64663 ns/op 208 B/op 7 allocs/op
BenchmarkMemOrigin 50000 33910 ns/op 96 B/op 6 allocs/op
ok command-line-arguments 5.879s

約半分くらいの処理速度になりました。

パッケージはエラー処理やcastなどが入っており安全に使用できることが考慮されているので必ずしも自作するべきだとは思いませんが、memcacheのSetやGetくらいの処理であれば、自作してしまったほうがパフォーマンス的に良さそうですね。

※パッケージごとのボトルネックは時間が無くて追えなかったので、後日調査します(;´Д`)