Connection Pool: Как Go убивает базу данных (и как этого избежать) Выкатываете вы новый сервис, запускаете нагрузочное тестирование, и тут логи начина…
Выкатываете вы новый сервис, запускаете нагрузочное тестирование, и тут логи начинают истекать кровью:
FATAL: sorry, too many clients already.
Вы бежите к админам (или в консоль AWS) и видите, что ваш скромный сервис на Go открыл 1500 соединений к PostgreSQL и положил базу. Почему так вышло?
Потому что по умолчанию стандартный пакет database/sql не имеет лимита на количество открытых соединений. Если к вам прилетит 1000 запросов одновременно, Go честно попытается открыть 1000 TCP-соединений. Для БД каждый коннект - это отдельный тяжелый процесс, который жрет память.
Чтобы не быть врагом своим девопсам, нужно всегда настраивать пул соединений. Это делается тремя магическими методами.
1. SetMaxOpenConns(n) - Ограничение жадности
Это жесткий лимит на количество одновременно открытых соединений к базе.
Если вы поставили лимит 50, а пришел 51-й запрос, горутина просто заблокируется и будет покорно ждать, пока кто-нибудь не освободит коннект (или пока не отвалится по таймауту контекста).
2. SetMaxIdleConns(n) - Пул в режиме ожидания
Сколько соединений держать открытыми, когда нет нагрузки?
Если поставить слишком мало, при скачке трафика Go начнет судорожно устанавливать новые TCP-соединения (это долгий хендшейк, потеря драгоценных миллисекунд).
Золотое правило: Часто MaxIdleConns ставят равным MaxOpenConns. Это гарантирует, что пул всегда прогрет и готов к бою.
3. SetConnMaxLifetime(d) - Защита от тухлых коннектов
Если коннект висит слишком долго, база данных, балансировщик (HAProxy) или файрвол могут закрыть его в одностороннем порядке. Go об этом не узнает. Когда приложение попытается послать запрос в такой коннект, вы получите классическую ошибку driver: bad connection.
Заставьте Go принудительно пересоздавать коннекты раз в час или несколько минут.
Как это выглядит в коде:
db, err := sql.Open("postgres", dsn)
if err != nil {
log.Fatal(err)
}
// Задаем лимиты в зависимости от размера вашего инстанса БД
// и количества запущенных подов сервиса.
db.SetMaxOpenConns(50)
db.SetMaxIdleConns(50)
db.SetConnMaxLifetime(30 * time.Minute)
// Опционально: время жизни коннекта в простое (Go 1.15+)
db.SetConnMaxIdleTime(5 * time.Minute)
🔥 Senior Tip: PgBouncer
Если у вас 20 подов микросервиса, и каждый имеет MaxOpenConns = 50, суммарно в базу может прилететь 1000 коннектов. Postgres от такого станет плохо (оптимально для него - пара сотен коннектов).
Поэтому в мире взрослых нагрузок между Go и базой всегда ставят PgBouncer (в режиме transaction pooling). Он мультиплексирует тысячи легковесных клиентских коннектов в десятки реальных коннектов к базе. В таком случае в Go можно ставить лимиты побольше, а общую нагрузку будет сглаживать PgBouncer.
У кого базы падали в пятницу вечером из-за дефолтных настроек sql.DB?
#golang #database #architecture #performance #bestpractices
👉 @golang_lib