← Назад к вопросам

Что делаешь, если приложение стало работать медленно?

1.8 Middle🔥 121 комментариев
#Observability

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI7 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Методика диагностики и устранения проблем производительности в Go

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

1. Сбор данных и определение симптомов

Первым шагом является точное определение что стало медленным: общее время ответа API, скорость обработки данных, потребление памяти или что-то иное.

// Пример добавления базового мониторинга времени ответа в middleware
func TimingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        next.ServeHTTP(w, r)
        duration := time.Since(start)
        log.Printf("Route: %s, Duration: %v", r.URL.Path, duration)
        // Здесь также можно отправлять метрики в Prometheus/OpenTelemetry
    })
}

Я собираю:

  • Метрики из системы мониторинга (Prometheus, Grafana) — загрузка CPU, память, GC, количество горутин.
  • Логи с временными марками ключевых операций.
  • Внешние данные — нагрузку на сеть, БД, кэши.

2. Профилирование приложения

Go предоставляет мощные инструменты pprof для глубокого анализа. Я запускаю профилирование в реальном времени или на нагрузочных тестах.

# Подключение pprof к работающему сервису
go tool pprof http://localhost:8080/debug/pprof/profile
go tool pprof http://localhost:8080/debug/pprof/heap

Анализирую:

  • CPU Profile — чтобы найти «горячие» функции, чрезмерные циклы или аллокации.
  • Memory Profile (Heap) — для обнаружения утечек памяти или неоптимальных аллокаций.
  • Block Profile — чтобы найти точки contention (блокировки горутин).
  • Goroutine Profile — для диагностики «утечек» горутин или их чрезмерного количества.

3. Анализ конкретных проблемных областей

На основе профилей я определяю root cause и применяю targeted fixes.

Если проблема в аллокациях памяти и сборке мусора (GC):

// Проблемный код: частые аллокации маленьких объектов
func processData(data []byte) {
    var result strings.Builder
    for _, b := range data {
        result.WriteString(strconv.Itoa(int(b))) // Аллокация на каждой итерации!
    }
}

// Оптимизация: уменьшить аллокации, использовать пулы
var bufferPool = sync.Pool{
    New: func() interface{} {
        return &bytes.Buffer{}
    },
}
func optimizedProcess(data []byte) {
    buf := bufferPool.Get().(*bytes.Buffer)
    defer bufferPool.Put(buf)
    buf.Reset()
    // ... обработка с минимумом аллокаций
}

Меры: использование sync.Pool для объектов, уменьшение размеров стеков, контроль за большими объектами (>10MB), влияющими на GC.

Если проблема в конкурентности и блокировках:

Анализирую Block Profile и мьютексы.

// Проблема: грубый мьютекс на часто используемом ресурсе
var globalMu sync.Mutex
var globalConfig map[string]string

func GetConfig(key string) string {
    globalMu.Lock()         // Блокировка на каждом чтении!
    defer globalMu.Unlock()
    return globalConfig[key]
}

// Оптимизация: использование sync.RWMutex или атомарных операций
var globalConfigMu sync.RWMutex
func OptimizedGetConfig(key string) string {
    globalConfigMu.RLock()  // Блокировка только для чтения
    defer globalConfigMu.RUnlock()
    return globalConfig[key]
}

Также проверяю select с default, чтобы избежать блокировки горутин, и каналы с буферами.

Если проблема в аллокациях CPU:

Ищу «тяжелые» алгоритмы, неоптимальные сериализации (json/xml), кривые регулярные выражения.

// Пример оптимизации: предварительная компиляция regexp
var compiledRegex = regexp.MustCompile(`expensive(pattern)`)

func process(text string) {
    // Использование уже скомпилированного выражения
    compiledRegex.FindAllString(text, -1)
}

4. Системный и инфраструктурный контекст

Производительность часто зависит не только от кода:

  • Базы данных: анализирую медленные queries (использую EXPLAIN), добавляю индексы, рассматриваю кэширование (Redis).
  • Внешние API: проверяю таймауты и retry логику, возможно добавляю circuit breakers.
  • Оркестрация: проверяю лимиты CPU/memory в контейнерах (Docker, Kubernetes), настройки GOGC.

5. Внедрение долгосрочных улучшений

После устранения конкретной проблемы я внедряю механизмы для профилактики:

  • Постоянное профилирование (например, с помощью Continuous Profiler от Datadog или Pyroscope).
  • Нагрузочное тестирование (load testing) на регулярной основе.
  • Статический анализ кода с помощью go vet и staticcheck для поиска потенциальных антипаттернов.
  • Обучение команды принципам написания эффективного Go-кода (минимизация аллокаций, правильная конкурентность).

Этот методичный подход позволяет не просто «залатать» текущую проблему, но и выстроить культуру performance awareness, предотвращая подобные ситуации в будущем. Ключевое — начинать с данных (профилирование), а не с предположений, и последовательно устранять bottleneck от самого значительного до менее важных.

Что делаешь, если приложение стало работать медленно? | PrepBro