Profiling 体系
这一篇解决什么
链路追踪能告诉你请求慢在哪一段,说不清那一段代码为什么慢。Profiling 补上这一刀:在普通监控之外对运行中进程做 CPU、内存、锁的栈采样,生成火焰图,把根因从"端点级"推到"代码行级"。SkyWalking OAP 有四套独立的 profiling 子系统——Thread Profiling、eBPF Profiling、pprof、async-profiler。这篇把四者逐个拆透并对比。
概念速查(对新人不友好的词先解释)
先认人,再认戏。这几个词后面反复出现,先一次性讲清。
| 词 | 解释 |
|---|---|
| Profiling(性能剖析) | 周期性"拍快照"记录程序当前正在执行的调用栈。成千上万次快照叠起来,某个函数出现得越多,它占的 CPU、内存或锁时间就越多 |
| 火焰图(Flame Graph) | 把成千上万条调用栈按"根→子"层级堆叠画出来的图。y 轴是调用深度,x 轴是某函数在所有栈里出现的频次。宽度越宽越"热",热点函数一眼就能看出 |
| on-CPU / off-CPU | on-CPU 采样"线程正在 CPU 上跑什么",找 CPU 热点;off-CPU 采样"线程被挂起在等什么"——等锁、等 IO、等网络,找阻塞点。两者互补,一个找"忙",一个找"等" |
| eBPF | Linux 内核里的"可编程沙箱"。能在内核态挂钩子抓任何进程的系统调用、网络包、调用栈,不改业务代码、不重启业务进程 |
| pprof | Go 自带的性能采样格式(runtime/pprof、net/http/pprof),产出 protobuf 二进制 .pb.gz,含 CPU、堆、goroutine、锁、线程创建等多种事件 |
| JFR(Java Flight Recorder) | JVM 自带的低开销事件记录格式。async-profiler 是社区工具,能把 JVM 内的 CPU、堆、锁采样导出成 JFR |
| Rover | SkyWalking 官方的 eBPF Agent,独立进程,跑在被监控节点上。用 eBPF 采数据再 gRPC 上报给 OAP |
OAP 把 Profiling 拆成四套独立子系统。它们在"谁采样、采什么、存哪、怎么画图"上各有取舍——这才是后面要讲的重点。

为什么要拆四套,而不是一套通吃?因为采样这件事天然有几组矛盾取舍:要不要侵入业务进程、能不能改代码、采 on-CPU 还是 off-CPU、要不要绑 trace、采样的开销能压多低。四套子系统各自站在不同的取舍点上,谁也替代不了谁。后面每一节会讲清它站在哪、为什么这么站。
Thread Profiling(线程级 CPU profiling,Java agent 上报)
谁产生数据 / 传输协议
数据由 SkyWalking Java Agent 产生。Java Agent 集成了 ThreadProfiler。它收到 OAP 下发的"对某 endpoint 做 N 分钟线程栈采样"任务后,在业务线程被该 endpoint 命中且耗时超过阈值时,按 dumpPeriod 周期性地 Thread.dumpStack() 抓栈,把多条 ThreadSnapshot 用 gRPC 流上报。
协议定义在 apm-protocol/apm-network/src/main/proto/profile/Profile.proto:30-93,service ProfileTask 共 4 个 RPC:
| RPC | 方向 | 作用 |
|---|---|---|
getProfileTaskCommands |
agent → OAP | agent 拉取需要执行的采样任务命令 |
collectSnapshot (stream) |
agent → OAP | 上报线程栈快照(Java) |
goProfileReport (stream) |
go agent → OAP | 上报 Go pprof 数据(复用本通道) |
reportTaskFinish |
agent → OAP | 通知任务结束 |
ThreadSnapshot 消息(Profile.proto:58-69)含 taskId / traceSegmentId / time / sequence / ThreadStack{codeSignatures[]}——栈以"代码签名列表"形式存。
为什么栈存成"代码签名列表"而不是完整字节流
Java 的Thread.dumpStack()拿到的是StackTraceElement[],每帧就是类名.方法名(文件:行号)。直接存字符串列表,反序列化几乎零成本,也不用引入额外的二进制协议。Go 那条路走的是 pprof 二进制,所以另存stackBinary——同一条通道,两种存储格式,靠language字段区分。
任务如何下发到 agent(命令下发链路)

