gocookbook icon indicating copy to clipboard operation
gocookbook copied to clipboard

用Go怎么监控Go程序的资源使用情况

Open kevinyan815 opened this issue 3 years ago • 0 comments

在谈监控Go程序的资源使用情况前,我们先来谈一谈有哪些指标是需要监控的,一般谈论进程的指标最常见的就是进程的没存占用率、CPU占用率、创建的线程数。因为Go语言又在线程之上自己维护了Goroutine,所以针对Go进程的资源指标还需要加一个创建的Goroutine数量。

又因为现在服务很多都部署在Kubernetes集群上,一个Go进程往往就是一个Pod,但是容器的资源是跟宿主机共享的,只是在创建的时候指定了其资源的使用上限,所以在获取CPUMemory这些信息的时候还需要具体情况分开讨论。

Go进程怎么获取自己的资源占比?

获取Go程序的资源使用情况使用gopstuil库即可完成。

package main
import (
	"fmt"
	"github.com/shirou/gopsutil/process"
	"os"
	"runtime"
	"runtime/pprof"
	"time"
)
func main() {
	// NewProcess 会返回一个持有PID的Process对象,方法会检查PID是否存在,如果不存在会返回错误
	// 通过Process对象上定义的其他方法我们可以获取关于进程的各种信息。
	p, err := process.NewProcess(int32(os.Getpid()))
	if err != nil {
		panic(err)
	}

	// 返回指定时间内进程占用CPU时间的比例
	cpuPercent, err := p.Percent(time.Second)
	if err != nil {
		panic(err)
	}
	// 上面返回的是占所有CPU核心时间的比例,如果想更直观的看占比,可以算一下占单个核心的比例
	cp := cpuPercent / float64(runtime.NumCPU())

	// 获取进程占用内存的比例
	mp, _ := p.MemoryPercent()

	// 创建的线程数
	threadCount := pprof.Lookup("threadcreate").Count()

	// Goroutine数

	gNum := runtime.NumGoroutine()

	time.Sleep(time.Second * 2)

	fmt.Println(cpuPercent, cp, mp, threadCount, gNum)

}

上面获取进程资源占比的方法只有在虚拟机和物理机环境下才能准确。因为类似Docker这样的Linux容器是靠着LinuxNamespaceCgroups技术实现的进程隔离和资源限制,加上现在基本上都是K8s集群部署,所以如果是在Docker中获取Go进程的资源使用情况需要根据Cgroups分配给容器的资源上限进行计算才准确。

在容器里怎么计算资源占比

Linux中,Cgroups给用户暴露出来的操作接口是文件系统,即它以文件和目录的方式组织在操作系统的/sys/fs/cgroup路径下,在 Ubuntu 系统里,可以用mount -t cgroup指令把它们展示出来。

/sys/fs/cgroup下面有很多诸如cpusetcpumemory这样的子目录,每个子目录都代表系统当前可以被Cgroups进行限制的资源种类。

针对我们监控Go进程内存和CPU指标的需求,我们只要知道cpu.cfs_period_uscpu.cfs_quota_usmemory.limit_in_bytes这三项就行,其他的有兴趣自己去研究吧。

cpu.cfs_period_uscpu.cfs_quota_us这两个参数需要组合使用,可以用来限制进程在长度为cfs_period的一段时间内,只能被分配到总量为cfs_quotaCPU时间, 可以简单的理解为容器能使用的核心数 = cfs_quota / cfs_period

所以在容器里获取Go进程CPU的占比的方法,需要做一些调整,要能计算出分配给了容器的最大CPU时间占比。

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)

而容器的能使用的最大内存数,自然就是在memory.limit_in_bytes里指定的啦,所以Go进程在容器中占用的内存比例需要通过下面这种方法获取

memLimit, err := readUint("/sys/fs/cgroup/memory/memory.limit_in_bytes")
memInfo, err := p.MemoryInfo
mp := memInfo.RSS * 100 / memLimit

上面进程内存信息里的RSS叫常驻内存,是在RAM里分配给进程,允许进程访问的内存量。而读取容器资源用的readUint,是containerd组织在cgroups实现里给出的方法,拷贝过来直接用就行啦,拷贝链接再下方参考链接里。

上面分析解释各种指标用的代码的比较晦涩,可以看自动采样监控工具holmes的实现,下面参考链接里有我读源码时记的一些关键代码。

名词解释

cpu.cfs_period_us & cpu.cfs_quota_us

cfs_period_us用来配置时间周期长度,cfs_quota_us用来配置当前cgroup在设置的周期长度内所能使用的CPU时间数,两个文件配合起来设置CPU的使用上限。两个文件的单位都是微秒(us),cfs_period_us的取值范围为1毫秒(ms)到1秒(s),cfs_quota_us的取值大于1ms即可,如果cfs_quota_us的值为-1(默认值),表示不受cpu时间的限制

cpu.cfs_quota_us = 250000, cfs_quota_us = 250000 ,每250ms能使用250ms的CPU时间, 即分配给的上限是1核心

memory.limit_in_bytes memory.limit_in_bytes 设置的是内存上限额度

RSS: (Resident Set Size) 进程的内存常驻集大小,用于表示分配在RAM中给该进程的内存量,它包括所有分配给进程的栈内存和堆内存。不会包括已经Swap out的内存 VSZ: (Virtual Memory Size) 进程的虚拟内存大小。它包括进程可以访问的所有内存,包括换出的内存、已分配但未使用的内存以及来自共享库的内存

参考链接:

  • https://segmentfault.com/a/1190000008323952
  • https://stackoverflow.com/questions/7880784/what-is-rss-and-vsz-in-linux-memory-management
  • https://github.com/containerd/cgroups/blob/318312a373405e5e91134d8063d04d59768a1bff/utils.go#L243
  • https://github.com/kevinyan815/gocookbook/blob/master/codes/holmes_notes/get_process_info.go

kevinyan815 avatar Nov 08 '21 02:11 kevinyan815