[k8s源碼分析][client-go] workqueue

1. 前言

轉載請說明原文出處, 尊重他人勞動成果!

源碼位置: https://github.com/nicktming/client-go/tree/tming-v13.0/util/workqueue
分支: tming-v13.0 (基于v13.0版本)

本文將分析util包中的workqueue. 在各類controller中經常會使用該workqueue中的一些類.

architecture.png

2. queue

2.1 Interface接口

type Interface interface {
    Add(item interface{})
    Len() int
    Get() (item interface{}, shutdown bool)
    Done(item interface{})
    ShutDown()
    ShuttingDown() bool
}

這里有幾點需要注意一下:
1. 增加了Done方法, 告訴queue對這個item的處理已經結束了.
2. 以往的queue在pop之后就對這個item的狀態不管了, 但是在該iterface中明顯需要進行管理, 因為需要實現Done方法, 所以需要知道哪些item正在處理.

2.2 實現類Type

type Type struct {
    // queue定義了需要處理的item的順序
    // 這些element應該出現在dirty中 而不會在processing中.
    queue []t
    // dirty保存著那些需要被處理的item
    dirty set
    // 代表那些正在被處理的元素,
    processing set
    cond *sync.Cond
    shuttingDown bool
    metrics queueMetrics
    unfinishedWorkUpdatePeriod time.Duration
    clock                      clock.Clock
}
type empty struct{}
type t interface{}
type set map[t]empty
func (s set) has(item t) bool {
    _, exists := s[item]
    return exists
}
func (s set) insert(item t) {
    s[item] = empty{}
}
func (s set) delete(item t) {
    delete(s, item)
}

2.3 方法

Get
// 返回頭部item 和 queue是否關閉
func (q *Type) Get() (item interface{}, shutdown bool) {
    q.cond.L.Lock()
    defer q.cond.L.Unlock()
    for len(q.queue) == 0 && !q.shuttingDown {
        q.cond.Wait()
    }
    if len(q.queue) == 0 {
        // We must be shutting down.
        return nil, true
    }

    // 獲得隊列的頭 并且更新queue數組
    item, q.queue = q.queue[0], q.queue[1:]

    q.metrics.get(item)

    // 1. 將該item加入到processing中, 表明該item正在被處理
    // 2. 將該item從dirty中刪除
    q.processing.insert(item)
    q.dirty.delete(item)

    return item, false
}
  1. 如果queue沒有關閉 但是目前沒有元素 一直waiting
  2. 如果queue已經關閉 則返回nil, true
  3. 獲得隊列的頭item并且更新queue數組
  4. 將該item加入到processing中 表明該item正在被處理
  5. dirtyqueue中刪除該item
  6. 返回該item, false
Add 和 Done
func (q *Type) Add(item interface{}) {
    q.cond.L.Lock()
    defer q.cond.L.Unlock()
    // 如果queue已經關閉了
    if q.shuttingDown {
        return
    }
    // 如果已經在queue中了 就直接返回了
    if q.dirty.has(item) {
        return
    }

    q.metrics.add(item)
    // 加入到dirty中
    q.dirty.insert(item)
    // 如果該元素正在被處理, 那直接返回了 不會加入到queue中
    if q.processing.has(item) {
        return
    }

    q.queue = append(q.queue, item)
    q.cond.Signal()
}

// 表明該item已經結束了
func (q *Type) Done(item interface{}) {
    q.cond.L.Lock()
    defer q.cond.L.Unlock()

    q.metrics.done(item)

    // 如果結束了 就從processing中刪除
    q.processing.delete(item)
    // 如果dirty中有 在把item加回去
    // 這里主要是因為在處理item的過程中, 上流程序又調用了Add方法
    // 因為該item正在被處理, 此時如果加入到queue中, 然后又Get拿到該item
    // 此時就會有兩個同樣的item正在被處理

    // 所以最終設計的目的是為了保證正在被處理的過程中保證每個都不一樣 不會出現有兩個相同的item正在被處理
    if q.dirty.has(item) {
        q.queue = append(q.queue, item)
        q.cond.Signal()
    }
}

該方法的實現是為了保證不能同時有兩個相同的item正在被處理

