這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
轉載自:http://luodw.cc/2016/08/23/golang01/
實習期間一直在用golang,今天想寫篇部落格,聊聊我對golang的一些思考,以及分析下groupcache的實現;
廈大美景
在沒接觸golang之前,在我的印象裡,伺服器開發就是常見的nginx多進程,memcache多線程,redis單線程;後來,golang這種多協程,一個串連對應一個協程的模式深深吸引了我,相比進程,線程,協程粒度相對較小,協程切換代價與進程和線程相比是非常小的,因此一個串連對應一個協程才得以行得通;
golang很適合寫服務,首先是語言層面支援高並發,其次是對http和rpc介面封裝,使用者在寫c/s架構伺服器時,既可以用基於http的rest api介面實現用戶端和伺服器端的通訊,同時還可以用rpc實現用戶端和伺服器端的通訊,而且支援多種資料格式傳輸,例如xml,json,gob等等;最後就是部署簡單,產生可執行檔,直接運行;
學習golang,首先是學習基礎文法,這個網上有很多教程,有一本《Go語言編程》很適合入門;其次就需要學習go語言層面底層的一些東西,例如協程切換,socket編程原理以及gc等等;這是非常有必要的,因為瞭解這些,對golang的使用會更加得心應手;例如golang網路編程,是怎麼通知某個描述符有事件到達的?協程一開始阻塞在read調用中,當有資料達到之後,是怎麼被喚醒的?等;我推薦兩本gitbook,一個適合入門,一個適合深入:
深入理解Go
Implemention of golang
【著作權聲明】部落格內容由羅道文的私房菜擁有著作權,允許轉載,但請標明原文連結http://luodw.cc/2016/08/23/golang01/
在瞭解基礎於文法以及一些底層原理之後,我覺得可以看看golang內建的架構,寫的非常棒,很適合學習,特別是http和rpc,因為學習這兩個架構可以瞭解到一次http請求是如何?的,以及一次rpc過程是怎麼實現的;當然還有一些第三方開源架構也寫得很好;
ok,下面就來看看緩衝庫groupcache是怎麼實現的。
groupcache
groupcache儲存的是kv結構,同是memcache作者出品,官方github上說明如下:
groupcache is a caching and cache-filling library, intended as a replacement for memcached in many cases.
也就是說groupcache是一個kv緩衝,用於在某些方面替代memcache,但是我在學習了這個架構之後,我發現這個架構的適用情境並不多,因為groupcache只能get,不能update和delete,也不能設定到期時間,只能通過lru淘汰最近最少訪問的資料;有些資料如果長時間不更改,那麼可以用groupcache作為緩衝;groupcache已經在dl.Google.com、Blogger、Google Code、Google Fiber、Google生產監視系統等項目中投入使用。
但是groupcache還是有它的優點的,groupcache既是伺服器,也是用戶端,當在本地groupcache緩衝中沒有尋找的資料時,通過一致性雜湊,尋找到該key所對應的peer伺服器,在通過http協議,從該peer伺服器上擷取所需要的資料;還有一點就是當多個用戶端同時訪問memcache中不存在的鍵時,會導致多個用戶端從mysql擷取資料並同時插入memcache中,而在相同情況下,groupcache只會有一個用戶端從mysql擷取資料,其他用戶端阻塞,直到第一個用戶端擷取到資料之後,再返回給多個用戶端;
groupcache是一個緩衝庫,也就是說不是一個完整的軟體,需要自己實現main函數。可以自己寫個測試程式,跑跑groupcache,我看了有些部落格是直接引用Playing With Groupcache這篇部落格的測試程式,這個測試程式,用戶端和groupcache通過rpc進行通訊,而groupcache peer之間通過http協議進行通訊;這是比較好的做法,因為如果用戶端與伺服器通訊和groupcache之間通訊採用的是同一個連接埠,那麼在並發量上去的時候,會嚴重影響效能;是這個測試程式的架構圖: groupcache結構圖
這個原理就是如果用戶端用的是set或get命令時,這時直接操作的是資料來源(資料庫或檔案),如果調用的是cget命令,則從groupcache中尋找資料;
groupcache內部實現了lru和一致性雜湊,我覺得大家可以看看,學習golang是如何?lru和一致性雜湊。下面簡單分析groupcache Get函數的實現以及peer之間的通訊;
groupcache Get函數實現
當用戶端連上groupcache時,能做的只有get擷取資料,如果本地有所需要的資料,則直接返回,如果沒有,則通過一致性雜湊函數判斷這個key所對應的peer,然後通過http從這個peer上擷取資料;如果這個peer上有需要的資料,則通過http回複給之前的那個groupcache;groupcache收到之後,儲存在本地hotCache中,並返回給用戶端;如果peer上也沒有所需要的資料,則groupcache從資料來源(資料庫或者檔案)擷取資料,並將資料儲存在本地mainCache,並返回給用戶端;
func (g *Group) Get(ctx Context, key string, dest Sink) error {
g.peersOnce.Do(g.initPeers)
g.Stats.Gets.Add(1)//這是groupcache狀態資料,即Get的次數+1
if dest == nil {
return errors.New(“groupcache: nil dest Sink”)
}
//尋找本機快取,包括mainCache和hotCache
value, cacheHit := g.lookupCache(key)
if cacheHit {
//如果命中,直接返回
g.Stats.CacheHits.Add(1)
return setSinkView(dest, value)
}
// 如果本地沒有命中,則從peer擷取
destPopulated := false
value, destPopulated, err := g.load(ctx, key, dest)
if err != nil {
return err
}
if destPopulated {
return nil
}
//將value賦值給dest返回
return setSinkView(dest, value)
}
這個Get函數很簡單,先檢查本地cache是否存在,存在即返回,不存在則向peer擷取,接下來看下load函數是如何?的;
func (g *Group) load(ctx Context, key string, dest Sink) (value ByteView, destPopulated bool, err error) {
g.Stats.Loads.Add(1)
//下面這個loadGroup是保證當資料不存在時,只有一個用戶端從peer或者資料來源擷取資料,
//其他用戶端阻塞,直到第一個用戶端資料之後,所有用戶端再返回;這個主要是通過sync.WaitGroup實現
viewi, err := g.loadGroup.Do(key, func() (interface{}, error) {
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 {
//從peer擷取資料
value, err = g.getFromPeer(ctx, peer, key)
if err == nil {
g.Stats.PeerLoads.Add(1)
return value, nil
}
g.Stats.PeerErrors.Add(1)
}
//從資料來源擷取資料
value, err = g.getLocally(ctx, key, dest)
if err != nil {
g.Stats.LocalLoadErrs.Add(1)
return nil, err
}
g.Stats.LocalLoads.Add(1)
destPopulated = true
//將資料來源擷取的資料存放區在本地mainCache中
g.populateCache(key, value, &g.mainCache)
return value, nil
})
if err == nil {
value = viewi.(ByteView)
}
return
}
這個load函數先是從peer擷取資料,如果peer沒有資料,則直接從資料來源(資料庫或檔案)擷取資料;ok,先看下groupcache是如何從資料來源擷取資料,然後再分析下如果從peer中擷取資料;
func (g *Group) getLocally(ctx Context, key string, dest Sink) (ByteView, error) {
err := g.getter.Get(ctx, key, dest)
if err != nil {
return ByteView{}, err
}
return dest.view()
}
getLocallly函數主要是利用NewGroup建立Group時傳進去的Getter,在調用這個Getter的Get函數從資料來源擷取資料。
func NewGroup(name string, cacheBytes int64, getter Getter) *Group {
return newGroup(name, cacheBytes, getter, nil)
}
也就是說當groupcache以及peer不存在所需資料時,使用者可以自己定義從哪擷取資料以及如何擷取資料,即定義Getter的執行個體即可;
從peer擷取資料
當本地groupcache中不存在資料時,會先從peer處擷取資料,我們來看下getFromPeer函數實現
func (g *Group) getFromPeer(ctx Context, peer ProtoGetter, key string) (ByteView, error) {
//為了減少傳輸資料量,在peer之間,通過pb來傳輸資料
req := &pb.GetRequest{
Group: &g.name,
Key: &key,
}
res := &pb.GetResponse{}
err := peer.Get(ctx, req, res)
if err != nil {
return ByteView{}, err
}
value := ByteView{b: res.Value}
if rand.Intn(10) == 0 {//10%的機率將從peer擷取的資料存放區在本地hotCache
g.populateCache(key, value, &g.hotCache)
}
return value, nil
}
這個ProtoGetter是個介面,httpGetter結構體實現了這個介面,而上述傳進getFromPeer函數的peer就是httpGetter,因此,我們可以來看下httpGet這個結構體的Get函數
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)
}
//bufferPool是bytes.Buffer類型的對象池
b := bufferPool.Get().(*bytes.Buffer)
b.Reset()
defer bufferPool.Put(b)
_, err = io.Copy(b, res.Body)//將擷取的資料copy給b
if err != nil {
return fmt.Errorf(“reading response body: %v”, err)
}
err = proto.Unmarshal(b.Bytes(), out)//將資料存在out中
if err != nil {
return fmt.Errorf(“decoding response body: %v”, err)
}
return nil
}
這個函數首先向peer發起一個http請求,然後將請求得到的封裝在out *pb.GetResponse,返回給getFromPeer,並最終返回給用戶端;
總結
這篇文章主要是聊聊我對學習golang的一些看法,以及分析下groupcache的實現原理,分析的不是很細,主要是對這個架構進行了分析,對groupcache有了整體的認識之後,再去看細節部分,會簡單很多。
這幾天再看sqlmock開源架構,這個主要作用就是,在單元測試時用來類比資料庫操作;主要原理就是實現一個驅動程式。在看這個sqlmock過程中,首先必須把database/sql以及go-sql-driver看懂,知道這兩個是如何一起運作的,這樣才能瞭解sqlmock的實現;過幾天再把database/sql以及go-sql-driver的實現原理髮出來。