はじめに
Go言語で作るプログラムはほとんどの場合、そのままでも十分高速に稼働しますが、場合によってはキャッシュを使用して高速化を図る場合があります。
Goにはgocache
という強力なローカルキャッシュパッケージが存在しますが、gocache
だと下記の点でまれに問題が発生することがあります。
- ホスト・プログラム・サービスなどをまたいでキャッシュを参照させられない
- プログラムが終了するとキャッシュが消える
これらを回避するため、ローカルで済むキャッシュについてはgocache
でキャッシュしておき、それ以外のキャッシュはmemcache
やredis
などに載せる、といった戦略を採ることが多いと思いますので、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の強みでもあります。
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
が追加ベンチマークです。
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のソースコードが大変参考になります)。
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接続プログラムの結果です。
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くらいの処理であれば、自作してしまったほうがパフォーマンス的に良さそうですね。
※パッケージごとのボトルネックは時間が無くて追えなかったので、後日調査します(;´Д`)