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/pprofnet/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 拆成四套独立子系统。它们在"谁采样、采什么、存哪、怎么画图"上各有取舍——这才是后面要讲的重点。

图1

为什么要拆四套,而不是一套通吃?因为采样这件事天然有几组矛盾取舍:要不要侵入业务进程、能不能改代码、采 on-CPU 还是 off-CPU、要不要绑 trace、采样的开销能压多低。四套子系统各自站在不同的取舍点上,谁也替代不了谁。后面每一节会讲清它站在哪、为什么这么站。


Thread Profiling(线程级 CPU profiling,Java agent 上报)

谁产生数据 / 传输协议

数据由 SkyWalking Java Agent 产生。Java Agent 集成了 ThreadProfiler。它收到 OAP 下发的"对某 endpoint 做 N 分钟线程栈采样"任务后,在业务线程被该 endpoint 命中且耗时超过阈值时,按 dumpPeriod 周期性地 Thread.dumpStack() 抓栈,把多条 ThreadSnapshotgRPC 流上报。

协议定义在 apm-protocol/apm-network/src/main/proto/profile/Profile.proto:30-93service 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(命令下发链路)

图2

用户在 UI 创建任务,走 ProfileTaskMutationService.createTask(...)oap-server/server-core/.../profiling/trace/ProfileTaskMutationService.java:64)。它先算任务执行时间窗,做合法性校验(checkDataSuccess:103——duration 上下限、dumpPeriod 下限、maxSamplingCount 上限、同 service 同时段不可重叠),再构造 ProfileTaskRecordNoneStreamProcessor.getInstance().in(task) 落库(:98)。任务表是 NoneStream——不参与聚合,只存原始任务记录。

ProfileTaskRecordProfileTaskRecord.java:46-84)字段:taskId / serviceId / endpointName / startTime / duration / minDurationThreshold / dumpPeriod / createTime / maxSamplingCount,索引名 profile_task,scope PROFILE_TASK

下发靠命令轮询 + 缓存。agent 周期性调 getProfileTaskCommands,handler 是 ProfileTaskServiceHandler.getProfileTaskCommandsskywalking-profile-receiver-plugin/.../handler/ProfileTaskServiceHandler.java:67)。它先从 ProfileTaskCacheserver-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.collectSnapshotProfileTaskServiceHandler.java:100)是 gRPC StreamObserver<ThreadSnapshot>。每收到一条 ThreadSnapshot 就构造成 ProfileThreadSnapshotRecordRecordStreamProcessor.getInstance().in(record):119Record 流,时序数据)。

ProfileThreadSnapshotRecordProfileThreadSnapshotRecord.java:47-74)字段:taskId / segmentId / dumpTime / sequence / stackBinary(byte[]) / language,索引名 profile_task_segment_snapshot,scope PROFILE_TASK_SEGMENT_SNAPSHOTstackBinary 存序列化的 ThreadStackid()taskId+segmentId+sequence 组成(:77-82),保证同一栈序列不重复。language 字段(ProfileLanguageTypeJAVA(0)/GO(1)ProfileLanguageType.java:23-25)支持 Java 与 Go 共存——这是后加的,让 Go 也能复用这套存储与 UI。

任务完成由 reportTaskFinishProfileTaskServiceHandler.java:229)记录 EXECUTION_FINISHED 日志(:237)。

Go pprof 复用通道

goProfileReportProfileTaskServiceHandler.java:143)接收 Go agent 分块上报的 pprof 字节流,攒齐后(isLast=true 时,:178)用 library-pprof-parserPprofParser.parseProfile(自动嗅探 gzip 魔数 0x1F 0x8B)+ PprofSegmentParser.parseSegments 解析(:181-182),按 segment_id/trace_id label 拆成多个 segment。每个 segment 过滤出属于自己的 sample 后再存成 ProfileThreadSnapshotRecord{language=GO, stackBinary=过滤后的pprof}storeGoProfileSegmentProfileTaskServiceHandler.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

ProfileModuleskywalking-profile-receiver-plugin/.../module/ProfileModule.java:26services() 返回空数组——它本身不暴露 Service,只通过 ProfileModuleProvider.start()ProfileModuleProvider.java:56)把 ProfileTaskServiceHandler 注册到共享 gRPC server。真正的业务逻辑 Service 在 CoreModule 里注册:

  • ProfileTaskMutationService(创建任务)——CoreModuleProvider.java:341
  • ProfileTaskQueryService(查询/分析)——CoreModuleProvider.java:343

Analyzer 侧(生成火焰图)

