在 Moby 中用好 prometheus/procfs:Go 语言从 /proc 与 /sys 采集 Linux 系统与进程指标
2026/9/8 21:33:13 网站建设 项目流程

在 Moby 中用好 prometheus/procfs:Go 语言从 /proc 与 /sys 采集 Linux 系统与进程指标

【免费下载链接】mobyThe Moby Project - a collaborative project for the container ecosystem to assemble container-based systems项目地址: https://gitcode.com/GitHub_Trending/mo/moby

导读

procfs README 所描述的是 Prometheus 生态中一个以纯 Go 编写的核心基础库github.com/prometheus/procfs。它以只读的伪文件系统/proc/sys为数据源,把内核导出的文本文件解析成强类型的 Go 结构体,为系统、内核与进程级指标采集提供了统一入口。在本仓库(Moby / moby)中它作为间接依赖以v0.21.1版本内嵌于 vendor/github.com/prometheus/procfs,服务于容器生态中的监控与指标链路。读完本文,你将掌握该库的组织方式、FS核心抽象、基于FS的典型用法、构建与测试流程,以及如何在自己的 Linux 监控程序中安全使用它。


一、库定位:为什么容器生态离不开 /proc 与 /sys 的解析

在 Linux 上,内核并不通过常规 API 暴露大部分运行态信息,而是把数据「导出」为两个只读伪文件系统:

  • /proc:以进程为中心的系统与进程信息,例如/proc/stat(CPU 汇总统计)、/proc/meminfo(内存)、/proc/[pid]/stat(单进程状态);
  • /sys:设备、内核子系统等结构化信息,例如块设备、网络与硬件相关的内核对象。

prometheus/procfs的价值在于把这些以「空格分隔文本」形式存在的内核文件封装成类型安全的 Go API。其包注释(doc.go)这样概括它的职责:

"Package procfs provides functions to retrieve system, kernel and process metrics from the pseudo-filesystem proc."

需要特别留意的是该库自带的一条重要声明(原文照录自 README 开头):

WARNING: This package is a work in progress. Its API may still break in backwards-incompatible ways without warnings. Use it at your own risk.

即:该库属于「进行中」状态,API 可能在无通知的情况下发生不向后兼容的变更。这一点决定了工程接入时必须把版本锁定并随之升级,正如本仓库在 go.mod 中固定github.com/prometheus/procfs v0.21.1 // indirect的方式。

在 Moby 仓库中的角色

在 go.mod 中可以看到,moby 将github.com/prometheus/procfs v0.21.1标记为indirect(间接依赖),同时引入了github.com/prometheus/client_golang v1.24.1github.com/prometheus/common v0.70.1等 Prometheus 家族依赖。也就是说,moby 自身并不直接 import 该库,而是由上游指标采集与 Prometheus 客户端链路传递引入,这正是go mod vendor之后把完整库目录裁剪进 vendor/github.com/prometheus/procfs 的原因。vendor 目录中保留了库的源码、internal/内部包以及构建工具ttar,但按依赖裁剪规则省略了测试夹具(fixtures)等开发期文件。


二、核心抽象FS:把挂载点变成类型安全的句柄

整个库的设计围绕一个贯穿始终的抽象展开:FS类型代表某个伪文件系统(通常是/proc/sys)的路径。数据从哪来、如何拼接路径、如何校验挂载点,都收敛在这一层。

2.1 根包的FS结构

以库根包的实现 fs.go 为例:

// FS represents the pseudo-filesystem sys, which provides an interface to // kernel data structures. type FS struct { proc fs.FS isReal bool }

源码还导出了两个与 Linux 语义强相关的常量:

  • DefaultMountPoint:proc 文件系统的默认挂载点,值为/proc
  • SectorSize = 512:单扇区字节数,专用于 Linux 块 I/O 计算,供读取磁盘统计的模块换算使用。

构造函数有两个:

// NewDefaultFS 使用默认挂载点 /proc;若该目录不可读或不是目录则返回错误。 func NewDefaultFS() (FS, error) { return NewFS(DefaultMountPoint) } // NewFS 使用给定的 proc 挂载点;同样会校验路径合法性。 func NewFS(mountPoint string) (FS, error) { ... }

NewFS内部首先调用内层 internal/fs/fs.go 的fs.NewFS,后者通过os.Stat校验传入路径必须是一个可读的目录,否则分别返回 "could not read %q" 或 "mount point %q is not a directory" 的错误;随后还会通过isRealProc检查该挂载点是否指向真实的 proc 文件系统。

内层internal/fs包统一定义了所有伪文件系统的默认挂载点,可直接查阅 internal/fs/fs.go:

  • DefaultProcMountPoint = "/proc"
  • DefaultSysMountPoint = "/sys"
  • DefaultConfigfsMountPoint = "/sys/kernel/config"
  • DefaultSelinuxMountPoint = "/sys/fs/selinux"

