Skip to content

Performans Analizi

Bir program yazıldıktan sonra, ondan sadece çalışmasını değil, aynı zamanda stabil ve verimli bir uygulama olmasını da bekleriz. Çeşitli testler yoluyla, programın çoğu stabilitesini garanti edebiliriz. Programın verimli olup olmadığı ise performans analizi yapmamızı gerektirir. Önceki içerikte, performans analizinin tek aracı Benchmark ile belirli bir fonksiyon biriminin ortalama yürütme süresini, bellek tahsisi durumunu vb. test etmekti. Ancak gerçekte program performans analizi için gereksinimler bundan çok daha fazlasıdır. Bazen programın genel CPU kullanımını, bellek kullanımını, yığın tahsis durumunu, goroutine durumunu, hotspot kod yollarını vb. analiz etmemiz gerekir. Bu Benchmark'ın karşılayamayacağı bir durumdur. Neyse ki go araç zinciri, geliştiricilerin kullanımı için birçok performans analiz aracı entegre etmiştir. Aşağıda tek tek açıklanacaktır.

Kaçış Analizi

Go'da, değişkenlerin bellek tahsisi derleyici tarafından belirlenir. Genellikle yığın (stack) ve heap olmak üzere iki yere tahsis edilir. Eğer heap'e tahsis edilmesi gereken bir değişken heap'e tahsis edilirse, bu duruma kaçış (escape) denir. Kaçış analizi programdaki bellek tahsis durumunu analiz etmektir. Derleme zamanında yapıldığı için statik analizin bir türüdür.

::: ipucu

Go'nun belleği nasıl tahsis ettiğini öğrenmek için Bellek Tahsisi makalesine gidin.

:::

Yerel İşaretçi Referansı

go
package main

func main() {
  GetPerson()
}

type Person struct {
  Name string
  Mom  *Person
}

func GetPerson() Person {
  mom := Person{Name: "lili"}
  son := Person{Name: "jack", Mom: &mom}
  return son
}

GetPerson fonksiyonunda mom değişkeni oluşturulur. Fonksiyon içinde oluşturulduğu için aslında yığına tahsis edilmeliydi. Ancak son'un Mom alanı tarafından referans alınır ve son fonksiyon dönüş değeri olarak dışarı döndürülür. Bu nedenle derleyici bunu heap'e tahsis eder. Bu çok basit bir örnektir, bu yüzden anlamak için çok fazla çaba gerektirmez. Ancak daha büyük bir projede, kod satırı on binlerce olduğunda, manuel analiz o kadar kolay olmaz. Bu nedenle analiz için araç kullanmak gerekir. Yukarıda belirtildiği gibi bellek tahsisi derleyici tarafından yönetildiğinden, kaçış analizi de derleyici tarafından tamamlanır. Kullanımı çok basittir, aşağıdaki komutu çalıştırmanız yeterlidir:

bash
$ go build -gcflags="-m -m -l"

gcflags yani derleyici gc'nin parametreleri:

  • -m, kod optimizasyon önerilerini yazdırır, iki tane kullanıldığında daha detaylı çıktı verir
  • -l, satır içi optimizasyonu devre dışı bırakır

Çıktı şu şekildedir

bash
$ go build -gcflags="-m -m -l" .
# golearn/example
./main.go:13:2: mom escapes to heap:
./main.go:13:2:   flow: son = &mom:
./main.go:13:2:     from &mom (address-of) at ./main.go:14:35
./main.go:13:2:     from Person{...} (struct literal element) at ./main.go:14:15
./main.go:13:2:     from son := Person{...} (assign) at ./main.go:14:6
./main.go:13:2:   flow: ~r0 = son:
./main.go:13:2:     from return son (return) at ./main.go:15:2
./main.go:13:2: moved to heap: mom

