groupcache 架構設計,memcached的go語言實現

groupcache 是一個分佈式緩存 go 語言庫,支持多節點互備熱數據,有良好的穩定性和較高的併發性。

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

groupcache 架構設計,memcached的go語言實現

當 GET foo 打到 groupcache-1 後:

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

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

groupcache 架構設計,memcached的go語言實現

如上圖,利用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.GetRequest 和 pb.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,節點和調度器之間的連接方式,可以有效地控制耦合度,也提供了比較大的靈活性。


分享到:


相關文章: