При ежедневной разработке Golang иногда необходим анализ производительности программ, и Golang предоставляет встроенный pprof инструмент, помогающий достичь этого.

Пакет pprof это runtime/pprof .

pprof в основном используется для анализа производительности программы, включая следующее содержимое:

  • Профилирование ЦП: отслеживает использование ЦП своими программами Go в соответствии с указанным сбором времени и может определить, в каком сегменте программы программа Go потребляет ЦП в течение длительного времени.
  • Профилирование памяти: используется для анализа использования области стека памяти программой и для обнаружения утечек памяти.
  • Профилирование горутины: параллельный анализ производительности, используемый для составления отчетов об операциях горутины и их количестве для текущей среды выполнения.
  • Профилирование блоков. Егоможно использовать для записи количества времени, которое горутина тратит на ожидание общих ресурсов.
  • Профилирование мьютексов.Профилирование мьютексов похоже на профилирование блоков, но оно регистрирует только ожидания или задержки, вызванные конфликтом блокировок.

Для нас наиболее часто используемыми методами профилирования являются CPU profiling и Memory profiling. Поэтому основное содержание этого блога будет сосредоточено на использовании этих двух методов.

Профилирование ЦП.

Чтобы записать производительность с помощью pprof, просто вызовите pprof.StartCPUProfile(file) в нужном месте.

func StartCPUProfile(w io.Writer) error {
   // The runtime routines allow a variable profiling rate,
   // but in practice operating systems cannot trigger signals
   // at more than about 500 Hz, and our processing of the
   // signal is not cheap (mostly getting the stack trace).
   // 100 Hz is a reasonable choice: it is frequent enough to
   // produce useful data, rare enough not to bog down the
   // system, and a nice round number to make it easy to
   // convert sample counts to seconds. Instead of requiring
   // each client to specify the frequency, we hard code it.
   const hz = 100
  
   cpu.Lock()
   defer cpu.Unlock()
   if cpu.done == nil {
      cpu.done = make(chan bool)
   }
   // Double-check.
   if cpu.profiling {
      return fmt.Errorf("cpu profiling already in use")
   }
   cpu.profiling = true
   runtime.SetCPUProfileRate(hz)
   go profileWriter(w)
   return nil
}

Этот метод принимает параметр типа io.Writer, где pprof запишет результат анализа.

После того, как код будет проанализирован, вызовите StopCPUProfile(), чтобы завершить анализ выполнения кода между StartCPUProfile() и StopCPUProfile().

package main

func main() {
    fmt.Println("Starting CPU-intensive task...")
    start := time.Now()

    // Create cpu profiling
    file, err := os.OpenFile("cpu.profiling", os.O_CREATE|os.O_RDWR, 0655)
    if err != nil {
        log.Fatal("open file error", err)
    }
    defer file.Close()
    pprof.StartCPUProfile(file)
    defer pprof.StopCPUProfile()
    
    // Perform intensive calculations
    calculate()
    
    elapsed := time.Since(start)
    fmt.Printf("Finished CPU-intensive task. Elapsed time: %s\n", elapsed)
}

func calculate() {
    for i := 0; i < 1000000000; i++ {
        _ = math.Sqrt(float64(i))
    }
}

При выполнении go run main.go будет создан файл cpu.profiling, в котором записано состояние выполнения программы.

Файл cpu.profiling представляет собой двоичный файл, который невозможно понять при непосредственном открытии. Записанные в него данные о производительности не подходят для чтения человеком, поэтому нам необходимо разобрать содержимое файла с помощью команды go tool pprof.

Мы можем выполнить команду go tool pprof cpu.profiling для анализа сгенерированных данных о производительности.

$ go tool pprof cpu.profiling

