groupcache使用及源码分析

groupcache是一个缓存系统,开始应用在Google下载站点dl.google.com,后来也使用在Google Blogger和Google Code这些数据更改频率较低的系统中。

groupcache没有update/delete 命令,只有set命令,使用lru存储策略,空间占满时便淘汰最不常使用的缓存,所以适合数据更改频率较低的应用。

groupcache集群使用“一致性哈希“分布节点,单节点出现问题对整体系统影响较小。

 

一. groupcache 使用

下面创建有三个groupcache节点的集群

"http://127.0.0.1:8001", "http://127.0.0.1:8002", "http://127.0.0.1:8003"

 

1. 创建本地httppool,并添加对等节点。httppool是一个集群节点选取器,保存所有节点信息,获取对等节点缓存时,通过计算key的“一致性哈希值”与节点的哈希值比较来选取集群中的某个节点

local_addr = “http://127.0.0.1:8001”
peers := groupcache.NewHTTPPool("http://" + local_addr)

peers_addrs = []string{"http://127.0.0.1:8001", "http://127.0.0.1:8002", "http://127.0.0.1:8003"}
peers.Set(peers_addrs...)

 

2. 创建一个group(一个group是一个存储模块,类似命名空间,可以创建多个) “image_cache”。NewGroup参数分别是group名字、group大小byte、getter函数(当获取不到key对应的缓存的时候,该函数处理如何获取相应数据,并设置给dest,然后image_cache便会缓存key对应的数据)

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
}))

 

3. group查找对应key的缓存,data需要使用sink(一个数据包装结构)包装一下

var data []byte
image_cache.Get(nil, key, groupcache.AllocatingByteSliceSink(&data))

 

其他两个节点需修改local_addr 地址,然后3个节点的groupcache集群就设置成功了。

 

二. groupcache源码分析

groupcache主要分为httppool和group两部分,httppool负责集群管理,group负责缓存管理。

 

1. httppool的作用就是管理所有节点,并通过http协议使节点之间相互通信,获取存储在其他节点上的缓存

type HTTPPool struct {
    // Context optionally specifies a context for the server to use when it
    // receives a request.
    // If nil, the server uses a nil Context.
    Context func(*http.Request) Context // Transport optionally specifies an http.RoundTripper for the client
    // to use when it makes a request.
    // If nil, the client uses http.DefaultTransport.
    Transport func(Context) http.RoundTripper // this peer's base URL, e.g. "https://example.net:8000"
    self string //本地节点url

    // opts specifies the options.
    opts HTTPPoolOptions 

    mu          sync.Mutex // guards peers and httpGetters
    peers       *consistenthash.Map  //包含 一致性hash map、hash函数的结构体,保存集群节点(都是用url代表,同下)和其对应一致性hash值
    httpGetters map[string]*httpGetter // keyed by e.g. "http://10.0.0.2:8008"     保存节点url和其对应的http数据请求器
}

type HTTPPoolOptions struct {
    // BasePath specifies the HTTP path that will serve groupcache requests.
    // If blank, it defaults to "/_groupcache/".
    BasePath string //peers间url请求路径

    // Replicas specifies the number of key replicas on the consistent hash.
    // If blank, it defaults to 50.
    Replicas int //单一节点在一致性hash map中的虚拟节点数

    // HashFn specifies the hash function of the consistent hash.
    // If blank, it defaults to crc32.ChecksumIEEE.
    HashFn consistenthash.Hash //一致性hash函数
}

type Map struct { //consistenthash.Map
    hash     Hash //一致性hash函数
    replicas int  //单一节点在一致性hash map中的虚拟节点数
    keys     []int // Sorted  //所有节点生成的虚拟节点hash值slice
    hashMap  map[int]string  //hash值和节点对应map
}

//httppool选取节点算法
func (p *HTTPPool) PickPeer(key string) (ProtoGetter, bool) {
    p.mu.Lock()
    defer p.mu.Unlock()
    if p.peers.IsEmpty() {
        return nil, false
    }
    if peer := p.peers.Get(key); peer != p.self { //判断获取到的节点是不是本地节点
        return p.httpGetters[peer], true //返回节点对应的httpGetter
    }
    return nil, false
}

func (m *Map) Get(key string) string {  //p.peers.Get(key)
    if m.IsEmpty() {
        return ""
    }

    hash := int(m.hash([]byte(key))) //使用一致性hash函数计算"缓存数据"key的hash值

    // Binary search for appropriate replica.
    idx := sort.Search(len(m.keys), func(i int) bool { return m.keys[i] >= hash }) //选取最小的大于 key的hash值 的 节点hash值

    // Means we have cycled back to the first replica.
    if idx == len(m.keys) { 
        idx = 0
    }

    return m.hashMap[m.keys[idx]] //返回节点(url)
}

