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

    瞭解了前面兩篇基於本地緩存的策略以後,咱們再來看看分佈式緩存groupcache。它的做者也是memcache的做者,在github中對groupcache的簡介有以下一句:node

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

    直譯過來就是groupcache是一個kv緩存庫,而且致力於在某些場景下替代mc。但願讀者經過接下來的內容能對groupcache的使用場景有一些本身的想法。    github

    筆者簡單總結了一下它應該具有的一些特性,若是有不全的歡迎你們補充:緩存

  • 只是一個代碼包,能夠直接使用,不須要單獨配置服務器,既是客戶端也是服務器端,這主要是由於它既可能直接返回請求的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的場景着實很少。。。bash

    使用案例

        PS:   在啓動groupcache的時候須要監聽兩個端口,一個端口是用來外部訪問groupcache的,另一個端口是集羣內部peer互相通訊使用的。                     服務器

    接下來看下如何使用groupcache:併發

run_peer1.gocurl

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數據了,第二次直接從緩存中就拿到了數據。memcached

        

     若是想要單機模擬多節點集羣,只須要將上述代碼配置稍微修改一下保存成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,看一下輸出結果:

        

        

    由於訪問的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。

具體的結構圖以下所示,這個只畫出了一半的流程,但願你們能看明白。

            

至此,關於groupcache的總體運做流程描述完畢。可能有一些地方沒有細講,好比singleflight, 怎麼實現的一致性哈希等等。。。若是有疑問歡迎拍磚留言,我和你們一塊兒研究一塊兒學習。。

相關文章
相關標籤/搜索