pprof — menemukan di mana waktunya habis
Menebak di mana aplikasi lambat hampir selalu salah. pprof memberi jawabannya dalam dua menit, dan ia sudah ada di dalam setiap binari Go tanpa dependensi apa pun.
Intisari
- Impor
net/http/pprofuntuk mendapat endpoint profil — di port terpisah, tidak pernah terbuka ke publik. - Profil CPU menunjukkan di mana waktu habis;
heapmenunjukkan apa yang memakan memori. go tool pprof -http=:8081membuka graf api (flame graph) di browser.- Overhead profil CPU sekitar 5% — aman diambil dari produksi selama 30 detik.
goroutineadalah profil pertama yang dilihat saat memori naik pelan tanpa sebab.
Memasangnya dengan aman
import (
"net/http"
_ "net/http/pprof" // mendaftarkan handler ke DefaultServeMux
)
// ❌ JANGAN: kalau handler utamamu memakai DefaultServeMux, seluruh
// endpoint pprof jadi terbuka ke internet.
// ✅ Port terpisah yang hanya bisa dijangkau dari dalam VPC / sidecar.
func mulaiDebug(addr string, log *slog.Logger) {
go func() {
srv := &http.Server{
Addr: addr, // "127.0.0.1:6060" atau port internal
Handler: http.DefaultServeMux,
ReadHeaderTimeout: 5 * time.Second,
}
if err := srv.ListenAndServe(); err != nil &&
!errors.Is(err, http.ErrServerClosed) {
log.Error("server debug berhenti", "err", err)
}
}()
}
Endpoint pprof yang terbuka adalah kebocoran dan sekaligus alat serangan. Profil heap memuat
potongan data yang sedang di memori; /debug/pprof/cmdline memuat argumen proses; dan
meminta profil CPU 60 detik berulang kali membebani prosesmu. Di ECS, jadikan port debug tidak
terekspos di target group mana pun (Fase 10).
Profil yang tersedia
| Profil | Menjawab | Kapan dipakai |
|---|---|---|
profile (CPU) | Fungsi mana yang memakai CPU | CPU tinggi, respons lambat |
heap | Apa yang menahan memori sekarang | Memori tinggi, OOM |
allocs | Total alokasi sejak proses mulai | Tekanan GC tinggi |
goroutine | Semua goroutine dan tumpukannya | Kebocoran goroutine (Fase 2) |
block | Menunggu channel dan mutex | Throughput rendah, CPU rendah |
mutex | Perebutan lock | Tidak naik meski core ditambah |
trace | Lini masa penjadwal dan GC | Latensi ekor yang tidak jelas |
Mengambil dan membaca
# CPU selama 30 detik, langsung buka antarmuka web
go tool pprof -http=:8081 http://localhost:6060/debug/pprof/profile?seconds=30
# Heap saat ini
go tool pprof -http=:8081 http://localhost:6060/debug/pprof/heap
# Goroutine — versi teks paling cepat dibaca
curl -s 'http://localhost:6060/debug/pprof/goroutine?debug=1' | head -40
# Simpan dulu, analisis nanti (mis. dari produksi)
curl -o cpu.pprof 'http://localhost:6060/debug/pprof/profile?seconds=30'
go tool pprof -http=:8081 ./bin/server cpu.pprof
| Tampilan | Untuk |
|---|---|
| Flame Graph | Melihat sekaligus; lebar batang = porsi waktu |
| Top | Daftar fungsi paling mahal |
| Source | Biaya per baris kodemu |
| Graph | Hubungan pemanggilan |
Baca kolom cum, bukan flat, saat mencari penyebab.
flat adalah waktu di fungsi itu sendiri; cum termasuk semua yang dipanggilnya.
Handler-mu hampir selalu punya flat mendekati nol dan cum besar — dan yang
ingin kamu temukan adalah titik di mana cum besar berubah jadi flat besar.
Pola yang khas dan artinya
| Yang terlihat di profil | Biasanya berarti |
|---|---|
runtime.mallocgc besar | Terlalu banyak alokasi (Fase 8) |
runtime.gcBgMarkWorker besar | GC bekerja keras — kurangi alokasi atau naikkan GOGC |
encoding/json.Marshal besar | Respons terlalu gemuk, atau serialisasi diulang |
runtime.selectgo, chanrecv | Menunggu — cek profil block |
sync.(*Mutex).Lock | Perebutan lock — cek profil mutex |
syscall.Syscall besar | Menunggu I/O; bukan masalah CPU |
| Profil merata tanpa puncak | Bottleneck-nya di luar proses — biasanya database |
Menemukan kebocoran goroutine
curl -s 'http://localhost:6060/debug/pprof/goroutine?debug=1' | head -5
goroutine profile: total 48213
41077 @ 0x43e1c5 0x44f3f8 0x7c2a41
# 0x7c2a40 contoh.com/toko/internal/notif.(*Pengirim).kirim+0x120
↑ 41.077 goroutine menumpuk di satu baris — ini kebocoran,
bukan beban tinggi.
Bandingkan dua profil goroutine berselang sepuluh menit. Kalau jumlahnya naik terus tanpa
kenaikan trafik, ada goroutine yang tidak pernah selesai — dan tumpukannya langsung menunjuk barisnya.
Go 1.26 juga punya profil goroutineleak eksperimental yang melakukan analisis ini untukmu
(Fase 2).
Membandingkan sebelum dan sesudah
go tool pprof -http=:8081 -base lama.pprof baru.pprof
Tampilannya jadi selisih: hijau untuk yang berkurang, merah untuk yang bertambah. Ini cara tercepat membuktikan sebuah optimasi benar-benar bekerja — dan cara tercepat menemukan regresi setelah deploy.
Profil terus-menerus di produksi
// Perekam terbang (flight recorder), stabil sejak Go 1.25: simpan jejak
// beberapa detik terakhir di memori, tulis HANYA saat sesuatu terjadi.
fr := trace.NewFlightRecorder(trace.FlightRecorderConfig{
MinAge: 5 * time.Second,
})
_ = fr.Start()
// Di middleware: kalau ada permintaan yang sangat lambat, abadikan
// beberapa detik sebelum kejadiannya.
if durasi > 2*time.Second {
var buf bytes.Buffer
if _, err := fr.WriteTo(&buf); err == nil {
simpanKeS3(ctx, buf.Bytes())
}
}
Ini menjawab masalah klasik "lambatnya cuma sesekali dan tidak bisa direproduksi". Alih-alih mencoba menangkapnya saat terjadi, perekam terbang selalu merekam ke buffer melingkar dan hanya menulis keluar saat pemicunya terpenuhi — jejak dari sebelum kejadian, bukan sesudahnya.
Latihan: tambahkan endpoint yang sengaja boros (menyambung string dalam loop sejuta kali),
bebani dengan hey -n 200 -c 10, lalu ambil profil CPU 15 detik dan buka flame graph-nya.
Temukan fungsimu, perbaiki dengan strings.Builder, ambil profil kedua, dan bandingkan
dengan -base.
Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.