//httppool实现了http Handler,可以给peers提供http服务,用来查询缓存
func (p *HTTPPool) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    // Parse request.
    if !strings.HasPrefix(r.URL.Path, p.opts.BasePath) {
        panic("HTTPPool serving unexpected path: " + r.URL.Path)
    }
    parts := strings.SplitN(r.URL.Path[len(p.opts.BasePath):], "/", 2)
    if len(parts) != 2 {
        http.Error(w, "bad request", http.StatusBadRequest)
        return
    }
    groupName := parts[0]
    key := parts[1]

    // Fetch the value for this group/key.
    group := GetGroup(groupName) //获取group名字,因为可以有多个group
    if group == nil {
        http.Error(w, "no such group: "+groupName, http.StatusNotFound)
        return
    }
    var ctx Context
    if p.Context != nil {
        ctx = p.Context(r)
    }

    group.Stats.ServerRequests.Add(1) //设置统计数据
    var value []byte
    err := group.Get(ctx, key, AllocatingByteSliceSink(&value)) //此处和groupcache使用处一致
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    // Write the value to the response body as a proto message.
    body, err := proto.Marshal(&pb.GetResponse{Value: value}) //protobuf协议编码数据
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    w.Header().Set("Content-Type", "application/x-protobuf")
    w.Write(body)
}

//向其他节点发起http请求,获取缓存数据
func (h *httpGetter) Get(context Context, in *pb.GetRequest, out *pb.GetResponse) error {
    u := fmt.Sprintf(
        "%v%v/%v",
        h.baseURL,
        url.QueryEscape(in.GetGroup()),
        url.QueryEscape(in.GetKey()),
    )
    req, err := http.NewRequest("GET", u, nil)
    if err != nil {
        return err
    }
    tr := http.DefaultTransport
    if h.transport != nil {
        tr = h.transport(context)
    }
    res, err := tr.RoundTrip(req)
    if err != nil {
        return err
    }
    defer res.Body.Close()
    if res.StatusCode != http.StatusOK {
        return fmt.Errorf("server returned: %v", res.Status)
    }
    b := bufferPool.Get().(*bytes.Buffer)
    b.Reset()
    defer bufferPool.Put(b)
    _, err = io.Copy(b, res.Body)
    if err != nil {
        return fmt.Errorf("reading response body: %v", err)
    }
    err = proto.Unmarshal(b.Bytes(), out)
    if err != nil {
        return fmt.Errorf("decoding response body: %v", err)
    }
    return nil
}

 

2. group的作用是管理缓存,并通过httppool选取节点,获取其他节点的缓存。

type Group struct {
    name       string //group 名字
    getter     Getter //getter 当缓存中不存在对应数据时,使用该函数获取数据并缓存
    peersOnce  sync.Once
    peers      PeerPicker //http实现了该接口,使用 func (p *HTTPPool) PickPeer(key string) (ProtoGetter, bool) 函数选取节点
    cacheBytes int64 // limit for sum of mainCache and hotCache size //缓存最大空间 byte

    // mainCache is a cache of the keys for which this process
    // (amongst its peers) is authoritative. That is, this cache
    // contains keys which consistent hash on to this process's
    // peer number.
    mainCache cache //使用lru策略实现的缓存结构,也是key hash值在本地的缓存

    // hotCache contains keys/values for which this peer is not
    // authoritative (otherwise they would be in mainCache), but
    // are popular enough to warrant mirroring in this process to
    // avoid going over the network to fetch from a peer.  Having
    // a hotCache avoids network hotspotting, where a peer's
    // network card could become the bottleneck on a popular key.
    // This cache is used sparingly to maximize the total number
    // of key/value pairs that can be stored globally.
    hotCache cache //使用lru策略实现的缓存结构,key hash值不再本地,作为热点缓存,负载均衡

    // loadGroup ensures that each key is only fetched once
    // (either locally or remotely), regardless of the number of
    // concurrent callers.
    loadGroup flightGroup //使用该结构保证当缓存中不存在key对应的数据时,只有一个goroutine 调用getter函数取数据,其他正在并发的goroutine会等待直到第一个goroutine返回数据,然后大家一起返回数据

    // Stats are statistics on the group.
    Stats Stats //统计信息
}

//创建group
func NewGroup(name string, cacheBytes int64, getter Getter) *Group {
    return newGroup(name, cacheBytes, getter, nil)
}

func newGroup(name string, cacheBytes int64, getter Getter, peers PeerPicker) *Group {
    if getter == nil {
        panic("nil Getter")
    }
    mu.Lock()
    defer mu.Unlock()
    initPeerServerOnce.Do(callInitPeerServer)
    if _, dup := groups[name]; dup {
        panic("duplicate registration of group " + name)
    }
    g := &Group{
        name:       name,  //maincache、hotcache、peerPick都是在函数调用过程中赋值或初始化的
        getter:     getter,
        peers:      peers,
        cacheBytes: cacheBytes,
        loadGroup:  &singleflight.Group{},
    }
    if fn := newGroupHook; fn != nil {
        fn(g) //此处函数空
    }
    groups[name] = g //保存创建的group
    return g
}