ProfileAnalyzerserver-core/.../profiling/trace/analyze/ProfileAnalyzer.java:47)做栈聚合。先用时间窗算出每个 segment 的 minSequence..maxSequencegetAllSequenceRange:183),分批(threadSnapshotAnalyzeBatchSize)并行从 DAO 拉记录(:81),反序列化成 ProfileStackProfileStack.deserializeProfileStack.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)

ProfileQueryserver-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:31service EBPFProcessServicereportProcesses / keepAlive
  • profiling 数据:ebpf/profiling/Profile.proto:30service EBPFProfilingServicequeryTasks / collectProfilingData stream)
  • 持续 profiling 策略:ebpf/profiling/Continuous.proto:29service ContinuousProfilingServicequeryPolicies / reportProfilingTask
  • 网络访问日志:ebpf/accesslog.proto:29service EBPFAccessLogServicecollect stream)

关键 Handler

四个 handler 都在 skywalking-ebpf-receiver-plugin/.../provider/handler/,由 EBPFReceiverProvider.start()EBPFReceiverProvider.java:79)注册——addHandler 四个 handler(:123-126),同时加载 EBPFOALDefineoal/ebpf.oal)做 K8s 网络指标的 OAL 聚合(:84)。

EBPFProcessServiceHandlerEBPFProcessServiceHandler.java:59 — 进程注册与保活。

  • reportProcesses:72):Rover 发现的进程(hostProcess 虚机 / k8sProcess 容器)转成 core 的 Process source(ProcessDetectType.VM/KUBERNETES)经 SourceReceiver 落库,并把生成的 processId 下行回 Rover。属性里若带 support_ebpf_profiling=true 则标记 ProfilingSupportStatus.SUPPORT_EBPF_PROFILINGgetProfilingSupportStatusEBPFProcessServiceHandler.java:233-237)——只有支持 eBPF 的进程才能被采样。这个门槛必须卡:eBPF 采栈要靠进程符号表解析,进程信息不全就采不出有意义的栈。
  • keepAlive:103):心跳,同时刷新 Process/ServiceInstanceUpdate/ServiceMeta/ServiceLabel

EBPFProfilingServiceHandlerEBPFProfilingServiceHandler.java:72 — profiling 任务下发与数据收集。

  • queryTasks:94):按 Rover 最近 5 分钟内上报的进程(QUERY_TASK_PROCESSES_RANGE_MINUTES=5:79),反查这些 service 下的 EBPFProfilingTaskRecordFIXED_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_ORDEREBPFProfilingServiceHandler.java:74-75),组装 EBPFProfilingData source 经 SourceReceiver 落库(:205)。

为什么栈要先排 KERNEL_SPACE 再排 USER_SPACE
一次 on-CPU 采样,eBPF 能同时抓到内核态和用户态的栈。排序是为了让火焰图里"内核做了什么 → 用户代码调了什么"的因果关系稳定可读——内核栈在下、用户栈在上,函数谁调谁一目了然。stackIdList 把排序后的 stackId 用 _ 拼起来当主键的一部分,保证同一组栈不会重复存。

ContinuousProfilingServiceHandlerContinuousProfilingServiceHandler.java:72 — 持续 profiling(按阈值自动触发)。

  • queryPolicies:88):Rover 上报当前各 service 的 policy uuid,OAP 与 DB 中 ContinuousProfilingPolicy 比对(带 Guava 缓存 continuousPolicyCacheTimeout),uuid 不一致就下发 ContinuousProfilingPolicyCommand
  • reportProfilingTask:174):当 Rover 监控指标(CPU/线程数/系统负载/HTTP 错误率/HTTP 平均响应时间,见 ContinuousProfilingTriggeredMonitorTypeContinuous.proto:82)超过阈值,Rover 主动上报触发原因,handler 构造一条 EBPFProfilingTaskRecord{triggerType=CONTINUOUS_PROFILING}:186)落库,调 generateLogicalId():198),并回 logicalIdnewContinuousProfilingReportCommand:204),Rover 再用它走 collectProfilingData 采样。

AccessLogServiceHandlerAccessLogServiceHandler.java:97 — eBPF 抓的"网络访问日志"(L7 协议 + L4 内核操作)。

  • collect(stream,:137):每条 EBPFAccessLogMessagenode(节点信息/网卡/启动时间)、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,generateLogicalIdEBPFProfilingTaskRecord.java:107)、triggerTypeUNKNOWN/FIXED_TIME/CONTINUOUS_PROFILINGEBPFProfilingTriggerType.java:31-43)、targetTypeUNKNOWN/ON_CPU/OFF_CPU/NETWORKEBPFProfilingTargetType.java:33-41)、processLabelsJsonextensionConfigJson(网络采样规则)、continuousProfilingJson(触发原因)。注意 id() 是 sha256(logicalId+createTime)(:95-101),而 logicalId 本身又是 sha256(serviceId+labels+startTime)——两层 hash:logicalId 用来跨实例去重同一逻辑任务,id 用来区分同一逻辑任务的不同创建批次。

