post Image
GoのMemcacheパッケージ比較

はじめに

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くらいの処理であれば、自作してしまったほうがパフォーマンス的に良さそうですね。

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


『 Go 』Article List
Category List

Eye Catch Image
Read More

Androidに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

AWSに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Bitcoinに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

CentOSに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

dockerに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

GitHubに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Goに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Javaに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

JavaScriptに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Laravelに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Pythonに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Rubyに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Scalaに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Swiftに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Unityに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Vue.jsに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Wordpressに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

機械学習に関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。