Kubernetes學習筆記之LRU算法源碼解析

Overview

本文章基於k8s release-1.17分支代碼。git

以前一篇文章學習 Kubernetes學習筆記之ServiceAccount TokensController源碼解析 ,主要學習
ServiceAccount有關知識,發現其中使用了LRU Cache,代碼在 L106
k8s本身封裝了一個LRU cache的對象 MutationCache
正好趁此機會複習下 LRU 算法知識。github

LRU算法通常也是面試必考算法題,算法內容也很簡單很直觀,主要是經過在固定容量空間內,不常被訪問被認爲舊數據能夠先刪除,最近被訪問的數據能夠認爲後面被訪問機率很大,做爲最新的數據。好比,
漫畫:什麼是LRU算法? 這幅漫畫描述的那樣,在容量有限狀況下,能夠刪除那些最老的用戶數據,留下最新的用戶數據。
這樣就感受數據按照倒敘排列似的,最前面的是最新的,最末尾的是最舊的數據。golang

數據存儲能夠經過雙向鏈表存儲,而不是單向鏈表,由於當知道鏈表的一個元素element時,能夠經過element.prev和element.next指針就能知道當前元素的前驅元素和
後驅元素,刪除和添加操做算法複雜度都是O(1),而單向鏈表沒法作到這一點。面試

另一個問題是如何知道O(1)的查詢到一個元素element的值,這能夠經過哈希表即 map[key]*element 結構知道,只要知道key,就馬上O(1)知道element,
再結合雙向鏈表的O(1)刪除和O(1)添加操做。算法

經過組合雙向鏈表和哈希表組成的一個lru數據結構,就能夠實現刪除舊數據、讀取新數據和插入新數據算法複雜度都是O(1),這就很厲害很高效的算法了。緩存

設計編寫LRU算法代碼

首先是設計出一個雙向鏈表list,能夠直接使用golang自帶的雙向鏈表,代碼在 /usr/local/go/src/container/list/list.go ,本文這裏參考源碼寫一個並學習之。數據結構

首先設計雙向鏈表的結構,Element對象是鏈表中的節點元素。這裏最關鍵設計是list的佔位元素root,是個值爲空的元素,其root.next是鏈表的第一個元素head,
其root.prev是鏈表的最後一個元素tail,這個設計是直接O(1)知道鏈表的首位元素,這樣鏈表list就構成了一個鏈表環ring:函數

// 算法設計:使用哈希表+雙向鏈表實現
type Element struct {
    prev, next *Element

    Value interface{}
}

// root這個設計很巧妙,連着雙向鏈表的head和tail,能夠看Front()和Back()函數
// 獲取雙向鏈表的第一個和最後一個元素。root相似一個佔位元素
type list struct {
    root Element
    len  int
}

// root是一個empty Element,做爲補位元素使得list爲一個ring
// list.root.next 是雙向鏈表的第一個元素;list.root.prev 是雙向鏈表的最後一個元素
func (l *list) Init() *list {
    l.root.prev = &l.root
    l.root.next = &l.root
    l.len = 0
    
    return l
}

func (l *list) Len() int {
    return l.len
}

而後就是雙向鏈表的新加入一個元素並置於最前面、移動某個元素置於最前面、從鏈表中刪除某個元素這三個重要方法。
新加入一個元素並置於最前面方法,比較簡單:post

// element置於newest位置,置於最前
func (l *list) PushFront(v interface{}) *Element {
    e := &Element{
        Value: v,
    }

    return l.insert(e, &l.root)
}

// e插入at的位置,at/e/at.next指針須要從新賦值
func (l *list) insert(e, at *Element) *Element {
    // 插入當前位置
    e.prev = at
    e.next = at.next
    e.prev.next = e
    e.next.prev = e

    l.len++

    return e
}

移動某個元素置於最前面方法:學習

// 把e置雙向鏈表最前面
func (l *list) MoveToFront(e *Element) {
    if e == l.root.next {
        return
    }

    l.move(e, &l.root)
}

func (l *list) move(e, at *Element) {
    if e == at {
        return
    }

    // 從原來位置刪除
    e.prev.next = e.next
    e.next.prev = e.prev

    // 插入當前位置
    e.prev = at
    e.next = at.next
    e.prev.next = e
    e.next.prev = e
}

從鏈表中刪除某個元素方法:

func (l *list) Remove(e *Element) {
    e.prev.next = e.next
    e.next.prev = e.prev
    e.prev = nil
    e.next = nil

    l.len--
}

以上邏輯都比較簡單,最後加上返回鏈表的head和tail元素等等方法:

// 返回list的最後一個元素
func (l *list) Back() *Element {
    if l.len == 0 {
        return nil
    }

    // 這裏list是一個ring
    return l.root.prev
}

// 返回list的最前一個元素
func (l *list) Front() *Element {
    if l.len == 0 {
        return nil
    }

    // 這裏list是一個ring
    return l.root.next
}

func (l *list) Prev(e *Element) *Element {
    p := e.prev
    if p != &l.root {
        return p
    }

    return nil
}

可見設計出這樣的一個雙向鏈表仍是比較簡單的,接下來就是LRU對象了。LRU對象包含雙向鏈表,同時包含哈希表 map[interface{}]*Element 來O(1)查詢某個
key的Element數據,完整LRU代碼以下:

type LRU struct {
    // 指定LRU固定長度,超過的舊數據則移除
    capacity int

    // 雙向鏈表,鏈表存儲每個*list.Element
    cache *list

    // 哈希表,每個key是Entry的key
    items map[interface{}]*Element
}

