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. Это не баг лимита.