Skip to content

Оптимизация производительности

HTTPC изначально спроектирован высокопроизводительным: повторное использование пула соединений, мультиплексирование HTTP/2, пулинг объектов и единичное выделение памяти под объект результата. В большинстве сценариев достаточно использовать предустановки конфигурации для отличной производительности; для дальнейшей настройки нужно понимать низкоуровневые механизмы, чтобы действовать точечно.

Сравнение предустановок конфигурации

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

Конфигурация таймаутов

ПолеDefaultSecurePerformanceTestingMinimal
Timeouts.Request180s15s60s180s180s
Timeouts.Dial10s5s15s5s5s
Timeouts.TLSHandshake10s5s15s5s5s
Timeouts.ResponseHeader0 (отключено)10s0 (отключено)0 (отключено)0 (отключено)
Timeouts.IdleConn90s30s120s30s30s

Конфигурация соединений

ПолеDefaultSecurePerformanceTestingMinimal
MaxIdleConns50201001010
MaxConnsPerHost1052052
EnableHTTP2ВклВклВклВыклВкл
EnableCookiesВыклВыклВклВклВыкл
EnableDoHВыклВыклВыклВыклВыкл

Конфигурация безопасности

ПолеDefaultSecurePerformanceTestingMinimal
MaxResponseBodySize10MB5MB50MB10MB1MB
MaxDecompressedBodySize100MB100MB100MB100MB100MB
ValidateURLВклВклВклВыклВкл
ValidateHeadersВклВклВклВыклВкл
StrictContentLengthВклВклВыклВклВкл
AllowPrivateIPsfalsefalsefalsetruefalse
InsecureSkipVerifyfalsefalsefalsetruefalse

Конфигурация повторов

ПолеDefaultSecurePerformanceTestingMinimal
MaxRetries31310
Delay1s2s500ms100ms0
BackoffFactor2.02.01.52.01.0
MaxRetryDelay30s30s30s30s30s
EnableJitterВклВклВклВыклВыкл

Значения по умолчанию запроса

ПолеDefaultSecurePerformanceTestingMinimal
FollowRedirectsВклВыклВклВклВыкл
MaxRedirects1010101010
UserAgenthttpc/1.0httpc/1.0httpc/1.0httpc-test/1.0httpc/1.0

TestingConfig запрещён для продакшена

TestingConfig() отключает проверку URL/заголовков, проверку сертификатов TLS и защиту SSRF — только для локальной разработки и тестирования. При вызове в не-тестовой среде выводит предупреждение безопасности. В продакшене используйте SecureConfig() или DefaultConfig().

Выбор по сценарию

СценарийРекомендованная предустановкаРекомендации по настройке
Обычные веб-сервисыDefault
Обработка URL от пользователейSecure
Внутренние микросервисы с высокой нагрузкойPerformanceУвеличить MaxIdleConns по числу бэкендов
Одноразовые скриптыMinimal
Сервис загрузки файловPerformanceУвеличить MaxResponseBodySize
Финансовые/медицинские APISecure + пользовательскаяДобавить промежуточное ПО аудита
Локальная разработка/модульные тестыTestingНе развертывать в продакшен
go
// Сценарий высокой пропускной способности — используйте пресет напрямую
client, _ := httpc.New(httpc.PerformanceConfig())

// Тонкая настройка отдельных полей на основе пресета
cfg := httpc.PerformanceConfig()
cfg.Timeouts.Request = 120 * time.Second
cfg.Connection.MaxIdleConns = 200
client, _ := httpc.New(cfg)

Принципы настройки пула соединений

Пул соединений — основа производительности HTTP-клиента. Пул HTTPC основан на http.Transport стандартной библиотеки Go, но добавляет автоматическую логику вычислений и безопасные значения по умолчанию.

Автоматический расчёт простаивающих соединений

MaxIdleConnsPerHost (лимит простаивающих соединений на хост) не нужно задавать вручную — HTTPC автоматически выводит его из MaxConnsPerHost:

Простаивающие соединения = MaxConnsPerHost / 2, ограничено диапазоном [2, 10]

Конкретные правила (calculateIdleConnsPerHost):

MaxConnsPerHostАвтоматическое число простаивающихОписание
0 (без лимита)10Значение по умолчанию (верхний предел)
12Не ниже нижнего предела
22Ровно нижний предел
52Половина округляется к нижнему
105Пресет Default
2010Пресет Performance, верхний предел
10010Превышает верхний предел — берётся 10