1. 如果一個item正在被處理, 此時又add了一個相同的item, 那此時不能加入到queue中(把該item暫時加到了dirty), 此時如果加入到queue中, 然后又Get拿到該item, 那么同一時間就會有兩個相同的item在被處理
2. 所以在該item處理結束(Done方法)的時候會檢查該item是否有在被處理的過程中再次加入queue, 也就是判斷dirty中是否含有該item, 如果有, 則加入到queue

3. delaying_queue

3.1 DelayingInterface接口

type DelayingInterface interface {
    Interface
    // 在此刻過duration時間后再加入到queue中
    AddAfter(item interface{}, duration time.Duration)
}

DelayingInterface is an Interface that can Add an item at a later time. This makes it easier to requeue items after failures without ending up in a hot-loop.

該接口涉及的目的就是可以避免某些失敗的items陷入熱循環. 因為某些item在出隊列進行處理有可能失敗, 失敗了用戶就有可能將該失敗的item重新進隊列, 如果短時間內又出隊列有可能還是會失敗(因為短時間內整個環境可能沒有改變等等), 所以可能又重新進隊列等等, 因此陷入了一種hot-loop.

所以就為用戶提供了AddAfter方法, 可以允許用戶告訴DelayingInterface過多長時間把該item加入隊列中.

3.2 實現類delayingType

因為有延遲的時間, 所以那些item到時間了可以進入隊列了, 那些沒到時間還不能進入隊列, 所以waitForwaitForPriorityQueue應運而生.

waitFor: 保存了包括數據data和該item什么時間起(readyAt)就可以進入隊列了.
waitForPriorityQueue: 是用于保存waitFor的優先隊列, 按readyAt時間從早到晚排序. 先readyitem先出隊列.

// delayingType wraps an Interface and provi`des delayed re-enquing
// 提供有延遲的重進隊列
type delayingType struct {
    Interface
    // clock tracks time for delayed firing
    clock clock.Clock
    // stopCh lets us signal a shutdown to the waiting loop
    stopCh chan struct{}
    // stopOnce guarantees we only signal shutdown a single time
    stopOnce sync.Once
    // heartbeat ensures we wait no more than maxWait before firing
    heartbeat clock.Ticker
    // waitingForAddCh is a buffered channel that feeds waitingForAdd
    waitingForAddCh chan *waitFor
    metrics retryMetrics
}
// waitFor holds the data to add and the time it should be added
type waitFor struct {
    data    t
    // 該data可以加入queue的時間
    readyAt time.Time
    // index in the priority queue (heap)
    index int
}
type waitForPriorityQueue []*waitFor

3.3 方法

newDelayingQueue
func newDelayingQueue(clock clock.Clock, name string) DelayingInterface {
    ret := &delayingType{
        Interface:       NewNamed(name),
        clock:           clock,
        heartbeat:       clock.NewTicker(maxWait),
        stopCh:          make(chan struct{}),
        waitingForAddCh: make(chan *waitFor, 1000),
        metrics:         newRetryMetrics(name),
    }

    go ret.waitingLoop()

    return ret
}

這里需要注意的是
1. 啟動了goroutine執行waitingLoop方法, 關于waitingLoop在后面介紹.
2. NewNamed(name)是一個Type對象實例(2.2中的Type)

AddAfter
func (q *delayingType) AddAfter(item interface{}, duration time.Duration) {
    // don't add if we're already shutting down
    // 如果隊列已經關閉 直接返回
    if q.ShuttingDown() {
        return
    }

    q.metrics.retry()

    // immediately add things with no delay
    // 如果不需要延遲 直接調用iterface(Type)中的Add方法
    if duration <= 0 {
        q.Add(item)
        return
    }

    select {
    case <-q.stopCh:
        // unblock if ShutDown() is called
        // 隊列關閉的時候才會進入這里
    case q.waitingForAddCh <- &waitFor{data: item, readyAt: q.clock.Now().Add(duration)}:
        // 構造一個waitFor放到waitingForAddCh(一個channel, 緩沖1000)
    }
}

1. 構造一個waitFor放到channel(waitingForAddCh)中, waitingLoop會在另外一端接收.

