Введение: почему профилирование Go — это маст-хев для разработчика
Go славится своей производительностью, но даже самый быстрый код может тормозить из-за неэффективных алгоритмов, утечек памяти или неправильной работы с горутинами. Профилирование — это единственный способ точно узнать, где ваше приложение тратит ресурсы. В этой статье мы разберем практические методы профилирования Go кода с помощью встроенных инструментов: pprof, trace и benchstat. Вы научитесь находить узкие места и устранять их.
1. CPU профилирование с pprof: ищем горячие точки
CPU профилирование показывает, какие функции потребляют больше всего процессорного времени. Встроенный пакет runtime/pprof позволяет легко собирать профили.
1.1. Добавляем профилирование в код
Рассмотрим пример простого HTTP-сервера, который выполняет тяжелые вычисления:
package main
import (
"fmt"
"log"
"net/http"
_ "net/http/pprof"
"os"
"runtime/pprof"
)
func heavyTask() {
sum := 0
for i := 0; i < 1_000_000; i++ {
sum += i * i
}
_ = sum
}
func handler(w http.ResponseWriter, r *http.Request) {
heavyTask()
fmt.Fprintf(w, "Done")
}
func main() {
f, _ := os.Create("cpu.prof")
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
http.HandleFunc("/", handler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
После запуска сервера и нескольких запросов, у вас появится файл cpu.prof. Анализируем его командой:
go tool pprof -http=:8081 cpu.prof
В браузере откроется интерактивный граф. Обратите внимание на красные узлы — они указывают на самые "горячие" функции. В нашем случае это heavyTask.
1.2. Интерпретация результатов
В веб-интерфейсе pprof вы увидите:
- Graph — граф вызовов с размерами узлов, пропорциональными времени выполнения.
- Flame Graph — "огненный" график, где длинные полосы — самые медленные участки.
- Top — таблица функций, отсортированных по времени CPU.
Если вы видите, что heavyTask занимает 90% времени, значит, нужно оптимизировать именно её (например, заменить цикл на формулу суммы квадратов).
2. Профилирование памяти: ищем утечки и аллокации
Утечки памяти в Go — редкость, но избыточные аллокации могут сильно замедлить приложение. Используем runtime/pprof для heap-профиля.
2.1. Сбор heap профиля
Добавим в код сборку профиля памяти по сигналу SIGUSR1:
import (
"os"
"os/signal"
"runtime/pprof"
"syscall"
)
func main() {
// ... ваш код ...
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGUSR1)
go func() {
<-sigs
f, _ := os.Create("mem.prof")
pprof.WriteHeapProfile(f)
f.Close()
os.Exit(0)
}()
// ...
}
После отправки сигнала kill -SIGUSR1 <PID> получите файл mem.prof. Анализируем:
go tool pprof -http=:8082 mem.prof
2.2. Типичные проблемы с памятью
- Большое количество мелких аллокаций — например, создание строк через
+вместоstrings.Builder. - Удержание ссылок на большие объекты — после использования нужно обнулять указатели.
- Горутины, которые не завершаются — каждая горутина потребляет минимум 4 КБ стека.
Пример неоптимального кода:
func badConcat(n int) string {
s := ""
for i := 0; i < n; i++ {
s += "x" // каждая итерация создает новую строку
}
return s
}
Оптимизация:
func goodConcat(n int) string {
var b strings.Builder
b.Grow(n)
for i := 0; i < n; i++ {
b.WriteByte('x')
}
return b.String()
}
Профиль покажет, что badConcat создает тысячи аллокаций, а goodConcat — всего одну.
3. Трассировка горутин и блокировок с go tool trace
Если приложение "зависает" или работает медленно из-за блокировок, используйте трассировку. Она показывает, когда горутины спят, ожидают ввода-вывода или блокируются на мьютексах.
3.1. Включаем трассировку
import (
"os"
"runtime/trace"
)
func main() {
f, _ := os.Create("trace.out")
trace.Start(f)
defer trace.Stop()
// ваш код
}
После выполнения программы анализируем:
go tool trace trace.out
Откроется веб-интерфейс с временной шкалой.
3.2. Чтение трассировки
Основные элементы:
- Goroutines — цветные полосы, показывающие состояние каждой горутины (зеленый — выполняется, желтый — ожидание, красный — блокировка).
- Network/Syscall — когда горутина ждет ввода-вывода.
- Mutex — если горутина ждет освобождения мьютекса.
Если вы видите длинные красные участки, значит, есть проблема с блокировками. Например, если 10 горутин ждут один мьютекс, стоит подумать о разделении данных или использовании sync.Map.
4. Бенчмарки и benchstat: сравниваем производительность
Бенчмарки — это не профилирование, но они помогают измерить влияние изменений. Используйте testing