Derleyici açıkça mom değişkeninin kaçış yaptığını ve nedeninin dönüş değerinin fonksiyon içindeki yerel işaretçiyi içermesi olduğunu söyler. Bu durumun dışında kaçış fenomeni oluşabilecek diğer durumlar şunlardır:

::: ipuçları

Eğer kaçış analizi detayları ile ilgileniyorsanız, standart kütüphane cmd/compile/internal/escape/escape.go içinde daha fazla içerik öğrenebilirsiniz.

:::

Closure Referansı

Closure fonksiyon dışındaki bir değişkeni referans alırsa, bu değişken de heap'e kaçar. Bu anlaşılması kolaydır.

go
package main

func main() {
  a := make([]string, 0)
  do(func() []string {
    return a
  })
}

func do(f func() []string) []string {
  return f()
}

Çıktı

$ go build -gcflags="-m -m -l" .
# golearn/example
./main.go:10:9: f does not escape
./main.go:4:2: main capturing by value: a (addr=false assign=false width=24)
./main.go:4:11: make([]string, 0) escapes to heap:
./main.go:4:11:   flow: a = &{storage for make([]string, 0)}:
./main.go:4:11:     from make([]string, 0) (spill) at ./main.go:4:11
./main.go:4:11:     from a := make([]string, 0) (assign) at ./main.go:4:4
./main.go:4:11:   flow: ~r0 = a:
./main.go:4:11:     from return a (return) at ./main.go:6:3
./main.go:4:11: make([]string, 0) escapes to heap
./main.go:5:5: func literal does not escape

Yetersiz Alan

Yığın alanı yetersiz olduğunda da kaçış fenomeni oluşur. Aşağıda oluşturulan slice 1<<15 kapasitesi talep eder

go
package main

func main() {
  _ = make([]int, 0, 1<<15)
}

Çıktı

$ go build -gcflags="-m -m -l" .
# golearn/example
./main.go:4:10: make([]int, 0, 32768) escapes to heap:
./main.go:4:10:   flow: {heap} = &{storage for make([]int, 0, 32768)}:
./main.go:4:10:     from make([]int, 0, 32768) (too large for stack) at ./main.go:4:10
./main.go:4:10: make([]int, 0, 32768) escapes to heap

Bilinmeyen Uzunluk

Slice'ın uzunluğu bir değişken olduğunda, uzunluğu bilinmediğinden kaçış fenomeni oluşur (map oluşmaz)

go
package main

func main() {
  n := 100
  _ = make([]int, n)
}

Çıktı

$ go build -gcflags="-m -m -l" .
# golearn/example
./main.go:5:10: make([]int, n) escapes to heap:
./main.go:5:10:   flow: {heap} = &{storage for make([]int, n)}:
./main.go:5:10:     from make([]int, n) (non-constant size) at ./main.go:5:10
./main.go:5:10: make([]int, n) escapes to heap

Bir diğer özel durum ise fonksiyon parametresi ...any türü olduğunda da kaçış fenomeni oluşabilir

go
package main

import "fmt"

func main() {
  n := 100
  fmt.Println(n)
}

Çıktı

$ go build -gcflags="-m -m -l" .
# golearn/example
./main.go:7:14: n escapes to heap:
./main.go:7:14:   flow: {storage for ... argument} = &{storage for n}:
./main.go:7:14:     from n (spill) at ./main.go:7:14
./main.go:7:14:     from ... argument (slice-literal-element) at ./main.go:7:13
./main.go:7:14:   flow: {heap} = {storage for ... argument}:
./main.go:7:14:     from ... argument (spill) at ./main.go:7:13
./main.go:7:14:     from fmt.Println(... argument...) (call parameter) at ./main.go:7:13
./main.go:7:13: ... argument does not escape
./main.go:7:14: n escapes to heap

