libkperf

描述

libkperf是一个轻量级linux性能采集库,它能够让开发者以API的方式执行性能采集,包括pmu采样和符号解析。libkperf把采集数据内存化,使开发者能够在内存中直接处理采集数据,避免了读写perf.data带来的开销。

在什么时候可能会需要libkperf

  • 想要轻量化采集系统和应用的性能数据。
  • 当分析一个高负载的应用,却不想对应用性能有较大影响的时候。
  • 不想管理多个性能分析进程(perf),想获取易于解析的性能数据。
  • 想轻量化地获取cpu的热点。
  • 想分析cache的miss分布和时延。
  • 想要跟踪系统调用函数的耗时。
  • 想获取内存读写带宽。
  • 想获取网络带宽和时延。

支持的CPU架构

  • 鲲鹏

支持的OS

  • openEuler
  • OpenCloudOS
  • TencentOS
  • KylinOS
  • CentOS

Release Notes

v2.1:

  • 支持SPE Data datasrc采集
  • 支持metric-based profiling

v2.0:

  • 支持编译反馈优化,提供生成perf.data格式文件的API。
  • Counting模式支持基于BPF的低开销采集。
  • Counting模式支持kernel bypass采集。
  • 支持cgroup的性能计数和采样采集。

v1.4:

  • 支持采集DDRC带宽、PCIe带宽,以及部分L3C和SMMU事件。
  • 支持CPU频率采集。
  • 提供blocked sample采集模式。
  • 提供go的API。

v1.3:

  • 支持对系统调用函数的耗时采集。
  • 支持历史分支记录的采集(BRBE)。

v1.2:

  • 支持tracepoint的采集和跟踪。
  • 支持事件分组采集。

v1.1:

  • 提供python的API。

v1.0:

  • 支持采集pmu计数、采样和SPE采样功能。
  • 支持core和uncore事件的采集。
  • 支持符号解析功能。

文档

依赖

依赖项 版本要求
GCC / glibc 最低依赖版本 gcc-4.8.5glibc-2.17
GCC / glibc 最高支持版本 gcc-12.2.0glibc-2.36
Python 最低依赖版本 python-3.6

说明:

  • 如果编译报错提示没有numa.h文件,需要先安装对应的numactl-devel包。
  • 如果编译连接在Found PythonInterp报CMake错误,需要先安装所需的python3-devel包。

快速使用

下面的流程将从获取源码开始,完成基础编译,并运行一个最小的C++示例。Python与Go的示例请参考后续的其他示例章节。

libkperf的PMU采集通常遵循以下调用顺序:

API 作用
PmuOpen 输入pid、core id和event,打开PMU设备
PmuEnable 开始采集
PmuRead 读取采集数据
PmuDisable 停止采集
PmuDatafree 释放PmuData
PmuClose 关闭PMU设备

1. 获取源码

git clone --recurse-submodules https://atomgit.com/openeuler/libkperf.git
cd libkperf

2. 编译并安装基础 C/C++ 库

bash build.sh install_path=/path/to/install

如果不指定install_path,默认安装到当前目录下的output目录。后续示例中统一使用/path/to/install表示用户指定的安装目录,请根据实际环境替换为真实路径。

编译完成后,安装目录下会包含头文件和库文件:

/path/to/install/
├── include
└── lib

如果库没有安装到系统默认库路径,运行依赖 libkperf 的程序前需要设置:

export LD_LIBRARY_PATH=/path/to/install/lib:$LD_LIBRARY_PATH

3. 编译运行示例程序

新建 example.cpp:获取进程的PMU计数

#include <iostream>
#include "symbol.h"
#include "pmu.h"
#include "pcerrc.h"

int main() {
    int pidList[1];
    pidList[0] = getpid();
    char *evtList[1];
    evtList[0] = "cycles";
    // 初始化事件列表,指定需要计数的事件cycles。
    PmuAttr attr = {0};
    attr.evtList = evtList;
    attr.numEvt = 1;
    attr.pidList = pidList;
    attr.numPid = 1;
    // 调用PmuOpen,返回pd。pd表示该任务的id。
    int pd = PmuOpen(COUNTING, &attr);
    // 开始采集。
    PmuEnable(pd);
    // 采集1秒。
    sleep(1);
    // 停止采集。
    PmuDisable(pd);
    PmuData *data = NULL;
    // 读取PmuData,它是一个数组,长度是len。
    int len = PmuRead(pd, &data);
    for (int i = 0; i < len; ++i) {
        PmuData *d = &data[i];
        std::cout << "evt=" << d->evt << " count=" << d->count << std::endl;
    }
    // 释放PmuData。
    PmuDataFree(data);
    // 类似fd,当任务结束时调用PmuClose释放资源。
    PmuClose(pd);
}