pprof go tool pprof cpu.profiling
Type: cpu
Time: May 6, 2023 at 10:36am (CST)
Duration: 400.73ms, Total samples = 240ms (59.89%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top
Showing nodes accounting for 240ms, 100% of 240ms total
      flat  flat%   sum%        cum   cum%
     240ms   100%   100%      240ms   100%  main.calculate (inline)
         0     0%   100%      240ms   100%  main.main
         0     0%   100%      240ms   100%  runtime.main
(pprof)

Описание основных параметров:

  • flat: Для запуска самой функции требуется время.
  • flat%: пропорция flat.
  • cum: общее время выполнения текущей функции и всех стеков вызовов, которые происходят внутри нее.
  • cum%: пропорция cum.
  • sum%: это сумма flat% каждой строки и flat% всех строк выше.

В приведенных выше результатах мы можем заметить, что метод main.calculate требует наибольшего времени, что составляет 100%.

Мы также можем использовать команды top1 и topN для просмотра самых дорогих функций 1 и N соответственно.

Однако, поскольку у нас есть только результаты 3, команда topN (if N > 3) отобразит тот же результат, что и команда top3.

Когда мы идентифицируем функцию, которая потребляет значительное количество времени, мы можем использовать команду list <function> для просмотра стека вызовов функции и времени, затрачиваемого на каждый путь вызова.

Это предоставляет более подробную информацию о том, как вызывается функция и где тратится время внутри функции.

(pprof) list calculate
Total: 240ms
ROUTINE ======================== main.calculate in /pprof/main.go
     240ms      240ms (flat, cum)   100% of Total
         .          .     41:
         .          .     42:   elapsed := time.Since(start)
         .          .     43:   fmt.Printf("Finished CPU-intensive task. Elapsed time: %s\n", elapsed)
         .          .     44:}
         .          .     45:
     240ms      240ms     46:func calculate() {
         .          .     47:   for i := 0; i < 1000000000; i++ {
         .          .     48:           _ = math.Sqrt(float64(i))
         .          .     49:   }
         .          .     50:}
         .          .     51:
(pprof) 

Вы можете ввести команду help, чтобы получить список доступных команд и просмотреть инструкции по их использованию. Кроме того, вы можете ввести help [command], чтобы получить подробную информацию о конкретной команде.

(pprof) help
  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
    gif              Outputs a graph image in GIF format
    gv               Visualize graph through gv
    kcachegrind      Visualize report in KCachegrind
    list             Output annotated source for functions matching regexp
    ...

Профилирование памяти.

Подобно профилированию ЦП, мы можем использовать пакет pprof для анализа использования памяти кучи нашей программой.

Для этого нам нужно вызвать функцию WriteHeapProfile() из пакета runtime/pprof, которая записывает данные профиля кучи в место назначения io.Writer.

package main

func main() {
    fmt.Println("Starting memory-intensive task...")
    start := time.Now()
    
    // Create memory profiling
    file, err := os.OpenFile("mem.profiling", os.O_CREATE|os.O_RDWR, 0655)
    if err != nil {
        log.Fatal("open file error", err)
    }
    defer file.Close()
    
    // Perform intensive calculations
    calculate()
    
    elapsed := time.Since(start)
    fmt.Printf("Finished memory-intensive task. Elapsed time: %s\n", elapsed)
    
    // Record memory usage
    if err := pprof.WriteHeapProfile(file); err != nil {
        log.Fatal(err)
    }
}

func calculate() {
    buf := make([]byte, 1024*1024*100)
    var m map[int]string = make(map[int]string)
    for i := 0; i < 50; i++ {
        m[i] = string(buf)
    }
}

При выполнении go run main.go будет создан файл mem.profiling, в котором записано состояние выполнения программы.

Здесь мы можем использовать команду go tool pprof <file> для анализа использования памяти с использованием данных профиля кучи, сгенерированных WriteHeapProfile().

$ go tool pprof mem.profiling
               
Type: inuse_space
Time: May 6, 2023 at 12:01pm (CST)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top
Showing nodes accounting for 1.86GB, 99.84% of 1.86GB total
Dropped 13 nodes (cum <= 0.01GB)
      flat  flat%   sum%        cum   cum%
    1.86GB 99.84% 99.84%     1.86GB 99.84%  main.calculate (inline)
         0     0% 99.84%     1.86GB 99.84%  main.main
         0     0% 99.84%     1.86GB 99.84%  runtime.main