Kaçış analizi yapmamızın nedeni, bellek tahsisini bu kadar detaylı kontrol etmemiz esas olarak GC baskısını azaltmaktır. Ancak go c dili değildir, bellek tahsisinin nihai kararı hala derleyicinin elindedir. Aşırı performans gereksinimleri durumları dışında, çoğu zaman bellek tahsisi detaylarına çok fazla odaklanmamıza gerek yoktur. Sonuçta GC'nin ortaya çıkış amacı geliştiricileri özgürleştirmektir.

::: ipucu Küçük Detay

Bazı referans türleri için, artık kullanılmayacağı onaylandıktan sonra, GC'ye bunu geri alabileceğini söylemek için nil olarak ayarlayabiliriz.

go
type Writer struct {
  buf []byte
}

func (w Writer) Close() error {
  w.buff = nil
  return nil
}

:::

pprof

pprof (program profiling), program performansı analizi için güçlü bir araçtır. Program çalışma zamanı verilerinin bir kısmını örnekleme yapar. CPU, bellek, goroutine, kilit, yığın bilgileri ve birçok diğer yönü kapsar. Ardından örnekleme verilerini analiz etmek ve sonuçları göstermek için araçlar kullanılır.

Bu nedenle pprof kullanım adımları sadece iki adımdır:

  1. Veri toplama
  2. Sonuç analizi

Toplama

Veri toplamanın iki yolu vardır: otomatik ve manuel. Her ikisinin de avantajları ve dezavantajları vardır. Bundan önce, bellek ve cpu tüketimini simüle etmek için basit bir fonksiyon yazalım

go
func Do() {
  for i := 0; i < 10; i++ {
    slice := makeSlice()
    sortSlice(slice)
  }
}

func makeSlice() []int {
  var s []int
  for range 1 << 24 {
    s = append(s, rand.Int())
  }
  return s
}

func sortSlice(s []int) {
  slices.Sort(s)
}

Manuel

Manuel toplama kod ile kontrol edilir. Avantajı kontrollü, esnek ve özelleştirilebilir olmasıdır. Kodda doğrudan pprof kullanmak için runtime/pprof paketini import etmeniz gerekir

go
package main

import (
  "log"
  "os"
  "runtime/pprof"
)

func main() {
    Do()
  w, _ := os.Create("heap.pb")
  heapProfile := pprof.Lookup("heap")
  err := heapProfile.WriteTo(w, 0)
  if err != nil {
    log.Fatal(err)
  }
}

pprof.Lookup tarafından desteklenen parametreler aşağıdaki kodda gösterildiği gibidir

go
profiles.m = map[string]*Profile{
    "goroutine":    goroutineProfile,
    "threadcreate": threadcreateProfile,
    "heap":         heapProfile,
    "allocs":       allocsProfile,
    "block":        blockProfile,
    "mutex":        mutexProfile,
}

Bu fonksiyon toplanan verileri belirtilen dosyaya yazar. Yazma sırasında geçirilen sayının birkaç anlamı vardır:

  • 0, sıkıştırılmış Protobuf verilerini yazar, okunabilirliği yoktur
  • 1, metin formatında veri yazar, okunabilir, http arayüzü bu tür verileri döndürür
  • 2, sadece goroutine için kullanılabilir, panic stili yığın bilgilerini yazdırır anlamına gelir

CPU verilerini toplamak için ayrı olarak pprof.StartCPUProfile fonksiyonu kullanılır. Örnekleme için belirli bir süre gerekir ve ham veriler okunamaz. Aşağıda gösterildiği gibi

go
package main

import (
  "log"
  "os"
  "runtime/pprof"
  "time"
)

func main() {
    Do()
  w, _ := os.Create("cpu.out")
  err := pprof.StartCPUProfile(w)
  if err != nil {
    log.Fatal(err)
  }
  time.Sleep(time.Second * 10)
  pprof.StopCPUProfile()
}

Trace verilerini toplamak da aynı şekildedir

go
package main

import (
  "log"
  "os"
  "runtime/trace"
  "time"
)