EBPFProfilingDataRecordEBPFProfilingDataRecord.java:44-77)字段:scheduleId / taskId / stackIdList / targetType / dataBinary(storageOnly) / uploadTimeid()scheduleId+stackIdList+uploadTime sha256(:69-76)。dataBinary 存重新组装的 EBPFOnCPUProfiling/EBPFOffCPUProfiling protobuf。它先经 EBPFProfilingData source(source/EBPFProfilingData.java:31,scope EBPF_PROFILING_DATA)由 SourceReceiver 接收,再由 EBPFProcessProfilingDataDispatcherEBPFProcessProfilingDataDispatcher.java:26)转成 Record 落库。

Analyzer 侧

EBPFProfilingAnalyzerprofiling/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)

  • EBPFProcessProfilingMutationEBPFProcessProfilingMutation.java:32):createEBPFProfilingFixedTimeTask(on/off CPU,:50)、createEBPFNetworkProfiling(网络,:54)、keepEBPFNetworkProfiling(续期网络任务,:58,默认 10 分钟)。
  • EBPFProcessProfilingQueryEBPFProcessProfilingQuery.java:41):queryPrepareCreateEBPFProfilingTaskData:59)、queryEBPFProfilingTasks:66)、queryEBPFProfilingSchedules:80)、analysisEBPFProfilingResult(出火焰图,:84)。
  • 持续 profiling 策略由 ContinuousProfilingMutationService/ContinuousProfilingQueryServiceCoreModuleProvider.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:29service PprofTask 两个 RPC:

  • getPprofTaskCommands → 拉 pprof 任务(:33
  • collect (双向 stream PprofData/PprofCollectionResponse) → 上传 pprof 二进制(:31

PprofData 第一帧带 PprofMetaData{service, serviceInstance, taskId, type(状态), contentSize}Pprof.proto:59-68),后续帧 content 是 pprof 字节;状态枚举 PprofProfilingStatusPROFILING_SUCCESS / EXECUTION_TASK_ERROR / TERMINATED_BY_OVERSIZE:46-53)。

关键 handler 与配置

PprofServiceHandlerskywalking-pprof-receiver-plugin/.../handler/PprofServiceHandler.java:53)实现 PprofTaskGrpc.PprofTaskImplBase,构造时注入两个关键参数(来自 PprofModuleConfigPprofModuleConfig.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(落临时文件再解析)——前者省去磁盘挂载,后者省内存。PprofByteBufCollectionObserverPprofByteBufCollectionObserver.java:43)用 ByteBuffer 攒齐字节,onCompleted 时调 PprofParser.dumpTree(buf) 解析成 FrameTree,包成 PprofProfilingData source(parsePprofAndStoragePprofByteBufCollectionObserver.java:145-155)。File 版用 Files.createTempFile + FileOutputStream

memoryParserEnabled 的取舍
默认 true(内存解析)能避免 OAP 没挂卷导致落盘失败,但大 pprof 文件会吃内存。改成 false 省内存却要求容器挂了可写卷——没挂卷会直接报错。这是个"内存 vs 磁盘"的二选一,没有免费午餐。

library-pprof-parser 如何解析

PprofParserlibrary-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,内联帧用 ; 拼。

支持的事件类型 PprofEventTypeserver-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,查询时直接合并,省去每次查询都重新解析原始二进制。

数据存储

  • 任务表 PprofTaskRecordPprofTaskRecord.java:48):pprof_task,NoneStream,scope PPROF_TASK,字段 taskId / serviceId / serviceInstanceIds(JSON) / createTime / events / duration / dumpPeriod
  • 数据表 PprofProfilingDataRecordPprofProfilingDataRecord.java:41):pprof_profiling_data,Record,scope PPROF_PROFILING_DATAdataBinaryFrameTree 的 JSON(不是原始 pprof!PprofProfilingDataDispatcher.java:35GSON.toJson(source.getFrameTree()).getBytes() 转换;查询时 GSON.fromJson 反序列化,PprofQueryService.java:96)。先经 PprofProfilingData source(scope PPROF_PROFILING_DATA,含 eventType + frameTree)由 PprofProfilingDataDispatcher 落库。
  • 日志表 PprofTaskLogRecord:任务下发/执行/错误日志。

