学会这几招让 Go 程序自己监控自己
谈到让 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开发参考书》里了,有需要的可以公众号回复 gocookbook
领取阅读,如果想在容器环境下尝试的话需要自己动手起个 Docker
或者 Kubernetes
集群才能进行测试。
如果想入门K8s,安利下我的Kubernetes学习笔记,包治不会~!
你可能会问,为啥让 Go
程序自己监控自己,有什么用呢?那肯定是能以这个为基点做一些服务治理的事情啦,具体的应用场景以后再分享,感兴趣的可以关注一波。
参考链接
-
Contianerd utils: http://github.com/containerd/cgroups/blob/318312a373405e5e91134d8063d04d59768a1bff/utils.go#L243
-
What is RSS: http://stackoverflow.com/questions/7880784/what-is-rss-and-vsz-in-linux-memory-management
- END -
扫码关注公众号「网管叨bi叨」
给网管个星标,第一时间吸我的知识 :point_up_2:
网管为大家整理了一本超实用的《Go 开发参考书》收集了70多条开发实践。去公众号回复【gocookbook】即刻领取!
觉得有用就点个在看 :point_down::point_down::point_down:
- Go社区大佬,谈怎么避免技术内卷化
- Go 切片面试真题八连问
- Go 的切片支持并发吗?
- Go单元测试-- 全能打桩工具Monkey的使用介绍
- 世界读书日,推荐几本我读过的好书
- Go 单元测试--Mock接口实现和对接口打桩
- Go单测测试 — 数据库 CRUD 的 Mock 测试
- 春天里,一场关于技术人成长的直播
- Go单元测试--模拟服务请求和接口返回
- Go1.18 新特性TryLock一出,面试题库里少了一道题,好慌。
- 绝对零门槛,IDEA两步搭建好Java开发环境
- Ballast,一种精准控制 Go GC 提高性能的方法
- かいわ面试官 の 两个事务并发写,能保证数据唯一吗?
- 想在研发群里装?先学会这几个排查K8s问题的办法
- Go单元测试从入门到放弃—0.单元测试基础
- 想不到吧,这些都是 Go 语言的语法糖
- 整洁架构之道--三种经典的编程范式
- 读书分享|高效能人士要培养哪些习惯
- 这道 Go 题目外网超过 80% 的人都答错了,你来试试...
- 图解算法基础--快速排序,附 Go 代码实现