func main() {
    Do()
  w, _ := os.Create("trace.out")
  err := trace.Start(w)
  if err != nil {
    log.Fatal(err)
  }
  time.Sleep(time.Second * 10)
  trace.Stop()
}

Otomatik

net/http/pprof paketi yukarıdaki analiz fonksiyonlarını http arayüzlerine paketler ve varsayılan rotaya kaydeder. Aşağıda gösterildiği gibi

go
package pprof

import ...

func init() {
    http.HandleFunc("/debug/pprof/", Index)
    http.HandleFunc("/debug/pprof/cmdline", Cmdline)
    http.HandleFunc("/debug/pprof/profile", Profile)
    http.HandleFunc("/debug/pprof/symbol", Symbol)
    http.HandleFunc("/debug/pprof/trace", Trace)
}

Bu, pprof veri toplamayı tek tuşla çalıştırmamıza olanak tanır

go
package main

import (
  "net/http"
    // Bu paketi import etmeyi unutmayın
  _ "net/http/pprof"
)

func main() {
    go func(){
        http.ListenAndServe(":8080", nil)
    }
    for {
        Do()
    }
}

Bu noktada tarayıcıda http://127.0.0.1:8080/debug/pprof adresini ziyaret ederseniz, böyle bir sayfa görünür

Sayfada birkaç seçenek bulunur. Şunları temsil ederler:

  • allocs: Bellek tahsisi örnekleme
  • block: Senkronizasyon ilkelilerinin engelleme takibi
  • cmdline: Mevcut programın komut satırı çağrısı
  • goroutine: Tüm goroutine'leri takip et
  • heap: Canlı nesnelerin bellek tahsisi örnekleme
  • mutex: Karşılıklı kilit ile ilgili bilgi takibi
  • profile: CPU analizi, bir süre analiz eder ve bir dosya indirir
  • threadcreate: Yeni OS iş parçacığı oluşturma nedenlerinin analizi
  • trace: Mevcut program yürütme durumunun takibi, aynı zamanda bir dosya indirir

Buradaki verilerin çoğunun okunabilirliği yüksek değildir. Esas olarak araç analizi için kullanılır. Aşağıdaki resimde gösterildiği gibi

Spesifik analiz çalışması daha sonra yapılacaktır. profile ve trace iki seçeneği dışında, web sayfasında veri dosyasını indirmek istiyorsanız, query parametresi debug=1'i kaldırabilirsiniz. Bu arayüzleri varsayılan rota yerine kendi rotanıza da entegre edebilirsiniz. Aşağıda gösterildiği gibi

go
package main

import (
  "net/http"
  "net/http/pprof"
)

func main() {
  mux := http.NewServeMux()
  mux.HandleFunc("/trace", pprof.Trace)
  servre := &http.Server{
    Addr:    ":8080",
    Handler: mux,
  }
  servre.ListenAndServe()
}

Bu şekilde, diğer web framework'lerine de entegre edilebilir. Örneğin gin, iris vb.

Analiz

Toplanan veri dosyalarını aldıktan sonra, analiz etmek için iki yol vardır: komut satırı veya web sayfası. Her ikisi de pprof komut satırı aracını kullanır. go bu aracı varsayılan olarak entegre eder, bu nedenle ekstra indirme gerekmez.

pprof açık kaynak adresi: google/pprof: pprof is a tool for visualization and analysis of profiling data (github.com)

Komut Satırı

Toplanan veri dosyasını parametre olarak kullanın

bash
$ go tool pprof heap.pb

Eğer veriler web tarafından toplanmışsa, dosya adı yerine web url kullanın.

bash
$ go tool pprof -http :8080 http://127.0.0.1/debug/pprof/heap

Ardından etkileşimli bir komut satırı görünür

bash
15:27:38.3266862 +0800 CST
Type: inuse_space
Time: Apr 15, 2024 at 3:27pm (CST)
No samples were found with the default sample value type.
Try "sample_index" command to analyze different sample values.
Entering interactive mode (type "help" for commands, "o" for options)
(pprof)

