學(xué)會這幾招讓 Go 程序自己監(jiān)控自己
談到讓Go程序監(jiān)控自己進程的資源使用情況,那么就讓我們先來談一談有哪些指標是需要監(jiān)控的,一般談?wù)撨M程的指標最常見的就是進程的內(nèi)存占用率、CPU占用率、創(chuàng)建的線程數(shù)。因為Go語言又在線程之上自己維護了Goroutine,所以針對Go進程的資源指標還需要加一個創(chuàng)建的Goroutine數(shù)量。
又因為現(xiàn)在服務(wù)很多都部署在Kubernetes集群上,一個Go進程往往就是一個Pod,但是容器的資源是跟宿主機共享的,只是在創(chuàng)建的時候指定了其資源的使用上限,所以在獲取CPU和Memory這些信息的時候還需要具體情況分開討論。
怎么用Go獲取進程的各項指標
我們先來討論普通的宿主機和虛擬機情況下怎么獲取這些指標,容器環(huán)境放在下一節(jié)來說。
獲取Go進程的資源使用情況使用gopstuil庫即可完成,它我們屏蔽了各個系統(tǒng)之間的差異,幫助我們方便地獲取各種系統(tǒng)和硬件信息。gopsutil將不同的功能劃分到不同的子包中,它提供的模塊主要有:
- cpu:系統(tǒng)CPU 相關(guān)模塊;
- disk:系統(tǒng)磁盤相關(guān)模塊;
- docker:docker 相關(guān)模塊;
- mem:內(nèi)存相關(guān)模塊;
- net:網(wǎng)絡(luò)相關(guān);
- process:進程相關(guān)模塊;
- winservices:Windows 服務(wù)相關(guān)模塊。
我們這里只用到了它的process子包,獲取進程相關(guān)的信息。
聲明:process 模塊需要 import "github.com/shirou/gopsutil/process"后引入到項目,后面演示的代碼會用到的os等模塊會統(tǒng)一省略import相關(guān)的信息和錯誤處理,在此提前說明。
創(chuàng)建進程對象
process模塊的NewProcess會返回一個持有指定PID的Process對象,方法會檢查PID是否存在,如果不存在會返回錯誤,通過Process對象上定義的其他方法我們可以獲取關(guān)于進程的各種信息。
- p, _ := process.NewProcess(int32(os.Getpid()))
進程的CPU使用率
進程的CPU使用率需要通過計算指定時間內(nèi)的進程的CPU使用時間變化計算出來
- cpuPercent, err := p.Percent(time.Second)
上面返回的是占所有CPU時間的比例,如果想更直觀的看占比,可以算一下占單個核心的比例。
- cp := cpuPercent / float64(runtime.NumCPU())
內(nèi)存使用率、線程數(shù)和goroutine數(shù)
這三個指標的獲取過于簡單咱們就放在一塊說
- // 獲取進程占用內(nèi)存的比例
- mp, _ := p.MemoryPercent()
- // 創(chuàng)建的線程數(shù)
- threadCount := pprof.Lookup("threadcreate").Count()
- // Goroutine數(shù)
- gNum := runtime.NumGoroutine()
上面獲取進程資源占比的方法只有在虛擬機和物理機環(huán)境下才能準確。類似Docker這樣的Linux容器是靠著Linux的Namespace和Cgroups技術(shù)實現(xiàn)的進程隔離和資源限制,是不行的。
現(xiàn)在的服務(wù)很多公司是K8s集群部署,所以如果是在Docker中獲取Go進程的資源使用情況需要根據(jù)Cgroups分配給容器的資源上限進行計算才準確。
容器環(huán)境下獲取進程指標
在Linux中,Cgroups給用戶暴露出來的操作接口是文件系統(tǒng),它以文件和目錄的方式組織在操作系統(tǒng)的/sys/fs/cgroup路徑下,在 /sys/fs/cgroup下面有很多諸cpuset、cpu、 memory這樣的子目錄,每個子目錄都代表系統(tǒng)當前可以被Cgroups進行限制的資源種類。
針對我們監(jiān)控Go進程內(nèi)存和CPU指標的需求,我們只要知道cpu.cfs_period_us、cpu.cfs_quota_us 和memory.limit_in_bytes 就行。前兩個參數(shù)需要組合使用,可以用來限制進程在長度為cfs_period的一段時間內(nèi),只能被分配到總量為cfs_quota的CPU時間, 可以簡單的理解為容器能使用的核心數(shù) = cfs_quota / cfs_period。
所以在容器里獲取Go進程CPU的占比的方法,需要做一些調(diào)整,利用我們上面給出的公式計算出容器能使用的最大核心數(shù)。
- cpuPeriod, err := readUint("/sys/fs/cgroup/cpu/cpu.cfs_period_us")
- cpuQuota, err := readUint("/sys/fs/cgroup/cpu/cpu.cfs_quota_us")
- cpuNum := float64(cpuQuota) / float64(cpuPeriod)
然后再把通過p.Percent獲取到的進程占用機器所有CPU時間的比例除以計算出的核心數(shù)即可算出Go進程在容器里對CPU的占比。
- cpuPercent, err := p.Percent(time.Second)
- // cp := cpuPercent / float64(runtime.NumCPU())
- // 調(diào)整為
- cp := cpuPercent / cpuNum
而容器的能使用的最大內(nèi)存數(shù),自然就是在memory.limit_in_bytes里指定的啦,所以Go進程在容器中占用的內(nèi)存比例需要通過下面這種方法獲取
- memLimit, err := readUint("/sys/fs/cgroup/memory/memory.limit_in_bytes")
- memInfo, err := p.MemoryInfo
- mp := memInfo.RSS * 100 / memLimit
上面進程內(nèi)存信息里的RSS叫常駐內(nèi)存,是在RAM里分配給進程,允許進程訪問的內(nèi)存量。而讀取容器資源用的readUint,是containerd組織在cgroups實現(xiàn)里給出的方法。
- func readUint(path string) (uint64, error) {
- v, err := ioutil.ReadFile(path)
- if err != nil {
- return 0, err
- }
- return parseUint(strings.TrimSpace(string(v)), 10, 64)
- }
- func parseUint(s string, base, bitSize int) (uint64, error) {
- v, err := strconv.ParseUint(s, base, bitSize)
- if err != nil {
- intValue, intErr := strconv.ParseInt(s, base, bitSize)
- // 1. Handle negative values greater than MinInt64 (and)
- // 2. Handle negative values lesser than MinInt64
- if intErr == nil && intValue < 0 {
- return 0, nil
- } else if intErr != nil &&
- intErr.(*strconv.NumError).Err == strconv.ErrRange &&
- intValue < 0 {
- return 0, nil
- }
- return 0, err
- }
- return v, nil
- }
我在下方參考鏈接里會給出他們源碼的鏈接。