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.After
和time.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占用率啥纸,你會大吃一驚婴氮,截圖如下:
從上面兩圖中可以看出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é)一下基本原理:
- 所有timer統(tǒng)一使用一個最小堆結(jié)構(gòu)去維護痕支,按照timer的when(到期時間)比較大小采转;
- timer處理線程從堆頂開始處理每個timer, 對于到期的timer, 如果其period>0, 則表明該timer 屬于Ticker類型,調(diào)整其下次到期時間并調(diào)整其在堆中的位置板熊,否則從堆中移除該timer;
- 調(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í)行時間越短越好。