help yazarak diğer komutları görüntüleyebilirsiniz

  Commands:
    callgrind        Outputs a graph in callgrind format
    comments         Output all profile comments
    disasm           Output assembly listings annotated with samples
    dot              Outputs a graph in DOT format
    eog              Visualize graph through eog
    evince           Visualize graph through evince
  ...

Komut satırında verilere bakmak için genellikle top komutu kullanılır. traces komutu da kullanılabilir ancak çıktısı çok uzundur. top komutu sadece genel bir bakış için kullanılır.

(pprof) top 5
Showing nodes accounting for 117.49MB, 100% of 117.49MB total
      flat  flat%   sum%        cum   cum%
  117.49MB   100%   100%   117.49MB   100%  main.makeSlice (inline)
         0     0%   100%   117.49MB   100%  main.Do
         0     0%   100%   117.49MB   100%  main.main
         0     0%   100%   117.49MB   100%  runtime.main

Buradaki bazı göstergeleri kısaca tanıtayım (cpu için de aynı):

  • flat, mevcut fonksiyonun tükettiği kaynağı temsil eder
  • cum, mevcut fonksiyon ve sonraki çağrı zincirinin tükettiği toplam kaynak
  • flat%, flat/total
  • cum%, cum/total

Tüm çağrı yığınının bellek kullanımının 117.49MB olduğunu açıkça görebiliriz. Do fonksiyonu kendisi hiçbir şey yapmadığından, sadece diğer fonksiyonları çağırdığından, flat göstergesi 0'dır. Slice oluşturma işi makeSlice fonksiyonu tarafından yapıldığından, flat göstergesi 100%'dür.

Görselleştirme formatına dönüştürebiliriz. pprof oldukça fazla formatı destekler. Örneğin pdf, svg, png, gif vb. (Graphviz yüklenmesi gerekir).

(pprof) png
Generating report in profile001.png

Resim aracılığıyla tüm çağrı yığınının bellek durumunu daha net görebiliriz.

Kaynak kodu şeklinde görüntülemek için list komutunu kullanabiliriz

