Go 調度器跟蹤

sl6124 8年前發布 | 14K 次閱讀 Goroutine Google Go/Golang開發

 

這是William Kennedy寫的第二篇文章: Scheduler Tracing In Go , 第一篇為Stack Traces In Go。

本文主要介紹如何跟蹤Go的調度器Scheduler的活動。

介紹

我喜歡Go的原因之一就是能夠產生概要(profiling)和調試信息。當你的程序運行時, GODEBUG 環境變量可以產生運行時的調試信息。你可以請求垃圾回收器和調度器(scheduler)的摘要信息和細節。關鍵是你不需要額外創建單獨的編譯程序就可以實現。

在本文中,我將演示如何理解一個簡單的并發Go程序的調度器跟蹤信息。如果你已經有Go調度器的基本概念,對閱讀本文會很有幫助, 在開始下面的介紹前我建議你閱讀下面的兩篇文章:

  1. Concurrency, Goroutines and GOMAXPROCS
  2. Go Scheduler

代碼

下面的代碼就是用來演示用的例子。

列表1

 
package main

import (
 "sync"
 "time"
)

func main() {
 var wg sync.WaitGroup
 wg.Add(10)

 for i :=0; i <10; i++ {
 go work(&wg)
 }

 wg.Wait()

 // Wait to see the global run queue deplete.
 time.Sleep(3 * time.Second)
}

func work(wg *sync.WaitGroup) {
 time.Sleep(time.Second)

 var counter int
 for i :=0; i <1e10; i++ {
 counter++
 }

 wg.Done()
}

列表1的第12行使用一個for循環創建10個goroutine,第16行main函數中等待所有的goroutine完成任務。第22行的work函數先sleep 1秒,然后計數 10億次。一旦計數完成,調用waitGroup的Done方法然后返回。

首先 go build 程序,然后運行時設置GODEBUG環境變量。這個變量會被運行時讀取,所以Go命令也會產生跟蹤信息。如果在運行 go run 命令的時候設置了GODEBUG變量,在程序運行之前就會看到跟蹤信息。

所以還是讓我們先編譯程序:

gobuild example.go

調度器摘要信息

schedtrace參數告訴運行時打印一行調度器的摘要信息到標準err輸出中,時間間隔可以指定,單位毫秒,如下所示:

GOMAXPROCS=1GODEBUG=schedtrace=1000./example

譯者注: 如果在windows下可以運行 set GOMAXPROCS=1 && set GODEBUG=schedtrace=1000 && example

程序開始后每個一秒就會打印一行調度器的概要信息,程序本身沒有任何輸出,所以我們只關注輸出的跟蹤信息。讓我們先看前兩行:

SCHED 0ms:gomaxprocs=1idleprocs=0threads=2spinningthreads=0idlethreads=0
runqueue=0[1]

SCHED 1009ms:gomaxprocs=1idleprocs=0threads=3spinningthreads=0idlethreads=1
runqueue=0[9]

讓我們分解每一個項,看看它們分別代表什么含義。

輸出項 意義
1009ms 自從程序開始的毫秒數
gomaxprocs=1 配置的處理器數(邏輯的processor,也就是Go模型中的P,會通過操作系統的線程綁定到一個物理處理器上)
threads=3 運行期管理的線程數,目前三個線程
idlethreads=1 空閑的線程數,當前一個線程空閑,兩個忙
idleprocs=0 空閑的處理器數,當前0個空閑
runqueue=0 在全局的run隊列中的goroutine數,目前所有的goroutine都被移動到本地run隊列
[9] 本地run隊列中的goroutine數,目前9個goroutine在本地run隊列中等待

Go運行時給了我們很多有用的摘要信息。當我們看第一秒的跟蹤數據時,我們看到一個goroutine正在運行,而其它9個都在等待:

圖1中處理器由字符P代表,線程由M代表,goroutine由G代表。我們看到全局run隊列為空,處理器正在執行一個goroutine。其余9在本地隊列中等待。

如果我們配置多個處理器會怎樣呢?讓我們增加GOMAXPROCS 看看輸出結果:

GOMAXPROCS=2GODEBUG=schedtrace=1000./example

SCHED 0ms:gomaxprocs=2idleprocs=1threads=2spinningthreads=0
idlethreads=0runqueue=0[00]

SCHED 1002ms:gomaxprocs=2idleprocs=0threads=4spinningthreads=1
idlethreads=1runqueue=0[04]

SCHED 2006ms:gomaxprocs=2idleprocs=0threads=4spinningthreads=0
idlethreads=1runqueue=0[44]

…

SCHED 6024ms:gomaxprocs=2idleprocs=0threads=4spinningthreads=0
idlethreads=1runqueue=2[33]

…

SCHED 10049ms:gomaxprocs=2idleprocs=0threads=4spinningthreads=0
idlethreads=1runqueue=4[22]
…

