golang

Работа с базой данных для джунов и вайбкодеров. Соединения

  • понедельник, 12 мая 2025 г. в 00:00:05
https://habr.com/ru/articles/908376/

В эпоху генеративного ИИ многим может показаться, что знать основы тех или иных технологий вовсе не нужно - все сделает добрая LLM, а вам надо лишь правильно дать ей задание. Вот только в этом рассуждении опускается ответ на вопрос о том, что значит это "правильно". Нейросеть может сломать код в самых неожиданных местах и не исправлять его по общему запросу "найди и исправь баги". Вам придется самостоятельно искать причину поломки и тыкать бездушную машину в ее же ошибку носом. Чтобы это сделать, надо уметь читать код, понимать, что нейросеть делает, и в целом быть подкованным в теме. А чтобы вам не пришлось страдать над толстыми и сложными книжками, я сделал это за вас и собрал информацию в упрощенном виде. Сегодня речь пойдет про реляционные базы данных, в частности - про соединения. От вас ожидаются только базовое понимание SQL и интерес к backend-разработке.

Как вообще выглядит взаимодействие с базой?

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

  1. Устанавливаем соединение - получаем коннект. Это своего рода труба, по которой мы можем общаться с нашей БД.

  2. Посылаем базе запрос, написанные на языке SQL.

  3. База анализирует запрос, строит план, выполняет его и формирует ответ.

  4. Получаем ответ.

  5. Фиксируем изменения - делаем коммит.

  6. Закрываем соединение - отпускаем коннект.

Технически пункт 5 является частным случаем пунктов 2-4. Но я вынес его отдельно специально и расскажу об этом в следующей статье.

Давайте подробнее про коннекты

Коннекты - это первое, с чем мы соприкасаемся при работе с базой. Их можно воспринимать, как физические границы нашей работы. Главное, что стоит понимать - SQL выражение мы выполняем в рамках одного коннекта. Мы не можем отправить запрос по одному коннекту и получить по другому. Сам коннект мы храним на стороне приложения, но сама логика обработки запроса выполняется в БД. Такое распределение дает простор для некоторых очень неприятных ошибок.

Представим ситуацию: пользователь нажимает кнопку в приложении, кнопка отправляет тяжелый запрос в базу данных. База данных начинает запрос обрабатывать, а в этот момент пользователь случайно завершает приложение. Запрос все равно продолжает выполнятся, пока не сформирует ответ и не попытается отправить его обратно - только в этот момент возникнет ошибка. Если пользователь сразу после завершения запустит приложение обратно и нажмет ту же кнопку еще раз, новый запрос начнет работать параллельно с предыдущим. Если пользователь будет делать так несколько раз, то очень скоро он придет к тому, что БД начнет тротлить (от англ. throttle - душить) обработку запросов из-за большого количества тяжелых запросов и это сильно замедлит вообще все запросы к ней.

Эта ситуация может показаться надуманной, но это специально упрощенная модель. Поставьте между кнопкой и базой API Gateway и backend-сервер с таймаутами на запросы к ним и получится куда более реалистичная ситуация: gateway отдает ошибку 504, фронтенд выводит что-то непонятное и пользователь думает, что кнопка его не услышала и надо нажать еще раз.

Работа с коннектами

Обычно коннектами управляют специальные программы или библиотеки - пулы соединений (HikariCP, PgBouncer, ...). Работают они под капотом различных фреймворков по работе с данными, так что вам не приходится думать об этом постоянно. Достаточно их подключить и настроить.

Настраивать пулы соединений стоит с учетом архитектуры вашего проекта, его профиля нагрузки и особенностей. Из общих советов могу только обратить ваше внимание на количество коннектов в общем и незанятых - в частности. Установление соединения - дело не бесплатное, поэтому лучше переиспользовать уже готовый коннект, чем создавать новый. Но даже поддержание коннекта - совсем не бесплатно. Вы тратите ресурсы сервера базы данных на поддержание коннекта - CPU БД все время мониторит коннект на предмет новых запросов. Немного подумав над этим, вы поймете, что количество коннектов ограничено. Причем оно ограничено не тем, сколько вы их можете поддерживать в незанятом состоянии, а тем, сколько параллельных запросов может выполнять база данных без тротлинга. В качестве стартовой точки я обычно считаю, сколько % CPU занимает один самый тяжелый запрос, затем смотрю, сколько раз это уместится в 100% (просто делю 100 на это число) и получаю лимит на количество коннектов в общем, а на незанятые я оставляю половину от этого. Если вы чувствуете в себе уверенность, вы можете отходить от этой формулы.

Но у пулов соединений тоже есть свои минусы - может произойти утечка соединений. Это когда старые соединения все еще живут, но воспользоваться ими мы не можем. Поэтому ресурсы базы уходят в никуда, а мы можем упереться в свои же лимиты даже при незначительной нагрузке. Механизмы их возникновения бывают разные и они специфичны для каждого конкретного случая - где-то это работа прокси, где-то - особенности API для работы с базой данных. Как раз на такой случай лимит на количество незанятых коннектов и ставится ниже, чем на количество в общем. Это даст некоторое время, чтобы засечь утечку и предпринять меры.

Подытожим

Коннекты обычно не приносят много головной боли на начальных этапах разработки. Вообще работа с ними обычно делается один раз, во время настройки, и дальше тюнится по необходимости. Но эта необходимость возникает часто в виде непонятных ошибок, которые выкидываются в случайных местах, непонятных графиков в графане и суеты админов вашей базы данных. Я постарался собрать ту информацию, которая позволит вам не потерятся в такой ситуации и даже поможет определить суть проблемы.

Если вам понравилось - можете подписаться на мой TG канал: https://t.me/nvantropov, там еще больше моих размышлений.