//group查找
func (g *Group) Get(ctx Context, key string, dest Sink) error {
    g.peersOnce.Do(g.initPeers) //把httppool赋值给 groupcache.PeerPicker
    g.Stats.Gets.Add(1) //统计信息
    if dest == nil {
        return errors.New("groupcache: nil dest Sink")
    }
    value, cacheHit := g.lookupCache(key) //从maincache、hotcache查找

    if cacheHit {
        g.Stats.CacheHits.Add(1)
        return setSinkView(dest, value)
    }

    // Optimization to avoid double unmarshalling or copying: keep
    // track of whether the dest was already populated. One caller
    // (if local) will set this; the losers will not. The common
    // case will likely be one caller.
    destPopulated := false
    value, destPopulated, err := g.load(ctx, key, dest) //从对等节点或自定义查找逻辑(getter)中获取数据
    if err != nil {
        return err
    }
    if destPopulated {
        return nil
    }
    return setSinkView(dest, value) //把数据设置给sink
}

//从maincache、hotcache查找,cache底层使用链表实现并使用lru策略修改链表
func (g *Group) lookupCache(key string) (value ByteView, ok bool) {
    if g.cacheBytes <= 0 {
        return
    }
    value, ok = g.mainCache.get(key)
    if ok {
        return
    }
    value, ok = g.hotCache.get(key)
    return
}

//从对等节点或自定义查找逻辑(getter)中获取数据
func (g *Group) load(ctx Context, key string, dest Sink) (value ByteView, destPopulated bool, err error) {
    g.Stats.Loads.Add(1)
    viewi, err := g.loadGroup.Do(key, func() (interface{}, error) { //此函数使用flightGroup执行策略,保证只有一个goroutine 调用getter函数取数据
        // Check the cache again because singleflight can only dedup calls
        // that overlap concurrently.  It's possible for 2 concurrent
        // requests to miss the cache, resulting in 2 load() calls.  An
        // unfortunate goroutine scheduling would result in this callback
        // being run twice, serially.  If we don't check the cache again,
        // cache.nbytes would be incremented below even though there will
        // be only one entry for this key.
        //
        // Consider the following serialized event ordering for two
        // goroutines in which this callback gets called twice for hte
        // same key:
        // 1: Get("key") //展示了一个有可能2个以上的goroutine同时执行进入了load,这样会导致同一个key对应的数据被多次获取并统计,所以又执行了一次g.lookupCache(key)
        // 2: Get("key")
        // 1: lookupCache("key")
        // 2: lookupCache("key")
        // 1: load("key")
        // 2: load("key")
        // 1: loadGroup.Do("key", fn)
        // 1: fn()
        // 2: loadGroup.Do("key", fn)
        // 2: fn()
        if value, cacheHit := g.lookupCache(key); cacheHit {
            g.Stats.CacheHits.Add(1)
            return value, nil
        }
        g.Stats.LoadsDeduped.Add(1)
        var value ByteView
        var err error
        if peer, ok := g.peers.PickPeer(key); ok { //通过一致性hash获取对等节点,与httppool对应
            value, err = g.getFromPeer(ctx, peer, key) //构造protobuf数据,向其他节点发起http请求,查找数据,并存储到hotcache
            if err == nil {
                g.Stats.PeerLoads.Add(1)
                return value, nil
            }
            g.Stats.PeerErrors.Add(1)
            // TODO(bradfitz): log the peer's error? keep
            // log of the past few for /groupcachez?  It's
            // probably boring (normal task movement), so not
            // worth logging I imagine.
        }
        value, err = g.getLocally(ctx, key, dest) //调用getter函数获取数据,并存储到maincache
        if err != nil {
            g.Stats.LocalLoadErrs.Add(1)
            return nil, err
        }
        g.Stats.LocalLoads.Add(1)
        destPopulated = true // only one caller of load gets this return value
        g.populateCache(key, value, &g.mainCache)
        return value, nil
    })
    if err == nil {
        value = viewi.(ByteView)
    }
    return
}

 

 

参考

groupcache:https://github.com/golang/groupcache

一致性哈希:http://blog.codinglabs.org/articles/consistent-hashing.html

protobuf:https://developers.google.com/protocol-buffers/docs/overviewgo

protobuf例子:https://godoc.org/github.com/golang/protobuf/proto

posted @ 2016-08-23 14:02  B0-1  阅读(2597)  评论(0编辑  收藏  举报