Почему именно MaxConnsPerHost / 2

Простаивающие соединения — это «кеш соединений»: уже установленные, но временно не используемые. Установка их в половину от максимального числа соединений обеспечивает баланс между «переиспользованием существующих» (попадание в кеш) и «созданием новых» (промах кеша требует повторного рукопожатия), предотвращая избыточное потребление ресурсов сервера простаивающими соединениями.

TCP Keep-Alive

Пул соединений HTTPC использует фиксированный 30-секундный интервал TCP keep-alive (defaultKeepAlive = 30 * time.Second). После установки соединения ОС периодически отправляет probe-пакеты keep-alive для обнаружения мёртвых соединений. Таймаут IdleConn контролирует время жизни простаивающего соединения в пуле (Default — 90s), оба работают в связке.

go
package main

import (
    "fmt"
    "log"
    "time"

    "github.com/cybergodev/httpc"
)

func main() {
    // Микросервис с высоким QPS: увеличиваем пул соединений
    cfg := httpc.PerformanceConfig()
    cfg.Connection.MaxIdleConns = 200   // Глобальный лимит простаивающих соединений
    cfg.Connection.MaxConnsPerHost = 50 // Максимум соединений на хост (простаивающие авто-расчёт = 10)
    cfg.Timeouts.IdleConn = 300 * time.Second // Простаивающие соединения живут дольше — выше повторное использование

    client, err := httpc.New(cfg)
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()

    // Горячий путь: запросы напрямую переиспользуют соединения из пула
    for i := 0; i < 100; i++ {
        result, err := client.Get("https://api.example.com/data")
        if err != nil {
            log.Printf("Запрос %d не удался: %v", i, err)
            continue
        }
        fmt.Printf("Запрос %d: %d\n", i, result.StatusCode())
    }
}

Преимущества производительности HTTP/2

HTTP/2 включён по умолчанию (EnableHTTP2 = true), предоставляя три ключевых улучшения:

ХарактеристикаHTTP/1.1HTTP/2
МультиплексированиеКаждое запрос занимает отдельное соединениеНесколько запросов разделяют одно соединение
Сжатие заголовковПовторная отправка открытым текстомHPACK-сжатие заголовков
Повторное использование соединенийKeep-alive последовательноеПараллельные потоки (stream)

Связь HTTP/2 и пула соединений

Мультиплексирование HTTP/2 позволяет одному TCP-соединению одновременно обслуживать несколько запросов, значительно сокращая накладные расходы на установку соединений. В сценариях с высокой параллельной нагрузкой на один хост пропускная способность HTTP/2 значительно превышает HTTP/1.1. Откат к HTTP/1.1 происходит только при использовании TestingConfig() (явно отключает HTTP/2) или когда соединение не поддерживает согласование ALPN.

go
package main

import (
    "fmt"
    "log"
    "time"

    "github.com/cybergodev/httpc"
)

func main() {
    // HTTP/2 включён в конфигурации по умолчанию
    cfg := httpc.DefaultConfig()
    cfg.Connection.EnableHTTP2 = true // По умолчанию true, явное указание для ясности

    client, err := httpc.New(cfg)
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()

    // Параллельные запросы к сайтам с поддержкой HTTP/2 (большинство CDN/облаков)
    // Одно TCP-соединение мультиплексируется, не нужно создавать новое для каждого запроса
    start := time.Now()
    for i := 0; i < 10; i++ {
        result, err := client.Get("https://http2.golang.org/")
        if err != nil {
            log.Printf("Запрос %d не удался: %v", i, err)
            continue
        }
        // Proto() возвращает версию протокола, например "HTTP/2.0"
        fmt.Printf("Запрос %d: %s, статус %d\n", i, result.Proto(), result.StatusCode())
    }
    fmt.Printf("10 запросов заняло: %v\n", time.Since(start))
}

Механизмы оптимизации памяти

HTTPC применяет многоуровневую оптимизацию памяти — основная идея: сократить выделения в куче и переиспользовать объекты.

Единичное выделение resultBundle

Каждый возвращаемый *Result несёт три вложенных структуры: RequestInfo (информация запроса), ResponseInfo (информация ответа), RequestMeta (метаданные, например длительность). Традиционный подход требует отдельных выделений для Result и трёх вложенных структур — четырёх выделений в куче. HTTPC упаковывает их в один resultBundle, решая всё одним выделением:

Традиционный способ: 4 независимых выделения (Result + RequestInfo + ResponseInfo + RequestMeta)
HTTPC: 1 выделение (resultBundle), три указателя Result ссылаются на одну область памяти

