protobuf、LRU、sigleflight

cloudgeek發表於2018-08-18

今天我們一次講3個吧,趕一下進度,好早點開始聊kubernetes!

從groupcache的專案目錄結構看,我們今天要學習groupcachepb、lru、singleflight這3個package:

一、protobuf

  這個目錄咋一看有2個檔案:go和proto字尾的。proto字尾的檔案和protocol buffers有關,所以先看看protocol buffers是什麼吧。

  在github上可以看到這個專案:https://github.com/google/protobuf

  google的,是不是瞬間來了興趣?

  官方介紹是:Protocol Buffers (a.k.a., protobuf) are Google`s language-neutral, platform-neutral, extensible mechanism for serializing structured data.簡單說就是跨語言跨平臺的可擴充的結構資料序列化用的。翻譯著有點彆扭,還是直接看英文好理解。。。行,現在大家知道這個是用來做資料序列化的了,大家是否記得Golang自帶的一個資料結構序列化編碼/解碼工具gob?之前我們有專門介紹過:《golang – gob與rpc》。

  ok,看過gob這篇文章,大家就知道protobuf需要解決的基本問題了,下面我們結合原始碼來看protobuf的知識點。

$GOPATHsrcgithub.comgolanggroupcachegroupcachepbgroupcache.proto內容如下:

 1syntax = "proto2";
2
3package groupcachepb;
4
5message GetRequest {
6  required string group = 1;
7  required string key = 2// not actually required/guaranteed to be UTF-8
8}
9
10message GetResponse {
11  optional bytes value = 1;
12  optional double minute_qps = 2;
13}
14
15service GroupCache {
16  rpc Get(GetRequest) returns (GetResponse) {
17  };
18}

  可以看到這是某種語法的資料定義格式,我們先介紹一下這裡涉及的概念:

  protobuf中主要資料型別有:

  • 標準資料型別:整型,浮點,字串等

  • 複合資料型別:列舉和message型別

  看message部分:

message GetResponse {
    optional bytes value = 1;
    optional double minute_qps = 2;
}

  • 每個欄位末尾有一個tag,這個tag要求不重複,如這裡的1、2;

  • 每個欄位有一個型別,如這裡的bytes、double;

  • 每個欄位開頭的optional含義為:

    • required: 必須賦值,不能為空

    • optional:可以賦值,也可以不賦值

    • repeated: 該欄位可以重複任意次數,包括0次

  現在我們可以看懂這個message的名字是GetResponse,有2個可選欄位value和minute_qps,兩個欄位的型別分別為bytes和double,2個欄位都是optional的。

  protobuf也提供了包的定義,只要在檔案開頭定義package關鍵字即可,所以這裡的package groupcachepb;這行也好理解;第一行syntax = “proto2”;明顯是宣告版本的,除了proto2外還有proto3版本,類似與py2後有了py3。

  到這裡就剩下最後幾行有點疑惑了:

service GroupCache {

    rpc Get(GetRequest) returns (GetResponse) {

    };

}

  這裡可以看到打頭的是service,中間的欄位是一個rpc相關的類似函式的東西,引數和返回值都是上面定義的message:GetRequestGetResponse,明顯這裡和rpc要有關係了,細節我們先不講,到後面呼叫到的地方我們再結合業務程式碼來理解這裡的細節。

 

二、LRU

  查一下百度百科,可以得到LRU的解釋如下:

  記憶體管理的一種頁面置換演算法,對於在記憶體中但又不用的資料塊(記憶體塊)叫做LRU,作業系統會根據哪些資料屬於LRU而將其移出記憶體而騰出空間來載入另外的資料。

什麼是LRU演算法? LRU是Least Recently Used的縮寫,即最近最少使用,常用於頁面置換演算法,是為虛擬頁式儲存管理服務的。

  所以這裡的lru包也就是用來實現lru演算法的,詳細的解釋我放在註釋中:$GOPATHsrcgithub.comgolanggroupcachelrulru.go:

  1// Package lru implements an LRU cache.
2//【lru包用於實現LRU cache】
3package lru
4
5import "container/list"
6
7// Cache is an LRU cache. It is not safe for concurrent access.
8//【Cache結構用於實現LRU cache演算法;併發訪問不安全】
9type Cache struct {
10    // MaxEntries is the maximum number of cache entries before
11    // an item is evicted. Zero means no limit.
12    //【最大入口數,也就是快取中最多存幾條資料,超過了就觸發資料淘汰;0表示沒有限制】
13    MaxEntries int
14
15    // OnEvicted optionally specificies a callback function to be
16    // executed when an entry is purged from the cache.
17    //【銷燬前回撥】
18    OnEvicted func(key Key, value interface{})
19
20    //【連結串列】
21    ll *list.List
22    //【key為任意型別,值為指向連結串列一個結點的指標】
23    cache map[interface
{}]*list.Element
24}
25
26// A Key may be any value that is comparable. 
27// See http://golang.org/ref/spec#Comparison_operators
28//【任意可比較型別】
29type Key interface{}
30
31//【訪問入口結構,包裝鍵值】
32type entry struct {
33    key   Key
34    value interface{}
35}
36
37// New creates a new Cache.
38// If maxEntries is zero, the cache has no limit and it`s assumed
39// that eviction is done by the caller.
40//【初始化一個Cache型別例項】
41func New(maxEntries int) *Cache {
42    return &Cache{
43        MaxEntries: maxEntries,
44        ll:         list.New(),
45        cache:      make(map[interface{}]*list.Element),
46    }
47}
48
49// Add adds a value to the cache.
50//【往快取中增加一個值】
51func (c *Cache) Add(key Key, value interface{}) {
52    //【如果Cache還沒有初始化,先初始化,建立cache和l1】
53    if c.cache == nil {
54        c.cache = make(map[interface{}]*list.Element)
55        c.ll = list.New()
56    }
57    //【如果key已經存在,則將記錄前移到頭部,然後設定value】
58    if ee, ok := c.cache[key]; ok {
59        c.ll.MoveToFront(ee)
60        ee.Value.(*entry).value = value
61        return
62    }
63    //【key不存在時,建立一條記錄,插入連結串列頭部,ele是這個Element的指標】
64    //【這裡的Element是一個*entry型別,ele是*list.Element型別】
65    ele := c.ll.PushFront(&entry{key, value})
66    //cache這個map設定key為Key型別的key,value為*list.Element型別的ele
67    c.cache[key] = ele
68    //【連結串列長度超過最大入口值,觸發清理操作】
69    if c.MaxEntries != 0 && c.ll.Len() > c.MaxEntries {
70        c.RemoveOldest()
71    }
72}
73
74// Get looks up a key`s value from the cache.
75//【根據key查詢value】
76func (c *Cache) Get(key Key) (value interface{}, ok bool) {
77    if c.cache == nil {
78        return
79    }
80    //【如果存在】
81    if ele, hit := c.cache[key]; hit {
82        //【將這個Element移動到連結串列頭部】
83        c.ll.MoveToFront(ele)
84        //【返回entry的值】
85        return ele.Value.(*entry).value, true
86    }
87    return
88}
89
90// Remove removes the provided key from the cache.
91//【如果key存在,呼叫removeElement刪除連結串列and快取中的元素】
92func (c *Cache) Remove(key Key) {
93    if c.cache == nil {
94        return
95    }
96    if ele, hit := c.cache[key]; hit {
97        c.removeElement(ele)
98    }
99}
100
101// RemoveOldest removes the oldest item from the cache.
102//【刪除最舊的元素】
103func (c *Cache) RemoveOldest() {
104    if c.cache == nil {
105        return
106    }
107    //【ele為*list.Element型別,指向連結串列的尾結點】
108    ele := c.ll.Back()
109    if ele != nil {
110        c.removeElement(ele)
111    }
112}
113
114func (c *Cache) removeElement(e *list.Element) {
115    //【連結串列中刪除一個element】
116    c.ll.Remove(e)
117    //【e.Value本質是*entry型別,entry結構體就包含了key和value2個屬性】
118    //【Value本身是interface{}型別,通過型別斷言轉成*entry型別】
119    kv := e.Value.(*entry)
120    //【刪除cache這個map中key為kv.key這個元素;也就是連結串列中刪了之後快取中也得刪】
121    delete(c.cache, kv.key)
122    if c.OnEvicted != nil {
123        c.OnEvicted(kv.key, kv.value)
124    }
125}
126
127// Len returns the number of items in the cache.
128//【返回快取中的item數,通過連結串列的Len()方法獲取】
129func (c *Cache) Len() int {
130    if c.cache == nil {
131        return 0
132    }
133    return c.ll.Len()
134}
135
136// Clear purges all stored items from the cache.
137//【刪除快取中所有條目,如果有回撥函式OnEvicted(),則先呼叫所有回撥函式,然後置空】
138func (c *Cache) Clear() {
139    if c.OnEvicted != nil {
140        for _, e := range c.cache {
141            kv := e.Value.(*entry)
142            c.OnEvicted(kv.key, kv.value)
143        }
144    }
145    c.ll = nil
146    c.cache = nil
147}

