Интеграционная или shared база данных это архитектурный подход с которым мне часто приходилось сталкиваться, и практически никогда эта встреча не сулила ничего хорошего. Как правило, команда выбирает данный подход по нескольким причинам:
Не надо писать никакие контракты и схемы для интеграций сервисов между собой через API, а каждый может читать/писать из одной БД.
Не надо думать о синхронизации данных, если данные в БД записались значит консистентность достигнута.
Не надо снимать бэкапы с нескольких хранилищ, если можно снимать с одной единственной БД.
Есть еще одна причина, это когда горели сроки и надо было срочно добавить новый сервис, а потом переделывать всем было лень. И вжух, у нас через год пяток сервисов, которые работают с одной БД и с одними и теми же таблицами, но это скорей организационная проблема, чем осознанный выбор.
Но по мере развития проекта и увеличения команды все эти плюсы улетучиваются или даже превращаются в минусы. Об этом и поговорим ниже.
Страшно менять
По мере развития проекта рано или поздно вам придется менять схему БД. И тут вылезет самая очевидная проблема, менять схему вам будет страшно и больно.
Для начала разберем случай с переименованием колонок.
id | name | chat_id |
1 | Вася | 24540841 |
Например, у нас есть таблица пользователей (users
) с id
, name
, chat_id
, где сhat_id
- это id пользователя в telegram, но логика поменялась, и теперь надо еще хранить id пользователей из whatsapp. Если бы вы разрабатывали обычный монолит или сервис со своим хранилищем, то вы просто переименовали бы колонку в telegram_id
, а также поправили код, который с ней взаимодействует. Если у вас интеграционная БД, то велик шанс, что вы сломаете код другого сервиса. Например, сервис нотификаций использует chat_id
для рассылки сообщений пользователям.
С удалением все аналогично, любой сервис, который использует удаляемую колонку для чтения/обновления/удаления будет моментально сломан.
Но вот с добавлением новых колонок не должно быть проблем, а вот и нет. Например, один сервис занимается регистрацией пользователей через web, а другой регистрирует через реферальную программу, если один из них добавит новую колонку в таблицу users
с NOT NULL
другой будет моментально сломан. Ведь другой сервис ничего не знает об этой колонке и не сможет осуществлять записи в эту таблицу...
Как итог вам придется бегать по всем сервисам и проверять не используют ли они колонку, которую вы собираетесь изменять в своих целях. И если вам не повезло, то вам придется общаться с командами этих сервисов, чтобы они внесли эти изменения к себе в код, а затем проводить увлекательную синхронизацию релиза нескольких сервисов в продакшен. Из за такой «удобной» процедуры разработчики не будут удалять колонки, а новые колонки будут создавать с DEFAULT NULL
или другим дефолтным значением, что не очень хорошо по многим причинам. А переименовывать колонки будут только в самом крайнем случае. Думаю, не надо объяснять, что таблицы в БД при таком подходе будут быстро превращаться в хламовник со старой мебелью.
Никто не владеет схемой
С shared БД, по факту, за миграцию схемы будет отвечать один человек/команда, который будет владеть репозиторием, куда все остальные разработчики должны пушить новые миграции. А владелец должен ревьюить все эти изменения на то, что они не сломают код в остальных сервисах. По факту этот человек/команда становится бутылочным горлышком всей разработки, так как они могут не успевать это делать: у них могут быть другие задачи или они просто захлебываются под merge request на изменение схемы. И роли владельца схемы тут сложно позавидовать, ибо бегать по пачке репозиториев в поисках потенциально сломанного кода, дело утомительное, особенно если сервисы написаны на разных языках. Также встает вопрос, а кто должен согласовывать изменения в схеме БД с другими командами, владелец схемы или тот, кто сделал merge request? А когда мы даже все согласовали, встает довольно непростая задача, как все это зарелизить синхронно? Плюс откатываться назад мы по сути не можем, ведь надо откатывать назад не один сервис с БД, а сразу целую пачку плюс сервис, который накатывает миграции.
Другой вариант, что миграции пишутся в каждом отдельном сервисе и накатываются им же при деплое. Но тут вообще остается надеяться только на устные договоренности и на то, что никто из разработчиков не совершит ошибку. По факту, схемой БД начинает владеть «коллективное бессознательное». Вводить в курс дела нового человека становится очень сложно, и требуется по 50 раз перечитывать миграции и код, работающий с БД.
Курица не птица, а БД не API
Один из плюсов интеграционной БД, указанных выше: «Не надо писать никакие контракты и схемы для интеграций сервисов между собой через API». Это же и минус. В начале все удобно и прикольно, но затем все становится очень больно.
Во-первых, API снижает когнитивную нагрузку на потребителя. Оно предоставляет схему данных и ресурсы, над которыми можно совершать какие-то действия. Когда же вы смотрите на структуры таблиц вам требуется гораздо больше мозговых усилий на то, чтобы выделить ресурсы и отбросить все лишние колонки, которые вам не требуются в данный момент для решения задачи. Кроме того, сущность может быть разбита на несколько таблиц (например, фильм и его расписание это скорее всего разные таблицы), а в API она скорее всего представлена одной сущностью с вложенными атрибутами. Ко всему прочему таблица может содержать кучу технических колонок или колонок для других сервисов, и разработчику придется прорубаться сквозь этот лес. Я уже не говорю о том, что проблема «страх изменения схемы» описанная ранее, ведет к тому, что таблицы будут гораздо быстрее замусориваться, чем в раздельных БД. Грубо говоря, БД это как большая и толстая книга, а API как краткое содержание этой книги.
Во-вторых, вам придется выкинуть на помойку все плюшки кодогенерации из схемы вашего API, а также будут большие проблемы с валидацией. Например, есть таблица files
с колонкой link
, которая содержала ссылку на файл и это был полный путь вместе с доменом, а затем у нас стало множество файловых хранилищ и ссылка теперь должна записываться без домена. Как защититься от того, чтобы не произошло записи link
в старом формате? И где писать валидацию? Ведь писать можно из разных сервисов, и защититься от этого становится сложно. Да, можно писать триггеры и хранимые процедуры, но тогда этот код становится прибит к БД, и будет находиться вне git, к тому же SQL не лучший язык для написания сложной логики и последующей ее поддержке. Ко всему прочему при большой нагрузке, БД не скажет вам спасибо за ваши навороченные констрайнты и триггеры.
В-третьих, аудит данных, rate limit и логирование практически нереально реализовать, ведь добраться до данных может любой сервис и что-то с ними сделать. Да, можно завести отдельных пользователей с правами, но тогда все плюсы от интеграционной БД уйдут, ведь придется писать API для других сервисов. Опять же остаются хранимые процедуры и триггеры, но тогда разработка становится БД ориентированной...
Данные целостны, почти....
Это правда, c интеграционной БД вы получаете консистентные данные из коробки. Это по сути единственный плюс такого подхода. В случае, если данные записались в таблицу БД не требуются разные очереди сообщений, поддержка retry, webhooks и прочие вещи для достижения консистентности данных при наличии множества хранилищ. Но тут есть две оговорки, во-первых, вы будете иметь очень хорошо синхронизированную помойку, которую страшно трогать, а во-вторых, проблемы синхронизации полностью все равно не уйдут. Ведь ваш сервис исполняет код вне БД, и остается возможность работы с устаревшими данными. Например, один сервис прочитал всю таблицу users
в память и начинает рассылать email, используя фио пользователя, в это время пользователь через другой сервис изменил свое фио, в результате пользователь получит email со старыми данными.
Одним выстрелом двух зайцев
В плане администрирования кажется, что проще работать с одним хранилищем, чем с множеством (все данные в одном месте, снимать бэкапы и т.д), но тут много но....
Во-первых, одна БД – одна точка отказа для вашего проекта. Если она легла, то лежит все. Если у вас набор сервисов с независимыми хранилищами и они взаимодействуют друг с другом не в стиле «распределенного монолита», то отказ одной БД лишь ведет к отказу одного конкретного участка системы.
Во-вторых, если ваш проект доживет до большой нагрузки, то ваша интеграционная БД станет раскаленной сковородкой. В одну и ту же таблицу может писать множество сервисов, возможно блокируя друг друга или вызывая серьезные перестроения индексов, что плохо сказывается на быстродействии системы. Кто-то в силу глупости, неопытности или срочности может запустить «дико тяжелый» sql запрос от которого БД встанет на уши и все сервисы, которые с ней работают начнут тормозить. И какой бы крутой не была бы ваша БД, вы задумаетесь о ее масштабировании, а это будет сложно, ведь поддержку шардирования и репликации будет сложно внедрять, придется во всех сервисах переписывать код для чтения и записи. И скорее всего для простоты вы будете накидывать ресурсы виртуалке с БД и больше ничего не делать, пока не упретесь в потолок вертикального масштабирования. В случае аварии тяжелая и нагруженная БД может подниматься значительное время, а у вас тем временем недоступны вообще все сервисы. Снимать бэкапы с «горячей» и большой БД тоже отдельное приключение. Для цельного бэкапа надо поднимать реплику с такими же большими дисками, как на мастере, и при этом выделять много-много места под такие бэкапы где-то на других дисках. Все эти проблемы с бэкапом возможно подтолкнут к стратегии разной частотности бэкапов для разных данных, что не решит всех проблем, а даже создаст множество других. Как ранее мы выяснили, все описанные выше вещи будут осложняться тем, что БД будет замусориваться гораздо быстрее, чем с раздельными хранилищами под каждый сервис.
Имея раздельные БД для разных сервисов, нагрузка размажется или окажется в нескольких сервисах, которые можно кастомно масштабировать или докидывать ресурсов, а всем остальным хранилищам как раз можно будет их подурезать. Да, бэкапы придется снимать с множества инстансов, но размер и нагруженность баз будет меньше. И отказоустойчивость станет выше, см. пункт первый.
В-третьих, сменить тип хранилища становится невозможно или очень сложно. Например, одному из сервисов удобнее хранить данные в графовой БД, а другому в key-value. Если этими данными пользуются или их создают другие сервисы, команды этих сервисов должны будут переписать свой код для работы с новой БД, и они могут не хотеть этого делать по разным причинам. Если бы общение шло через то или иное API, то вопрос хранилища оставался вопросом команды этого сервиса.
Когда будет нормально
Есть частный случай, когда данный подход будет приемлем. Если каждый из ваших сервисов имеет свои таблицы, схемы (Postgres) или базы (MySQL) в физической БД и только он может писать и читать из них. Разделение прав на чтение и запись осуществляется через создание пользователей в БД под каждый сервис. А все взаимодействие между сервисами происходит через API или вызов модулей, если это части одной кодовой базы. Такой подход решает проблемы с управлениями данными и схемой, а также позволяет легко переехать сервисам на другой инстанс БД или вообще на другой вид БД, например, NoSQL.
Итог
Несмотря на то, что данный архитектурный паттерн присутствует в ряде книг по архитектуре и описывается не в столь мрачных тонах, мне кажется, что на данный момент это антипаттерн, от которого надо держаться подальше. Подобно тому как в коде проекта мы обычно стремимся к инкапсуляции, т.е. чтобы данные и методы для работы с ними находились бы в одном классе/модуле, аналогичный подход следует использовать для построения архитектуры всего проекта. Интеграционная БД это пример нарушения инкапсуляции только на уровне архитектуры всей системы, а не отдельного сервиса, когда данные оказываются оторваны от их владельцев (сервисов/модулей). За исключением случая, когда все модули/сервисы имеют изолированные таблицы/базы, которые доступны только им самим (см. предыдущий раздел).