任务下发与 thread profiling 一样走命令 + 缓存:PprofTaskCachePprofTaskCache.java:32,写后 1 分钟过期,:43)。getPprofTaskCommandsPprofServiceHandler.java:79)还会按 serviceInstanceIds 过滤——pprof 任务可以限定只发给某些实例(:97-100)。

UI 查询

PprofQueryPprofQuery.java:40)+ PprofMutationqueryPprofTaskListqueryPprofAnalyze(按 taskId+instanceIds 查数据,PprofQueryService.queryPprofData 把多个 instance 的 FrameTreePprofMergeBuilder 合并成一棵,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:29service AsyncProfilerTaskgetAsyncProfilerTaskCommands + collect(双向 stream AsyncProfilerData/AsyncProfilerCollectionResponse)。结构与 pprof proto 几乎对称:第一帧 AsyncProfilerMetaData{type, contentSize}AsyncProfiler.proto:60-69),后续 content 是 JFR 二进制;状态枚举 AsyncProfilingStatusPROFILING_SUCCESS / EXECUTION_TASK_ERROR / TERMINATED_BY_OVERSIZE:51-58)。

关键 handler 与配置

AsyncProfilerServiceHandlerskywalking-async-profiler-receiver-plugin/.../handler/AsyncProfilerServiceHandler.java:54)实现 AsyncProfilerTaskGrpc.AsyncProfilerTaskImplBase,构造时注入(来自 AsyncProfilerModuleConfigAsyncProfilerModuleConfig.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

JFRParserlibrary-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 文件可同时含多种事件类型的树。
  • 支持的事件类型 JFREventTypelibrary-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 监护",一个像"对某个慢工位贴身跟拍"。

数据存储

  • 任务表 AsyncProfilerTaskRecordAsyncProfilerTaskRecord.java:46):async_profiler_task,NoneStream,scope ASYNC_PROFILER_TASK,字段 taskId / serviceId / serviceInstanceIds / createTime / duration / events(JSON List) / execArgsexecArgs 是传给 async-profiler 的命令行参数,如 event=cpu,interval=10ms——这是 async-profiler 的原生用法,agent 把参数透传给工具进程。
  • 数据表 JFRProfilingDataRecordJFRProfilingDataRecord.java:44):jfr_profiling_data,Record,scope JFR_PROFILING_DATA,字段 taskId / instanceId / eventType / uploadTime / dataBinary。多了 eventType 维度(一个任务一次上传可分多种事件),id()eventType:76-84)。dataBinary 同样存 FrameTree 的 JSONJFRProfilingDataDispatcher.java:36GSON.toJson,查询时 GSON.fromJsonAsyncProfilerQueryService.java:96)。先经 source → JFRProfilingDataDispatcher 落库。
  • 日志表 AsyncProfilerTaskLogRecord

UI 查询

AsyncProfilerQueryAsyncProfilerQuery.java:42)+ AsyncProfilerMutationqueryAsyncProfilerTaskList:60)、queryAsyncProfilerAnalyze(按 taskId+instanceIds+eventType 查,:67AsyncProfilerQueryService.queryJFRDataJFRMergeBuilder 合并多实例的 FrameTreeAsyncProfilerQueryService.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-parserPprofParser/FrameTreeBuilder library-async-profiler-jfr-parserJFRParser/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 CacheProfileTaskCache/PprofTaskCache/AsyncProfilerTaskCache,均写后 1 分钟过期;eBPF 按 process 反查),都受 CoreModuleProvider 集中注册的 Mutation/Query Service 驱动(CoreModuleProvider.java:341-367)。任务表统一用 NoneStreamProcessor,采样数据表统一用 RecordStreamProcessor

核心差异六条:

  1. 侵入性 — eBPF 是唯一不需要在业务进程内装探针的,适合无法改代码或语言五花八门的环境。
  2. off-CPU / 阻塞剖析 — 只有 eBPF 有原生 off-CPU;async-profiler 通过锁事件(LOCK)间接覆盖;thread/pprof 的 block/mutex 是 Go/Java agent 内采的。
  3. 网络剖析 — 只有 eBPF 有 L4/L7 + mesh��AccessLogServiceHandler),其它三种纯函数栈。
  4. 数据存储格式 — thread/eBPF 存原始 protobuf 字节(stackBinary/dataBinary),查询时反序列化;pprof/async-profiler 在接收时就解析成 FrameTree JSON 存库,查询时直接 GSON.fromJson 合并——用空间换 CPU,查询快但存储大。
  5. 持续/自动触发 — 只有 eBPF 有 ContinuousProfiling(监控指标超阈值自动拉起 profiling 任务)。
  6. 与 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。四种都能生成火焰图,区别在于谁能拍、拍什么、要不要侵入。