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

Приведи пример задачи, когда улучшил производительность REST API

1.7 Middle🔥 142 комментариев
#Основы Go

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

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

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

Пример оптимизации производительности REST API в Go

В моей практике был показательный случай с API для агрегации финансовых транзакций. Исходный эндпоинт GET /api/v1/transactions возвращал до 10,000 записей с множеством связей (пользователи, счета, категории) и обрабатывался 2.8 секунды при 50 одновременных запросах.

Исходная проблема

// Проблемный код (упрощённо)
func GetTransactionsHandler(w http.ResponseWriter, r *http.Request) {
    var filters TransactionFilters
    json.NewDecoder(r.Body).Decode(&filters)
    
    // 1. N+1 запросов в базе
    transactions := []Transaction{}
    db.Where(filters).Find(&transactions)
    
    for i := range transactions {
        // Отдельные запросы для связанных данных
        db.First(&transactions[i].User)
        db.First(&transactions[i].Account)
        db.Where("transaction_id = ?", transactions[i].ID).Find(&transactions[i].Tags)
    }
    
    // 2. Множество JSON преобразований
    enriched := []EnrichedTransaction{}
    for _, t := range transactions {
        enriched = append(enriched, EnrichTransaction(t))
    }
    
    json.NewEncoder(w).Encode(enriched)
}

Проведённый анализ

  1. Профилирование через pprof показало:

    • 65% времени - запросы к БД (N+1 проблема)
    • position% - сериализация JSON
    • 10% - бизнес-логика обогащения данных
  2. Load testing с Vegeta:

    Requests [total, rate]            5000, 100.02
    Duration [total, attack, wait]    49.995s, 49.990s, 5.004ms
    Latencies [mean, 50, 95, 99, max] 2.801s,我们这个2.654s, 4.112s, 4.890s, 5.112s
    

Реализованные оптимизации

// Оптимизированная версия
func GetTransactionsOptimizedHandler(w http.ResponseWriter, r *http.Request) {
    filters := parseFiltersFast(r.URL.Query()) // Быстрый парсинг query params
    
    // 1. Единый сложный запрос с JOIN и предзагрузкой
    transactions := []Transaction{}
    db.Preload("User").
       Preload("Account").
       Preload("Tags").
       Where(filters).
       Limit(1000). // Добавили пагинацию
       Find(&transactions)
    
    // 2. Параллельное обогащение данных
    resultChan := make(chan EnrichedTransaction, len(transactions))
    var wg sync.WaitGroup
    
    semaphore := make(chan struct{}, 10) // Ограничиваем параллелизм
    
    for _, t := range transactions {
        wg.Add(1)
        go func(tr Transaction) {
            defer wg.Done()
            semaphore <- struct{}{}
            defer func() { <-semaphore }()
            
            // Кэшируем дорогие вычисления
            if enriched, ok := cache.Get(tr.ID); ok {
                resultChan <- enriched.(EnrichedTransaction)
                return
            }
            
            enriched := EnrichTransactionFast(tr)
            cache.Set(tr.ID, enriched, 5*time.Minute)
            resultChan <- enriched
        }(t)
    }
    
    go func() {
        wg.Wait()
        close(resultChan)
    }()
    
    // 3. Стриминг JSON ответа
    w.Header().Set("Content-Type", "application/json")
    encoder := json.NewEncoder(w)
    
    w.Write([]byte("{\"data\":["))
    first := true
    
    for enriched := range resultChan {
        if !first {
            w.Write([]byte(","))
        }
        encoder.Encode(enriched)
        first = false
    }
    w.Write([]byte("]}"))
}

Дополнительные улучшения

  1. Кэширование в Redis:
func getCachedTransactions(filtersHash string) ([]byte, bool) {
    data, err := redisClient.Get(ctx, "transactions:"+filtersHash).Bytes()
    if err == nil {
        return data, true
    }
    return nil, false
}
  1. Database indexing:
CREATE INDEX idx_transactions_user_date ON transactions(user_id, created_at DESC);
CREATE INDEX idx_transactions_account ON transactions(account_id) INCLUDE (amount, currency);
  1. Pooling для уменьшения аллокаций:
var transactionPool = sync.Pool{
    New: func() interface{} {
        return &Transaction{}
    },
}

func getTransactionFromPool() *Transaction {
    return transactionPool.Get().(*Transaction)
}

Результаты оптимизаций

МетрикаДо оптимизацииПосле оптимизации
Средняя latency2.8s340ms
95-й перцентиль4.1stargetms
Пропускная способность36 req/s280 req/s
Потребление памяти450 MB210 MB
Время CPU на запрос180ms45ms

Ключевые уроки

  1. Измеряйте перед оптимизацией - pprof стал основным инструментом диагностики
  2. N+1 проблема в ORM - критична для производительности
  3. Параллелизм с ограничениями - избегайте resource contention
  4. Кэширование многоуровневое - in-memory + Redis + database query cache
  5. Стриминг ответов - уменьшает memory footprint для больших данных

Оптимизация REST API в Go требует системного подхода: от анализа узких мест через профилирование до комбинации кэширования, улучшения запросов и контроля ресурсов. Наиболее эффективными обычно оказываются исправление проблем с базой данных и уменьшение аллокаций памяти.