waitingLoop
func (q *delayingType) waitingLoop() {
    defer utilruntime.HandleCrash()
    // Make a placeholder channel to use when there are no items in our list
    never := make(<-chan time.Time)
    waitingForQueue := &waitForPriorityQueue{}
    heap.Init(waitingForQueue)
    waitingEntryByData := map[t]*waitFor{}
    for {
        // queue關閉返回
        if q.Interface.ShuttingDown() {
            return
        }
        now := q.clock.Now()
        // 將那些已經ready好了的item可以加入到queue中
        for waitingForQueue.Len() > 0 {
            entry := waitingForQueue.Peek().(*waitFor)
            if entry.readyAt.After(now) {
                break
            }
            entry = heap.Pop(waitingForQueue).(*waitFor)
            q.Add(entry.data)
            delete(waitingEntryByData, entry.data)
        }
        // nextReadyAt啟一個channel用于存著下次可以加入queue的時間
        nextReadyAt := never
        if waitingForQueue.Len() > 0 {
            // 優先隊列中的第一個肯定是最早ready的
            entry := waitingForQueue.Peek().(*waitFor)
            nextReadyAt = q.clock.After(entry.readyAt.Sub(now))
        }
        select {
        case <-q.stopCh:
            return
        // ticker的操作 每隔10s會調用
        // 有可能去增加已經ready items
        case <-q.heartbeat.C():
            // continue the loop, which will add ready items
        // 下次ready的時間到了
        case <-nextReadyAt:
            // continue the loop, which will add ready items
        case waitEntry := <-q.waitingForAddCh:
            // 從AddAfter過來的數據
            if waitEntry.readyAt.After(q.clock.Now()) {
                // 如果時間沒到 就加入waitingForQueue中
                // waitingEntryByData用于保存waitEntry.data與waitEntry的關系
                insert(waitingForQueue, waitingEntryByData, waitEntry)
            } else {
                // 直接加入到queue中
                q.Add(waitEntry.data)
            }
            drained := false
            for !drained {
                select {
                case waitEntry := <-q.waitingForAddCh:
                    if waitEntry.readyAt.After(q.clock.Now()) {
                        insert(waitingForQueue, waitingEntryByData, waitEntry)
                    } else {
                        q.Add(waitEntry.data)
                    }
                default:
                    drained = true
                }
            }
        }
    }
}
func insert(q *waitForPriorityQueue, knownEntries map[t]*waitFor, entry *waitFor) {
    // if the entry already exists, update the time only if it would cause the item to be queued sooner
    // 如果已經在waitingQueue中了 取readyAt最小的那個 為了可以讓它早點出queue
    // 就是說增加的item重復了 ready的時間取最早的一個
    existing, exists := knownEntries[entry.data]
    if exists {
        if existing.readyAt.After(entry.readyAt) {
            existing.readyAt = entry.readyAt
            heap.Fix(q, existing.index)
        }

        return
    }

    heap.Push(q, entry)
    knownEntries[entry.data] = entry
}

這里啟動一個for循環:
1. 只有隊列關閉的時候才會退出.
2. 利用channel(q.waitingForAddCh)一直接受從AddAfter過來的waitFor.
3. 一直將已經readyitem加入到隊列中.

注意: 如果有兩個相同的data加入時, waitFor中取ready時間最早的那一個.

4. default_rate_limit

4.1 RateLimiter接口

type RateLimiter interface {
    // 返回該item應該要等待多長時間
    When(item interface{}) time.Duration
    // 停止跟蹤該item
    Forget(item interface{})
    // 返回該item失敗的次數
    NumRequeues(item interface{}) int
}

一個控制速率的接口. 在3.1 DelayingInterface接口中提到hot-loop, 那關于等待多長時間可以根據失敗的次數有關, 比如最簡單的失敗一次等待時間就增加一倍, 第一次失敗等待1s, 第二次失敗等待2s, 第三次失敗等待4s, 以此類推. 那RateLimiter接口就是這樣一個控制速率的抽象定義, 接下來看一個它的實現類.

4.2 ItemExponentialFailureRateLimiter