(pprof) 

Вы также можете использовать команду list <function> в инструменте pprof для просмотра выполнения определенной функции в программе.

(pprof) list calculate
Total: 1.86GB
ROUTINE ======================== main.calculate in /Users/shutiao/Desktop/Mac下软件使用/learn_go/pprof/main.go
    1.86GB     1.86GB (flat, cum) 99.84% of Total
         .          .     43:           log.Fatal(err)
         .          .     44:   }
         .          .     45:}
         .          .     46:
         .          .     47:func calculate() {
     100MB      100MB     48:   buf := make([]byte, 1024*1024*100)
         .          .     49:   var m map[int]string = make(map[int]string)
         .          .     50:   for i := 0; i < 50; i++ {
    1.76GB     1.76GB     51:           m[i] = string(buf)
         .          .     52:   }
         .          .     53:}
         .          .     54:
         .          .     55:func mapString1() {
         .          .     56:   buf := make([]byte, 1024*1024*100)
(pprof) 

В нашем коде мы генерируем строку размером 100 МБ и повторяем ее 50 раз.

График пламени.

Действительно, Flame Graphs — это мощный инструмент визуализации для анализа потребления ЦП и памяти. Они обеспечивают визуальное представление трассировки стека и соответствующего использования ресурсов.

В случае с инструментом Go pprof в более новые версии интегрирована поддержка создания Flame Graphs. Однако для создания Flame Graphs вам необходимо установить инструмент graphviz, который является предварительным условием.

Мак:

brew install graphviz

Чтобы установить Graphviz, вы можете следовать инструкциям на официальном сайте Graphviz. На сайте представлены различные установочные пакеты для разных операционных систем.

Вы можете перейти по следующей ссылке, чтобы найти инструкции по установке и варианты загрузки для различных операционных систем: Страница загрузки Graphviz

После установки graphviz вы можете создать Flame Graph с помощью инструмента pprof, выполнив следующую команду:

go tool pprof -http=localhost:8080 <profile>

Замените <profile> файлом профиля (например, профилем ЦП или профилем кучи).

Эта команда запустит локальный веб-сервер для просмотра визуализации в веб-браузере по адресу localhost:8080.

Вы можете получить доступ к графику пламени, щелкнув опцию «График пламени» в строке меню VIEW.

Удаленный проф.

Если вы столкнулись с проблемами в производственной среде и хотите выполнить профилирование, отправка кода непосредственно на сервер не является подходящим подходом.

Однако вы можете использовать функцию удаленного доступа инструмента pprof для анализа производительности.

Для этого вы можете использовать пакет net/http/pprofpackage для онлайн-анализа производительности.

Это позволяет удаленно получать доступ к файлам профиля и анализировать их.

Если в вашем приложении еще не запущен http-сервер, вам необходимо его запустить. Добавьте net/http и log к вашему импорту и следующий код к вашей основной функции:

go func() {
   log.Println(http.ListenAndServe("localhost:6060", nil))
}()

Далее давайте развернем код в веб-сервисе.

package main

import (
    "fmt"
    "log"
    "html"
    "net/http"
    _ "net/http/pprof"
)

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/cpu/", cpuCalculate)
    mux.HandleFunc("/memory/", memoryCalculate)
    
    // Main server
    server := &http.Server{
        Addr:    ":8088",
        Handler: mux,
    }

    // http pprof server
    go func() {
        log.Println(http.ListenAndServe("localhost:6060", nil))
    }()

    if err := server.ListenAndServe(); err != nil {
        log.Fatal(err)
    }
}

func cpuCalculate(w http.ResponseWriter, r *http.Request) {
    for i := 0; i < 1000000000; i++ {
        _ = math.Sqrt(float64(i))
    }
    fmt.Fprintf(w, "cpu calcaulate done, %q", html.EscapeString(r.URL.Path))
}

