grgoupcache源码走读(三):groupcache的特性和使用案例

    了解了前面两篇基于本地缓存的策略之后,我们再来看看分布式缓存groupcache。它的作者也是memcache的作者,在github中对groupcache的简介有如下一句:

groupcache is a caching and cache-filling library, intended as a replacement for memcached in many cases.

    直译过来就是groupcache是一个kv缓存库,并且致力于在某些场景下替代mc。希望读者通过接下来的内容能对groupcache的使用场景有一些自己的想法。    

    笔者简单总结了一下它应该具备的一些特性,如果有不全的欢迎大家补充:

  • 只是一个代码包,可以直接使用,不需要单独配置服务器,既是客户端也是服务器端,这主要是因为它既可能直接返回请求的key的结果,也可能将请求转给其他peer来处理。对访问的用户来说,它是服务端,对其他peer来说它又是客户端(稍微有点绕,不明白的看之后的demo吧);
  • 分布式缓存,支持一致性哈希,这里主要是通过peers和consistenthash实现的;
  • 采用的缓存机制是LRU,LRU主要通过list.List队列来实现的,不支持过期机制;(所以。。是的,如果有长期不用的数据,只能寄希望于缓存满了之后自动剔除队尾了)
  • 对于客户端或者使用用户来说,对数据的操作只能get,不支持set,update以及delete;
  • 实现了缓存过滤机制(这个概念也是笔者网上看到的), 代码中叫singlefilght;一般情况下,缓存存在被击穿的风险,即大量相同的请求并发访问,如果此时cache miss,那么这些请求都会落地到服务器端的db上,导致db压力过大甚至最终造成整个服务不可用的情况。而singleflight所做的就是针对并发访问的相同miss key,只有一个请求会真正load data(如访问db),其余请求都会等待,最终将结果返回给其他请求,使得所有请求能返回一致的结果。
  • 对热点数据进行备份,虽然是一致性哈希,但是对于过热数据,也会进行多节点备份,缓解过热数据访问对节点造成的压力;

    以上就是笔者暂时能想到的关于groupcache的特性。其中第四点,groupcache只支持get操作可能是和mc差别最大的地方了,因为真正只能get的场景着实不多。。。

    使用案例

        PS:   在启动groupcache的时候需要监听两个端口,一个端口是用来外部访问groupcache的,另外一个端口是集群内部peer互相通信使用的。                     

    接下来看下如何使用groupcache:

run_peer1.go

const defaultHost = "127.0.0.1:9001"
const group_addr = ":8081"

func main() {
        if len(os.Args) <= 1 { 
                fmt.Fprintf(os.Stderr, "Usage: %s peer1 [peer2...]", os.Args[0])
                os.Exit(1)
        }   

        //本地peer地址
        self := flag.String("self", defaultHost, "self node")
        flag.Parse()

        //cache集群所有节点
        cluster := os.Args[1:]

        //初始化本地groupcache, 并监听groupcache相应的端口
        setUpGroup("test_cache")
        //本地peer
        peers := groupcache.NewHTTPPool(addrsToUrl(*self)[0])
        peers.Set(addrsToUrl(cluster...)...) //设置集群信息 用以本机缓存没命中的时候,一致性哈希查找key的存储节点, 并通过http请求访问

        selfPort := strings.Split(*self, ":")[1]
        http.ListenAndServe(":"+selfPort, peers) //监听本机集群内部通信的端口

}