type Entry struct {
    key   interface{}
    value interface{}
}

func NewLRU(capacity int) (*LRU, error) {
    if capacity <= 0 {
        return nil, fmt.Errorf("capacity must be positive")
    }

    cache := &LRU{
        capacity: capacity,
        cache:    new(list).Init(),
        items:    make(map[interface{}]*Element),
    }

    return cache, nil
}

func (c *LRU) Purge() {
    c.cache.Init()
    c.items = make(map[interface{}]*Element)
    c.capacity = 0
}

// 添加一個Entry,O(1)
func (c *LRU) Add(key, value interface{}) (evicted bool) {
    // (key,value)已經存在LRU中
    if element, ok := c.items[key]; ok {
        c.cache.MoveToFront(element)         // 從雙向鏈表中置前,從原有位置刪除,而後置最前
        element.Value.(*Entry).value = value // 更新值

        return false
    }

    entry := &Entry{key: key, value: value}
    ent := c.cache.PushFront(entry) // 新元素置最前
    c.items[key] = ent

    evict := c.cache.Len() > c.capacity
    if evict {
        // 若是超過指定長度,移除舊數據
        c.removeOldest()
    }

    return evict
}

func (c *LRU) Get(key interface{}) (value interface{}, ok bool) {
    if element, ok := c.items[key]; ok {
        // 置前,複雜度O(1)
        c.cache.MoveToFront(element)
        return element.Value.(*Entry).value, true
    }

    return nil, false
}

// 刪除最舊的數據O(1)
func (c *LRU) removeOldest() {
    element := c.cache.Back()
    if element != nil {
        c.removeElement(element)
    }
}

// 算法複雜度O(1)
func (c *LRU) removeElement(element *Element) {
    // 直接使用雙向鏈表的Remove(),複雜度O(1)
    c.cache.Remove(element)
    key := element.Value.(*Entry).key
    // 別忘了從哈希表中刪除Entry.key
    delete(c.items, key)
}

// 雙向鏈表的長度
func (c *LRU) Len() int {
    return c.cache.Len()
}

// Keys returns a slice of the keys in the cache, from oldest to newest.
func (c *LRU) Keys() []interface{} {
    keys := make([]interface{}, len(c.items))
    i := 0
    // 這裏從最末端,即最舊的數據開始查詢
    for ent := c.cache.Back(); ent != nil; ent = c.cache.Prev(ent) {
        keys[i] = ent.Value.(*Entry).key
        i++
    }

    return keys
}

func (c *LRU) Remove(key interface{}) bool {
    e, ok := c.items[key]
    if !ok {
        return false
    }

    // 從雙向鏈表中刪除element,複雜度O(1),同時從哈希表items中刪除
    c.cache.Remove(e)
    delete(c.items, key)

    return true
}

設計好了LRU對象,而後代碼測試驗證下結果正確性:

// 執行結果沒問題
func TestSimpleLRU(test *testing.T) {
    l, _ := NewLRU(128)
    for i := 0; i < 256; i++ {
        l.Add(i, i)
    }
    if l.Len() != 128 {
        panic(fmt.Sprintf("bad len: %v", l.Len()))
    }

    // 這裏v==i+128才正確,0-127已經被刪除了
    for i, k := range l.Keys() {
        if v, ok := l.Get(k); !ok || v != k || v != i+128 {
            test.Fatalf("bad key: %v", k)
        }
    }

    for i := 0; i < 128; i++ {
        _, ok := l.Get(i)
        if ok {
            test.Fatalf("should be evicted")
        }
    }
    for i := 128; i < 256; i++ {
        _, ok := l.Get(i)
        if !ok {
            test.Fatalf("should not be evicted")
        }
    }

    for i := 128; i < 192; i++ {
        ok := l.Remove(i)
        if !ok {
            test.Fatalf("should be contained")
        }
        ok = l.Remove(i)
        if ok {
            test.Fatalf("should not be contained")
        }
        _, ok = l.Get(i)
        if ok {
            test.Fatalf("should be deleted")
        }
    }
    l.Get(192) // expect 192 to be last key in l.Keys()

    for i, k := range l.Keys() {
        if (i < 63 && k != i+193) || (i == 63 && k != 192) {
            test.Fatalf("out of order key: %v", k)
        }
    }

    l.Purge()
    if l.Len() != 0 {
        test.Fatalf("bad len: %v", l.Len())
    }
    if _, ok := l.Get(200); ok {
        test.Fatalf("should contain nothing")
    }
}

漫畫:什麼是LRU算法? 這篇文章中小灰遇到了一個難題,用戶系統要爆炸了,不知道怎麼去刪除那些
緩存的用戶數據來減小內存使用,確定不是隨機刪除。可是經過雙向鏈表加上哈希表簡單組合,構成了一個強大靠譜的LRU結構,刪除最舊的數據,保留最新的數據
(這裏假設最近被訪問的數據是新數據,未被訪問的數據則排隊置後),就完美解決了難題,可見LRU算法的巧妙強大。k8s源碼中一樣使用了LRU結構,
不會LRU算法看k8s源碼都費勁。可見算法和數據結構的重要性,刷leetcode是個須要一直堅持下去的活。

參考文獻

漫畫:什麼是LRU算法?

mutation_cache.go使用LRU Cache

golang自帶雙向鏈表:/usr/local/go/src/container/list/list.go

golang-lru

leetcode #146

leetcode #460

leetcode #1625

相關文章
相關標籤/搜索