Я писал обратный прокси на Go. Не учебный, а боевой — такой, что стоит на живом трафике, обрабатывает каждый запрос перед отправкой дальше. Долгое время я не заморачивался по поводу производительности: все работало, запросы уходили, ответы возвращались.
Но потом я решил провести нормальное бенчмаркирование. Цифры оказались удручающими. Ничего не падало, ошибок не было, но скорость хромала так, что я не мог сразу понять причину.
Исходный код
В основе прокси лежал стандартный httputil.ReverseProxy — идиоматичный выбор для Go. Сам по себе он отличный, проблема была в том, как я его использовал. Обработчик выглядел так:
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { upstream := h.resolveUpstream(r) proxy := &httputil.ReverseProxy{ Director: func(req *http.Request) { req.URL.Scheme = upstream.Scheme req.URL.Host = upstream.Host }, } proxy.ServeHTTP(w, r) }Выглядит безобидно. Для каждого запроса определяется апстрим, создается новый прокси, запрос обрабатывается. Читаемо. И смертельно медленно под нагрузкой.
Что происходило на самом деле
Каждый запрос создавал свежий ReverseProxy, а вместе с ним — новый http.Transport. Именно Transport отвечает за пул соединений. Когда вы создаете его каждый раз, пул не работает — каждое соединение приходится устанавливать с нуля, вместо того чтобы переиспользовать уже открытое.
Я заметил это не при чтении кода, а во время нагрузочного теста. Латенция выглядела странно: большинство запросов шли нормально, но был заметный «хвост» медленных, который не вписывался в общую картину.
Фикс
Достаточно перестать создавать ReverseProxy на каждый запрос. Сделать один на апстрим и кешировать его:
var proxyCache sync.Map // map[string]*httputil.ReverseProxy func (h *Handler) getProxy(upstream Upstream) *httputil.ReverseProxy { if p, ok := proxyCache.Load(upstream.Host); ok { return p.(*httputil.ReverseProxy) } proxy := &httputil.ReverseProxy{ Director: func(req *http.Request) { req.URL.Scheme = upstream.Scheme req.URL.Host = upstream.Host }, } actual, _ := proxyCache.LoadOrStore(upstream.Host, proxy) return actual.(*httputil.ReverseProxy) } func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { upstream := h.resolveUpstream(r) proxy := h.getProxy(upstream) proxy.ServeHTTP(w, r) }Я использовал sync.Map вместо обычной мапы с мьютексом, потому что чтений тут много, а записей мало — только при появлении нового апстрима. Это как раз тот паттерн, для которого sync.Map и создавался.
Теперь http.Transport и его пул соединений создаются один раз на апстрим и живут всё время. Соединения остаются горячими, ничего не пересоздается на горячем пути.
Результаты
Сразу скажу: тест не на hello-world. Прокси делает реальную работу — проверяет политики, трансформирует поля, детектит PII, пишет аудит. Абсолютные цифры не сравнимы с сырым бенчмарком, но они наглядно показывают эффект одного изменения.
| Метрика | До | После |
|---|---|---|
| Запросов/с | 1 710 | 6 541 |
| Средняя задержка | ~58 мс | 7,6 мс |
| p50 | ~49 мс | 5,9 мс |
| p90 | ~110 мс | 13,7 мс |
| p95 | ~123 мс | 18,1 мс |
| Ошибки | 0% | 0% |
Тест: hey -n 400000 -c 50 на реальный апстрим со всеми включенными политиками. 400 000 запросов, 50 конкурентных, каждый вернул 200. Производительность стабильна от первой секунды до последней. 97,5% запросов уложились в 25 мс — при работающей проверке политик, сканировании PII, аудите и трансформации JSON.
Пропускная способность выросла примерно в 3,8 раза, средняя задержка упала в 8 раз. Ошибок не было ни до, ни после — это чисто оптимизация скорости, не корректировка логики.
Урок
Это очень легко пропустить в Go. Код выглядит нормально, компилятор не ругается, ничего не падает. Просто тихо убивается одна из ключевых фич HTTP-клиента — пул соединений. Единственный способ заметить — измерить.
Если вы строите что-то на httputil.ReverseProxy, http.Client или http.Transport — создавайте один экземпляр на destination и держитесь за него. Не пересоздавайте внутри обработчика. Мелочь, а в моём случае дала +3x производительности бесплатно.
Комментарии (0)
Войдите, чтобы комментировать.
Пока нет комментариев. Будьте первым.