APM 解决的是"哪里慢了",Profiling 解决的是"为什么慢"。 Metrics 告诉你延迟增加了,Traces 告诉你哪个服务是瓶颈,但只有 Profiling 能精确到某一行代码、某个函数调用、某个锁竞争——找到那 20% 的代码消耗了 80% 的 CPU。
一、APM 架构
1.1 APM vs Metrics/Traces/Logs
APM(Application Performance Monitoring)= 面向应用代码层级的性能监控
对比:
├── Metrics → "系统的 CPU 用了 80%"
├── Traces → "支付服务花了 3s"
├── Logs → "支付服务在第 3 次重试时超时"
└── APM/Profiling → "payment.process() 函数占用 45% CPU,其中 json.Marshal() 占 30%"
APM 核心能力:
├── 代码级性能剖析(函数热点)
├── 内存分配追踪
├── 数据库查询分析(N+1 检测)
├── 外部调用分析
├── 错误追踪(堆栈、影响范围)
└── 用户体验关联(RUM → APM)
1.2 Agent 模式
| 模式 | 实现 | 侵入性 | 语言支持 | 代表 |
|---|---|---|---|---|
| 字节码注入 | Java Agent / .NET Profiler | 低(启动参数) | Java, .NET | New Relic, Dynatrace |
| 源码插桩 | OpenTelemetry SDK | 中(代码修改) | 全语言 | OTel APM |
| eBPF 零侵扰 | Kernel-level hook | 极低 | 全语言 | Pixie, Groundcover |
| Sidecar | Envoy WASM / 独立容器 | 中 | 全语言 | Istio APM |
二、Go 性能剖析(pprof)
2.1 启用 pprof
package main
import (
"net/http"
_ "net/http/pprof" // 自动注册 /debug/pprof 路由
)
func main() {
go func() {
http.ListenAndServe("localhost:6060", nil)
}()
// ...
}
# 采集 30 秒 CPU Profile
curl -s http://localhost:6060/debug/pprof/profile?seconds=30 > cpu.prof
# 采集堆内存 Profile
curl -s http://localhost:6060/debug/pprof/heap > heap.prof
# 采集 Goroutine 阻塞
curl -s http://localhost:6060/debug/pprof/block > block.prof
# 采集锁竞争
curl -s http://localhost:6060/debug/pprof/mutex > mutex.prof
# 分析
go tool pprof -http=:8080 cpu.prof
2.2 火焰图解读
火焰图(CPU Profile):
每个矩形 = 一个函数
宽度 = 该函数在采样中的占比(越宽 = 占用越多 CPU)
颜色 = 区分不同函数(无特殊含义)
Y 轴 = 调用栈深度(越往上调用层级越深)
X 轴 = 按字母排序,非时间顺序
示例:
┌─────────────────────────────────────────────┐
│ runtime.mcall │ ← 最顶层(入口)
├──────────────┬──────────────────────────────┤
│ http.Serve │ db.Query │
├──────┬───────┤ │
│json. │ gzip. │ │
│Encod │Writer │ │
│e │ │ │
└──────┴───────┴──────────────────────────────┘
解读:
- json.Encode 占 30% → JSON 序列化是热点
- gzip.Writer 占 15% → 压缩也是成本
- db.Query 占 25% → 查询也慢
优化方向:缓存序列化结果、预计算 gzip、优化 SQL
2.3 堆内存分析
# Top 内存分配
(pprof) top
Showing nodes accounting for 512MB, 85% of 602MB total
flat flat% sum% cum cum%
200MB 33.22% 33.22% 300MB 49.83% main.processLargeData
150MB 24.92% 58.14% 150MB 24.92% bytes.makeSlice
100MB 16.61% 74.75% 100MB 16.61% json.Marshal
# Inuse(当前存活)vs Total(累计分配)
curl -http://localhost:6060/debug/pprof/heap?gc=1 # 强制 GC 后采集
三、Java async-profiler
# 安装
wget https://github.com/jvm-profiling-tools/async-profiler/releases/...
# CPU 火焰图
./profiler.sh -d 30 -f cpu.svg <java-pid>
# 内存分配
./profiler.sh -d 30 -e alloc -f alloc.svg <java-pid>
# 锁竞争
./profiler.sh -d 30 -e lock -f lock.svg <java-pid>
# Wall-clock(包含 IO 等待,不只是 CPU)
./profiler.sh -d 30 -e wall -f wall.svg <java-pid>
四、Python py-spy
# 安装
pip install py-spy
# 实时 Top(类似 htop)
py-spy top --pid <pid>
# 录制火焰图
py-spy record -o profile.svg --pid <pid>
# 子命令
py-spy dump --pid <pid> # 当前调用栈
py-spy speedscope --pid <pid> # 导出 speedscope 格式
五、Continuous Profiling(持续剖析)
5.1 为什么需要持续剖析
传统 Profiling:
- 本地开发时手动采集
- 生产环境"不敢"跑(担心开销)
- 问题发生时没有数据
Continuous Profiling:
- 7×24 自动采集(低频率、最低开销)
- 历史数据回看("上周这个时候也慢")
- Diff 对比(发布前后性能变化)
- 基线检测(自动发现回归)
5.2 Pyroscope 部署
# docker-compose.yml
version: '3'
services:
pyroscope:
image: grafana/pyroscope:latest
ports:
- "4040:4040"
volumes:
- pyroscope-data:/data
# Go 应用集成
myapp:
build: .
environment:
- PYROSCOPE_SERVER_ADDRESS=http://pyroscope:4040
- PYROSCOPE_APPLICATION_NAME=myapp
command: ["./myapp"]
volumes:
pyroscope-data:
// Go 应用集成 Pyroscope
import "github.com/grafana/pyroscope-go"
pyroscope.Start(pyroscope.Config{
ApplicationName: "myapp",
ServerAddress: "http://pyroscope:4040",
Logger: pyroscope.StandardLogger,
Tags: map[string]string{
"hostname": os.Getenv("HOSTNAME"),
"region": "us-east-1",
},
ProfileTypes: []pyroscope.ProfileType{
pyroscope.ProfileCPU,
pyroscope.ProfileInuseObjects,
pyroscope.ProfileAllocObjects,
pyroscope.ProfileInuseSpace,
pyroscope.ProfileAllocSpace,
pyroscope.ProfileGoroutines,
pyroscope.ProfileMutexCount,
pyroscope.ProfileMutexDuration,
pyroscope.ProfileBlockCount,
pyroscope.ProfileBlockDuration,
},
})
六、Grafana Profiles
Grafana Profiles 数据源 = Pyroscope/Parca/Phlare
查询界面:
├── Compare(对比不同时间段)
│ └── "对比今天和上周同一时间的 CPU 火焰图"
├── Diff(差异分析)
│ └── "发布 v1.3.0 后多了哪些热点函数?"
├── Flame Graph(火焰图)
├── Top Table(函数排行)
└── Labels(按标签过滤:按 pod/region/version)
集成 Grafana Dashboard:
- CPU % 升高时,一键跳转到对应的火焰图
- 内存 OOM 前,查看分配热点
七、Profiling 开销控制
| 类型 | 采样频率 | 运行时开销 | 用途 |
|---|---|---|---|
| CPU | 100Hz / core | 1-5% | 函数热点 |
| Memory | 每 512KB / 次分配 | 5-10% | 分配追踪 |
| Goroutine | 按需 | <1% | 阻塞分析 |
| Lock | 按需 | 2-5% | 锁竞争 |
| Wall-clock | 10Hz | <1% | IO 等待 |
生产环境推荐:CPU 采样 10-50Hz,内存事件采样,其他按需开启。
参考与延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。