Когда ваше Go-приложение начинает двигаться со скоростью continental drift (смещение тектонических плит), пора доставать инструменты профилирования и проводить бенчмаркинг, как будто от этого зависит работа вашего продакшн-кластера (потому что это так). Давайте превратим ваш код из «так себе» в «молниеносно быстрый» с помощью методов, которые заставят гофера покраснеть.
Цирк профилирования
Шаг 1: Установка трапеции
Сначала добавьте импорт профилирования в ваш основной пакет:
import _ "net/http/pprof"
Шаг 2: Ловля CPU-огня в воздухе
Запустите приложение с помощью:
go run main.go -cpuprofile=cpu.pprof
Затем создайте свой первый flame graph (график пламени):
go tool pprof -http=:8080 cpu.pprof
Шаг 3: Хождение по канату с памятью
Обнаруживайте утечки памяти с помощью:
import "runtime/pprof"
func main() {
f, _ := os.Create("mem.pprof")
pprof.WriteHeapProfile(f)
f.Close()
}
Однажды я обнаружил задержку в 400 мс, вызванную тем, что кто-то анализировал временные метки в горячем цикле. Исправление? Кэширование. Урок? Никогда не доверяйте форматированию даты в критически важных для производительности путях.
Бенчмаркинг: метание ножей
Создайте свой первый бенчмарк:
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
add(42, 69)
}
}
Запустите его стильно:
go test -bench=. -benchmem
Pro Tip: если ваши результаты бенчмарка выглядят как телефонные номера, вы либо Линус Торвальдс, либо в глубокой беде. Используйте -benchtime
, чтобы увеличить продолжительность для более чётких закономерностей.
Оптимизация танго
1. Хода down sync.Pool
Переиспользуйте объекты, как будто ваша оперативная память зависит от этого:
var bufferPool = sync.Pool{
New: func() interface{} {
return bytes.NewBuffer(make([]byte, 0, 4096))
},
}
func getBuffer() *bytes.Buffer {
return bufferPool.Get().(*bytes.Buffer)
}
func returnBuffer(b *bytes.Buffer) {
b.Reset()
bufferPool.Put(b)
}
2. Жонглирование горутинами
Не будьте тем разработчиком, который создаёт горутины как конфетти:
func processBatch(items []string) {
var wg sync.WaitGroup
sem := make(chan struct{}, runtime.NumCPU())
for _, item := range items {
wg.Add(1)
sem <- struct{}{}
go func(i string) {
defer wg.Done()
processItem(i)
<-sem
}(item)
}
wg.Wait()
}
3. Танго с встраиванием
Сделайте простые функции невидимыми (в хорошем смысле):
//go:noinline
func slowAdd(a, b int) int { return a + b }
func fastAdd(a, b int) int { return a + b }
Чек-лист «О, чёрт!»
- Когда ваш график pprof похож на Гималаи
- Когда вариации бенчмарков превышают колебания вашего портфеля акций
- Когда паузы GC длиннее, чем ваши перерывы на кофе
- Когда количество ваших горутин напоминает государственный долг
Помните: оптимизация без профилирования — это как абстрактное искусство — может выглядеть красиво, но никто не понимает, что происходит. Используйте go tool trace
, когда условия гонки заставляют вашу программу вести себя как реалити-шоу.
Финальный поклон
После применения этих методов к нашему устаревшему API мы сократили задержку 99-го процентиля с 1,2 с до 89 мс. Секрет успеха? Сочетание данных профилирования со стратегическим пулингом и контролем параллелизма. Теперь сделайте свой код достаточно быстрым, чтобы нарушить причинно-следственные связи — только не путешествуйте во времени, чтобы удалить собственные вызовы профилирования!