Shared/Integration DB
Shared/Integration DB

Интеграционная или 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.

Итеграционная БД «здорового человека»
Итеграционная БД «здорового человека»

Итог

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

Комментарии (12)


  1. tkutru
    08.09.2022 15:56
    +5

    Один из не упомянутых плюсов shared db: вы можете использовать поддержку целостности данных на уровне одной бд, foreign keys, triggers, вот это все.
    Если вам надо добавить табличку, где есть FK на пять других табличек, не надо плясать с бубном как сделать это консистенто.

    Если у вас разные сервисы работают с shared db, неплохо бы разделить их на юзеров с соотв. правами. Иногда писать/лочить определенную табличку и не требуется более чем одному сервису, что также уберет ряд проблем.

    > если один из них добавит новую колонку в таблицу users с NOT NULL другой будет моментально сломан

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


  1. boopiz
    08.09.2022 18:42
    +5

    Автору следует познакомиться с такими вещами, как:

    • распределенные БД, обеспечивающие взаимодейсивие между разными экземплярами целевой бд (разные сервисы взаимодействуют со своей копией)

    • горизонтальная и вертикальная проекция. для локализаци и разграничения прав доступа и нотации предикатов отношений

    • хранимые процедуры, обеспечивающие единый интерфейс взаимодействия с данными.

      acid принцип это единственный правильный принцип для реляционных баз данных. Всё остальное это костыли.

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


  1. LaRN
    08.09.2022 19:13

    Если бы вы разрабатывали обычный монолит или сервис со своим хранилищем, то вы просто переименовали бы колонку в telegram_id, а также поправили код, который с ней взаимодействует.

    Врядли кто-то станет так делать, это очень регрессионно и трудозатратно. Для описанной вами задачи проще было бы добавить новую таблицу в которой для userId хранить список его каналов.


    1. mitya_k Автор
      08.09.2022 20:43

      Ну, это всего лишь частный случай, схема рано или поздно будет меняться.


      1. GlukKazan
        09.09.2022 08:23
        +1

        Вы так говорите, как будто это плохо


  1. abyrvalg
    08.09.2022 19:29

    один сервис занимается регистрацией пользователей через web, а другой регистрирует через реферальную программу

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

    Скорее всего, я просто не допёр или выпустил из текста что-то важеое..


    1. mitya_k Автор
      08.09.2022 20:48

      Более простой пример: один сервис пишет данные в таблицу, а второй читает из нее. Изменили схему таблицу и потенциально сломали один из них или оба.


      1. abyrvalg
        09.09.2022 02:43

        1. давайте разберём первоначальный пример: вы предлагаете делать отдельные базы под каждый способ регистрации? Если да, то как со всем этим работать? Если нет - зачем тогда это всё?

        2. Что меняется с более простым примером? Проблема просто переносится с уровня БД на уровень АПИ. И отследить её становится гораздо сложнее. Версионность АПИ может помочь, но далеко не всегда.

        Я не против самодостаточных микросервисов (какими они и должны быть), но примеры в статье, мягко говоря, странные.


      1. PrinceKorwin
        09.09.2022 09:32

        Тут нужно понимать зачем логика для работы на чтение и на запись были разделены в разные деплоймент единицы. Может быть разделение было ошибкой?

        Как знать, может и CQRS вам помочь. А может только усугубить.


  1. lebedec
    08.09.2022 21:39
    +12

    Интеграционная или shared база данных это архитектурный подход с которым мне часто приходилось сталкиваться, и практически никогда эта встреча не сулила ничего хорошего.

    Так это вы ещё не встречались значит с проблемами подходов, которые не используют общую базу. 

    Суть ведь не меняется. У вас есть одна предметная область, но разные сценарии и требования, которые вызывают конфликты в моделях.  

    Что вам сулит много хорошего: микросервисы, serverless, акторы? Допустим, микросервисы. Решать придётся точно такие же проблемы: 

    • Менять API микросервиса страшно. Вам так же придется бегать по потребителям и проверять не используют ли они изменяемый API. 

    • Точно так же у вас появится shared микросервис. Если им владеет другая команда, вы получите все перечисленные организационные проблемы. 

    • Отказавшись от shared базы в пользу микросервисов, вам придется так же отказаться от многих плюшек. Опытные DB девелоперы умеют аудит через механизмы СУБД, умеют генерировать динамический SQL, расширять возможности базы, гибко настраивать политики доступа, встраиваться в аналитические системы и прочее. 

    • Объединять потоки коммуникации микросервисов тоже становится невозможно или очень сложно. Например, у вас один сервис GraphQL, другой REST, а третий gRPC. Новая команда аналитики хочет получить доступ к некоторым данным в потоке из этих сервисов. Им вместо банальной выгрузки из базы придется писать адаптеры для всех перечисленных протоколов. Или идти напрямую в хранилища этих сервисов... Oh, wait! Это же нарушение инкапсуляции. 

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

    Так что пока эти решения не найдём, называть shared базу данных антипаттерном рановато. 


    1. maxp
      09.09.2022 08:26

      Пожалуй, наиболее информативный комментарий к данной статье на данный момент.


  1. R72
    09.09.2022 14:39
    +1

    Кто б автору про ESB технологии рассказал, про архитектуру данных, проектирование интеграции.....