(pprof) list Do
Total: 117.49MB
ROUTINE ======================== main.Do in D:\WorkSpace\Code\GoLeran\golearn\example\main.go
         0   117.49MB (flat, cum)   100% of Total
         .          .     21:func Do() {
         .          .     22:   for i := 0; i < 10; i++ {
         .   117.49MB     23:           slice := makeSlice()
         .          .     24:           sortSlice(slice)
         .          .     25:   }
         .          .     26:}
         .          .     27:
         .          .     28:func makeSlice() []int {

Resim ve kaynak kodu için, web ve weblist komutları ile tarayıcıda resim ve kaynak kodu görüntülenebilir.

Web Sayfası

Bundan önce, verilerin daha çeşitli olması için simüle edilen fonksiyonu biraz değiştirelim

go
func Do1() {
  for i := 0; i < 10; i++ {
    slice := makeSlice()
    sortSlice(slice)
  }
}

func Do2() {
  for i := 0; i < 10; i++ {
    slice := makeSlice()
    sortSlice(slice)
  }
}

func makeSlice() []int {
  var s []int
  for range 1 << 12 {
    s = append(s, rand.Int())
  }
  return s
}

func sortSlice(s []int) {
  slices.Sort(s)
}

Web analizi sonuçları görselleştirebilir, komut satırını manuel olarak çalıştırmaktan kurtarır. Web analizi kullanırken, sadece aşağıdaki komutu çalıştırmanız gerekir

bash
$ go tool pprof -http :8080 heap.pb

Eğer veriler web tarafından toplanmışsa, dosya adı yerine web url kullanın

bash
$ go tool pprof -http :8080 http://127.0.0.1:9090/debug/pprof/heap
$ go tool pprof -http :8080 http://127.0.0.1:9090/debug/pprof/profile
$ go tool pprof -http :8080 http://127.0.0.1:9090/debug/pprof/goroutine

::: ipucu

Verilerin nasıl analiz edileceği hakkında daha fazla bilgi için pprof: How to read the graph adresine gidin

:::

Web sayfasında toplam 6 görüntülenebilir öğe vardır:

  • Top, komut top ile aynı
  • Graph, çizgi grafik
  • Flame Graph, alev grafiği
  • Peek,
  • Source, kaynak kodu görüntüleme
  • Disassemble, tersine montaj görüntüleme

Bellek için dört boyut analiz edilebilir:

  • alloc_objects: Şu ana kadar tahsis edilen tüm nesne sayısı, serbest bırakılanlar dahil
  • alloc_spcae: Şu ana kadar tahsis edilen tüm bellek alanı, serbest bırakılanlar dahil
  • inuse_objects: Kullanımda olan nesne sayısı
  • inuse_space: Kullanımda olan bellek alanı

Bellek analiz grafiği

Yukarıdaki resimde en alttaki beyaz yaprak düğümler farklı boyuttaki nesnelerin kullanımını temsil eder.

CPU analiz grafiği

Çizgi grafik için dikkat edilmesi gereken birkaç nokta vardır:

  • Blokların rengi ne kadar koyu ise, kullanım o kadar yüksektir. Çizgi ne kadar kalınsa, kullanım o kadar yüksektir
  • Düz çizgi doğrudan çağrıyı temsil eder, kesikli çizgi bazı çağrı zincirlerinin atlandığını temsil eder.

Bellek alev grafiği

CPU alev grafiği

Alev grafiği için, yukarıdan aşağıya bakıldığında çağrı zinciri, soldan sağa bakıldığında cum kullanım yüzdesidir.

trace

pprof esas olarak programın kaynak kullanımını analiz etmekten sorumludur. trace ise programın çalışma detaylarını takip etmek için daha uygundur. Veri dosyası önceki ile uyumlu değildir. go tool trace komutu ile ilgili analiz çalışmaları tamamlanır.

Eğer manuel olarak toplanan verilerse, dosya adı parametre olarak kullanılabilir

$ go tool trace trace.out

Eğer otomatik olarak toplanmışsa, aynı mantık geçerlidir

bash
$ curl http://127.0.0.1:8080/debug/pprof/trace > trace.out && go tool trace trace.out

Çalıştırıldıktan sonra bir web sunucusu açılır

2024/04/15 17:15:40 Preparing trace for viewer...
2024/04/15 17:15:40 Splitting trace for viewer...
2024/04/15 17:15:40 Opening browser. Trace viewer is listening on http://127.0.0.1:51805

Açtıktan sonra sayfa yaklaşık olarak şu şekildedir

Burada esas olarak aşağıdaki bölümler bulunur. Bu verileri anlamak oldukça kolay değildir.

  • Event timelines for running goroutines

    • trace by proc: O anda bu işlemcide çalışan goroutine zaman çizelgesini gösterir

    • trace by thread: O anda OS iş parçacığında çalışan goroutine zaman çizelgesini gösterir

    • Goroutine analysis: Her grup ana fonksiyonun goroutine ile ilgili istatistik bilgilerini gösterir

  • Profiles

    • Network blocking profile: Ağ IO nedeniyle engellenen goroutine bilgileri
    • Synchronization blocking profile: Senkronizasyon ilkesi nedeniyle engellenen goroutine bilgileri
    • Syscall profile: Sistem çağrısı nedeniyle engellenen goroutine bilgileri
  • User-defined tasks and regions

    • User-defined tasks: Kullanıcı tanımlı görevlerin ilgili goroutine bilgileri
    • User-defined regions: Kullanıcı tanımlı kod bölgelerinin ilgili goroutine bilgileri
  • Garbage collection metrics

    • Minimum mutator utilization: Son GC'nin maksimum zaman aşımını gösterir

Golang by www.golangdev.cn edit