此外FS.Path(p ...string)通过filepath.Join把相对路径安全地拼接为形如/proc/stat的完整读取路径,统一了目录拼接逻辑。

2.2 典型用法:读取 CPU 统计

README 给出的是最小可运行范式:先初始化挂载点,再读取对应统计。以读取 CPU 汇总统计(/proc/stat)为例:

fs, err := procfs.NewFS("/proc") stats, err := fs.Stat()

fs.Stat()返回的Stat结构体(定义见 stat.go)聚合了内核级统计,主要包括:

字段来源含义
BootTime/proc/statbtime自 Unix 纪元以来的启动时间(秒)
CPUTotalcpu汇总行全核累计 CPU 时间
CPU map[int64]CPUStatcpu0cpu1… 行按 CPU 编号的独立统计
IRQTotal/IRQ []uint64intr中断总次数与按编号中断次数
ContextSwitchesctxt上下文切换次数
ProcessCreatedprocesses已创建进程总数
ProcessesRunning/ProcessesBlockedprocs_running/procs_blocked当前运行 / 阻塞(等待 I/O)的进程数
SoftIRQsoftirq软中断分类型统计

其中CPUStat定义了User / Nice / System / Idle / Iowait / IRQ / SoftIRQ / Steal / Guest / GuestNice共 10 个维度,类型均为float64,单位为(内核原始单位是 USER_HZ 时钟滴答,读取后除以常量换算而来,详见下文第六节)。CPU 读取的解析实现位于 stat.go 中的parseCPUStat:若行首 token 为cpu,表示聚合行(CPU id 记为 -1);否则取cpu前缀后的数字作为 CPU 编号。

2.3 双文件系统场景:同时需要 /proc 与 /sys 的子包

并非所有指标都能只从/proc拿到。README 特别举例:部分子包(例如blockdevice)需要同时访问 proc 与 sys 两个伪文件系统,于是对应构造函数显式接收两个挂载点:

fs, err := blockdevice.NewFS("/proc", "/sys") stats, err := fs.ProcDiskstats()

这种「一个FS管一个挂载点、双挂载点则用两个参数」的设计,让磁盘等横跨两类内核接口的指标也能以统一方式获取。需要说明的是,moby 本次 vendor 的快照按依赖裁剪只保留了库根包与internal/包(从 vendor/github.com/prometheus/procfs 的目录结构可见),blockdevice等上游子包存在于完整版上游工程中,这里作为官方用法示例引用。


三、包组织结构:按「数据来源 + 信息类型」两个维度划分

README 明确指出库的组织遵循两条原则:

  1. 数据来源:信息来自/proc/sys,还是两者都要;
  2. 信息类型:是进程级信息、系统级统计,还是块设备等子系统信息。

3.1 根包procfs:进程 + 系统两级指标

「大多数进程信息都可以由根procfs包中的函数获得」(README 原文)。从 vendor/github.com/prometheus/procfs 的源码文件清单可以直观印证这一组织原则:

  • 进程级(process):以proc_*.go命名的文件覆盖单个进程的各方面——proc.go(进程句柄与目录枚举)、proc_stat.go/proc/[pid]/stat)、proc_status.goproc_statm.goproc_maps.goproc_smaps.goproc_limits.goproc_environ.goproc_io.goproc_cgroup.goproc_fdinfo.goproc_ns.goproc_psi.goproc_interrupts.goproc_snmp.goproc_snmp6.goproc_netstat.goproc_sys.go等;
  • 系统级(kernel/system)stat.go(CPU、中断、上下文切换)、meminfo.go(内存)、loadavg.go(负载)、cpuinfo.go(CPU 详情,并按架构拆分出cpuinfo_x86.gocpuinfo_armx.gocpuinfo_riscvx.gocpuinfo_s390x.gocpuinfo_ppcx.gocpuinfo_mipsx.gocpuinfo_loong64.go)等。

doc.go中给出了一个可直接照搬的进程级示例,演示如何拿到「当前进程」并读取其状态:

func main() { p, err := procfs.Self() if err != nil { log.Fatalf("could not get process: %s", err) } stat, err := p.Stat() if err != nil { log.Fatalf("could not get process stat: %s", err) } fmt.Printf("command: %s\n", stat.Comm) fmt.Printf("cpu time: %fs\n", stat.CPUTime()) fmt.Printf("vsize: %dB\n", stat.VirtualMemory()) fmt.Printf("rss: %dB\n", stat.ResidentMemory()) }

