← Все статьи

RDS отказывал в подключениях при 10% CPU: дефолтные пулы, умноженные на реплики

Нагрузочные тесты упирались в ровное плато ~420 подключений, и просили «базу побольше». Реальный потолок — max_connections против дефолтных пулов Npgsql.

Во время нагрузочных тестов на стейдже в Gambchamp .NET-сервисы перестали открывать подключения к базе. До меня дошла простая просьба: «нужна база сильно побольше». CPU инстанса RDS в те же окна подскакивал до 97%, так что просьба выглядела разумной.

Форма графика говорила другое.

Плато, а не кривая

CloudWatch DatabaseConnections показывал ровную линию на 412–424 в течение десятков минут. Среднее за семь дней — около 7. Проблемы ёмкости обычно гнут кривую; ровная вершина — это упор в лимит.

Инстанс был db.t4g.medium с формулой max_connections по умолчанию — это 399. Минус superuser_reserved_connections=3 и reserved_connections=2 — обычным ролям остаётся 394 слота.

Откуда 400 подключений

Ни в одной строке подключения не было Max Pool Size, так что Npgsql брал дефолт: 100 подключений на процесс на строку подключения. Ни PgBouncer, ни RDS Proxy не было; каждый под подключался напрямую.

В логе PostgreSQL в пиковое окно было три клиентских IP на одной базе — три реплики интеграционного сервиса — и ещё один на другой базе. Трижды по 100 плюс 100 — примерно 400: потолок для приложений почти до подключения.

Доказательство — конкретная FATAL-строка, 18 раз в одном тесте и 47 в следующем:

remaining connection slots are reserved for roles with privileges of the "pg_use_reserved_connections" role

Два показания, которые сбивают с толку

  • CloudWatch в пике был выше max_connections (424 > 399). Метрика считает и бэкенды, которые форкнулись, а потом получили FATAL, поэтому в шторм она перелетает.
  • too many clients already не появлялось ни разу. Это сообщение значит, что закончились даже зарезервированные слоты. «Remaining connection slots are reserved» срабатывает раньше, когда кончаются только обычные, — ровно это и произошло.

Увидеть это практически можно было только через Logs Insights с окном пика: обычный фильтр по лог-группе с бесконечным хранением просто не заканчивался.

Инстанс побольше лишь двигает стену

Коллега поднял инстанс до db.m7g.large — лимит вырос до 832 (827 для приложений). Это запас, но не снятие стены: строки подключения к базе монтируют 17 подов, значит теоретический спрос — минимум 17 × 100 = 1 700.

Настоящая починка — на стороне приложений: явный Max Pool Size для каждого сервиса, чтобы сумма «реплики × размер пула» оставалась в пределах ~80% слотов для приложений. Я передал это владельцам сервисов с цифрами. Если число реплик станет динамическим с HPA, следующий шаг — RDS Proxy или PgBouncer. Ещё я добавил дашборд нод, чтобы следующий тест показывал и сатурацию нод: у 19 из 20 деплойментов в том namespace не было лимитов ресурсов.

Выводы

  • Ровное плато подключений — это лимит, а не кривая ёмкости. Проверь текст FATAL, прежде чем что-то увеличивать.
  • Пулы Npgsql — на процесс и на строку подключения: под, который ходит в три базы, может держать 300 подключений.
  • Планируй подключения явно: Σ(реплики × размер пула) ≤ ~80% от max_connections минус зарезервированные слоты.
  • CloudWatch DatabaseConnections в шторм отказов может превышать max_connections. Это не баг лимита.