type ItemExponentialFailureRateLimiter struct {
    failuresLock sync.Mutex
        // 存的失敗的item
    failures     map[interface{}]int

    baseDelay time.Duration
    maxDelay  time.Duration
}
var _ RateLimiter = &ItemExponentialFailureRateLimiter{}
func NewItemExponentialFailureRateLimiter(baseDelay time.Duration, maxDelay time.Duration) RateLimiter {
    return &ItemExponentialFailureRateLimiter{
        failures:  map[interface{}]int{},
        baseDelay: baseDelay,
        maxDelay:  maxDelay,
    }
}
func DefaultItemBasedRateLimiter() RateLimiter {
    return NewItemExponentialFailureRateLimiter(time.Millisecond, 1000*time.Second)
}

4.3 方法

func (r *ItemExponentialFailureRateLimiter) When(item interface{}) time.Duration {
    r.failuresLock.Lock()
    defer r.failuresLock.Unlock()
        // 該item失敗的次數
    exp := r.failures[item]
    r.failures[item] = r.failures[item] + 1
       
    // The backoff is capped such that 'calculated' value never overflows.
    backoff := float64(r.baseDelay.Nanoseconds()) * math.Pow(2, float64(exp))
    if backoff > math.MaxInt64 {
        return r.maxDelay
    }
    calculated := time.Duration(backoff)
    if calculated > r.maxDelay {
        return r.maxDelay
    }
    return calculated
}
func (r *ItemExponentialFailureRateLimiter) NumRequeues(item interface{}) int {
    r.failuresLock.Lock()
    defer r.failuresLock.Unlock()

    return r.failures[item]
}
func (r *ItemExponentialFailureRateLimiter) Forget(item interface{}) {
    r.failuresLock.Lock()
    defer r.failuresLock.Unlock()

    delete(r.failures, item)
}

直接通過一個例子更好說明:

example.png

5. rate_limiting_queue

5.1 RateLimitingInterface接口

type RateLimitingInterface interface {
    DelayingInterface
    AddRateLimited(item interface{})
    Forget(item interface{})
    NumRequeues(item interface{}) int
}

因為DelayingInterface已經有了AddAfter方法, 但是到底是after多長時間并沒有進行控制, 最簡單的就是一個常量時間, 對所有的item都一樣. 但是這樣的話也有可能會造成一些問題, 有些item因為失敗來回來回進入隊列十幾次, 而有些item才失敗一次, 就有可能排在這個失敗十幾次的item后面, 如果還是不成功, 這樣就會造成資源的浪費.

所以RateLimitingInterface就在DelayingInterface加入對item的速率進行控制, 并且與失敗的次數進行相關.

5.2 實現類以及方法

很常規.

type rateLimitingType struct {
    DelayingInterface
    rateLimiter RateLimiter
}
func (q *rateLimitingType) AddRateLimited(item interface{}) {
    q.DelayingInterface.AddAfter(item, q.rateLimiter.When(item))
}

func (q *rateLimitingType) NumRequeues(item interface{}) int {
    return q.rateLimiter.NumRequeues(item)
}

// 刪除該item
func (q *rateLimitingType) Forget(item interface{}) {
    q.rateLimiter.Forget(item)
}
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,156評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,401評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,069評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,873評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,635評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,128評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,203評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,365評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,881評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,733評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,935評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,475評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,172評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,582評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,821評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,595評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,908評論 2 372

推薦閱讀更多精彩內容

  • Swift1> Swift和OC的區別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴謹 對...
    cosWriter閱讀 11,120評論 1 32
  • http://liuxing.info/2017/06/30/Spring%20AMQP%E4%B8%AD%E6%...
    sherlock_6981閱讀 15,961評論 2 11
  • 一:base.h 二:block.h 1. dispatch_block_flags:DISPATCH_BLOCK...
    小暖風閱讀 2,468評論 0 0
  • 下午閑無聊事約上好友去圖書館看書,本想看上一本書回來方便寫書評。書看到一半我們就開始聊起來,起初是宣泄悲傷學,后來...
    鐘無迭笙閱讀 135評論 0 0
  • 身體調養之輻射 輻射,在科學上,分為電離輻射和非電離輻射,醫院的照片、CT等,都是基于能使分子發生電離效應的電離輻...
    蓬頭小龍蝦閱讀 389評論 0 0