用户在 UI 创建任务,走 ProfileTaskMutationService.createTask(...)(oap-server/server-core/.../profiling/trace/ProfileTaskMutationService.java:64)。它先算任务执行时间窗,做合法性校验(checkDataSuccess,:103——duration 上下限、dumpPeriod 下限、maxSamplingCount 上限、同 service 同时段不可重叠),再构造 ProfileTaskRecord 并 NoneStreamProcessor.getInstance().in(task) 落库(:98)。任务表是 NoneStream——不参与聚合,只存原始任务记录。
ProfileTaskRecord(ProfileTaskRecord.java:46-84)字段:taskId / serviceId / endpointName / startTime / duration / minDurationThreshold / dumpPeriod / createTime / maxSamplingCount,索引名 profile_task,scope PROFILE_TASK。
下发靠命令轮询 + 缓存。agent 周期性调 getProfileTaskCommands,handler 是 ProfileTaskServiceHandler.getProfileTaskCommands(skywalking-profile-receiver-plugin/.../handler/ProfileTaskServiceHandler.java:67)。它先从 ProfileTaskCache(server-core/.../cache/ProfileTaskCache.java:42,Guava Cache,写后 1 分钟过期,:62)按 serviceId 取任务列表,跳过 createTime <= lastCommandTime 的(agent 已领过,:84),再调 commandService.newProfileTaskCommand(task) 把任务序列化成 Commands 下发,同时写一条 ProfileTaskLogRecord{operationType=NOTIFIED} 日志(:89)。
为什么用命令轮询而不是长连接推送
Agent 通常部署在业务进程里,网络环境复杂,未必能被 OAP 反向连上。轮询把主动权交给 agent——它什么时候有空什么时候来领任务,OAP 只管把任务放进缓存。缓存 1 分钟过期是为了让任务列表能被定期刷新(新任务进得来、老任务出得去),又不至于每次轮询都打 DB。
快照如何上报与存储
ProfileTaskServiceHandler.collectSnapshot(ProfileTaskServiceHandler.java:100)是 gRPC StreamObserver<ThreadSnapshot>。每收到一条 ThreadSnapshot 就构造成 ProfileThreadSnapshotRecord 并 RecordStreamProcessor.getInstance().in(record)(:119,Record 流,时序数据)。
ProfileThreadSnapshotRecord(ProfileThreadSnapshotRecord.java:47-74)字段:taskId / segmentId / dumpTime / sequence / stackBinary(byte[]) / language,索引名 profile_task_segment_snapshot,scope PROFILE_TASK_SEGMENT_SNAPSHOT。stackBinary 存序列化的 ThreadStack;id() 由 taskId+segmentId+sequence 组成(:77-82),保证同一栈序列不重复。language 字段(ProfileLanguageType:JAVA(0)/GO(1),ProfileLanguageType.java:23-25)支持 Java 与 Go 共存——这是后加的,让 Go 也能复用这套存储与 UI。
任务完成由 reportTaskFinish(ProfileTaskServiceHandler.java:229)记录 EXECUTION_FINISHED 日志(:237)。
Go pprof 复用通道
goProfileReport(ProfileTaskServiceHandler.java:143)接收 Go agent 分块上报的 pprof 字节流,攒齐后(isLast=true 时,:178)用 library-pprof-parser 的 PprofParser.parseProfile(自动嗅探 gzip 魔数 0x1F 0x8B)+ PprofSegmentParser.parseSegments 解析(:181-182),按 segment_id/trace_id label 拆成多个 segment。每个 segment 过滤出属于自己的 sample 后再存成 ProfileThreadSnapshotRecord{language=GO, stackBinary=过滤后的pprof}(storeGoProfileSegment,ProfileTaskServiceHandler.java:265-307)。Go 的 sequence 固定为 0(每个 segment 只一条记录,:288)。
为什么 Go 要复用 Thread Profiling 通道而不是走独立的 pprof receiver
Thread Profiling 绑traceSegmentId,能和 trace 联动看"这条慢链路里 Go 那一段 CPU 烧在哪"。独立的 pprof receiver(见后文)是 service/instance 级、不绑 trace。两套各有适用场景,所以 Go 同时支持两条路:要 trace 联动走这里,要纯服务级 profiling 走 pprof receiver。
ProfileModule 的 Service
ProfileModule(skywalking-profile-receiver-plugin/.../module/ProfileModule.java:26)services() 返回空数组——它本身不暴露 Service,只通过 ProfileModuleProvider.start()(ProfileModuleProvider.java:56)把 ProfileTaskServiceHandler 注册到共享 gRPC server。真正的业务逻辑 Service 在 CoreModule 里注册:
ProfileTaskMutationService(创建任务)——CoreModuleProvider.java:341ProfileTaskQueryService(查询/分析)——CoreModuleProvider.java:343
Analyzer 侧(生成火焰图)
ProfileAnalyzer(server-core/.../profiling/trace/analyze/ProfileAnalyzer.java:47)做栈聚合。先用时间窗算出每个 segment 的 minSequence..maxSequence(getAllSequenceRange,:183),分批(threadSnapshotAnalyzeBatchSize)并行从 DAO 拉记录(:81),反序列化成 ProfileStack(ProfileStack.deserialize,ProfileStack.java:40,Java 走 ThreadStack.codeSignatures,Go 走 pprof FrameTree),按栈顶函数 groupingBy 聚合成 ProfileStackTree(火焰图树,analyzeByStack,:218)。
若 Java 时间窗内没数据,回退到 Go 的全量范围分析(ProfileAnalyzer.java:135-160,忽略时间窗、按 language=GO 全量拉,交给 GoProfileAnalyzer.analyzeRecords)。这个回退逻辑是新加的——Java 和 Go 共用一张 snapshot 表,但 Go 的采样语义不同(一个 segment 一条记录,没有时间窗内的多 sequence),所以不能套同一套分析逻辑。
UI 查询(GraphQL resolver)
ProfileQuery(server-query-plugin/query-graphql-plugin/.../resolver/ProfileQuery.java:39)暴露 4 个查询:
getProfileTaskList(serviceId, endpointName)→ 任务列表(:57)getProfileTaskLogs(taskID)→ 任务下发/完成日志(:61)getProfileTaskSegments(taskId)→ 被采样的 trace segment(含跨进程父子段合并,ProfileTaskQueryService.java:205)getSegmentsProfileAnalyze(queries)→ 调ProfileAnalyzer.analyze出火焰图树(:69)
用途
定位 Java 服务单个慢端点的 on-CPU 热点函数。和 trace 联动是它的杀手锏:先看 trace 发现某 segment 慢,再对该 segment 做 thread profiling 画出火焰图,锁定是哪段代码吃 CPU。代价是必须装 Java/Go agent、只覆盖 on-CPU、必须命中特定 endpoint 才采样。
eBPF Profiling(网络 + 进程级,Rover 上报)
谁产生数据 / 传输协议
数据由 SkyWalking Rover(eBPF Agent,独立部署在被监控节点)产生,全部 gRPC,对应 proto 在 apm-protocol/apm-network/src/main/proto/ebpf/:
- 进程上报:
ebpf/profiling/Process.proto:31→service EBPFProcessService(reportProcesses/keepAlive) - profiling 数据:
ebpf/profiling/Profile.proto:30→service EBPFProfilingService(queryTasks/collectProfilingDatastream) - 持续 profiling 策略:
ebpf/profiling/Continuous.proto:29→service ContinuousProfilingService(queryPolicies/reportProfilingTask) - 网络访问日志:
ebpf/accesslog.proto:29→service EBPFAccessLogService(collectstream)
关键 Handler
四个 handler 都在 skywalking-ebpf-receiver-plugin/.../provider/handler/,由 EBPFReceiverProvider.start()(EBPFReceiverProvider.java:79)注册——addHandler 四个 handler(:123-126),同时加载 EBPFOALDefine(oal/ebpf.oal)做 K8s 网络指标的 OAL 聚合(:84)。
① EBPFProcessServiceHandler(EBPFProcessServiceHandler.java:59) — 进程注册与保活。
reportProcesses(:72):Rover 发现的进程(hostProcess虚机 /k8sProcess容器)转成 core 的Processsource(ProcessDetectType.VM/KUBERNETES)经SourceReceiver落库,并把生成的processId下行回 Rover。属性里若带support_ebpf_profiling=true则标记ProfilingSupportStatus.SUPPORT_EBPF_PROFILING(getProfilingSupportStatus,EBPFProcessServiceHandler.java:233-237)——只有支持 eBPF 的进程才能被采样。这个门槛必须卡:eBPF 采栈要靠进程符号表解析,进程信息不全就采不出有意义的栈。keepAlive(:103):心跳,同时刷新Process/ServiceInstanceUpdate/ServiceMeta/ServiceLabel。
② EBPFProfilingServiceHandler(EBPFProfilingServiceHandler.java:72) — profiling 任务下发与数据收集。
queryTasks(:94):按 Rover 最近 5 分钟内上报的进程(QUERY_TASK_PROCESSES_RANGE_MINUTES=5,:79),反查这些 service 下的EBPFProfilingTaskRecord(FIXED_TIME触发,:112),构造EBPFProfilingTaskCommand下发,按 serviceId +SUPPORT_EBPF_PROFILING+ 进程 label 匹配(buildProfilingCommands,:128)。collectProfilingData(stream,:158):首帧带EBPFProfilingTaskMetadata{taskId, processId, profilingStartTime, currentTime},handler 据此构造EBPFProcessProfilingSchedule(一次调度的 schedule 记录)经SourceReceiver落库(:170-175);后续帧按oneof profiling{onCPU, offCPU}(Profile.proto:52-54)分别processOnCPUProfiling/processOffCPUProfiling,把栈按KERNEL_SPACE → USER_SPACE排序(COMMON_STACK_TYPE_ORDER,EBPFProfilingServiceHandler.java:74-75),组装EBPFProfilingDatasource 经SourceReceiver落库(:205)。
为什么栈要先排 KERNEL_SPACE 再排 USER_SPACE
一次 on-CPU 采样,eBPF 能同时抓到内核态和用户态的栈。排序是为了让火焰图里"内核做了什么 → 用户代码调了什么"的因果关系稳定可读——内核栈在下、用户栈在上,函数谁调谁一目了然。stackIdList把排序后的 stackId 用_拼起来当主键的一部分,保证同一组栈不会重复存。
③ ContinuousProfilingServiceHandler(ContinuousProfilingServiceHandler.java:72) — 持续 profiling(按阈值自动触发)。
queryPolicies(:88):Rover 上报当前各 service 的 policy uuid,OAP 与 DB 中ContinuousProfilingPolicy比对(带 Guava 缓存continuousPolicyCacheTimeout),uuid 不一致就下发ContinuousProfilingPolicyCommand。reportProfilingTask(:174):当 Rover 监控指标(CPU/线程数/系统负载/HTTP 错误率/HTTP 平均响应时间,见ContinuousProfilingTriggeredMonitorType,Continuous.proto:82)超过阈值,Rover 主动上报触发原因,handler 构造一条EBPFProfilingTaskRecord{triggerType=CONTINUOUS_PROFILING}(:186)落库,调generateLogicalId()(:198),并回logicalId(newContinuousProfilingReportCommand,:204),Rover 再用它走collectProfilingData采样。
④ AccessLogServiceHandler(AccessLogServiceHandler.java:97) — eBPF 抓的"网络访问日志"(L7 协议 + L4 内核操作)。
collect(stream,:137):每条EBPFAccessLogMessage含node(节点信息/网卡/启动时间)、connection(本地/远端地址、角色 client/server、TLS、协议)、kernelLogs[](���核 read/write/connect/accept/close 各层 L2/L3/L4 时延指标)和可选protocolLog(HTTP 等应用层协议)。- handler 把内核日志转成
K8SMetrics(L2 网卡、L3 IP/MAC/NetFilter、L4 包数/重传/时延),把协议日志转成 service/instance/relation/endpoint 指标;若连接经过 ztunnel(Istio Ambient),还生成 mesh 指标经TelemetryDataDispatcher喂给 service-mesh 分析器。 - 这部分不是火焰图 profiling,而是 eBPF 网络性能剖析 + 服务拓扑/网格监控。放在这个 receiver 是因为都靠 eBPF 采,但语义上是另一回事。
数据如何存储
| Record | 位置 | 类型 | scope |
|---|---|---|---|
EBPFProfilingTaskRecord (EBPFProfilingTaskRecord.java:47) |
ebpf_profiling_task |
NoneStream | EBPF_PROFILING_TASK |
EBPFProfilingDataRecord (EBPFProfilingDataRecord.java:44) |
ebpf_profiling_data |
Record | EBPF_PROFILING_DATA |
EBPFProcessProfilingSchedule (source/EBPFProcessProfilingSchedule.java) |
schedule | Source | — |
ContinuousProfilingPolicy (ContinuousProfilingPolicy.java) |
policy 表 | NoneStream | — |
Process/ProcessTraffic |
进程表 | Metrics | — |
EBPFProfilingTaskRecord 关键字段:logicalId(sha256 of serviceId+labels+startTime,generateLogicalId,EBPFProfilingTaskRecord.java:107)、triggerType(UNKNOWN/FIXED_TIME/CONTINUOUS_PROFILING,EBPFProfilingTriggerType.java:31-43)、targetType(UNKNOWN/ON_CPU/OFF_CPU/NETWORK,EBPFProfilingTargetType.java:33-41)、processLabelsJson、extensionConfigJson(网络采样规则)、continuousProfilingJson(触发原因)。注意 id() 是 sha256(logicalId+createTime)(:95-101),而 logicalId 本身又是 sha256(serviceId+labels+startTime)——两层 hash:logicalId 用来跨实例去重同一逻辑任务,id 用来区分同一逻辑任务的不同创建批次。
EBPFProfilingDataRecord(EBPFProfilingDataRecord.java:44-77)字段:scheduleId / taskId / stackIdList / targetType / dataBinary(storageOnly) / uploadTime,id() 由 scheduleId+stackIdList+uploadTime sha256(:69-76)。dataBinary 存重新组装的 EBPFOnCPUProfiling/EBPFOffCPUProfiling protobuf。它先经 EBPFProfilingData source(source/EBPFProfilingData.java:31,scope EBPF_PROFILING_DATA)由 SourceReceiver 接收,再由 EBPFProcessProfilingDataDispatcher(EBPFProcessProfilingDataDispatcher.java:26)转成 Record 落库。
Analyzer 侧
EBPFProfilingAnalyzer(profiling/ebpf/analyze/EBPFProfilingAnalyzer.java:51)把时间窗切成 10 秒一段(FETCH_DATA_DURATION,:54)并行拉数据,线程池名 EBPFProfiling-<n>(:67,大小=maxThreadCountOfQueryEBPFProfilingData),反序列化成 EBPFProfilingStack,按栈顶 groupingBy 聚合成 EBPFProfilingTree(火焰图,generateTrees,:103)。On-CPU 用 dumpCount 权重,Off-CPU 用 switchCount/duration 权重——on-CPU 的"热"是出现次数多,off-CPU 的"热"是等待时间长,权重得分别给,否则 off-CPU 会被高频低耗时的切换淹没。
UI 查询(GraphQL resolver)
EBPFProcessProfilingMutation(EBPFProcessProfilingMutation.java:32):createEBPFProfilingFixedTimeTask(on/off CPU,:50)、createEBPFNetworkProfiling(网络,:54)、keepEBPFNetworkProfiling(续期网络任务,:58,默认 10 分钟)。EBPFProcessProfilingQuery(EBPFProcessProfilingQuery.java:41):queryPrepareCreateEBPFProfilingTaskData(:59)、queryEBPFProfilingTasks(:66)、queryEBPFProfilingSchedules(:80)、analysisEBPFProfilingResult(出火焰图,:84)。- 持续 profiling 策略由
ContinuousProfilingMutationService/ContinuousProfilingQueryService(CoreModuleProvider.java:365-367)+ 对应 resolver 维护。
用途与和 thread profiling 的区别
eBPF Profiling 不侵入业务进程。Rover 在内核态采样任意进程(Java/Go/Python/Node…)的 on-CPU、off-CPU 栈,外加网络级 L4/L7 剖析和 mesh 监控。适合无法注入 agent 的语言、容器/多语言混合环境。Thread profiling 必须装 Java/Go agent,只覆盖 Java/Go、只做 on-CPU、必须命中特定 endpoint 才采样。eBPF 的 off-CPU 与 network profiling 是 thread profiling 没有的能力——这也是它最难被替代的地方。
什么时候该选 eBPF
语言五花八门、容器密集、不能改业务代码、需要看网络阻塞或 mesh——这几条命中任意一条,eBPF 基本上是唯一选项。代价是 Rover 要独立部署、需要较高内核版本、符号解析依赖进程信息,运维门槛比装 agent 高。
pprof(Go pprof)
谁产生数据 / 传输协议
数据由 Go Agent 用 Go 自带 runtime/pprof 采集后上报,gRPC。协议 apm-protocol/apm-network/src/main/proto/pprof/Pprof.proto:29,service PprofTask 两个 RPC:
getPprofTaskCommands→ 拉 pprof 任务(:33)collect(双向 streamPprofData/PprofCollectionResponse) → 上传 pprof 二进制(:31)
PprofData 第一帧带 PprofMetaData{service, serviceInstance, taskId, type(状态), contentSize}(Pprof.proto:59-68),后续帧 content 是 pprof 字节;状态枚举 PprofProfilingStatus(PROFILING_SUCCESS / EXECUTION_TASK_ERROR / TERMINATED_BY_OVERSIZE,:46-53)。
关键 handler 与配置
PprofServiceHandler(skywalking-pprof-receiver-plugin/.../handler/PprofServiceHandler.java:53)实现 PprofTaskGrpc.PprofTaskImplBase,构造时注入两个关键参数(来自 PprofModuleConfig,PprofModuleConfig.java:27):
pprofMaxSize(默认 30MB,PprofModuleConfig.java:32):单文件大小上限,超限返回PPROF_TERMINATED_BY_OVERSIZE并记PPROF_UPLOAD_FILE_TOO_LARGE_ERROR日志。memoryParserEnabled(默认 true,PprofModuleConfig.java:45):决定用哪种接收 observer。
collect()(PprofServiceHandler.java:72)根据该开关返回 PprofByteBufCollectionObserver(内存解析)或 PprofFileCollectionObserver(落临时文件再解析)——前者省去磁盘挂载,后者省内存。PprofByteBufCollectionObserver(PprofByteBufCollectionObserver.java:43)用 ByteBuffer 攒齐字节,onCompleted 时调 PprofParser.dumpTree(buf) 解析成 FrameTree,包成 PprofProfilingData source(parsePprofAndStorage,PprofByteBufCollectionObserver.java:145-155)。File 版用 Files.createTempFile + FileOutputStream。
memoryParserEnabled 的取舍
默认 true(内存解析)能避免 OAP 没挂卷导致落盘失败,但大 pprof 文件会吃内存。改成 false 省内存却要求容器挂了可写卷——没挂卷会直接报错。这是个"内存 vs 磁盘"的二选一,没有免费午餐。
library-pprof-parser 如何解析
PprofParser(library-pprof-parser/.../pprof/parser/PprofParser.java:34)核心方法:
parseProfile(byte[])(:74):自动嗅探 gzip 魔数0x1F 0x8B(:79)决定是否GZIPInputStream解压,再ProfileProto.Profile.parseFrom解析 Google pprof protobuf。Go agent 走goProfileReport时用的就是它。dumpTree(ByteBuffer/String)(:36/:46):直接GZIPInputStream解压 +ProfileProto.Profile.parseFrom,再交给FrameTreeBuilder(profile).build()构造成FrameTree(调用栈树,供火焰图)。注意dumpTree不做 gzip 嗅探,默认输入就是 gzip 压缩的 pprof——pprof receiver 这条路假设 Go agent 上传的就是标准 gzip pprof。resolveSignature(:62):把 location id 解析成functionName:line,内联帧用;拼。
支持的事件类型 PprofEventType(server-core/.../query/type/PprofEventType.java:26):CPU / HEAP / BLOCK / MUTEX / GOROUTINE / THREADCREATE / ALLOCS——覆盖 Go 的 CPU、堆、阻塞、锁、goroutine、线程创建、分配采样。
什么是 FrameTree
FrameTree 是 pprof 解析后内部的"调用栈树"结构:根节点是栈底,子节点是栈顶上层调用者,每个节点带函数签名和累计/自身采样值。火焰图就是把 FrameTree 拍平画出来。pprof 和 async-profiler 都���它当中间表示——存库存 FrameTree 的 JSON,查询时直接合并,省去每次查询都重新解析原始二进制。
数据存储
- 任务表
PprofTaskRecord(PprofTaskRecord.java:48):pprof_task,NoneStream,scopePPROF_TASK,字段taskId / serviceId / serviceInstanceIds(JSON) / createTime / events / duration / dumpPeriod。 - 数据表
PprofProfilingDataRecord(PprofProfilingDataRecord.java:41):pprof_profiling_data,Record,scopePPROF_PROFILING_DATA,dataBinary存FrameTree的 JSON(不是原始 pprof!PprofProfilingDataDispatcher.java:35用GSON.toJson(source.getFrameTree()).getBytes()转换;查询时GSON.fromJson反序列化,PprofQueryService.java:96)。先经PprofProfilingDatasource(scopePPROF_PROFILING_DATA,含eventType+frameTree)由PprofProfilingDataDispatcher落库。 - 日志表
PprofTaskLogRecord:任务下发/执行/错误日志。
任务下发与 thread profiling 一样走命令 + 缓存:PprofTaskCache(PprofTaskCache.java:32,写后 1 分钟过期,:43)。getPprofTaskCommands(PprofServiceHandler.java:79)还会按 serviceInstanceIds 过滤——pprof 任务可以限定只发给某些实例(:97-100)。
UI 查询
PprofQuery(PprofQuery.java:40)+ PprofMutation:queryPprofTaskList、queryPprofAnalyze(按 taskId+instanceIds 查数据,PprofQueryService.queryPprofData 把多个 instance 的 FrameTree 用 PprofMergeBuilder 合并成一棵,PprofQueryService.java:92)、queryPprofTaskProgress(成功/失败实例列表)。
用途
Go 服务的 CPU/内存/阻塞/锁/goroutine 火焰图。比 thread profiling 的 Go 复用通道更"原生":thread profiling 把 Go pprof 拆进 segment 维度(与 trace 联动),而本节 pprof receiver 是独立的、按 service+instance 维度的通用 Go profiling,不绑定 trace。要 trace 联动选前者,要服务级全量 profiling 选后者。
async-profiler(Java JFR)
谁产生数据 / 传输协议
数据由 Java Agent 调用 async-profiler 工具采集 JFR 后上报,gRPC。协议 apm-protocol/apm-network/src/main/proto/asyncprofiler/AsyncProfiler.proto:29,service AsyncProfilerTask:getAsyncProfilerTaskCommands + collect(双向 stream AsyncProfilerData/AsyncProfilerCollectionResponse)。结构与 pprof proto 几乎对称:第一帧 AsyncProfilerMetaData{type, contentSize}(AsyncProfiler.proto:60-69),后续 content 是 JFR 二进制;状态枚举 AsyncProfilingStatus(PROFILING_SUCCESS / EXECUTION_TASK_ERROR / TERMINATED_BY_OVERSIZE,:51-58)。
关键 handler 与配置
AsyncProfilerServiceHandler(skywalking-async-profiler-receiver-plugin/.../handler/AsyncProfilerServiceHandler.java:54)实现 AsyncProfilerTaskGrpc.AsyncProfilerTaskImplBase,构造时注入(来自 AsyncProfilerModuleConfig,AsyncProfilerModuleConfig.java:28):
jfrMaxSize(默认 30MB,AsyncProfilerModuleConfig.java:33):JFR 文件大小上限,超限返回TERMINATED_BY_OVERSIZE+JFR_UPLOAD_FILE_TOO_LARGE_ERROR日志。memoryParserEnabled(默认 true,AsyncProfilerModuleConfig.java:46):选AsyncProfilerByteBufCollectionObserver(内存)或AsyncProfilerFileCollectionObserver(临时文件)。
任务下发同样走缓存:AsyncProfilerTaskCache(写后 1 分钟过期),按 serviceId 取,且按 serviceInstanceIds 过滤(AsyncProfilerServiceHandler.java:80-87)。
library-async-profiler-jfr-parser 如何解析 JFR
JFRParser(library-async-profiler-jfr-parser/.../jfr/parser/JFRParser.java:30)核心:
dumpTree(ByteBuffer buf, Arguments args)(:40):用one.jfr.JfrReader(第三方 jfr-converter 库)读 JFR 字节流,交给JFRToFrameTree转换器把事件转成FrameTree,返回Map<JFREventType, FrameTree>——一个 JFR 文件可同时含多种事件类型的树。- 支持的事件类型
JFREventType(library-async-profiler-jfr-parser/.../jfr/type/JFREventType.java:26):EXECUTION_SAMPLE(CPU 采样)、JAVA_MONITOR_ENTER/THREAD_PARK(锁竞争,合并为LOCK)、OBJECT_ALLOCATION_IN_NEW_TLAB/OBJECT_ALLOCATION_OUTSIDE_TLAB(堆分配)、PROFILER_LIVE_OBJECT(存活对象)。
async-profiler 和 thread profiling 都服务 Java,区别在哪
async-profiler 靠async-profiler这个独立工具用perf_events采 CPU 样本、用字节码插桩采内存/锁,开销低、不依赖 JVMTI 的重量级 instrumentation,能同时出 CPU+内存+锁多类火焰图,是整服务/实例级持续采样。thread profiling 走的是Thread.dumpStack(),端点级、只在命中慢端点时短时采样、只出 on-CPU。一个像"给 Java 进程做 Holter 监护",一个像"对某个慢工位贴身跟拍"。
数据存储
- 任务表
AsyncProfilerTaskRecord(AsyncProfilerTaskRecord.java:46):async_profiler_task,NoneStream,scopeASYNC_PROFILER_TASK,字段taskId / serviceId / serviceInstanceIds / createTime / duration / events(JSON List) / execArgs。execArgs是传给 async-profiler 的命令行参数,如event=cpu,interval=10ms——这是 async-profiler 的原生用法,agent 把参数透传给工具进程。 - 数据表
JFRProfilingDataRecord(JFRProfilingDataRecord.java:44):jfr_profiling_data,Record,scopeJFR_PROFILING_DATA,字段taskId / instanceId / eventType / uploadTime / dataBinary。多了eventType维度(一个任务一次上传可分多种事件),id()含eventType(:76-84)。dataBinary同样存FrameTree的 JSON(JFRProfilingDataDispatcher.java:36用GSON.toJson,查询时GSON.fromJson,AsyncProfilerQueryService.java:96)。先经 source →JFRProfilingDataDispatcher落库。 - 日志表
AsyncProfilerTaskLogRecord。
UI 查询
AsyncProfilerQuery(AsyncProfilerQuery.java:42)+ AsyncProfilerMutation:queryAsyncProfilerTaskList(:60)、queryAsyncProfilerAnalyze(按 taskId+instanceIds+eventType 查,:67,AsyncProfilerQueryService.queryJFRData 用 JFRMergeBuilder 合并多实例的 FrameTree,AsyncProfilerQueryService.java:93)、queryAsyncProfilerTaskProgress(:74)。
用途
Java 服务的 CPU、内存分配、锁竞争 火焰图,基于 async-profiler 的低开销采样。和 thread profiling 都服务 Java,但定位不同:async-profiler 看"整个服务/实例的 CPU/内存/锁长什么样",thread profiling 看"某条慢链路里 CPU 烧在哪"。
四者对比表
| 维度 | Thread Profiling | eBPF Profiling | pprof | async-profiler |
|---|---|---|---|---|
| 数据生产者 | SkyWalking Java/Go Agent | SkyWalking Rover(eBPF Agent) | Go Agent(runtime/pprof) |
Java Agent + async-profiler |
| 是否侵入业务进程 | 是,需装 agent | 否,内核态旁路 | 是,需装 agent | 是,需装 agent + async-profiler 二进制 |
| 传输协议 | gRPC(ProfileTask service) |
gRPC(4 个 service) | gRPC(PprofTask service) |
gRPC(AsyncProfilerTask service) |
| 关键 receiver handler | ProfileTaskServiceHandler |
EBPFProcessServiceHandler / EBPFProfilingServiceHandler / ContinuousProfilingServiceHandler / AccessLogServiceHandler |
PprofServiceHandler + PprofByteBuf/FileCollectionObserver |
AsyncProfilerServiceHandler + AsyncProfilerByteBuf/FileCollectionObserver |
| 任务下���机制 | 命令轮询 + ProfileTaskCache |
命令轮询(按 Rover 进程反查 task) + Rover 持续 profiling 上报 | 命令轮询 + PprofTaskCache |
命令轮询 + AsyncProfilerTaskCache |
| 触发方式 | 用户 UI 手动,端点级 | UI 手动(FIXED_TIME) / 指标超阈值自动(CONTINUOUS) | UI 手动 | UI 手动 |
| 采集粒度 | 端点 + trace segment 维度 | 进程 + network 维度 | service + instance 维度 | service + instance + event 维度 |
| 能采的样本类型 | on-CPU(Java 栈);Go pprof 复用通道 | on-CPU / off-CPU / network(L4+L7) | CPU/HEAP/BLOCK/MUTEX/GOROUTINE/THREADCREATE/ALLOCS | CPU(EXECUTION_SAMPLE)/堆分配/锁(LOCK)/存活对象 |
| 栈存储格式 | ThreadStack protobuf / 过滤后 pprof,存 stackBinary |
重组后的 on/off CPU protobuf,存 dataBinary |
FrameTree JSON,存 dataBinary |
FrameTree JSON,存 dataBinary |
| 存储结构 | ProfileTaskRecord(NoneStream) + ProfileThreadSnapshotRecord(Record) + 日志 |
EBPFProfilingTaskRecord + EBPFProfilingDataRecord(Record) + schedule + ContinuousProfilingPolicy + Process |
PprofTaskRecord(NoneStream) + PprofProfilingDataRecord(Record) + 日志 |
AsyncProfilerTaskRecord(NoneStream) + JFRProfilingDataRecord(Record, 带 eventType) + 日志 |
| 走 Record 还是专门结构 | Record(snapshot)+ NoneStream(task) | Record(data)+ NoneStream(task/policy),额外有 source EBPFProfilingData |
Record(data)+ NoneStream(task) | Record(data)+ NoneStream(task) |
| 解析库 | library-pprof-parser(仅 Go 路径) |
内置 EBPFProfilingStack.deserialize |
library-pprof-parser(PprofParser/FrameTreeBuilder) |
library-async-profiler-jfr-parser(JFRParser/one.jfr.JfrReader/JFRToFrameTree) |
| 大小限制配置 | 无单文件限制,靠 maxSamplingCount/dumpPeriod |
无单文件限制 | pprofMaxSize(30MB) |
jfrMaxSize(30MB) |
| 解析模式开关 | — | — | memoryParserEnabled(内存/临时文件) |
memoryParserEnabled(内存/临时文件) |
| Analyzer/火焰图 | ProfileAnalyzer(时间窗 + Go 回退) |
EBPFProfilingAnalyzer(10s 分片并行) |
PprofMergeBuilder 合并多实例 FrameTree |
JFRMergeBuilder 合并多实例 FrameTree |
| GraphQL resolver | ProfileQuery (+ ProfileMutation) |
EBPFProcessProfilingQuery / EBPFProcessProfilingMutation (+ Continuous resolvers) |
PprofQuery / PprofMutation |
AsyncProfilerQuery / AsyncProfilerMutation |
| UI 火焰图类型 | on-CPU 调用栈 | on-CPU / off-CPU / network | CPU/heap/block/mutex/goroutine/… | CPU/heap/lock/分配/存活对象 |
| 典型适用场景 | 定位 Java 慢端点的 CPU 热点函数(与 trace 联动) | 多语言/容器环境的 CPU、阻塞、网络剖析 + mesh,无需改业务代码 | Go 服务全方位 profiling | Java 服务的 CPU+内存+锁剖析,开销低 |
| 语言支持 | Java、Go | 任意语言(内核采栈,符号需进程信息) | Go | Java |
共性与差异要点
四者共性
四者都是"任务下发 → agent 采集 → gRPC 上报 → 落 Record/NoneStream → 查询时反序列化聚合成火焰图树"。任务下发都走命令轮询 + Guava Cache(ProfileTaskCache/PprofTaskCache/AsyncProfilerTaskCache,均写后 1 分钟过期;eBPF 按 process 反查),都受CoreModuleProvider集中注册的 Mutation/Query Service 驱动(CoreModuleProvider.java:341-367)。任务表统一用NoneStreamProcessor,采样数据表统一用RecordStreamProcessor。
核心差异六条:
- 侵入性 — eBPF 是唯一不需要在业务进程内装探针的,适合无法改代码或语言五花八门的环境。
- off-CPU / 阻塞剖析 — 只有 eBPF 有原生 off-CPU;async-profiler 通过锁事件(
LOCK)间接覆盖;thread/pprof 的 block/mutex 是 Go/Java agent 内采的。 - 网络剖析 — 只有 eBPF 有 L4/L7 + mesh��
AccessLogServiceHandler),其它三种纯函数栈。 - 数据存储格式 — thread/eBPF 存原始 protobuf 字节(
stackBinary/dataBinary),查询时反序列化;pprof/async-profiler 在接收时就解析成 FrameTree JSON 存库,查询时直接GSON.fromJson合并——用空间换 CPU,查询快但存储大。 - 持续/自动触发 — 只有 eBPF 有
ContinuousProfiling(监控指标超阈值自动拉起 profiling 任务)。 - 与 trace 联动 — thread profiling 独有:采样绑定
traceSegmentId,能和 trace 一起看"这条慢链路里 CPU 烧在哪"。
关键文件速查
| 子系统 | 关键类 | 路径 |
|---|---|---|
| Thread Profiling | ProfileTaskServiceHandler | oap-server/server-receiver-plugin/skywalking-profile-receiver-plugin/.../handler/ProfileTaskServiceHandler.java |
| ProfileTaskMutationService / ProfileTaskQueryService | oap-server/server-core/.../profiling/trace/ |
|
| ProfileAnalyzer / ProfileStack / GoProfileAnalyzer | oap-server/server-core/.../profiling/trace/analyze/ |
|
| Profile.proto | apm-protocol/apm-network/src/main/proto/profile/Profile.proto |
|
| eBPF | EBPFReceiverProvider | oap-server/server-receiver-plugin/skywalking-ebpf-receiver-plugin/.../EBPFReceiverProvider.java |
| EBPFProcessServiceHandler / EBPFProfilingServiceHandler / ContinuousProfilingServiceHandler / AccessLogServiceHandler | 同目录 .../provider/handler/ |
|
| EBPFProfilingAnalyzer | oap-server/server-core/.../profiling/ebpf/analyze/EBPFProfilingAnalyzer.java |
|
| ebpf proto | apm-protocol/apm-network/src/main/proto/ebpf/ |
|
| pprof | PprofServiceHandler | oap-server/server-receiver-plugin/skywalking-pprof-receiver-plugin/.../handler/PprofServiceHandler.java |
| PprofParser / FrameTreeBuilder | oap-server/server-library/library-pprof-parser/.../pprof/parser/PprofParser.java |
|
| Pprof.proto | apm-protocol/apm-network/src/main/proto/pprof/Pprof.proto |
|
| async-profiler | AsyncProfilerServiceHandler | oap-server/server-receiver-plugin/skywalking-async-profiler-receiver-plugin/.../handler/AsyncProfilerServiceHandler.java |
| JFRParser / JFRToFrameTree | oap-server/server-library/library-async-profiler-jfr-parser/.../jfr/parser/JFRParser.java |
|
| JFREventType | oap-server/server-library/library-async-profiler-jfr-parser/.../jfr/type/JFREventType.java |
|
| AsyncProfiler.proto | apm-protocol/apm-network/src/main/proto/asyncprofiler/AsyncProfiler.proto |
接下来
Profiling 数据存进去了,查询时三个存储后端怎么读?详见 11-存储查询DAO实现。
心智模型
把 Profiling 想成"工厂的高级体检设备"。普通监控(trace/metrics)是流水线仪表盘——看哪里慢;Profiling 是给工人拍 X 光——看为什么慢。Thread Profiling 是贴身跟拍,agent 装在工人身上,只盯某个慢工位;eBPF 是厂房监控摄像头,内核态旁路,谁都能拍,还能拍网络和阻塞;pprof 是 Go 工人的自带体检报告;async-profiler 是 Java 工人的低损耗 Holter。四种都能生成火焰图,区别在于谁能拍、拍什么、要不要侵入。