SCHED 13067ms:gomaxprocs=2idleprocs=0threads=4spinningthreads=0
idlethreads=1runqueue=6[11]

…

SCHED 17084ms:gomaxprocs=2idleprocs=0threads=4spinningthreads=0
idlethreads=1runqueue=8[00]

…

SCHED 21100ms:gomaxprocs=2idleprocs=2threads=4spinningthreads=0
idlethreads=2runqueue=0[00]

讓我們將視線放在第二秒:

SCHED 2002ms: gomaxprocs=2idleprocs=0threads=4spinningthreads=0
idlethreads=1runqueue=0[44]

2002ms : Thisisthetraceforthe2secondmark.
gomaxprocs=2:2processors are configuredforthis program.
threads=4:4threads exist.2forprocessorsand2fortheruntime.
idlethreads=1:1idle thread (3threadsrunning).
idleprocs=0:0processors are idle (2processors busy).
runqueue=0: All runnable goroutines have been movedtoalocalrunqueue.
[44] :4goroutines are waiting inside eachlocalrunqueue.

讓我們看第二秒的信息,可以看到goroutine是如何被處理器運行的。我們也可以看到有8個goroutine在本地run隊列中等待,每個本地run隊列包含4個等待的goroutine。

讓我們再來看第6秒的信息:

SCHED 6024ms:gomaxprocs=2idleprocs=0threads=4spinningthreads=0
idlethreads=1runqueue=2[33]

idleprocs=0:0processors are idle (2processors busy).
runqueue=2:2goroutines returnedandare waiting to be terminated.
[33] :3goroutines are waiting inside each local run queue.

第6秒數據有些變化,如圖3所示,兩個goroutine完成了它們的任務,放回到全局run隊列中。當然我們還是有兩個goruntine在運行(G6,G7),每個P運行一個。每個本地run隊列中還有3個goroutine在等待。

注意:

在很多情況下goroutine在終止前并沒有被放回到全局run隊列。本文中的例子比較特殊,它的方法體是一個for循環,并沒有調用其它函數,而且運行時間超過10ms。10毫秒是調度器的調度標桿,過了10毫秒的執行時間,調度器就會嘗試占用(preempt)這個goroutine。本例中的goroutine沒有被占用是因為它們沒有調用其它函數。本例中一旦goroutine執行到wg.Done調用,它們立即被占用,然后移動到全局run隊列等待終止。

在17秒我們看到只有兩個goroutine還在運行:

SCHED 17084ms:gomaxprocs=2idleprocs=0threads=4spinningthreads=0
idlethreads=1runqueue=8[00]

idleprocs=0:0processors are idle (2processors busy).
runqueue=8:8goroutines returnedandare waiting to be terminated.
[00] : No goroutines are waiting inside any local run queue.

從圖4可以看出,8個goroutine被移動到全局run隊列等待終止。兩個goroutine(G12和G13)還在運行,本地run隊列都為空。

最后在第21秒:

SCHED 21100ms:gomaxprocs=2idleprocs=2threads=4spinningthreads=0
idlethreads=2runqueue=0[00]

idleprocs=2:2processors are idle (0processors busy).
runqueue=0: All the goroutines that wereinthe queue have been terminated.
[00] : No goroutines are waiting inside any local run queue.

如圖5所示,這時那10個goroutine都已經完成任務被終止。

調度器跟蹤信息的細節

調度器的摘要信息很有幫助,但是有時候你需要更深入的了解它的細節。在這種情況下,我們可以使用scheddetail 參數,可以提供處理器P,線程M和goroutine G的細節。讓我們再運行一下程序,增加scheddetail 參數:

GOMAXPROCS=2GODEBUG=schedtrace=1000,scheddetail=1./example

下面的日志是第4秒的輸出:

SCHED 4028ms:gomaxprocs=2idleprocs=0threads=4spinningthreads=0
idlethreads=1runqueue=2gcwaiting=0nmidlelocked=0stopwait=0sysmonwait=0
P0: status=1schedtick=10syscalltick=0m=3runqsize=3gfreecnt=0
P1: status=1schedtick=10syscalltick=1m=2runqsize=3gfreecnt=0
M3: p=0curg=4mallocing=0throwing=0gcing=0locks=0dying=0helpgc=0spinning=0blocked=0lockedg=-1
M2: p=1curg=10mallocing=0throwing=0gcing=0locks=0dying=0helpgc=0spinning=0blocked=0lockedg=-1
M1: p=-1curg=-1mallocing=0throwing=0gcing=0locks=1dying=0helpgc=0spinning=0blocked=0lockedg=-1
M0: p=-1curg=-1mallocing=0throwing=0gcing=0locks=0dying=0helpgc=0spinning=0blocked=0lockedg=-1
G1: status=4(semacquire)m=-1lockedm=-1
G2: status=4(force gc (idle))m=-1lockedm=-1
G3: status=4(GC sweep wait)m=-1lockedm=-1
G4: status=2(sleep)m=3lockedm=-1
G5: status=1(sleep)m=-1lockedm=-1
G6: status=1(stack growth)m=-1lockedm=-1
G7: status=1(sleep)m=-1lockedm=-1
G8: status=1(sleep)m=-1lockedm=-1
G9: status=1(stack growth)m=-1lockedm=-1
G10: status=2(sleep)m=2lockedm=-1
G11: status=1(sleep)m=-1lockedm=-1
G12: status=1(sleep)m=-1lockedm=-1
G13: status=1(sleep)m=-1lockedm=-1
G17: status=4(timer goroutine (idle))m=-1lockedm=-1

