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

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

怎么用Go获取进程的各项指标

我们先来讨论普通的宿主机和虚拟机情况下怎么获取这些指标,容器环境放在下一节来说。

获取​​Go​​​进程的资源使用情况使用​​gopstuil​​​库即可完成,它我们屏蔽了各个系统之间的差异,帮助我们方便地获取各种系统和硬件信息。​​gopsutil​​将不同的功能划分到不同的子包中,它提供的模块主要有:


  • ​cpu​​:系统CPU 相关模块;
  • ​disk​​:系统磁盘相关模块;
  • ​docker​​:docker 相关模块;
  • ​mem​​:内存相关模块;
  • ​net​​:网络相关;
  • ​process​​:进程相关模块;
  • ​winservices​​:Windows 服务相关模块。

我们这里只用到了它的​​process​​子包,获取进程相关的信息。

声明:process 模块需要 import "github.com/shirou/gopsutil/process"后引入到项目,后面演示的代码会用到的os等模块会统一省略import相关的信息和错误处理,在此提前说明。

创建进程对象

​process​​​模块的​​NewProcess​​​会返回一个持有指定​​PID​​​的​​Process​​​对象,方法会检查​​PID​​​是否存在,如果不存在会返回错误,通过​​Process​​对象上定义的其他方法我们可以获取关于进程的各种信息。

p, _ := process.NewProcess(int32(os.Getpid()))

进程的CPU使用率

进程的CPU使用率需要通过计算指定时间内的进程的CPU使用时间变化计算出来

cpuPercent, err := p.Percent(time.Second)

上面返回的是占所有CPU时间的比例,如果想更直观的看占比,可以算一下占单个核心的比例。

cp := cpuPercent / float64(runtime.NumCPU())

内存使用率、线程数和goroutine数

这三个指标的获取过于简单咱们就放在一块说

// 获取进程占用内存的比例
mp, _ := p.MemoryPercent()
// 创建的线程数
threadCount := pprof.Lookup("threadcreate").Count()
// Goroutine数 
gNum := runtime.NumGoroutine()

上面获取进程资源占比的方法只有在虚拟机和物理机环境下才能准确。类似​​Docker​​​这样的​​Linux​​容器是靠着Linux的Namespace和Cgroups技术实现的进程隔离和资源限制,是不行的。

现在的服务很多公司是​​K8s​​​集群部署,所以如果是在​​Docker​​​中获取​​Go​​​进程的资源使用情况需要根据​​Cgroups​​分配给容器的资源上限进行计算才准确。

容器环境下获取进程指标

在​​Linux​​​中,​​Cgroups​​​给用户暴露出来的操作接口是文件系统,它以文件和目录的方式组织在操作系统的​​/sys/fs/cgroup​​​路径下,在 ​​/sys/fs/cgroup​​​下面有很多诸​​cpuset​​​、​​cpu​​​、 ​​memory​​这样的子目录,每个子目录都代表系统当前可以被​​Cgroups​​进行限制的资源种类

针对我们监控​​Go​​​进程内存和​​CPU​​​指标的需求,我们只要知道​​cpu.cfs_period_us​​​、​​cpu.cfs_quota_us​​​ 和​​memory.limit_in_bytes​​​ 就行。前两个参数需要组合使用,可以用来限制进程在长度为​​cfs_period​​​的一段时间内,只能被分配到总量为​​cfs_quota​​​的​​CPU​​时间, 可以简单的理解为容器能使用的核心数 = cfs_quota / cfs_period

所以在容器里获取​​Go​​​进程​​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)

然后再把通过​​p.Percent​​​获取到的进程占用机器所有​​CPU​​​时间的比例除以计算出的核心数即可算出​​Go​​​进程在容器里对​​CPU​​的占比。

cpuPercent, err := p.Percent(time.Second)
// cp := cpuPercent / float64(runtime.NumCPU())
// 调整为
cp := cpuPercent / cpuNum

而容器的能使用的最大内存数,自然就是在​​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​​实现里给出的方法。

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
}

总结

关于本文的完整源码和一些进程监控相关的有价值资料已经收入到《Go开发参考书》里了,有需要的可以“网管叨bi叨”公众号回复gocookbook 领取阅读,如果想在容器环境下尝试的话需要自己动手起个​​Docker​​​或者​​Kubernetes​​集群才能进行测试。

如果想入门K8s,安利下 ​​Kubernetes学习笔记​​,包治不会~!

你可能会问,为啥让​​Go​​程序自己监控自己,有什么用呢?那肯定是能以这个为基点做一些服务治理的事情啦,具体的应用场景以后再分享。

参考链接


  • Contianerd utils: https://github.com/containerd/cgroups/blob/318312a373405e5e91134d8063d04d59768a1bff/utils.go#L243
  • What is RSS: https://stackoverflow.com/questions/7880784/what-is-rss-and-vsz-in-linux-memory-management



学会这几招让 Go 程序自己监控自己_docker