一.groupcache介绍
memcached作者Brad Fitzpatrick用Go开发了前者的替代版,现已在Google多个生产环境(如dl.google.com)中投入使用。本文粗略介绍一下groupcache的实现方式。
memcached的业务架构如下图,memcache的分布式不是服务器端实现,而是通过客户端实现;是客户端根据key自己计算决定memcached实例 。
groupcahe的业务结构如下图,key的存储采用分布式方式,key通过一致性哈希分散在各个实例中,通过任意一个实例皆可得到数据。
二.groupcahe数据结构
1.byteview
byteview对字符串数组和string进行封装,存储何种形式的字符串对用户透明,定义如下
type struct {
b []byte
s string
}
具体的操作函数的实现方式如下
func at(i){
if b!=nil
return b[i]
return s[i]
}
2.singleflight
singleflight保证“同时”多个同参数的get请求只执行一次操作功能,整个流程如下
当Thread3在时间00:01执行Get("太阳")时候,在系统中以key为“太阳”标识正在翻译“太阳”,整个翻译过程需要8秒钟。
当Thread1和Thread2分别与00:02,00:03提交执行Get("太阳")时候,发现系统中存在以key为“太阳”标识的翻译执行,进程阻塞等待Thread3的翻译结果。
在时间00:09,Thread3得到“太阳”的翻译结果,返回给客户,此时Thread1和Thread2执行读取翻译结果返回给客户。
整个过程中,不仅减少Thread1和Thread2的得到结果时间,也减少读取数据库的次数,节约系统资源。
3. consistenthash
consistenthash提供一致性hash功能,将key按照一致性hash分布在各个实例中,主要作用是实例加入离开时不会导致映射关系的重大变化。
consistenthash 由数组和一个存储实例标识和标识key的hashmap实现,结构如下,
consistenthash会将实例标识复制replicas(可以设置)份放到hashmap结构中,选择数据key的时候,选择大于实例key的hash值最近的一个。
eg:如果 hash("太阳")的值为24,选择整数数组中的30(大于24最近的key)作为存储实例的key,从hashmap找到30的实例为192.168.0.1,判断192.168.0.1作为“太阳”的存储实例。
4.lru
lru提供缓存的清除算法,groupcache实现方式无特别之处,再次略过不说。
5.group
group是key-value的容器,每个group都有一个名字,相当于一批key-value的家族标识。
当用户访问某个key时,group现在本地内存中维持的hashmap中查找,如果没有则从远端的peer或者本地的文件系统等地方加载。
Getter定了如何从本地读取数据,这部分需要使用使用groupcache的开发者自己提供实现,group 函数getLocally将会调用。
PickPeer会按照一致性hash选择peer,发起http请求拉取数据,这便是group函数getFromPeer实现。
在getLocally和getFromPeer调用的过程中,用singleflight提供多次同参get请求只执行一次操作。
maincahe 是本地读取的数据的存储容器;hotcahe是从远端peer读取的热数据的存储,现在的热数据完全凭运气,从远端读取的数据有10%的概率会称为热数据。
maincahe和hotcahe的内存用量总和高于此group的cacheBytes限制时,便启用lru进行数据清理。
stats是指标统计,如Gets便是用户请求本实例的计次,cacheHits是命中本地内存maincahe和hotcahe的计次。
group通过key对应的value的伪代码如下:
func get(key){
/*从本地内存中读取*/
if maincache.has(key)
return maincache[key]
if hotcahe.has(key)
return hotcahe [key]
/*从远端peer读取*/
value := PeerPicker.PickPeer(key).get(key)
if value !=nil
if(rand()%10 == 1)
hotcahe[key] = value
return value
/*从本地文件等系统读取*/
value:= getLocally(key)
if value !=nil
maincache[key] = value
return value
}
6.httppool
httppool便是各个peer通讯的封装,开启通讯http,group的getFromPeer便是调用相对应peer的httpool提供的服务。
httppool同时保存了所有的远端peer实例的请求地址,实现pickPeer安装一致性hash取得某key对应的远端peer实例的地址。
三. groupcahe使用
groupcahe是个库,不是一个程序,如果需要使用,需要自己写部分逻辑,在此提供一个简单的例子
package main
import (
"fmt"
"github.com/groupcache"
"io/ioutil"
"log"
"net/http"
"os"
)
var (
peers_addrs = []string{"http://127.0.0.1:8001", "http://127.0.0.1:8002", "http://127.0.0.1:8003"}
)
func main() {
if len(os.Args) != 3 {
fmt.Println("\r\n Usage local_addr \t\n local_addr must in(127.0.0.1:8001,localhost:8002,127.0.0.1:8003)\r\n")
os.Exit(1)
}
local_addr := os.Args[1]
peers := groupcache.NewHTTPPool("http://" + local_addr)
peers.Set(peers_addrs...)
var image_cache = groupcache.NewGroup("image", 8<<30, groupcache.GetterFunc(
func(ctx groupcache.Context, key string, dest groupcache.Sink) error {
result, err := ioutil.ReadFile(key)
if err != nil {
fmt.Printf("read file error %s.\n", err.Error())
return nil
}
fmt.Printf("asking for %s from local file system\n", key)
dest.SetBytes([]byte(result))
return nil
}))
http.HandleFunc("/image", func(rw http.ResponseWriter, r *http.Request) {
var data []byte
k := r.URL.Query().Get("id")
fmt.Printf("user get %s from groupcache\n", k)
image_cache.Get(nil, k, groupcache.AllocatingByteSliceSink(&data))
rw.Write([]byte(data))
})
log.Fatal(http.ListenAndServe(local_addr, nil))
}
使用的时候
src.exe 127.0.0.1:8001 src.exe 127.0.0.1:8002 src.exe 127.0.0.1:8003创建3个peer实例即可
客户端访问http://127.0.0.1:8001/image?id=configure.ini即可得到数据
对源码进行修改打印出来的日志,各位看到的并不是这样,并没有这些日志。
四.最后说几点
groupcahe不提供过期、删除等操作,所有groupcahe只适合与静态不变的数据,不能用于取代memcahe。
一些地方实现比较粗略,如定义热点数据等,现在采用10%随机并不合理;peer节点的删除增加也没有处理。