//启动groupcache
func setUpGroup(name string) {
        //缓存池,
        stringGroup := groupcache.NewGroup(name, 1<<20, groupcache.GetterFunc(func(_ groupcache.Context, key string, dest groupcache.Sink) error {
                //当cache miss之后,用来执行的load data方法
                fp, err := os.Open("groupcache.conf")
                if err != nil {
                        return err
                }
                defer fp.Close()

                fmt.Printf("look up for %s from config_file\n", key)
                //按行读取配置文件
                buf := bufio.NewReader(fp)
                for {
                        line, err := buf.ReadString('\n')
                        if err != nil {
                                if err == io.EOF {
                                        dest.SetBytes([]byte{})
                                        return nil
                                } else {
                                        return err
                                }
                        }

                        line = strings.TrimSpace(line)
                        parts := strings.Split(line, "=")
                        if len(parts) > 2 {
                                continue
                        } else if parts[0] == key {
                                dest.SetBytes([]byte(parts[1]))
                                return nil
                        } else {
                                continue
                        }
                }
        }))

        http.HandleFunc("/config", func(rw http.ResponseWriter, r *http.Request) {
                k := r.URL.Query().Get("key")
                var dest []byte
                fmt.Printf("look up for %s from groupcache\n", k)
                if err := stringGroup.Get(nil, k, groupcache.AllocatingByteSliceSink(&dest)); err != nil {
                        rw.WriteHeader(http.StatusNotFound)
                        rw.Write([]byte("this key doesn't exists"))
                } else {
                        rw.Write([]byte(dest))
                }

        })

        //能够直接访问cache的端口, 启动http服务
        //http://ip:group_addr/config?key=xxx
        go http.ListenAndServe(group_addr, nil)

}

//将ip:port转换成url的格式
func addrsToUrl(node_list ...string) []string {
        urls := make([]string, len(node_list))
        for k, addr := range node_list {
                urls[k] = "http://" + addr
        }   

        return urls
}

    这里执行 go run run_peer1.go 127.0.0.1:9001 就能启动一个groupcache实例,其中8080是对外访问端口,9001是集群内部通信端口, 当然在这个例子中,这是一个只有单节点的集群。简单测试一下:

    执行 curl -i http://127.0.0.1:8081/config?key=name 命令两次, 可以看到第一次走到了GetterFunc类型的方法中去从配置文件load数据了,第二次直接从缓存中就拿到了数据。

        191854_ZIPC_3470972.png

     如果想要单机模拟多节点集群,只需要将上述代码配置稍微修改一下保存成run_peer2.go即可,修改如下:

//run_peer2.go
const defaultHost = "127.0.0.1:9002"
const group_addr = ":8082"

执行命令, 即可实现集群模式:

go run run_peer1.go 127.0.0.1:9001 127.0.0.1:9002
go run run_peer2.go 127.0.0.1:9001 127.0.0.1:9002

    此时可以观察一下整个集群的执行流程以及一致性hash,执行 curl -i http://127.0.0.1:8082/config?key=name,看一下输出结果:

        202825_9sx4_3470972.png

        202840_qo4z_3470972.png

    因为访问的8082端口,会先去访问8082端口,然后查找name这个键所属的存储节点,如图一;发现存储节点并非本节点之后,通过http请求访问对应存储节点,获取数据,如图二。据此,我们可以总结一下groupcache总的流程:

  1. 输入url地址,访问url对应的存储节点front server;
  2. 先查看groupcache中是否有该key,有直接返回;若无,进入第三步数据load;
  3. load data步骤,实现了前文提的缓存过滤机制;这里会重新查询groupcache,防止重复load;如果还没有,会先通过一致性哈希判断key所在的存储节点cache peer;如果非本机,则进入第四步;否则,进入第五步;
  4. 调用http请求,访问对应的存储节点cache peer,通过上文的集群内通信端口(9001/9002)访问,对应的server将依次执行2,3,5步骤,返回数据。
  5. 该key属于本机,则调用初始化group时定义的GetterFunc类型方法load数据,此处方法完全自定义,既可以访问配置文件,也可以访问db。

具体的结构图如下所示,这个只画出了一半的流程,希望大家能看明白。

            211623_EQHg_3470972.png

至此,关于groupcache的整体运作流程描述完毕。可能有一些地方没有细讲,比如singleflight, 怎么实现的一致性哈希等等。。。如果有疑问欢迎拍砖留言,我和大家一起研究一起学习。。

转载于:https://my.oschina.net/u/3470972/blog/1603171

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值