摘要信息是類似的,但是包含了處理器、線程和goroutine的信息。首先看一下處理器P的信息:

P0: status=1schedtick=10syscalltick=0m=3runqsize=3gfreecnt=0

P1: status=1schedtick=10syscalltick=1m=2runqsize=3gfreecnt=0

我們設置了兩個處理器,因此有兩行數據。下面是線程的信息:

M3: p=0curg=4mallocing=0throwing=0gcing=0locks=0dying=0helpgc=0
spinning=0blocked=0lockedg=-1

M2: p=1curg=10mallocing=0throwing=0gcing=0locks=0dying=0helpgc=0
spinning=0blocked=0lockedg=-1

M1: p=-1curg=-1mallocing=0throwing=0gcing=0locks=1dying=0helpgc=0
spinning=0blocked=0lockedg=-1

M0: p=-1curg=-1mallocing=0throwing=0gcing=0locks=0dying=0helpgc=0
spinning=0blocked=0lockedg=-1

在摘要信息中我們看到線程設置為4(threads=4),所以下面會有4行關于線程M的信息。而且日志細節顯示了哪個線程隸屬哪個處理器 M3: p=0

P0: status=1schedtick=10syscalltick=0m=3runqsize=3gfreecnt=0

M3: p=0curg=4mallocing=0throwing=0gcing=0locks=0dying=0helpgc=0
spinning=0blocked=0lockedg=-1

我們看到線程M3隸屬P0。

G代表goroutine。第4秒我們看到有14個goroutine, 但是自程序啟動已經創建了17個線程。我們怎么知道的17個線程呢,這是從最后的goroutine的標志 G17 得到:

G17: status=4(timer goroutine (idle))m=-1lockedm=-1

如果程序持續創建goroutine,這個數會線性的增大。如果使用標準庫net/http處理http請求,因為每個連接都會創建一個goroutine,我們可以根據這個數據得到多少request被處理,當然得在程序不會創建其它的goroutine情況下這個數才能作為預估的數量。

讓我們看看運行main函數的goroutine:

G1: status=4(semacquire)m=-1lockedm=-1

可以看到這個goroutine的status是4, 阻塞在semacquire上(WaitGroup的Wait方法)。

為了更好的理解其它goroutine的狀態,我們需要了解每個status值代表的含義。下面是一個列表:

status: http://golang.org/src/runtime/runtime.h
Gidle, // 0
Grunnable, // 1 runnable and on a run queue
Grunning, // 2 running
Gsyscall, // 3 performing a syscall
Gwaiting, // 4 waiting for the runtime
Gmoribund_unused, // 5 currently unused, but hardcoded in gdb scripts
Gdead, // 6 goroutine is dead
Genqueue, // 7 only the Gscanenqueue is used
Gcopystack, // 8 in this state when newstack is moving the stack

看看我們創建的10個goroutine,現在我們能更好的理解它們的狀態:

// Goroutines running ina processor. (idleprocs=0)
G4: status=2(sleep)m=3lockedm=-1– Thread M3 / Processor P0
G10: status=2(sleep)m=2lockedm=-1– Thread M2 / Processor P1

// Goroutines waiting to be run on a particular processor. (runqsize=3)
G5: status=1(sleep)m=-1lockedm=-1
G7: status=1(sleep)m=-1lockedm=-1
G8: status=1(sleep)m=-1lockedm=-1

// Goroutines waiting to be run on a particular processor. (runqsize=3)
G11: status=1(sleep)m=-1lockedm=-1
G12: status=1(sleep)m=-1lockedm=-1
G13: status=1(sleep)m=-1lockedm=-1

// Goroutines waiting on the global run queue. (runqueue=2)
G6: status=1(stack growth)m=-1lockedm=-1
G9: status=1(stack growth)m=-1lockedm=-1

了解了調度器的基本概念和我們的程序的行為,我們可以深入了解Go是如何進行調度的,每個P,M,G的狀態。

結論

GODEBUG變量非常好,可以深入了解調度器的行為。它也可以告訴你你的程序的行為。如果你想了解更多,動手寫一些簡單的程序,跟蹤調度器的運行,然后就可以跟蹤更復雜的程序了。

來自: http://colobu.com/2016/04/19/Scheduler-Tracing-In-Go/

 本文由用戶 sl6124 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!