Вызывающий получает *Result, чьи поля Request, Response, Meta (указатели) ссылаются на соответствующие структуры внутри bundle, полностью прозрачно. Поскольку вызывающий может долгое время удерживать *Result, здесь не подходит пул объектов (пулинг приведёт к гонке данных), поэтому используется автоматическая сборка мусора GC.

Пул объектов движка

Слой движка HTTPC широко использует sync.Pool для переиспользования короткоживущих объектов, снижая нагрузку на GC:

Пулированный объектНазначениеОписание
engine.ResponseОбъект ответаВозвращается в пул после запроса, переиспользуется следующим
engine.RequestОбъект запросаАналогично
strings.BuilderПостроение строкПостроение URL, форматирование ошибок, сериализация Config
http.Headermap заголовковОбработка заголовков запроса/ответа
bytes.BufferКодирование JSON/multipartПредварительное выделение по начальной ёмкости
time.TimerТаймер повторовИзбегает частого создания таймеров
gzip/flate readerРаспаковкаПереиспользование декомпрессоров

Разделение труда между пулом объектов и resultBundle

Внутренние объекты движка (Response/Request/Builder) имеют короткий жизненный цикл, выполняя цикл borrow-return внутри запроса — подходят для пулинга. Возвращаемый вызывающему *Result имеет неопределённый жизненный цикл — подходит для единичного выделения + сборки GC. Они дополняют друг друга, каждый на своём месте.

Вам не нужно об этом беспокоиться

Все эти оптимизации полностью прозрачны для вызывающего. Вы просто используете API, а повторное использование соединений, пулинг объектов и единичное выделение выполняются автоматически внутри:

go
package main

import (
    "fmt"
    "log"

    "github.com/cybergodev/httpc"
)

func main() {
    client, err := httpc.NewDefault()
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()

    // Result создаётся заново для каждого запроса, GC собирает автоматически — ручное освобождение не требуется
    result, err := client.Get("https://api.example.com/data")
    if err != nil {
        log.Fatal(err)
    }

    // На горячем пути предпочитайте RawBody() вместо Body()
    // RawBody() возвращает исходный срез байтов; Body() возвращает предсохранённую строку; String() — отладочное форматирование (наибольшие накладные расходы)
    data := result.RawBody()
    fmt.Printf("Размер ответа: %d байт\n", len(data))
    fmt.Printf("Длительность запроса: %v\n", result.Meta.Duration)
}

Примеры настройки под рабочие нагрузки

AI API с длинным опросом

API AI-вывода может отвечать несколько минут — нужно ослабить ограничения таймаута:

go
// AI API может отвечать 5-15 минут, не обрезайте стандартным таймаутом 180s
result, err := httpc.Post("https://api.ai.example.com/v1/completions",
    httpc.WithJSON(payload),
    httpc.WithTimeout(900*time.Second), // 15 минут
)

Почему ResponseHeader в Default равен 0

Timeouts.ResponseHeader = 0 означает, что таймаут заголовка ответа не принудителен на транспортном уровне — он контролируется context-уровневым таймаутом (Timeouts.Request или WithTimeout). Это гарантирует, что WithTimeout() имеет полный контроль над запросами с длинным ожиданием ответа. Для транспортной защиты от атак slowloris используйте SecureConfig() (установлено 10s).

Микросервисы с высоким QPS

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

go
package main

import (
    "fmt"
    "log"
    "time"

    "github.com/cybergodev/httpc"
)

func main() {
    cfg := httpc.PerformanceConfig()
    // Настройка пула по числу экземпляров бэкенда
    cfg.Connection.MaxIdleConns = 300   // Всего простаивающих соединений
    cfg.Connection.MaxConnsPerHost = 30 // На каждый экземпляр бэкенда
    // Микросервисы обычно отвечают быстро — сокращаем таймаут для быстрого отказа
    cfg.Timeouts.Request = 10 * time.Second
    cfg.Retry.Delay = 200 * time.Millisecond
    cfg.Retry.BackoffFactor = 2.0
    cfg.Retry.MaxRetries = 2

    client, err := httpc.New(cfg)
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()

    start := time.Now()
    // Высокочастотные запросы переиспользуют пул, без пересоздания TCP/TLS
    for i := 0; i < 50; i++ {
        result, err := client.Get("http://user-service:8080/api/users")
        if err != nil {
            log.Printf("Запрос %d не удался: %v", i, err)
            continue
        }
        _ = result
    }
    fmt.Printf("50 запросов заняло: %v\n", time.Since(start))
}

