從99.9%CPU淺談Golang的定時器實現(xiàn)原理

1. 情景描述:

線上某系統(tǒng)大約運行了半個多月的時間,突然發(fā)現(xiàn)系統(tǒng)的交易處理時間延遲從最初的70ms 變成7s吃靠,也就是系統(tǒng)性能下降了100倍左右撩笆。經(jīng)過一番盤查發(fā)現(xiàn)top命令下的系統(tǒng)CPU占用率幾乎達到99.9%,然后重啟系統(tǒng)之后問題未重現(xiàn)夕冲,第二天top一下CPU 20%, 第三天CPU占用率到30%,而且還有上升趨勢歹鱼。

從上面的情景描述看泣栈,顯然是系統(tǒng)中某種消耗CPU資源的任務(wù)發(fā)生了泄露。通過pprof最終定位到系統(tǒng)的timer調(diào)度占用幾乎所有的CPU時間南片。這引起了筆者的極大興趣庭敦,于是筆者通過google神奇進行g(shù)olang cpu timer相關(guān)的搜索。隨后golang語言Api系統(tǒng)中CPU功耗優(yōu)化300倍的過程這篇文章的博主給了筆者很大啟發(fā)伞广,應(yīng)該是程序中某些地方的Tick的使用姿勢不對疼电,于是筆者開始了對time.Aftertime.Tick背后原理的追溯。

2. 定時器的使用

golang的定時器的使用非常簡單蔽豺,一般我們用的比較多的就是Timer和Ticker,如下所示:

//場景1:
for {
    select {
    case <- time.After(10 * time.Microsecond):
        fmt.Println("hello timer")
    }
}

//場景2:
for {
    select {
    case <- time.Tick(10 * time.Microsecond):
        fmt.Println("hello, tick")
    }
}

乍一看這兩種調(diào)用方式都沒有問題沧侥,而且短時間測試兩種方式的效果看起來一樣,然后如果測試時你使用top命令查看兩種場景下的進程CPU占用率啥纸,你會大吃一驚婴氮,截圖如下:


場景1-time_after
場景2-time_tick

從上面兩圖中可以看出After的用法比同用法下的Tick的CPU占用率小很多主经,而且tick的CPU占用率是很快就上升到100%的,如果這里的間隔時間設(shè)置稍微大一些可以更加明顯的觀察到Tick進程的CPU占用率上升的情況穗酥,那這是什么原因呢惠遏?

3.定時器的實現(xiàn)原理

記得哪里看過一句話,源碼面前节吮,了無秘密透绩。我們還是順著Tick和After方法的源碼探尋事情的真相吧.

  • After的相關(guān)源碼
//1.After方法, 創(chuàng)建了一個Timer對象并返回其chanel
func After(d Duration) <-chan Time {
    return NewTimer(d).C
}

//2.NewTimer方法帚豪, 創(chuàng)建一個Timer對象并啟動之,這里when方法是輔助定時時間的莹桅,sendTime用于向通道中返回Time對象烛亦,然后啟動定時器
func NewTimer(d Duration) *Timer {
    c := make(chan Time, 1)
    t := &Timer{
        C: c,
        r: runtimeTimer{
            when: when(d),
            f:    sendTime,
            arg:  c,
        },
    }
    startTimer(&t.r)
    return t
}
  • Tick的相關(guān)源碼
//1.創(chuàng)建Ticker對象此洲,這里定時時間不允許小于0委粉,否則可能會發(fā)生panic
func Tick(d Duration) <-chan Time {
    if d <= 0 {
        return nil
    }
    return NewTicker(d).C
}