编译并运行:

g++ -o example example.cpp -I /path/to/install/include -L /path/to/install/lib -lkperf -lsym
export LD_LIBRARY_PATH=/path/to/install/lib:$LD_LIBRARY_PATH
./example

编译选项

build.sh 支持通过 option=value 形式配置编译行为:

bash build.sh option=value

例如:

bash build.sh install_path=/path/to/install build_type=debug

功能选项

选项 默认值 说明
install_path 当前目录下的 output 指定安装路径,编译产物将安装到该路径下
build_type Release 指定编译类型。设置 debug可编译调试版本
test false 是否编译并运行libkperf C/C++测试用例
asan false 是否启用AddressSanitizer编译,启动内存问题检测
bpf false 是否编译counting模式下的BPF采集功能
elf_llvm false ELF解析默认使用elfin-parser。启动该选项后,将使用llvm-symbolizer
utrace false 是否编译 utrace 模式,构建capstone

示例:

bash build.sh iinstall_path=/home/test build_type=debug test=true
bash build.sh install_path=/path/to/install asan=true
bash build.sh install_path=/path/to/install utrace=true

Python 包

选项 默认值 说明
python false 是否编译 Python 包
python_exe 系统默认 Python 指定用于安装Python包的解释器
whl false 是否生成 Python .whl 安装包

编译并安装 Python 包

bash build.sh install_path=/path/to/install python=true

如果环境中有多个 Python 版本:

bash build.sh install_path=/path/to/install python=true python_exe=$(which python3)

生成 .whl 安装包:

bash build.sh install_path=/path/to/install python=true whl=true

卸载 Python 包

python3 -m pip uninstall -y libkperf

:如果Python模块运行时报错类似下面内容:

OSError: /usr/lib/python3.9/site-packages/_libkperf/libsym.so: cannot open shared object file: No such file or directory

可能是 setuptools 版本不适配导致安装失败,可尝试降级:

python3 -m pip uninstall setuptools
python3 -m pip install setuptools==58

Python用例代码运行

cd python/tests
pytest test_*.py -s -v

Go 包

选项 默认值 说明
go false 是否编译 Go 包

编译Go包

bash build.sh install_path=/path/to/install go=true

执行成功后,将 go/src/libkperf 整个目录拷贝到 $GOPATH/src/ 目录下,其中 $GOPATH 为用户项目目录。

Go用例代码运行

cd go/src/libkperf_test
export GO111MODULE=off
export LD_LIBRARY_PATH=../libkperf/lib:$LD_LIBRARY_PATH
go test -v # 全部运行
go test -v -test.run TestCount #指定运行的用例

Java

选项 默认值 说明
java_agent false 是否编译java/java_agent模块,针对Java进程,增加perf-pid.map的数据解析
java_trace false 是否编译java/java_trace模块,使用ASM字节码插桩方法对Java程序进行trace

编译 Java Agent

bash build.sh install_path=/path/to/install java_agent=true

编译完成后,会在安装目录的 lib 子目录生成 libkperfmap.so。该目录设置LD_LIBRARY_PATH即可。

编译 Java Trace

bash build.sh java_trace=true

使用该选项前,请确保 Java 环境变量已正确配置,并且系统中已安装 Gradle 或 Maven。

其他示例

Python:读取 PMU 计数

import time
from collections import defaultdict
import subprocess

import kperf

evtList = ["r11", "cycles"]
pmu_attr = kperf.PmuAttr(evtList=evtList)
pd = kperf.open(kperf.PmuTaskType.COUNTING, pmu_attr)
if pd == -1:
    print(kperf.errorno())
    print(kperf.error())
    raise SystemExit(1)

kperf.enable(pd)

for _ in range(3):
    time.sleep(1)
    data_iter = kperf.read(pd)
    evtMap = defaultdict(int)
    for data in data_iter.iter:
        evtMap[data.evt] += data.count

    for evt, count in evtMap.items():
        print(f"event: {evt} count: {count}")

kperf.disable(pd)
kperf.close(pd)

运行:

python example.py

Go:读取 PMU 计数

package main
import "libkperf/kperf"
import "fmt"
import "time"

