比较Go语言中的Memcache包

首先

使用Go语言编写的程序通常都可以在原样运行的情况下具备足够的高速性,但在某些情况下,可以使用缓存来加速运行。

Go 中存在一个强大的本地缓存包叫做 gocache,但是使用 gocache 有时会出现以下问题:
* 无法在主机、程序或服务之间共享缓存
* 程序结束后缓存会被清空

为了避免这些问题,通常我们会使用gocache来在本地缓存中保存可处理的缓存,而其他缓存则会使用memcache或redis等来存储。因此,我们将比较一下memcache包。

Go的memcache包

首先,我在GitHub上搜索了与Go的memcache包有关的存储库。结果发现,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

尽管这个简单的设置和获取基准测试,看起来有很多的分配……

与其他包装进行比较。

接下来,最多星星的是rainycape/memcache。README中还写着为了进一步提高速度而fork了bradfitz/gomemcache,所以可以期待一下。

我将导入软件包。

$ 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

处理速度大约减慢了一半左右。

我认为不一定必须自己制作,因为包含了错误处理和类型转换等安全措施,可以安全地使用。但是,对于像memcache的Set和Get这样的处理,自己制作可能在性能方面更好。

因为时间不够,我无法跟进每个包裹的瓶颈问题,所以将在以后进行调查(;´Д`)