//2.從NewTicker的源碼來看,Ticker的基本數(shù)據(jù)結(jié)構(gòu)和Timer類似贾节,不同的是這里new的runtimeTimer加上了一個period的變量。
func NewTicker(d Duration) *Ticker {
    if d <= 0 {
        panic(errors.New("non-positive interval for NewTicker"))
    }
    // Give the channel a 1-element time buffer.
    // If the client falls behind while reading, we drop ticks
    // on the floor until the client catches up.
    c := make(chan Time, 1)
    t := &Ticker{
        C: c,
        r: runtimeTimer{
            when:   when(d),
            period: int64(d),
            f:      sendTime,
            arg:    c,
        },
    }
    startTimer(&t.r)
    return t
}

從Timer和Ticker的源碼中可以看出知牌,其啟動都是通過startTimer進行啟動的角寸,startTimer的實現(xiàn)在runtime/time.go中。

//1.startTimer將new的timer對象加入timer的堆數(shù)據(jù)結(jié)構(gòu)中
//startTimer adds t to the timer heap.
//go:linkname startTimer time.startTimer
func startTimer(t *timer) {
    if raceenabled {
        racerelease(unsafe.Pointer(t))
    }
    addtimer(t)
}

func addtimer(t *timer) {
    tb := t.assignBucket()
    lock(&tb.lock)
    tb.addtimerLocked(t)
    unlock(&tb.lock)
}

// Add a timer to the heap and start or kick timerproc if the new timer is
// earlier than any of the others.
// Timers are locked.
func (tb *timersBucket) addtimerLocked(t *timer) {
    // when must never be negative; otherwise timerproc will overflow
    // during its delta calculation and never expire other runtime timers.
    if t.when < 0 {
        t.when = 1<<63 - 1
    }
    t.i = len(tb.t)
    tb.t = append(tb.t, t)
    siftupTimer(tb.t, t.i)
    if t.i == 0 {
        // siftup moved to top: new earliest deadline.
        if tb.sleeping {
            tb.sleeping = false
            notewakeup(&tb.waitnote)
        }
        if tb.rescheduling {
            tb.rescheduling = false
            goready(tb.gp, 0)
        }
    }
    if !tb.created {
        tb.created = true
        go timerproc(tb)
    }
}
type timersBucket struct {
    lock         mutex
    gp           *g
    created      bool
    sleeping     bool
    rescheduling bool
    sleepUntil   int64
    waitnote     note
    t            []*timer
}

定時器處理邏輯

func timerproc(tb *timersBucket) {
    tb.gp = getg()
    for {
        lock(&tb.lock)
        tb.sleeping = false
        now := nanotime()
        delta := int64(-1)
        for {
            if len(tb.t) == 0 { //無timer的情況
                delta = -1
                break
            }
            t := tb.t[0] //拿到堆頂?shù)膖imer
            delta = t.when - now
            if delta > 0 { // 所有timer的時間都沒有到期
                break
            }
            if t.period > 0 { // t[0] 是ticker類型沮峡,調(diào)整其到期時間并調(diào)整timer堆結(jié)構(gòu)
                // leave in heap but adjust next time to fire
                t.when += t.period * (1 + -delta/t.period)
                siftdownTimer(tb.t, 0)
            } else {
                //Timer類型的定時器是單次的邢疙,所以這里需要將其從堆里面刪除
                // remove from heap
                last := len(tb.t) - 1
                if last > 0 {
                    tb.t[0] = tb.t[last]
                    tb.t[0].i = 0
                }
                tb.t[last] = nil
                tb.t = tb.t[:last]
                if last > 0 {
                    siftdownTimer(tb.t, 0)
                }
                t.i = -1 // mark as removed
            }
            f := t.f
            arg := t.arg
            seq := t.seq
            unlock(&tb.lock)
            if raceenabled {
                raceacquire(unsafe.Pointer(t))
            }
            f(arg, seq) //調(diào)用定時器處理函數(shù)
            lock(&tb.lock)
        }
        if delta < 0 || faketime > 0 {
            // No timers left - put goroutine to sleep.
            tb.rescheduling = true
            goparkunlock(&tb.lock, "timer goroutine (idle)", traceEvGoBlock, 1)
            continue
        }
        // At least one timer pending. Sleep until then.
        tb.sleeping = true
        tb.sleepUntil = now + delta
        noteclear(&tb.waitnote)
        unlock(&tb.lock)
        notetsleepg(&tb.waitnote, delta)
    }
}

