TGViewer
Daria’s room Daria’s room @dariasroom · 1.2K subscribers
Post #116 1.45K
Daria’s room Почему коннекты растут быстрее, чем RPS Недавно разбирала кейс: в pgx пуле резко выросло количество соединений, условно с 10 до 80. Но RPS вырос совсем немного, поэтому объяснение типа "стало больше запросов, значит нужно больше коннектов" не совсем сходилось.…
В продолжение темы про коннекты: выше описывала кейс про pgx-пул, где коннекты росли быстрее, чем RPS, потому что запросы стали дольше удерживать соединения.

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

Представим: есть сервис A. Внутри он ходит в другой сервис - B. Запрос может прийти с большим количеством ID, поэтому мы аккуратно режем его на батчи по 100 ID и ходим в сервис B конкурентно через waitgroup. Соответственно, результат ручки можно вернуть только после выполнения всех батчей.


RT сервиса A = RT максимального батча в сервис B + DB + сетевой оверхед


На запросах 100–300 ID всё выглядело ожидаемо: сервис A в p99 работал за 90–100 мс, а внутри включал 70 мс сервиса B + 20 мс в базе + небольшой оверхед

Но на 500 ID p99 сервиса A вырос примерно до 200 мс.

Сначала подумала на бд, но запросы туда подорожали максимум на 10 мс. Даже если заложить это в общий путь, ожидалось что-то вроде роста со 100 мс до 120, но точно не 200. Аналогично с сервисом B, время ответа которого в p99 особо не поднялось, поэтому он не был причиной роста ручки выше.

Коллега предложил интересную идею - посмотреть на хвостовые задержки.

Вспомним, что при распараллеливнии запросов сервиса А ждет самый долгий запрос в сервис B. Поэтому p99 может зависеть не от p99 сервиса B, а от более долгого запроса.

На 500 ID у нас получается 5 параллельных вызовов в сервис B. Если взять время, соответствующее p99 одного вызова в сервис B, то каждый отдельный вызов уложится в него с вероятностью 99%. Но для сервиса A нужно, чтобы в это время уложились все 5 вызовов:


0.99^5 ≈ 0.95


То есть p99 сервиса B для запроса с 5 батчами превращается примерно в p95 сервиса A. Чтобы получить именно p99 сервиса A, нужно смотреть глубже в хвост сервиса B:


x^5 = 0.99
x = 0.99^(1/5) ≈ 0.998


Выходит, что p99 сервиса A при 5 параллельных батчах примерно смотрит на p99.8 одного вызова в сервис B.

(В реальности эти вызовы не полностью независимы, у них может быть общий хост, клиент, пул соединений. Но доказывает главное: распараллеливание запросов ставит в зависимость от хвостовых задержек.)

Следующий вопрос: откуда у части вызовов в сервис B берётся хвост? нам просто не хватило свободных коннектов.

HTTP-клиент переиспользует уже открытые соединения. Если есть готовый idle-коннект, то запрос уходит на него без создания нового. На тот момент MaxIdleConnsPerHost был 25, то есть столько уже созданных свободных соединений можно держать для переиспользования на один хост.

На 500 ID и 50 RPS получалось:


50 RPS в сервис A * 5 батчей = до 250 RPS на сервис B


Но тут еще нужно учесть время вызова: если один вызов в сервис B занимает примерно 100 мс, то бы получаем 250 RPS * 0.1 сек = 25 одновременных вызовов, и это ок пока ок для нас. Но если часть вызовов уходит в хвост и начинает занимать 150–200 мс, то нужно уже больше коннектов: 250 RPS * 0.2 сек = 50 соединений.

В итоге готовых соединений стало не хватать. Часть батчей переиспользовала уже открытые соединения, а часть попадала на оверхед создания нового соединения. При waitgroup достаточно одного такого батча, чтобы p99 всей ручки улетел вверх.

Чтобы проверить гипотезу, коллега предложил свести на один график:


p99.9 latency сервиса A
open HTTP connections до сервиса B
p99 duration сервиса B


На котором стало видно корреляцию:


соединений мало / они нестабильны
→ хвост сервиса A растёт

соединений становится больше и они покрывают всплески
→ RT сервиса A падает


В итоге мы просто подняли лимит idle коннектов с 25 до 50. На проверке при тех же условиях с 500 ID и 50 RPS ручка стала отвечать примерно за 100–120 мс вместо 200 мс.

Вывод: если ручка распараллеливает запросы во внешний сервис и ждёт все батчи до последнего, её RT определяется самым медленным вызовом. А из-за хвостовых задержек вероятность поймать такой медленный батч растёт. Дальше задача - найти причину хвоста. В этом кейсе ей это нехватка HTTP коннектов.

#go #perf
  • ❤ 12
  • 👍 6
  • 🔥 6
  • 🏆 1
More from @dariasroom
  1. Sep 15, 2026Автоматическое сжатие на клиенте vs ручное на сервере Стандартный клиент http.Transport са…
  2. Sep 5, 2026Причина перекосов в уровне балансировки L4-балансировщик выбирает серверную ноду при созда…
  3. Aug 28, 2026Итераторы… TL;DR: в iter.Seq итератор сам передаёт следующие элементы в код внутри range,…
  4. Aug 17, 2026Что на самом деле нужно сохранять при сериализации сложной структуры? TL;DR: Важно отделит…
  5. Aug 13, 2026Привет! Вас стало больше, так что пора наконец представиться 🙂 Я Даша, давно пишу на Go,…
  6. Aug 12, 2026HNSW: как устроен графовый индекс для векторного поиска One million years later, я наконец…
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →