Skip to content

Пул соединений и DNS

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

Пул соединений — ключевой фактор производительности HTTP-клиента. HTTPC управляет пулом через ConnectionConfig.

go
cfg := httpc.DefaultConfig()

// Параметры пула соединений
cfg.Connection.MaxIdleConns = 100         // Глобальный максимум простаивающих соединений
cfg.Connection.MaxConnsPerHost = 20       // Максимум соединений на хост
cfg.Timeouts.IdleConn = 120 * time.Second // Время удержания простаивающих соединений

Описание параметров

ПараметрПо умолчаниюОписание
MaxIdleConns50Глобальный максимум простаивающих соединений
MaxConnsPerHost10Максимум соединений на хост (активные + простаивающие)
IdleConn90sТаймаут простаивающего соединения, по истечении закрывается
Dial10sТаймаут установки соединения
TLSHandshake10sТаймаут TLS-рукопожатия
ResponseHeader0Отключено (используется таймаут Request)
MaxResponseHeaderBytes0Лимит размера заголовков ответа; 0 = стандартные для библиотеки Go 10MB

Рекомендации по сценариям

СценарийMaxIdleConnsMaxConnsPerHostIdleConn
Высоконагруженные API10020120s
Обычные сервисы501090s
Редкие запросы10230s
Внутренние микросервисы501060s

TIP

MaxConnsPerHost включает активные и простаивающие соединения. Новые запросы сверх этого лимита встают в очередь и ожидают освобождения соединения.

Производные параметры и внутренние лимиты

У части параметров соединений нет отдельных полей конфигурации — движок выводит их из существующих параметров или использует фиксированные значения:

ПараметрЗначениеИсточник
MaxIdleConnsPerHostclamp(MaxConnsPerHost/2, 2, 10); при MaxConnsPerHost=0 — 10Выводится из MaxConnsPerHost, отдельного поля нет
Общий лимит соединений на клиент1000 (активные + простаивающие)Фиксированное значение; при превышении возвращается ошибка исчерпания пула соединений
Интервал TCP KeepAlive-проб30sФиксированное значение
ExpectContinueTimeout1sФиксированное значение (ожидание Expect: 100-continue)

Как проявляется общий лимит соединений

При достижении общего лимита в 1000 соединений установка нового соединения завершается ошибкой класса ClientError (ErrorTypeNetwork, Message — connection pool exhausted). При значениях по умолчанию MaxIdleConns=50 / MaxConnsPerHost=10 срабатывание практически невозможно — считайте это последней линией обороны при экстремальной конкурентности.

Управление простаивающими соединениями

  • IdleConn (по умолчанию 90s): простаивающие соединения автоматически закрываются транспортным уровнем по истечении таймаута. Увеличение значения повышает переиспользование соединений, уменьшение — быстрее освобождает ресурсы удалённой стороны (в сценариях частых коротких соединений следите за накоплением TIME_WAIT).
  • Принудительная очистка при ротации прокси: включение ProxyRotatePerRequest или пути повторов с ротацией по статус-коду автоматически закрывают все простаивающие соединения, заставляя следующий запрос заново выбирать прокси — иначе переиспользование HTTP/2-соединений через CONNECT-туннель обходило бы выбор прокси и ломало ротацию. Конфигурация прокси и стратегии ротации подробно разобраны в Прокси и пул прокси.
  • client.Close(): закрывает все простаивающие соединения, DoH-резолвер и внутренние ресурсы; последующие запросы возвращают ErrClientClosed.
  • Автоочистка статистики по хостам: движок поддерживает счётчики соединений по хостам; записи без активности в течение 30 минут и без активных соединений периодически вычищаются (не чаще раза в минуту, лимит записей о хостах — 10000), поэтому при длительной работе память не растёт бесконечно.

Особый случай: распаковка ответов

Транспортный уровень отключает автоматическую распаковку стандартной библиотеки — HTTPC обрабатывает gzip / deflate вручную на уровне обработки ответов: распаковка ограничена Security.MaxDecompressedBodySize (по умолчанию 100MB) и защищает от атак декомпрессионной бомбы. В повседневной работе об этом слое можно не думать — достаточно знать, что Result.RawBody() возвращает уже распакованные байты.

DNS-over-HTTPS

После включения DoH разрешение DNS идёт по зашифрованному HTTPS-каналу: это защищает от перехвата со стороны провайдера и отравления DNS, а также даёт встроенную отказоустойчивость между провайдерами:

go
cfg := httpc.DefaultConfig()
cfg.Connection.EnableDoH = true
cfg.Connection.DoHCacheTTL = 5 * time.Minute // значение 0 тоже откатывается к 5 минутам

Провайдеры DoH по умолчанию (по приоритету):

ПровайдерАдресОписание
Cloudflare1.1.1.1/dns-queryСамый быстрый, приоритет конфиденциальности
Googledns.google/resolveГлобальное покрытие
AliDNSdns.alidns.com/resolveОптимизация для региона Китай

Механизм работы

  • Параллельные запросы A + AAAA: каждое разрешение одновременно запрашивает записи IPv4 и IPv6 и объединяет результаты.
  • Разбор двух форматов: по Content-Type ответа автоматически выбирается JSON (стиль Google/AliDNS) или wire-формат RFC 1035 (стиль Cloudflare); если тип отсутствует или не распознан, сначала пробуется JSON, затем wire — это обеспечивает совместимость с нестандартно настроенными серверами.
  • Слияние конкурентных запросов: одновременные промахи кэша по одному хосту сливаются в один сетевой обход (singleflight), защищая провайдеров от пробоя кэша.
  • Отдельный внутренний клиент: запросы DoH идут через отдельный HTTP-клиент (таймаут 5s, HTTP/2 включён) и не расходуют пул соединений и бюджет таймаутов бизнес-запросов.

Цепочка отката

text
Cloudflare (1.1.1.1) → Google (dns.google) → AliDNS → системный DNS-резолвер

Успех любого провайдера сразу возвращает результат; при отказе всех автоматически выполняется откат к системному DNS-резолверу, а ошибки обоих уровней объединяются в сообщение об ошибке. Отказ одного провайдера не приводит к неудаче запроса.

Кэш

ПунктЗначение
TTLDoHCacheTTL (по умолчанию 5 минут)
Лимит ёмкости1000 записей; при заполнении сначала вытесняются истёкшие записи, затем ближайшие к истечению
Лимит размера ответа64KB (защита памяти от вредоносных DNS-ответов)

Взаимодействие с защитой от SSRF

IP-адреса, разрешённые через DoH, сначала проходят SSRF-фильтрацию (отбрасываются приватные/зарезервированные адреса, учитываются SSRFExemptCIDRs), после чего соединение устанавливается напрямую с проверенным IP — между проверкой и установкой соединения второго разрешения DNS нет, что отсекает атаки DNS rebinding у самого корня. Адреса прокси через DoH не разрешаются (прокси настраивается разработчиком явно, соединение устанавливается напрямую с хостом прокси — см. Прокси и пул прокси). Подробнее — в Защите от SSRF.

HTTP/2

HTTP/2 включён по умолчанию (требуется TLS):

go
cfg := httpc.DefaultConfig()
cfg.Connection.EnableHTTP2 = false // Отключить HTTP/2

Возможности HTTP/2:

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

При отключении транспортный уровень полностью перестаёт пытаться согласовать HTTP/2 (включая принудительные попытки в сценариях с кастомной конфигурацией TLS); HTTP/2 в открытом виде (h2c) не поддерживается. Учтите, что мультиплексирование HTTP/2 заставляет конкурентные запросы к одному хосту делить одно соединение — именно поэтому сценарию ротации прокси требуется закрывать простаивающие соединения (см. ротацию на каждый запрос в Прокси и пул прокси).

Переиспользование пула объектов

Внутри HTTPC переиспользует объекты ответов движка и построители строк через sync.Pool, снижая нагрузку на GC; Result же создаётся заново на каждый запрос и утилизируется GC автоматически.

go
result, err := client.Get(url)
if err != nil {
    return err
}
// Result создаётся заново на каждый запрос, утилизируется GC, ручное освобождение не требуется

В высоконагруженных сценариях переиспользование внутреннего пула объектов заметно снижает нагрузку на GC.

Паттерны конкурентных запросов

go
func fetchAll(ctx context.Context, urls []string) ([]*httpc.Result, error) {
    results := make([]*httpc.Result, len(urls))
    errs := make([]error, len(urls))

    var wg sync.WaitGroup
    for i, url := range urls {
        wg.Add(1)
        go func(idx int, u string) {
            defer wg.Done()
            result, err := client.Request(ctx, "GET", u)
            results[idx] = result
            errs[idx] = err
        }(i, url)
    }
    wg.Wait()

    for _, err := range errs {
        if err != nil {
            return nil, err
        }
    }
    return results, nil
}

Частые проблемы

ПроблемаПричинаРешение
Много TIME_WAITТаймаут простаивающих соединений слишком короткийУвеличьте таймаут IdleConn
Отказ в соединенииНе хватает соединений на хостУвеличьте MaxConnsPerHost
Запросы стоят в очередиПул соединений слишком малУвеличьте MaxIdleConns
Нужно ограничить число простаивающих соединений на хостОтдельного поля конфигурации нетВыводится из MaxConnsPerHost (÷2, зажимается в диапазон 2–10)
После включения DoH разрешение не удаётсяВсе провайдеры DoH недоступны или таймаутятВстроен откат к системному DNS; проверьте исходящую сеть или оставьте значения по умолчанию
Прокси не работает или часто размыкаетсяВзаимодействие конфигурации прокси и переиспользования соединенийСм. частые вопросы в Прокси и пул прокси

Полный разбор антипаттернов производительности и рекомендаций по оптимизации — в Оптимизации производительности.

Что дальше