Postgres №40
Продолжение про пулы и соединения.
Если вы часто видите ошибку “too many connections”, это не значит, что нужно
срочно повышать max_connections. Это то же самое, что дать деньги алкоголику в
надежде, что он выйдет на работу. Если приложение плохо работает с соединениями,
не сомневайтесь: оно угробит двойной лимит подключений, тройной и так
далее. Надо искать причину.
В прошлой заметке говорилось о том, что использование пула, пусть даже для одного соединения, снижает риск ошибки. Но что это за ошибки? Перечислим некоторые из них.
Первая – открывать и закрывать соединение на каждый запрос. Например, в одном месте выбираем пользователей:
conn = postgres.connect(host=localhost, port=5432...)
conn.execute("select * from users where...")
В другом — профили:
conn = postgres.connect(host=localhost, port=5432...)
conn.execute("select * from profiles where...")
В третьем — что-то другое. На ровном месте три соединения вместо одного, вдобавок не закрытых.
Бывают клиенты, которые принимают не соединение, а его конфигурацию. В этом случае они открывают соединение, выполняют запрос и закрывают его:
(dеf config {:host … :port …})
(jdbc/execute! config "select * from table")
Этим страдает кложурный JDBC. Функция execute! и другие принимают не
соединение, а экземпляр протокола Connectable. У протокола два метода:
borrow-connection и return-connection – занять и вернуть соединение (пишу по
памяти, но семантика такая). У обычного соединения borrow-connection вернет
его же, а return-connection ничего не делает. Для пула borrow-connection
займет одно из свободных соединений, return-connection – вернет в пул.
Наконец, можно передать словарь с параметрами подключения. Для него
borrow-connection откроет соединение, а return-connection – закроет. Из
этого следует, что каждый запрос будет открывать и закрывать
соединение. Производительность такого кода ниже на два порядка, вдобавок вы
насилуете сервер частыми fork.
Другая ошибка – не закрывать соединение при исключении:
conn = postgres.connect(host=..., port=...)
users = conn.execute("select …")
for user in users:
process_user(user) # exception!
Предположим, на очередном шаге process_user выбросил исключение. Начинается размотка стека, мы улетаем куда-то наверх. Но соединение осталось открытым – реакции на ошибку не было.
Решение в том, чтобы использовать конструкции, которые гарантированно что-то сделают даже при ошибке. Самое банальное – try/catch. В Джаве есть try with resources, когда у каждого объекта вызывается метод .close. В Кложе для этого служит макрос with-open. В питоне есть контекстный менеджер with, где в точке выхода можно проверить, было ли исключение.
Еще ошибка – держать соединение занятым, когда на самом деле оно свободно. Кложурный пример:
(with-open [conn (jdbc/get-connection ...)]
(let [users (jdbc/execute! conn "select ...")]
(doseq [user useres]
(process-user users))))
Соединение будет закрыто при выходе из with-open, что в данном случае
неверно. Большая часть времени уходит на обработку результата, и соединением мог
бы воспользоваться кто-то другой. Однако мы держим его, пока не обработаем всех
пользователей. Будет правильно переписать его так:
(let [users
(with-open [conn (jdbc/get-connection ...)]
(jdbc/execute! conn "select ..."))]
(doseq [user users]
...))
В этом коде макрос with-open содержит только запрос. Как только он выполнится,
соединение закроется, а результат окажется в переменной users, для работы с
которой соединение не нужно. Разумеется, если вы стримите из базы или
пагинируетесь по ней, способ не подойдет — соединение должно быть открыто все
время.
Всех этих ошибок можно избежать, если использовать пул. Многие клиенты возвращают в него соединения автоматом. Даже если этого не случилось, пул определяет, что соединение взяли и не вернули, и пишет лог. По умолчанию у него нет права закрывать потерянные соединения – это слишком сурово. Однако такое полномочие можно дать, и пул будет закрывать соединения, которые, по его мнению, заняли безвозвратно.
Главная мысль заметки: повышать max_connections следует только после того, как
вы разобрались с кодом и метриками. Если нет понимания, кто и как расходует
соединения, нет смысла в повышении лимита. Плохой код сожрет его и попросит еще.
Нашли ошибку? Выделите мышкой и нажмите Ctrl/⌘+Enter