groupcache 架構設計

Focinfi發表於2016-12-15

groupcache 是一個分散式快取 go 語言庫,支援多節點互備熱資料,有良好的穩定性和較高的併發性。

這裡有個簡單的應用場景:

groupcache.png

當 GET foo 打到 groupcache-1 後:

  1. groupcache-1 先看看自己的 cache 裡有沒有 foo,有的話直接返回
  2. 要是沒有,看看這個請求歸不歸自己管,若是,去 DataSever 獲取,否則問 group-2(假設 foo 歸 -2管) 要資料,成功返回後 groupcache-1 本地也快取一份
  3. 在 2 過程中,所有後來打到 groupcache-1 的 GET foo 都會阻塞,直到第一個請求返回

問題來了,如何判斷 foo 由誰來處理?

consistenthash.png

如上圖,利用hash將所有節點平均打散到全集,然後當 foo 進來後用相同hash演算法就會得到一個唯值,落在那個區間就屬於那個節點,要保證一致性。

因為 foo 和某資源一一對應,這就要求 groupcache 只有 get 沒有 update。

一個簡單的HTTP groupcache Server:

package main

import "github.com/golang/groupcache"

import "github.com/gin-gonic/gin"
import "net/http"
import "time"
import "bytes"

// 虛擬檔案生成方法
func generateThumbnail(fileName string) []byte {
    return []byte("fake file")
}

func main() {
    // 本機 ip
    me := "http://10.0.0.1"
    peers := groupcache.NewHTTPPool(me)
    // 設定互備的 node
    peers.Set("http://10.0.0.1", "http://10.0.0.2", "http://10.0.0.3")
    // 建立一個 cache group,最大快取為64M
    var thumbNails = groupcache.NewGroup("thumbnail", 64<<20, groupcache.GetterFunc(
        func(ctx groupcache.Context, key string, dest groupcache.Sink) error {
            fileName := key
            dest.SetBytes(generateThumbnail(fileName))
            return nil
        }))

    // 設定 thumbnail 的 peers
    groupcache.RegisterPeerPicker(func() groupcache.PeerPicker {
        return peers
    })

    // 起一個 HTTP server
    server := gin.Default()
    server.GET("/files/:name", gin.HandlerFunc(
        func(ctx *gin.Context) {
            var data []byte
            name := ctx.Param("name")
            // 獲取快取
            err := thumbNails.Get(ctx, name, groupcache.AllocatingByteSliceSink(&data))
            if err != nil {
                ctx.JSON(http.StatusBadRequest, gin.H{"mesage": "file not found"})
                return
            }
            // 返回給客戶端
            http.ServeContent(ctx.Writer, ctx.Request, name, time.Now(), bytes.NewReader(data))
        }))
    server.Run("10.0.0.1:80")
}

Group

groupcache.NewGroup(addr string) Group 代表一個 cache資源庫

type Group struct {
    name       string
    getter     Getter // cache 沒有命中,從資料庫獲取
    peersOnce  sync.Once 
    peers      PeerPicker // peer 節點排程器
    cacheBytes int64 // 最大cache位元組數
    mainCache cache // 此節點快取
    hotCache cache // 其他節點快取
    loadGroup flightGroup // 請求併發控制器
    Stats Stats // 統計資料
}

對於一個 Group 來說,會快取自己節點的資料和訪問比較頻繁的 peer節點 的資料,用LRU演算法控制快取。

當 cache 沒有命中的時候,首先看看這個請求歸不歸該節點管,若是就是呼叫 getter:

Getter

type Getter interface {
    Get(ctx Context, key string, dest Sink) error
}

對於一個 cache 來說,他不知道如何拉取需要快取的資料,所以他說啊,你要是想快取新的東西,就得有個 type 實現 Getter 介面,然後給我一個 Getter 物件,這樣cache沒有命中的時候我能靠這個物件拉取資料。

這個 Getter 類似於 http.Handler,抽象拉取要快取的資料這個行為,Context(interface{}) 是操作的附帶資訊,key 請求的 id,Sink 類似於 http.ResponseWriter,抽象了資料載體的行為:

Sink

type Sink interface {
    // SetString 寫入 string
    SetString(s string) error

    // SetBytes 寫入位元組陣列,呼叫者會保留 v 引用
    SetBytes(v []byte) error

    // SetProto 寫入proto.Message,呼叫者會保留 m 應用
    SetProto(m proto.Message) error

    // ...
}

groupcache 提供了一些常用的 Sink 如 StringSink,BytesSliceSink 和 ProtoSink,這個 proto 是github.com/golang/protobuf/proto,groupcache 規定內部 peer 節點之間資料通訊格式使用 google/protobuf,為了抽象 peer 節點,定義了 ProtoGetter:

ProtoGetter

type ProtoGetter interface {
    Get(context Context, in *pb.GetRequest, out *pb.GetResponse) error
}

pb.GetRequestpb.GetResponse 定義了請求和響應 struct,這個抽象可以分離底層傳輸方式。

當然還需要對節點排程器抽象,PeerPicker:

PeerPicker

type PeerPicker interface {
    // PickPeer 根據 key 返回應該處理這個 key 的節點
    // ok 為 true 代表找到了節點
    // nil, false 代表當前節點就是 key 的處理器
    PickPeer(key string) (peer ProtoGetter, ok bool)
}

排程器主要負責根據管理 key 和節點的一致性對映。

groupcache 實現了一個 HTTP 的 PeerPicker,HTTPPool。

至此,groupcache 通過 Getter,PeerPicker,ProtoGetter 三個 interface 定義了cache,節點和排程器之間的連線方式,可以有效地控制耦合度,也提供了比較大的靈活性。

相關文章