func memoryCalculate(w http.ResponseWriter, r *http.Request) {
    buf := make([]byte, 1024*1024*100)
    var m map[int]string = make(map[int]string)
    for i := 0; i < 50; i++ {
        m[i] = string(buf)
    }
    fmt.Fprintf(w, "memory calcaulate done, %q", html.EscapeString(r.URL.Path))
}

Чтобы запустить службу и сгенерировать данные о производительности, вы можете использовать команду go run main.go.

После запуска службы вы можете получить доступ к данным о производительности, используя следующие URL-адреса:

  • http://localhost:8088/cpu/: Этот URL-адрес будет предоставлять данные профилирования ЦП.
  • http://localhost:8088/memory/: Этот URL-адрес будет предоставлять данные профилирования памяти.

Получив доступ к этим URL-адресам, вы можете собрать необходимые данные о производительности для анализа.

Кроме того, вы можете получить доступ к http://localhost:6060/debug/pprof/ для получения более подробной информации.

Этот URL-адрес обеспечивает доступ к различным конечным точкам pprof, что позволяет вам собирать дополнительные данные о производительности и анализировать различные аспекты производительности вашего приложения.

$ go tool pprof -http=localhost:8081 http://localhost:6060/debug/pprof/profile

Fetching profile over HTTP from http://localhost:6060/debug/pprof/profile
Saved profile in /Users/pprof/pprof.samples.cpu.003.pb.gz
Serving web UI on http://localhost:8081

Эта команда извлечет данные профиля ЦП с указанного URL-адреса (http://localhost:6060/debug/pprof/profile) и запустит веб-сервер на localhost:8081 для отображения результатов анализа производительности в веб-интерфейсе.

$ go tool pprof -http=localhost:8081 http://localhost:6060/debug/pprof/heap

Saved profile in /Users/pprof/pprof.alloc_objects.alloc_space.inuse_objects.inuse_space.006.pb.gz
Serving web UI on http://localhost:8081

Эта команда извлечет данные профиля памяти с указанного URL-адреса (http://localhost:6060/debug/pprof/heap) и запустит веб-сервер на localhost:8081 для отображения результатов анализа производительности в веб-интерфейсе.

Для получения дополнительной информации о том, как использовать пакет net/http/pprof в Go, вы можете обратиться к документации по адресу https://pkg.go.dev/net/http/pprof.

Документация содержит подробные объяснения и примеры того, как использовать пакет для включения конечных точек профилирования в ваших веб-приложениях Go, анализа профилей ЦП и памяти и других задач, связанных с профилированием.

Это ценный ресурс для понимания возможностей и использования пакета net/http/pprof для анализа производительности в Go.

В производственной среде обычно ограничивают доступ к конечным точкам профилирования из соображений безопасности.

Поэтому рекомендуется загружать файл профиля из производственной среды для выполнения локального анализа.

Как упоминалось ранее, при использовании команды go tool pprof файл профиля сохраняется локально.

Путь и имя файла могут различаться в зависимости от используемых команды и параметров. Например, команда может сохранить файл профиля как pprof.alloc_objects.alloc_space.inuse_objects.inuse_space.006.pb.gz в каталоге /Users/xxx/.

Загрузив и проанализировав этот файл профиля локально, вы можете выполнить углубленный анализ производительности, не раскрывая конфиденциальную информацию и не открывая порты в производственной среде.

Если вам нравятся такие истории и вы хотите поддержать меня, пожалуйста, хлопните мне в ладоши.

Ваша поддержка очень важна для меня, спасибо.

Повышение уровня кодирования

Спасибо, что являетесь частью нашего сообщества! Перед тем, как ты уйдешь:

  • 👏 Хлопайте за историю и подписывайтесь на автора 👉
  • 📰 Смотрите больше контента в публикации Level Up Coding
  • 💰 Бесплатный курс собеседования по программированию ⇒ Просмотреть курс
  • 🔔 Подписывайтесь на нас: Twitter | ЛинкедИн | "Новостная рассылка"

🚀👉 Присоединяйтесь к коллективу талантов Level Up и найдите прекрасную работу