func main() {
  attr := kperf.PmuAttr{EvtList:[]string{"cycles"}, SymbolMode:kperf.ELF}
	fd, err := kperf.PmuOpen(kperf.COUNT, attr)
	if err != nil {
		fmt.Printf("kperf pmuopen counting failed, expect err is nil, but is %v\n", err)
    return
	}
	kperf.PmuEnable(fd)
	time.Sleep(time.Second)
	kperf.PmuDisable(fd)

	dataVo, err := kperf.PmuRead(fd)
	if err != nil {
		fmt.Printf("kperf pmuread failed, expect err is nil, but is %v\n", err)
    return
	}

	for _, o := range dataVo.GoData {
    fmt.Printf("event: %v count: %v\n", o.Evt, o.Count)
	}
	kperf.PmuDataFree(dataVo)
	kperf.PmuClose(fd)
}

编译运行:

go build example.go
./example

静态模式编译:

go build -tags="static" example.go

或直接运行:

go run example.go

C++:进程采样并解析调用栈

#include <iostream>

#include "symbol.h"
#include "pmu.h"
#include "pcerrc.h"

int main() {
    int pid = getpid();
    int pidList[1];
    pidList[0] = pid;
    char *evtList[1];
    evtList[0] = "cycles";
    // 初始化事件列表,指定需要计数的事件cycles。
    PmuAttr attr = {0};
    attr.evtList = evtList;
    attr.numEvt = 1;
    attr.pidList = pidList;
    attr.numPid = 1;
    attr.symbolMode = RESOLVE_ELF_DWARF;
    attr.callStack = 1;
    attr.freq = 200;
    attr.useFreq = 1;
    // 调用PmuOpen,返回pd。pd表示该任务的id。
    int pd = PmuOpen(SAMPLING, &attr);
    // 开始采集。
    PmuEnable(pd);
    // 采集1秒。
    sleep(1);
    // 停止采集。
    PmuDisable(pd);
    PmuData *data = NULL;
    // 读取PmuData,它是一个数组,长度是len。
    int len = PmuRead(pd, &data);
    for (int i = 0; i < len; ++i) {
        // 获取数组的一个元素。
        PmuData *d = &data[i];
        // 获取调用栈对象,它是一个链表。
        Stack *stack = d->stack;
        while (stack) {
            // 获取符号对象。
            if (stack->symbol) {
                Symbol *data = stack->symbol;
                std::cout << std::hex << data->addr << " " << data->symbolName << "+0x" << data->offset << " "
                          << data->codeMapAddr << " (" << data->module << ")"
                          << " (" << std::dec << data->fileName << ":" << data->lineNum << ")" << std::endl;

            }
            stack = stack->next;
        }
    }
    // 释放PmuData。
    PmuDataFree(data);
    // 类似fd,当任务结束时调用PmuClose释放资源。
    PmuClose(pd);
}

编译运行:

g++ -o sample sample.cpp -I /path/to/install/include -L /path/to/install/lib -lkperf -lsym
export LD_LIBRARY_PATH=/path/to/install/lib:$LD_LIBRARY_PATH
./sample

FAQ

1、Q:如何正确使用launch app模式进行进程采集

  • PmuOpen之后,再通过信号,唤醒子进程调用application
  • 无需调用PmuEnable
  • 推荐使用单fd打开,可显著降低多线程场景下的采集开销
  • 参考文档:详细使用参考

2、Q:为何多线程应用采集时会出现数据丢失

  • 原因:PmuOpen/PmuEnable/PmuDisable等操作在采集多线程应用场景时,其中每个线程加载和使能存在先后
  • 当前解决方案:
    • 使用launch模式(单fd打开,已验证可提高PmuOpen效率)详细使用参考
    • 使用--per-thread模式 (fd数量=事件数 X 线程数),减少核级数开销 参考

3、Q:如何提升符号解析速度?尤其在压测场景下?

  • 问题:原DWARF解析耗时>1s,影响性能
  • 已优化方案:
    • 集成llvm-symbolizer,目前行号解析效率提升30X
    • 支持配置模式,symbolMode使用RESOLVE_ELF模式,将不再去解析获取源文件和行号

4、Q:如何支持Cgroup采集?存在哪些限制?

  • 限制:
    • uncore与core事件需分两次PmuOpen
    • PA事件不支持在采集进程模式下与core事件共用PmuOpen
  • 建议:采集时注意事件分离,避免错误合并

5、Q:SPE采集时为何PmuRead耗时长?如何优化

  • 原因:SPE模式下,PmuRead会自动调用PmuDisable,读取完数据会自动调用PmuEnable
  • 建议:
    • 单线程顺序执行PmuOpen/PmuCollect/PmuClose.

6、Q:如何采集HITM事件(Flase Sharing)并保证数据稳定性

  • 采集方式: 指定CPU核心进行采集
  • 稳定性:
    • 指定cpu采集: 数据稳定、地址正确
    • 指定pid采集: 存在不一致,建议优先使用指定cpu采集