以上便是定時器調(diào)用的主要邏輯疟游,這里總結(jié)一下基本原理:

  1. 所有timer統(tǒng)一使用一個最小堆結(jié)構(gòu)去維護痕支,按照timer的when(到期時間)比較大小采转;
  2. timer處理線程從堆頂開始處理每個timer, 對于到期的timer, 如果其period>0, 則表明該timer 屬于Ticker類型,調(diào)整其下次到期時間并調(diào)整其在堆中的位置板熊,否則從堆中移除該timer;
  3. 調(diào)用該timer的處理函數(shù)以及其他相關(guān)工作察绷;

4. 再論定時器的正確使用姿勢

從第3節(jié)的源碼中我們可以看到After和Tick其實是一個創(chuàng)建了一個單次的timer一個是創(chuàng)建了一個永久性的timer。因此場景2中Tick的用法會導(dǎo)致進程中創(chuàng)建無數(shù)個Tick容劳,這最終導(dǎo)致了timer處理線程忙死闸度。因此,使用Tick進行定時任務(wù)的話我們可以將Tick對象建在循環(huán)外面:

    tick := time.Tick(10 * time.Microsecond)
    for {
        select {
        case <- tick:
            fmt.Printf("hello, tick 2")
        }
    }

其次golang的處理方式中也可以看出留量,go的timer的處理和用戶端程序定義的間隔時間不一定完全精準,用戶的回調(diào)函數(shù)執(zhí)行時間越長單個timer對堆中其他鄰近timer的影響越大楼熄。因此timer的回調(diào)函數(shù)一定是執(zhí)行時間越短越好。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末错敢,一起剝皮案震驚了整個濱河市缕粹,隨后出現(xiàn)的幾起案子平斩,更是在濱河造成了極大的恐慌,老刑警劉巖双戳,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件飒货,死亡現(xiàn)場離奇詭異,居然都是意外死亡塘辅,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門哲银,熙熙樓的掌柜王于貴愁眉苦臉地迎上來呻惕,“玉大人,你說我怎么就攤上這事做院”舫郑” “怎么了?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵屈雄,是天一觀的道長官套。 經(jīng)常有香客問我蓖议,道長讥蟆,這世上最難降的妖魔是什么纺阔? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任笛钝,我火速辦了婚禮,結(jié)果婚禮上玻靡,老公的妹妹穿的比我還像新娘。我一直安慰自己臼朗,他們只是感情好蝎土,可當我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著挡毅,像睡著了一般暴构。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上取逾,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天砾隅,我揣著相機與錄音,去河邊找鬼堕绩。 笑死,一個胖子當著我的面吹牛奴紧,可吹牛的內(nèi)容都是我干的晶丘。 我是一名探鬼主播唐含,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼捷枯,長吁一口氣:“原來是場噩夢啊……” “哼专执!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起本股,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤拄显,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后躬审,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡蹬碧,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年恩沽,在試婚紗的時候發(fā)現(xiàn)自己被綠了翔始。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡渤闷,死狀恐怖脖镀,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情蜒灰,我是刑警寧澤,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布凸椿,位于F島的核電站翅溺,受9級特大地震影響髓抑,放射性物質(zhì)發(fā)生泄漏优幸。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一羹饰、第九天 我趴在偏房一處隱蔽的房頂上張望跛璧。 院中可真熱鬧新啼,春花似錦、人聲如沸燥撞。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽冠胯。三九已至,卻和暖如春置蜀,著一層夾襖步出監(jiān)牢的瞬間悉盆,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工秋秤, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留脚翘,地道東北人。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓芥玉,卻偏偏與公主長得像备图,于是被迫代替她去往敵國和親赶袄。 傳聞我的和親對象是個殘疾皇子抠藕,可洞房花燭夜當晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容