三、singleflight

  這個package主要實現了這樣一個功能:抑制同一個函式呼叫重複執行。舉個例子:給一個常規程式輸入一個函式呼叫A()需要10s返回結果,這時候有10個客戶端都呼叫了這個A(),可能就需要100s才能完成所有的計算結果,但是這個計算是重複的,結果也是一樣的。所以可以想個辦法,判斷是同一個計算過程的情況,不需要重複執行,直接等待上一次計算完成,然後一下子返回結果就行了。下面看一下groupcache中是如何實現這個演算法的吧:

 1// Package singleflight provides a duplicate function call suppression
2// mechanism.
3//【“單航班”提供重複呼叫函式的抑制機制】
4package singleflight
5
6import "sync"
7
8// call is an in-flight or completed Do call
9//【在執行的或者已經完成的Do過程】
10type call struct {
11    wg  sync.WaitGroup
12    val interface{}
13    err error
14}
15
16// Group represents a class of work and forms a namespace in which
17// units of work can be executed with duplicate suppression.
18//【表示一類工作,組成一個名稱空間的概念,一個group的呼叫會有“重複抑制”】
19type Group struct {
20    mu sync.Mutex // protects m
21    //【懶惰地初始化;這個map的value是*call,call是上面那個struct】
22    m map[string]*call // lazily initialized
23}
24
25// Do executes and returns the results of the given function, making
26// sure that only one execution is in-flight for a given key at a
27// time. If a duplicate comes in, the duplicate caller waits for the
28// original to complete and receives the same results.
29
30//【Do接收一個函式,執行並返回結果,
31// 這個過程中確保同一個key在同一時間只有一個執行過程;
32// 重複的呼叫會等待最原始的呼叫過程完成,然後接收到相同的結果】
33func (g *Group) Do(key string, fn func() (interface{}, error)(interface{}, error) {
34    g.mu.Lock()
35    if g.m == nil {
36        g.m = make(map[string]*call)
37    }
38    //【如果這個call存在同名過程,等待初始呼叫完成,然後返回val和err】
39    if c, ok := g.m[key]; ok {
40        g.mu.Unlock()
41        c.wg.Wait()
42        //【當所有goroutine執行完畢,call中就儲存了執行結果val和err,然後這裡返回】
43        return c.val, c.err
44    }
45    //【拿到call結構體型別的指標】
46    c := new(call)
47    //【一個goroutine開始,Add(1),這裡最多隻會執行到一次,也就是不會併發呼叫下面的fn()】
48    c.wg.Add(1)
49    //【類似設定一個函式呼叫的名字“key”對應呼叫過程c】
50    g.m[key] = c
51    g.mu.Unlock()
52
53    //【函式呼叫過程】
54    c.val, c.err = fn()
55    //【這裡的Done對應上面if裡面的Wait】
56    c.wg.Done()
57
58    g.mu.Lock()
59    //【執行完成,刪除這個key】
60    delete(g.m, key)
61    g.mu.Unlock()
62
63    return c.val, c.err
64}

  今天講的可能有點多,其中設計到的List之類的沒有細講,希望大家通過網際網路掌握這類我沒有仔細提到的小知識點,徹底吃透這幾個package中的原始碼。

  回過頭看一下專案結果,除了testpb包外其他包我們都講完了,testpb是groupcachepb對應的測試程式,下一講我們就可以把這幾個包外的所有程式分析完,包括對protobuf部分的呼叫邏輯。

 

  今天就到這裡,groupcache原始碼解析還剩最後一講!

 

相關文章