Node Exporter内存指标代码解读 | Prometheus内存监控原理
关于Node Exporter的内存部分代码解读
性能分析中,我们通常有3个指标要特别关注:CPU、内存、网络。
一般来说,CPU和网络是比较容易理解的,但是内存是不好理解的,主要是内存的种类比较多,采集的时候分为free和available两种方式。
日常分析使用的是available的方式,主要是因为available更能反映当前系统的内存使用情况。
但是available的计算方式比较复杂,涉及到很多内核的知识。这里以prometheus的 node_exporter的内存采集代码为例,来分析一下内存的计算方式。
首先建立debug版本的程序
有了debug版本的程序,可以方便调试。
可以参考:
node_exporter的指标注册
当 node_exporter 启动的时候,会注册各种指标的采集器。 collector/meminfo.go
会init
// 这里是把一个函数注册到map中,key是指标名
package collector
func init() {
registerCollector("meminfo", defaultEnabled, NewMeminfoCollector)
}
// 注册到map中的采集器,会有人调用Update 获取最新数据
func (c *meminfoCollector) Update(ch chan<- prometheus.Metric) error {
var metricType prometheus.ValueType
memInfo, err := c.getMemInfo()
if err != nil {
return fmt.Errorf("couldn't get meminfo: %w", err)
}
c.logger.Debug("Set node_mem", "memInfo", fmt.Sprintf("%v", memInfo))
for k, v := range memInfo {
if strings.HasSuffix(k, "_total") {
metricType = prometheus.CounterValue
} else {
metricType = prometheus.GaugeValue
}
ch <- prometheus.MustNewConstMetric(
prometheus.NewDesc(
prometheus.BuildFQName(namespace, memInfoSubsystem, k),
fmt.Sprintf("Memory information field %s.", k),
nil, nil,
),
metricType, v,
)
}
return nil
}
NewMeminfoCollector 会有多个版本的注册,golang有自己的特性,编译linux的时候,会启用meminfo_linux.go的版本
collector/meminfo_linux.go
会声明函数
package collector
// 注册内存采集器
func NewMeminfoCollector(logger *slog.Logger) (Collector, error) {
fs, err := procfs.NewFS(*procPath)
if err != nil {
return nil, fmt.Errorf("failed to open procfs: %w", err)
}
return &meminfoCollector{
logger: logger,
fs: fs,
}, nil
}
// 采集内存的当前数据,无缓存
func (c *meminfoCollector) getMemInfo() (map[string]float64, error) {
meminfo, err := c.fs.Meminfo()
if err != nil {
return nil, fmt.Errorf("failed to get memory info: %w", err)
}
metrics := make(map[string]float64)
if meminfo.MemAvailableBytes != nil {
metrics["MemAvailable_bytes"] = float64(*meminfo.MemAvailableBytes)
}
if meminfo.MemFreeBytes != nil {
metrics["MemFree_bytes"] = float64(*meminfo.MemFreeBytes)
}
if meminfo.MemTotalBytes != nil {
metrics["MemTotal_bytes"] = float64(*meminfo.MemTotalBytes)
}
}
最终 c.fs.Meminfo()
的操作,实际会调用 procfs
库的 Meminfo()
函数
package procfs
func (fs FS) Meminfo() (Meminfo, error) {
b, err := util.ReadFileNoStat(fs.proc.Path("meminfo"))
if err != nil {
return Meminfo{}, err
}
m, err := parseMemInfo(bytes.NewReader(b))
if err != nil {
return Meminfo{}, fmt.Errorf("%w: %w", ErrFileParse, err)
}
return *m, nil
}
如何解析 /proc/meminfo 这里不再赘述,就是一个k:v的基本解析
那么 /proc/meminfo 中的内存指标是如何计算的呢?
具体代码在Linux的kernel的 mm/show_mem.c
文件中。
long si_mem_available(void)
{
long available;
unsigned long pagecache;
unsigned long wmark_low = 0;
unsigned long reclaimable;
struct zone *zone;
for_each_zone(zone)
wmark_low += low_wmark_pages(zone);
/*
* Estimate the amount of memory available for userspace allocations,
* without causing swapping or OOM.
*/
available = global_zone_page_state(NR_FREE_PAGES) - totalreserve_pages;
/*
* Not all the page cache can be freed, otherwise the system will
* start swapping or thrashing. Assume at least half of the page
* cache, or the low watermark worth of cache, needs to stay.
*/
pagecache = global_node_page_state(NR_ACTIVE_FILE) +
global_node_page_state(NR_INACTIVE_FILE);
pagecache -= min(pagecache / 2, wmark_low);
available += pagecache;
/*
* Part of the reclaimable slab and other kernel memory consists of
* items that are in use, and cannot be freed. Cap this estimate at the
* low watermark.
*/
reclaimable = global_node_page_state_pages(NR_SLAB_RECLAIMABLE_B) +
global_node_page_state(NR_KERNEL_MISC_RECLAIMABLE);
reclaimable -= min(reclaimable / 2, wmark_low);
available += reclaimable;
if (available < 0)
available = 0;
return available;
}
这段函数可以在网上找到很多分析文章,而且AI回答的很详细,这里不再赘述。简单说,就是:
这里不单考虑了FREE的内存,也不是简单粗暴地把所有的FREE和BUFFER的内存都计算到Available里。
考虑了一定的水位线去考虑FREE和BUFFER里可以按照一定的比例计算到Available里。