procfs.Self()即解析/proc/self(当前进程自身)的快捷方式,返回的ProcStat结构体从 proc_stat.go 可见,依次携带PIDComm(可执行文件名)、State(进程状态)、父进程 PID 等字段,并派生CPUTime()VirtualMemory()ResidentMemory()等便捷方法。

3.2 网络与子系统专项文件

根包中还有大量针对具体内核子系统的独立.go文件,几乎是一份「/proc 与 /sys 指标速查表」:

文件数据来源内容
net_dev.go/proc/net/dev网卡收发流量
net_tcp.go/net_udp.go/net_unix.go/proc/net/tcp各协议套接字表
net_sockstat.go/netstat.go/net_conntrackstat.go/proc/net/*socket、连接跟踪等
mdstat.go/proc/mdstat软件 RAID 状态
swaps.go/proc/swapsswap 设备
buddyinfo.gozoneinfo.goslab.go/proc/buddyinfo内存分配器细粒度信息
fscache.goipvs.gonfnetlink_queue.goarp.go对应/proc条目文件缓存、IPVS、netfilter、ARP 等

每个文件都遵循同一个「FS句柄 → 一次文件读取 → 类型化结构体」的模式,学习成本极低。


四、构建与测试:无二进制产物、以 ttar 夹具驱动单元测试

README 强调一个工程事实:procfs 是库而非可执行程序,没有可分发的二进制产物,它总是作为其他应用的一部分被编译。其质量保障方式,是「绝大部分 API 都带有单元测试,通过make test运行」。

4.1 测试夹具(fixtures)与 ttar

测试需要一个关键前提:CI 与开发机上不一定存在完整的/proc/sys内容。因此项目维护了一套来自真实内核文件系统的样例文件(fixtures),并在测试运行时把它们当作/proc/sys的快照喂给解析器。

这些夹具以 ttar 中的规则如实呈现了这一机制:

%/.unpacked: %.ttar @echo ">> extracting fixtures $*" ./ttar -C $(dir $*) -x -f $*.ttar touch $@ fixtures: testdata/fixtures/.unpacked update_fixtures: rm -vf testdata/fixtures/.unpacked ./ttar -c -f testdata/fixtures.ttar -C testdata/ fixtures/ .PHONY: test test: testdata/fixtures/.unpacked common-test

要点拆解:

  • make test先把testdata/fixtures/.unpacked作为前置依赖触发解包,再执行Makefile.common中的common-test(Go 测试套件);
  • 解包动作依赖仓库自带的ttar可执行文件(位于 vendor/github.com/prometheus/procfs/ttar),-x表示解包、-c表示打包;
  • 每次解包成功后通过touch生成.unpacked标记文件,避免重复解包。

4.2 更新测试夹具的标准流程

当新功能需要新的样例内核文件时,README 给出五步操作流:

  1. 确保testdata/fixtures目录是最新状态——先删除旧目录再重新解包:
rm -rf testdata/fixtures make test
  1. 在解包出的testdata/fixtures中修改/新增样例文件,例如为某个新解析函数补一条真实的/proc/xxx内容;
  2. 运行make update_fixtures,由ttar -c根据目录内容重新生成fixtures.ttar
  3. git diff testdata/fixtures.ttar复核改动是否符合预期,再随代码一起提交。

4.3 在 vendor 目录下的现实约束

需要指出的是:当前 moby 仓库的 vendor 裁剪版(vendor/github.com/prometheus/procfs)属于「只供编译使用的依赖快照」,并没有携带testdata/fixturesfixtures.ttar这些开发期资源。因此上述测试流程针对的是 procfs 自身作为独立仓库开发时的场景;在 moby 中以 vendor 方式使用时,验证重点转为「能通过编译 + 在目标 Linux 环境运行集成测试」,而不是在 vendor 目录内跑make test


五、源码级纵深:解析精度与 USER_HZ 之谜

把 README 的「使用说明」落到源码上,最能体现这个库工程取舍的地方是时间单位的换算

Linux 内核在/proc/statcpu行中导出的是以 USER_HZ 为单位的时钟滴答(jiffies)计数。procfs 为了让调用方拿到「秒」单位的浮点数,在 stat.go 的parseCPUStat中用fmt.Sscanf按固定格式一次性解析 10 个字段:

count, err := fmt.Sscanf(line, "%s %f %f %f %f %f %f %f %f %f %f", &cpu, &cpuStat.User, &cpuStat.Nice, &cpuStat.System, &cpuStat.Idle, &cpuStat.Iowait, &cpuStat.IRQ, &cpuStat.SoftIRQ, &cpuStat.Steal, &cpuStat.Guest, &cpuStat.GuestNice)

随后统一除以常量userHZ

cpuStat.User /= userHZ ...

关键问题随之而来:USER_HZ到底是多少?proc_stat.go 顶部的大段注释完整记录了这段工程史:

Originally, this USER_HZ value was dynamically retrieved via a sysconf call which required cgo... After much research it was determined that USER_HZ is actually hardcoded to 100 on all Go-supported platforms as of the time of this writing.

也就是说,最初该常量希望通过sysconf动态获取,但这会引入cgo依赖,严重阻碍交叉编译。经过调研,作者最终确认在 Go 官方支持的所有平台上USER_HZ都被硬编码为 100,于是在无 cgo 的前提下直接写死:

const userHZ = 100

注释同时坦承了最坏后果——如果真的存在异类平台,最多导致两个指标数值失真。这是一个典型的「用精确常量换取零 cgo、支持交叉编译」的设计决策,也是把 procfs 广泛嵌入容器监控链路(跨架构交叉构建是刚需)的技术前提。

解析层之下,internal/util子包(internal/util)承担通用的文本解析:parse.go提供ParseUint32sParseUint64sParsePInt64s(返回[]*int64,用于内核可空缺字段)等辅助函数,valueparser.go提供键值对解析,readfile.go/sysreadfile.go封装文件读取与错误包装。全部解析器都不依赖 cgo,这保证了库的可移植编译性。


六、接入指引:在 Moby 及自研 Linux 程序中引入

6.1 在 Go 工程中声明依赖

在 moby 仓库里,该库作为 Prometheus 监控链路的间接依赖出现(go.mod):

github.com/prometheus/procfs v0.21.1 // indirect

对普通 Go 模块工程,如需要直接使用,则显式引入并通过go mod vendor拉取到本地 vendor 目录:

go get github.com/prometheus/procfs@v0.21.1

引入路径与 moby 内嵌的 vendor 快照保持一致的版本即可复用 vendor/github.com/prometheus/procfs 内的同一份代码。

6.2 一个覆盖系统 + 进程的可运行示例

综合前文,一个典型的采集程序会同时覆盖系统级与进程级指标:

package main import ( "fmt" "log" "github.com/prometheus/procfs" ) func main() { // 系统级:/proc/stat 的 CPU 与系统统计 fs, err := procfs.NewFS("/proc") if err != nil { log.Fatalf("failed to open /proc: %s", err) } stats, err := fs.Stat() if err != nil { log.Fatalf("failed to read /proc/stat: %s", err) } fmt.Printf("boot time: %d\n", stats.BootTime) fmt.Printf("ctx switches: %d\n", stats.ContextSwitches) fmt.Printf("cpu total: user=%.2fs system=%.2fs idle=%.2fs\n", stats.CPUTotal.User, stats.CPUTotal.System, stats.CPUTotal.Idle) // 进程级:当前进程的 stat p, err := procfs.Self() if err != nil { log.Fatalf("failed to open self: %s", err) } pstat, err := p.Stat() if err != nil { log.Fatalf("failed to read process stat: %s", err) } fmt.Printf("process: %s (pid=%d)\n", pstat.Comm, pstat.PID) fmt.Printf("cpu time: %.2fs\n", pstat.CPUTime()) }

6.3 使用前提与限制

  • 平台限制:该库本质是对 Linux 伪文件系统的封装,只能在挂载了/proc/sys的 Linux(含容器内)环境中返回真实数据;非 Linux 平台无法工作。
  • 挂载点可定制:借助NewFS(自定义路径)FS.Path,可读取 chroot、容器根文件系统或测试快照中的伪文件系统,这也是其测试夹具得以工作的基础。
  • API 稳定性:务必记住 README 开篇的警告——库处于演进期、API 可能破坏性变更,升级版本时应回归验证所有字段与签名。

七、小结

prometheus/procfs以「一个FS句柄 = 一个伪文件系统挂载点」的简洁抽象,把繁琐的/proc/sys文本解析收敛为强类型结构体与方法调用,并按「数据来源 + 信息类型」组织成易于扩展的包结构。它牺牲了一定 API 稳定性,换来的是无 cgo、支持交叉编译与跨架构的纯 Go 实现,因此在 Moby 这类容器平台中被 Prometheus 监控链路以间接依赖方式内嵌。理解其 fs.go 的挂载点抽象、stat.go 的字段语义、USER_HZ=100的换算决策,以及 Makefile 的 ttar 夹具驱动测试流程,即可在自己的监控与系统工具中稳健地驾驭它。

本文以 moby 仓库 vendor 快照(vendor/github.com/prometheus/procfs/README.md)中的库文档为主干撰写;库本身的版本信息以 go.mod 为准(v0.21.1),读者可将文中代码直接用于自研项目的 Linux 主机监控场景。

【免费下载链接】mobyThe Moby Project - a collaborative project for the container ecosystem to assemble container-based systems项目地址: https://gitcode.com/GitHub_Trending/mo/moby

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询