Загрузка больших файлов (потоковая)

При загрузке больших файлов используйте WithStreamBody(true), чтобы не загружать весь ответ в память, а с методом Download() поддерживается возобновление:

go
package main

import (
    "context"
    "log"

    "github.com/cybergodev/httpc"
)

func main() {
    cfg := httpc.PerformanceConfig()
    cfg.Security.MaxResponseBodySize = 500 * 1024 * 1024 // Лимит 500MB

    client, err := httpc.New(cfg)
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()

    dcfg := httpc.DefaultDownloadConfig()
    dcfg.FilePath = "/tmp/large-file.zip"
    dcfg.ResumeDownload = true // Возобновление загрузки

    result, err := client.Download(
        context.Background(),
        "https://example.com/large-file.zip",
        dcfg,
    )
    if err != nil {
        log.Fatal(err)
    }
    log.Printf("Загрузка завершена: %d байт", result.BytesWritten)
}

Сканеры и пул прокси

В сценариях сканирования используется пул прокси для ротации IP, HTTPC автоматически увеличивает число повторов, чтобы каждый прокси был опробован хотя бы раз (подробнее см. Повторы и отказоустойчивость):

go
cfg := httpc.DefaultConfig()
cfg.Connection.ProxyPool = []string{
    "http://proxy1:8080",
    "http://proxy2:8080",
    "http://proxy3:8080",
    "http://proxy4:8080",
    "http://proxy5:8080",
}
cfg.Connection.ProxyRotateOnStatus = []int{403} // 403 запускает смену прокси
cfg.Connection.ProxyPoolStrategy = httpc.ProxyStrategyRoundRobin
// MaxRetries автоматически повышается до 4 (число прокси-1), обеспечивая попытку всех 5 прокси

Антипаттерны производительности

АнтипаттернПричинаПравильный подход
Создание клиента на каждый запросСоединения не переиспользуются, каждый раз TCP/TLS рукопожатие зановоГлобальное переиспользование одного экземпляра Client
Слишком большой MaxResponseBodySizeНеобоснованно высокий лимит памятиУстанавливать по реальному размеру ответа
result.String() на горячем путиДополнительные накладные расходы построения строкиИспользовать result.Body() или result.RawBody()
Слишком малый пул соединенийПри высокой параллельности соединений не хватает, образуется очередьНастраивать MaxConnsPerHost по уровню параллелизма
Отключение HTTP/2Деградация до последовательных HTTP/1.1 запросовДержать включённым по умолчанию
Игнорирование Close()Утечка соединенийdefer client.Close()
Глобальный доступ без переиспользованияПовторное создание/уничтожение ClientСоздать один раз, удерживать длительно

Client обязательно нужно переиспользовать

Основа производительности HTTP — повторное использование соединений. Создание нового Client на каждый запрос означает каждый раз TCP тройное рукопожатие + TLS рукопожатие, задержка возрастает от субмиллисекундной до десятков миллисекунд. В микросервисных сценариях внедряйте Client как одиночку (singleton) в структуру сервиса, живущий весь жизненный цикл сервиса.

go
package main

import (
    "fmt"
    "log"
    "time"

    "github.com/cybergodev/httpc"
)

// Демонстрация антипаттерна: создание клиента на каждый запрос
func main() {
    start := time.Now()

    for i := 0; i < 5; i++ {
        // ❌ Создаёт новый Client на каждой итерации — соединения не переиспользуются
        client, err := httpc.NewDefault()
        if err != nil {
            log.Fatal(err)
        }
        result, err := client.Get("https://httpbin.org/get")
        client.Close() // Каждый раз закрывает — пул соединений очищается
        if err != nil {
            log.Printf("Запрос %d не удался: %v", i, err)
            continue
        }
        _ = result
    }
    // 5 запросов занимают значительно больше, чем при переиспользовании Client
    fmt.Printf("Время антипаттерна: %v\n", time.Since(start))

    // ✅ Правильный подход: переиспользование Client
    client, err := httpc.NewDefault()
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()

    start = time.Now()
    for i := 0; i < 5; i++ {
        result, err := client.Get("https://httpbin.org/get")
        if err != nil {
            log.Printf("Запрос %d не удался: %v", i, err)
            continue
        }
        _ = result
    }
    fmt.Printf("Время при переиспользовании: %v\n", time.Since(start))
}

Что дальше