Java profiling¶
本文将介绍基于 async-profiler 来采集 java 应用,并将采集到的数据上报给 DataKit,从而可以在观测云平台进行分析。
async-profiler 介绍¶
async-profiler 是一款开源的 Java 性能分析工具,基于 HotSpot 的 API,可以收集程序运行中的堆栈和内存分配等信息。
async-profiler 可以收集以下几种事件:
- CPU cycles
- 硬件和软件性能计数器,如cache misses, branch misses, page faults, context switches 等
- Java 堆的分配
- Contented lock attempts, 包括 Java object monitors 和 ReentrantLocks
async-profiler 安装¶
官网提供了不同平台的安装包的下载(当前版本 2.8.3):
- Linux x64 (glibc): async-profiler-2.8.3-linux-x64.tar.gz
- Linux x64 (musl): async-profiler-2.8.3-linux-musl-x64.tar.gz
- Linux arm64: async-profiler-2.8.3-linux-arm64.tar.gz
- macOS x64/arm64: async-profiler-2.8.3-macos.zip
- 不同格式文件转换器: converter.jar
下载相应的安装包,并解压。
下面以 Linux x64 (glibc) 平台为例(其他平台类似):
$ wget https://github.com/jvm-profiling-tools/async-profiler/releases/download/v2.8.3/async-profiler-2.8.3-linux-x64.tar.gz
$ tar -zxf async-profiler-2.8.3-linux-x64.tar.gz
$ cd async-profiler-2.8.3-linux-x64 && ls
build CHANGELOG.md LICENSE profiler.sh README.md
async-profiler 使用¶
前置条件¶
- 设置
perf_events
参数
Linux 内核版本为 4.6 以后的,如果需要使用非 root 用户启动进程中的 perf_events
, 需要设置两个系统运行时变量,可通过如下方式设置:
- 安装 Debug Symbols (采集 alloc 事件时)
如果需要采集 alloc 相关事件,则要求安装 Debug Symbols 。Oracle JDK 已经内置这些 Symbols,可跳过此步骤。而 OpenJDK 则需要安装,安装方式参考如下:
Debian / Ubuntu:
CentOS, RHEL 和其他 RPM 版本,可以通过 debuginfo-install
:
linux 平台可以通过 gdb
查看是否正确安装:
输出结果如果包含 Symbol "UseG1GC" is at 0xxxxx
或 No symbol "UseG1GC" in current context
,则表明安装成功。
- 查看 java 进程 ID
采集之前,需要查看 java 进程的 PID (可以使用 jps
命令)
采集 java 进程¶
选定一个需要采集的 java 进程 (如上面的 8983 进程), 执行目录下的 profiler.sh
,采集数据
约 10 秒后,会在当前目录下生成一个名为 profiling.html
的 html 文件,通过浏览器打开该文件,就可以查看火焰图。
整合 DataKit 和 async-profiler¶
准备工作¶
- 准备 DataKit 服务,版本 DataKit >= 1.4.3
以下操作默认地址为 http://localhost:9529
。如果不是,需要修改为实际的 DataKit 服务地址。
整合步骤¶
整合方式,可以分为两种:
自动化脚本¶
自动化脚本可以方便地整合 async-profiler 和 DataKit,使用方法如下。
创建 shell 脚本
在当前目录下新建一个文件,命名为 collect.sh
, 输入以下内容:
set -e
LIBRARY_VERSION=2.8.3
# 允许上传至 DataKit 的 jfr 文件大小 (6 M),请勿修改
MAX_JFR_FILE_SIZE=6000000
# DataKit 服务地址
datakit_url=http://localhost:9529
if [ -n "$DATAKIT_URL" ]; then
datakit_url=$DATAKIT_URL
fi
# 上传 profiling 数据的完整地址
datakit_profiling_url=$datakit_url/profiling/v1/input
# 应用的环境
app_env=dev
if [ -n "$APP_ENV" ]; then
app_env=$APP_ENV
fi
# 应用的版本
app_version=0.0.0
if [ -n "$APP_VERSION" ]; then
app_version=$APP_VERSION
fi
# 主机名称
host_name=$(hostname)
if [ -n "$HOST_NAME" ]; then
host_name=$HOST_NAME
fi
# 服务名称
service_name=
if [ -n "$SERVICE_NAME" ]; then
service_name=$SERVICE_NAME
fi
# profiling duration, in seconds
profiling_duration=10
if [ -n "$PROFILING_DURATION" ]; then
profiling_duration=$PROFILING_DURATION
fi
# profiling event
profiling_event=cpu
if [ -n "$PROFILING_EVENT" ]; then
profiling_event=$PROFILING_EVENT
fi
# 采集的 java 应用进程 ID,此处可以自定义需要采集的 java 进程,比如可以根据进程名称过滤
java_process_ids=$(jps -q -J-XX:+PerfDisableSharedMem)
if [ -n "$PROCESS_ID" ]; then
java_process_ids=`echo $PROCESS_ID | tr "," " "`
return
fi
if [[ $java_process_ids == "" ]]; then
printf "Warning: no java program found, exit now\n"
exit 1
fi
is_valid_process_id() {
if [ -n "$1" ]; then
if [[ $1 =~ ^[0-9]+$ ]]; then
return 1
fi
fi
return 0
}
profile_collect() {
# disable -e
set +e
process_id=$1
is_valid_process_id $process_id
if [[ $? == 0 ]]; then
printf "Warning: invalid process_id: $process_id, ignore"
return 1
fi
uuid=$(uuidgen)
jfr_file=$runtime_dir/profiler_$uuid.jfr
event_json_file=$runtime_dir/event_$uuid.json
process_name=$(jps | grep $process_id | awk '{print $2}')
start_time=$(date +%FT%T.%N%:z)
./profiler.sh -d $profiling_duration --fdtransfer -e $profiling_event -o jfr -f $jfr_file $process_id
end_time=$(date +%FT%T.%N%:z)
if [ ! -f $jfr_file ]; then
printf "Warning: generating profiling file failed for %s, pid %d\n" $process_name $process_id
return
else
printf "generate profiling file successfully for %s, pid %d\n" $process_name $process_id
fi
jfr_zip_file=$jfr_file.zip
zip -q $jfr_zip_file $jfr_file
zip_file_size=`ls -la $jfr_zip_file | awk '{print $5}'`
if [ -z "$service_name" ]; then
service_name=$process_name
fi
if [ $zip_file_size -gt $MAX_JFR_FILE_SIZE ]; then
printf "Warning: the size of the jfr file generated is bigger than $MAX_JFR_FILE_SIZE bytes, now is $zip_file_size bytes\n"
else
cat >$event_json_file <<END
{
"tags_profiler": "library_version:$LIBRARY_VERSION,library_type:async_profiler,process_id:$process_id,process_name:$process_name,service:$service_name,host:$host_name,env:$app_env,version:$app_version",
"start": "$start_time",
"end": "$end_time",
"family": "java",
"format": "jfr"
}
END
res=$(curl $datakit_profiling_url \
-F "main=@$jfr_zip_file;filename=main.jfr" \
-F "event=@$event_json_file;filename=event.json;type=application/json" )
if [[ $res != *ProfileID* ]]; then
printf "Warning: send profile file to datakit failed\n"
printf "$res"
else
printf "Info: send profile file to datakit successfully\n"
rm -rf $event_json_file $jfr_file $jfr_zip_file
fi
fi
set -e
}
runtime_dir=runtime
if [ ! -d $runtime_dir ]; then
mkdir $runtime_dir
fi
# 并行采集 profiling 数据
for process_id in $java_process_ids; do
printf "profiling process %d\n" $process_id
profile_collect $process_id > $runtime_dir/$process_id.log 2>&1 &
done
# 等待所有任务结束
wait
# 输出任务执行日志
for process_id in $java_process_ids; do
log_file=$runtime_dir/$process_id.log
if [ -f $log_file ]; then
echo
cat $log_file
rm $log_file
fi
done
执行脚本
脚本执行完毕后,采集的 profiling 数据会通过 DataKit 上报给观测云平台,稍后可在"应用性能监测"-"Profile" 查看。
脚本支持如下环境变量:
DATAKIT_URL
: DataKit url 地址,默认为 http://localhost:9529APP_ENV
: 当前应用环境,如dev | prod | test
等APP_VERSION
: 当前应用版本HOST_NAME
: 主机名称SERVICE_NAME
: 服务名称PROFILING_DURATION
: 采样持续时间,单位为秒PROFILING_EVENT
: 采集的事件,如cpu,alloc,lock
等PROCESS_ID
: 采集的 java 进程 ID, 多个 ID 以逗号分割,如98789,33432
$ DATAKIT_URL=http://localhost:9529 APP_ENV=test APP_VERSION=1.0.0 HOST_NAME=datakit PROFILING_EVENT=cpu,alloc PROFILING_DURATION=20 PROCESS_ID=98789,33432 bash collect.sh
手动操作¶
相比自动化脚本,手动操作自由度高,可满足不同的场景需求。
采集 profiling 文件 (jfr 格式)
首先使用 async-profiler
收集 java 进程的 profiling 信息,并生成 jfr 格式的文件。
如:
准备元信息文件
编写 profiling 元信息文件, 如 event.json:
{
"tags_profiler": "library_version:2.8.3,library_type:async_profiler,process_id:16718,host:host_name,service:profiling-demo,env:dev,version:1.0.0",
"start": "2022-10-28T14:30:39.122688553+08:00",
"end": "2022-10-28T14:32:39.122688553+08:00",
"family": "java",
"format": "jfr"
}
字段含义:
tags_profiler
: profiling 数据标签,可包含自定义标签library_version
: 当前 async-profiler 版本library_type
: profiler 库类型, 即 async-profilerprocess_id
: java 进程 IDhost
: 主机名称service
: 服务名称env
: 应用的环境类型version
: 应用的版本- 其他自定义标签
start
: profiling 开始时间end
: profiling 结束时间family
: 语言种类format
: 文件格式
上传至 DataKit
上述的两种文件都准备完毕,即 profiling.jfr
和 event.json
,就可以通过 http POST 请求发送至 DataKit,方式如下:
$ curl http://localhost:9529/profiling/v1/input \
-F "main=@profiling.jfr;filename=main.jfr" \
-F "event=@event.json;filename=event.json;type=application/json"
当上述请求返回结果格式为 {"content":{"ProfileID":"xxxxxxxx"}}
时,表明上传成功。
DataKit 会产生一条 profiling 记录,并将 jfr 文件保存至相应的后端存储,便于后续分析使用。