Комментарии 65
4. Миграции БД

А в го вобще есть нормальные инструменты для миграций? В чем проблема инструментов которые я смотрел, там обязательное требование к имени миграции «возрастающие integer», иногда даже обязательно последовательные, без пропусков, это исходит из того, что в базе хранится только одна строка — номер последней примененной миграции.

Предположим ситуацию с migrate вы делаете новую ветку в git, создаете миграцию в момент времени t=100, ваш коллега делает новую ветку и создает миграцию в момент времени t=110, причем заканчивает фичу первым и заливает ее в продакшен. в итоге версия нашей базы на проде = 110. Т.е. при выкатке миграции с номером 100, мигратор ее просто проигнорирует, верно? Тесты тестами, но зачем такие головняки, почему бы не храить список всех миграций в базе и применять непримененные.
Миграции должны иметь последовательность. Будет сложно предсказать результат если невыполненные миграции будут накатываться в произвольном порядке ибо одна миграция обычно зависит от другой. У нас бывает конечно такая проблема, что два и более разработчика работают в одном сервисе и пишут миграции. Но это случается очень редко и в таком случае у нас действует правило: «Кто первым встал — того и тапки». Касательно инструментов, некоторые команды в Lamoda еще используют goose, основное отличие только в том что миграции «вверх» и «вниз» описываются в одном файле.
Ну я и не говорю, что они должны применяться в случайном порядке. Они так же будут применяться подряд, если сложилось впечатление что я против нумерации, то это не так. Но ситуация
что два и более разработчика работают в одном сервисе и пишут миграции

уйдет автоматически. Как пример — django.
т.е. я не вижу ни одного плюса в хранении только последней примененной миграции, но вижу минус.
Положим миграция 100 у нас просто добавляет индекс, в тестах можно и не заметить, что мигратор ее проигнорировал.
Если вы говорите что у вас все ок, я верю, но не может же у всех все быть ок.
Как пример — django.

В Django мы ловим те же самые проблемы. Собственно там мы чаще всего и ловим) Там же тоже внутри каждой миграции описывается от какой миграции она зависит. И вот когда два разработчика завязываются на одну базовую миграцию — возникает конфликт и Django точно так же не дает их накатить.
В Django достаточно просто прикручивается что-то вроде этого и все конфликты миграций становятся конфликтами для merge, заставляя разработчиков решать все конфликты перед вмерживанием ветки.
Мы в качестве номера миграции используем таймстамп. Все накаченные миграции сохраняются в базе, при запуске миграций выполняется полный перебор файлов с миграциями, в порядке увеличения таймстампа, и те, которых нет в базе — накатываются.
Я могу, чисто теоретически, представить конфликт — но для этого два разработчика должны параллельно работать с одной и той же таблицей, и производить над ней несовместимые изменения. Пока ни разу конфликтов не было, при том что ситуация когда три бэкендера параллельно пишут миграции каждый в своей ветке, а потом они сливаются в произвольном порядке — случается регулярно.
Это проблема конкретно данного инструмента, а не таймстампов — у нас самописный скрипт миграций занимается «ненужной» работой — вместо того чтобы проверять последнюю миграцию, он проходится по всему списку миграций, и все неприменённые(т.е. не имеющие записи в бд о успешном применении) -накатывает.

А ничего, что накат некоторых миграций не в том порядке может давать разные результаты?

Миграции из одной ветки в любом случае будут применены последовательно. Если миграции из двух параллельных веток разработки связаны логически, и должны накатываться в определённом порядке — то стоит уделить внимание организации постановки задач в команде.
Ну и, мне на самом деле сложно представить такой пример. Как правило, большинство миграций это альтеры на создание новых полей, или создание таблиц. Реже — изменение старых полей. Так же редко — инсерты справочных данных.
Вы можете привести последовательность из четырёх запросов, которые нормально работают попарно — каждый в своей ветке — так же нормально работают вместе в одной последовательности(сначала первая пара, потом вторая) но ломаются если их применить вперемешку(не меняя частные зависимости последовательности в паре)?

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


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

Конечно, могу. Равно как и Вы можете эти примеры опровергнуть

А вдруг не получится опровергнуть, мне тоже интересно посмотреть. я даже сам могу придумать такую ситуацию, но она никогда не возникнет в рабочем процессе.

Из того, что у меня недавно было на проекте — одна миграция это UPDATE который нормализует существующие записи, а вторая это INSERT который добавляет в справочник запись "по-старинке", ещё не нормализованную. В зависимости от порядка применения либо все записи в БД будут нормализованы, либо все кроме одной. При этом UPDATE — это часть срочного багфикса, INSERT — часть новой фичи. Ну т.е. один разработчик начал делать фичу, создал миграцию с INSERT, но процесс несколько затянулся, как обычно. А в это время нашли баг, и другой разработчик быстро пофиксил его UPDATE-ом. Багфикс смержили до фичи, хотя разработка багфикса началась позднее.

В истории полно дыр. я готов засчитать ее на 30%, почему? потому что тут человеческий фактор и порядковые номера миграций не спасли бы. Рассказываю.
2 пользователя создают 2 параллельные ветки для работы, 1 для фичи — b_insert, второй для багфикса — b_update.
В изначальной ветке у нас 99 миграйций.
Пользователь 1 создает миграцию 100_insert.sql
пользователь 2 создает миграцию 100_update.sql
пользователь 2 сливает свой коммит с мастером и пушит в репу.
пользователь 1 сливает свой коммит с мастером и пытается пропушить, git не разрешает, говоря о том что мастер убежал. Пользователь делает pull, к нему прилетает 100_update.sql, с мыслями «кто первый встал того и тапки» пользователь переименовывает свою миграцию 100_insert.sql -> 101_insert.sql, делает тесты и пушит на сервер.
Итог INSERT после UPDATE.

Нетушки. На то есть ревью. Плюс вменяемый разработчик посмотрит сначала на 100_update.sql, и подумает, как эти изменения должны сказаться на его миграции 101.


Но самое главное вовсе не это. Баг создать можно, протупить с миграцией и ревью тоже можно — это всё нормально. Главное, что в результате везде (локально у всех разработчиков, на стейдже, на проде, etc.) будет этот баг с некорректной записью в БД. БД будет везде одинаковая, не будет проблем с откатом последнего PR или можно надёжно (на всех площадках) пофиксить этот баг в 102_update.sql.

Погоди, а почему у нас при порядковой нумерации ревью есть. а без нее нету? Я ждал замечания про внимательного разработчика=)

разработчик точно так же увидит что прилетела миграция 05.06.2020_update.sql посмотрит внуть и поправит свою insert в соответствии с новой схемой.

миграция 100_update(05.06.2020_update.sql) уже накачена в прод и правильный путь это поправить 101_insert(01.01.2019) под новую схему. переименуешь свою 01.01.2019 в 10.06.2020_insert чтобы она у всех запускалась после update даже у тех кто все с нуля накатывает.

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

Починить прод, на котором по немнивательности применили миграцию 05/05/2020, а потом 01/01/2019 новой третьей миграцией от 06/06/2021 наверно не всегда возможно,
она должна быть написана таким образом чтобы порядок
01 -> 05 -> 06
и
05 -> 01 -> 06
давал один и тот же результат при том что 01 -> 05 и 05-> 01 дают разные результаты, но можно откатить миграции и переприменить в ручном режиме в крайнем случае.
откатить миграции

откатить, лол, попробуйте откатить DELETE/UPDATE/ALTER на столбец. Скажем так — существуют миграции, которые возможно откатить, есть правила хорошего тона (писать чтобы все было откатываемым), но это совершенно не означает, что в реальном кейсе не случится какой-то факап.

я коментировал конкретный случай. откат здесь нужен для применения в правильном порядке. Зачем тебе откатывать DELETE чтобы его снова накатить в нужном порядке?
ALTER? а ты уверен что у тебя возможно применить миграции в проивольном порядке при альтере? Ну типа добавил я not null колонку в update.sql, как ты после этого сделаешь INSERT, который об этой колонке ни сном ни духом?

как минимум это так не работает. Вы те же миграции Алхимии под пайтон видели?

а причем тут алхимия если обсуждается go и инструменты миграций с последовальныой нумерацией?
разве алхимия сама по себе умеет миграции? я думал нужен alembic. Там хранится 1 идентефикатор последней миграции(хэш?), но с механизмом работы я не знаком. Но снова вопрос, какое отношение это имеет в го и последовательным номерам?)

Если ты расскажешь юзкейс с которым справится алхимия или последовательная нумерация в go инструментах но не справятся django/diesel миграции я буду рад почитать.
Погоди, а почему у нас при порядковой нумерации ревью есть. а без нее нету?

Потому что без неё оба PR могут спокойно пройти ревью, потому что по отдельности они оба корректные. Проблемы начинаются в момент, когда один из PR-ов смержили первым, но второй к этому моменту уже может быть аппрувнут. Ну и в целом такая ситуация усложняет процесс ревью, потому что нужно смотреть не только текущий PR, но и все остальные открытые, и не только в момент ревью, но и непосредственно перед мержем… в результате продолбать это на ревью становится намного проще, чем при порядковой нумерации.

и оно правда, ради одного такого случая стоит
время придется переименовывать свою миграцию, даже когда нет разницы в порядке запуска

Так уж вышло что с таким процессом работы не сталкивался. Как оно работает? Прилетают pr от 5 человек, везде миграции 100, где-то еще 101, 102, 103. Ревьювер 1 подтверждает, остальные на мороз перенумеровывать. В итоге прилетает 4 pr, с номерами 101 103 104?
В каких то очень редких случаях, порядковая нумерация может быть помогает предотвратить косяк, но плата слишком высока, нет я не куплю=)

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

т.е. звезды должны сойтись, чтобы взаимозависимые миграции прилетели практически одновременно. если апдейт влили в продакшен еще 3 дня назад, то сегодня разработчик заливающий свой insert перенумерует миграцию с номера 100 на номер 115 и ревьювер не будет просматривать кучу, да даже 1 предыдущую и молча пропустит, верно?

Работает очень просто. Очень небольшое количество PR-ов затрагивает миграции, поэтому необходимость переименовывать их возникает редко, и, на моей памяти, ещё не разу это не затрагивало более одного PR. Даже если переименование продолбали — вторая миграция просто не накатится, проблему обнаружат на стейдже, и следом выкатят фикс переименующий миграцию.


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

Пользователь 1 создает миграцию 100_insert.sql
пользователь 2 создает миграцию 100_update.sql

как минимум это так не работает. Вы те же миграции Алхимии под пайтон видели?

Ну у нас были проблемы, когда миграции затрагивали одни и те же таблицы.
Если два программиста одновременно работают над пересекающимися задачами — это уже плохая идея. Но даже в таком случае — если им нужно, например, добавить в таблицу новое поле, и они нормально именуют поля, вряд ли они назовут поле одинаково. А следовательно, нет никакой разницы, в каком порядке применять эти два альтера. Так же хорошая практика на дэйли рассказывать, кто над чем работает — это минимизирует вероятность, что два программиста независимо друг от друга создадут в одной таблице поле с одинаковым именем, заложив в него разную логику.
Если вы работает в разных ветках с одной базой и в обоих есть миграции, то ничто не спасет от наслоений и пред мержем всегда есть вероятность, что придется переименовывать миграции, если порядок вмерживания не совпадает нумерацией миграций.
goose, основное отличие только в том что миграции «вверх» и «вниз» описываются в одном файле.

На самом деле основное отличие в том, что goose поддерживает миграции в виде функций на Go, что важно, потому что далеко не всегда возможно описать требуемую миграцию на чистом SQL. И в каком проекте и на каком этапе это понадобится — заранее предсказать невозможно, поэтому лучше иметь такую возможность изначально, и пусть лучше не пригодится, чем наоборот.

Еще полезно, когда приходится менять очень много данных и чтобы не блокировать таблицу одной мегатранзакцией, из гоу менять порциями.
github.com/rubenv/sql-migrate
Вот рабочий вариант миграций с атомарностью и раздельным хранением.
Не так популярно, как go-migrate, но мне тож так больше нравится. (не только верхняя версия, а весь список примененных миграций к конкретной БД и когда они были выполнены.)
Из плюшек — миграции вшиваются в бинарник, и деплой базы упирается в указание, какая из нод должна накатить изменения. Требования обратной совместимости и не мигрировать всеми разворачиваемыми нодами одновременно естественно на релиз инженере.
github.com/lancer-kit/service-scaffold/tree/master/dbschema — пример интеграции.
(надеюсь, что не обидел автора топика разместив ссылки в комментариях)
Насколько я знаю migrate тоже поддерживает «вшивание» миграций в бинарник, через go-bindata. http://https://github.com/golang-migrate/migrate#migration-sources Мы у себя правда используем вариант с docker образом migrate и монтируем в него папку с миграциями (примерно также как в docker-compose.yml в статье). Все это осуществляется в рамках отдельной deploy'ной job'ы, вроде хватает. Но ваш вариант обязательно изучу.
Да, спасибо, вроде то что надо. Странно даже что непопулярно, мне вот непонятно, как можно работать с миграциями, сохраняя только последнюю примененную для опеределения состояния базы.
А потому что миграции с меньшей версией чем в базе не должны мигрироваться, потому что уже могут не соответствовать текущей структуре базы.
А если я займусь головняком по переименованию миграции, она резко начнет соответствовать?

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

а можно пример, порядок событий, который может привести к такому результату.
И все равно немного не то.
Заголовок спойлера
swelf@swelf-home:~/src/test$ sql-migrate status
+---------------------------+--------------------------------------+
| MIGRATION | APPLIED |
+---------------------------+--------------------------------------+
| 20200414145002-init.sql | 2020-04-14 11:50:10.30518 +0000 UTC |
| 20200414145011-second.sql | 2020-04-14 11:51:36.585836 +0000 UTC |
| 20200414145021-third.sql | 2020-04-14 11:50:38.786196 +0000 UTC |
| 20200414151630-new.sql | 2020-04-14 12:17:41.554844 +0000 UTC |
+---------------------------+--------------------------------------+

##Делаем вид что переключаемся на ветку, в которой нет файла 20200414151630-new.sql, в тоже время в базе миграция отмечена как применная.

swelf@swelf-home:~/src/test$ mv migrations/20200414151630-new.sql.
swelf@swelf-home:~/src/test$ sql-migrate status
Could not find migration file: 20200414151630-new.sql
+---------------------------+--------------------------------------+
| MIGRATION | APPLIED |
+---------------------------+--------------------------------------+
| 20200414145002-init.sql | 2020-04-14 11:50:10.30518 +0000 UTC |
| 20200414145011-second.sql | 2020-04-14 11:51:36.585836 +0000 UTC |
| 20200414145021-third.sql | 2020-04-14 11:50:38.786196 +0000 UTC |
+---------------------------+--------------------------------------+
swelf@swelf-home:~/src/test$ sql-migrate up
Migration failed: Unable to create migration plan because of 20200414151630-new.sql: unknown migration in database

в итоге не совсем понятно, как работать с миграциями и системой контроля версий. Все время откатывать перед переключением веток?

droppoint
Ничего, что я здесь про django отвечу?)
В Django мы ловим те же самые проблемы. Собственно там мы чаще всего и ловим)

Ну они же бывают крайне реже, более того, у меня не было случая когда джанго сама бы не смержила миграции, спросив у меня разрешения.
гошный migrate в принципе не сладит с 2мя миграциями слитыми с 2 веток с одинаковым номером. или в разными номерами, но к базе в данный момент уже применена 110, таким образом 100 не применится. А джанго не сладит в каких-то крайних случаях, которые у меня и не случались даже. Более того, косяки джанго никак не «оправдывают» миграторов в го, я привел джанго как пример модели работы с миграциями, пусть и не всегда идеальной работы.
Изменения в ветке вы же тоже комитите или сташите, логично, что изменения в базе тоже нужно откатить. По этому у вас и есть down миграции.
Иначе как понять состояние базы, если база считает, что была миграция, а кодовая база не знает что в ней было, следовательно не понимает в каком состоянии база, и совпадает ли это состояние с кодом.
Там есть возможность перенакатить базу с форсом. Если это имеет смысл и локальный стейт не содержит нужных данных, или есть подготовленные скрипты наполнения базы случайными данными.
У мигрейта есть схожая функциональность. Можно указать на сколько шагов нужно откатить базу. Но при наложении миграций, или мерже «вчерашних» — он не сообщает о проблемах, что создает дополнительные риски при релизе.
Иначе как понять состояние базы, если база считает, что была миграция, а кодовая база не знает что в ней было

А всегда ли это надо кодовой базе. Допустим в нашей миграции я создал таблицу t1, потом переключился на старую ветку, где этой миграции нет. Зачем мне эту таблицу удалять? Код о ней ничего не знает, ну есть она и есть. Какую цель несет это требование?
Вариант когда я наоброт что-то удалил, например t2, потом вернулся в старую ветку, код думает что t2 должна быть, t2 на самом деле нету. Тут конечно надо запустить down, который создаст снова t2. Но помойму это и без ограничений со стороны мигратора понятно. В первом случае ограничения мешают, во втором бесполезны.
Вариант 1 — вторая ветка не имеет новых миграций для этой базы.
Миграцию выполнять не нужно. Проблемы нет.
Вариант 2 — во второй ветке есть невыполненная миграция. При ее выполнении будет создано новое состояние базы, которое не будет воспроизведено нигде. (результат мержа этих двух веток не всегда будет совпадать с результатом работы каждой из них отдельно выполненной в произвольном порядке)
Если обе меняют схему в БД — откатили и накатили другую.
Если нет — можно и так. По быстрому.
Если же по нормальному — нам нужно работать с тем же состоянием и не создавать себе проблемы с хранением в голове веток, их миграций, взаимосвязей в коде и в базе. Лучше эти ресурсы потратить на разработку самой фичи, и концентрацию на ней.
и не создавать себе проблемы с хранением в голове веток, их миграций, взаимосвязей в коде и в базе

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

а давай представим в каком кол-ве случаем они не будут совпадать, а в каком будут. И что проще, всегда откатывать миграции, ради 0.1%, либо откатить их только в случае 0.1%?

Я вот совсем не могу себе представить, чтобы я создал 2 миграции, находящихся только у меня, в 2х параллельных независимых ветках, чтобы миграции зависели друг от друга, как это вобще возможно?

более того, вот сломается у меня в этом 0.1% случае код локально, не будет схема совпадать с кодом, что мешает именно в этом случае все дропнуть и создать миграции с 0/накатить бэкап базы.
Но зачем каждый раз то этим заниматься, это как с зонтиком ходить каждый день, даже когда на небе ни облачка.
Вы так говорите как будто каждая миграция выполняется по минуте.
Для сравнения — на довольно старом (ruby) проекте 100 миграций испольняются секунд за 7. Поэтому я сразу делаю себе алиас а баше, который дропает базу, потом создает ее, и накатывает все миграции. Все! Нет больше никаких страданий «а тут мы написали неправильный down для миграции и все рассыпалось». Проще накатить начисто при переключении ветки.
Это если ты руками базу не забивал. Для чистых тестов я тоже с 0 накатываю миграции и создаю сущности, каждый раз. С рабочей базой, которая может быть даже дампом продакшена, это неудобно.
Да. Поэтому у меня есть второй алиас. Он удаляет базу, создает заново, вливает туда дамп с прода (кторый лежит локально по известному пути), и докатывает миграции которые отсуствуют на проде. После этого можнон прогать.
Если говорить про прод, и конкретно про наш опыт в этом вопросе, то у нас такие кейсы вылавливаются либо при мердже feature ветки в основную, либо при накатке миграций на продовую базу. Как я упоминал ранее у нас есть отдельная deploy'ная job'а для накатки миграций и при ее работе сразу видно что накатилось, а что нет. Мы правда еще обычно и руками проверяем на всякий.

Если говорить про локальную машину — то в нашем случае если в разных ветках разные миграции и ребейзиться прямо вот сейчас не хочется, то никто не мешает убить контейнер с базой, а потом поднять его снова чистым и накатить миграции. Если посмотреть на docker-compose файл в статье, то мы делаем
make dev-down
make dev-server

и живем дальше.
В migrate тоже можно вшивать в бинарник даже без go-bindata. Достаточно свой migrationSource написать. Делов на десяток другой строк. Не люблю возиться с миграциями где-то за пределами кода.
Спасибо за статью, думаю более-менее все команды, работающие с микросервисной архитектурой, двигаются в похожем направлении.

Интересно, а как вы решаете вопрос обновлений общего шаблона? Вот например был неплохой шаблон, его за полгода использовали для создания 10 новых микросервисов. Всё получилось замечательно и унифицировано. А через полгода осознали, что в шаблон надо бы добавить еще одну общую для всех микросервисов ручку для мониторинга. А может и не через полгода, а через месяц. Как вы поступаете в таких случаях?
Пока никак, шаблон создали сравнительно недавно. В любом случае будем что-то добавлять, и да, не ожидается что это будет просто. Но у шаблона есть меинтейнеры, он открыт для Pull Request'ов со стороны любого разработчика в Lamoda, да и меинтейнеры всегда готовы помочь адаптировать существующие проекты под шаблон.

Я обычно в таких случаях временно подключаю репо с шаблоном к репо с уже существующим сервисом (да, для git это вполне штатная ситуация, когда в "одном" репо по факту находится несколько несвязанных между собой "деревьев" коммитов с разными корневыми коммитами) как ещё один remote, после чего делаю cherry-pick нужных коммитов из шаблона.


Раньше ещё пробовал вариант слияния этих двух деревьев коммитов в один общий "ствол", чтобы иметь возможность постоянно "подтягивать" изменения из репо шаблона в репо сервиса (фактически в репо сервиса при этом было два "апстрима" из которых затягивались изменения). Пробовал начинать каждый новый сервис с честного форка репо шаблона, чтобы было проще изменения затягивать. Более того, пробовал делать несколько таких корней-репо-шаблонов, которые не являлись полноценными шаблонами сервисов, а привносили в проект отдельные "фичи". Но, в целом, это работало не очень хорошо — конфликтов было прилично, разруливать их было не просто. Поэтому вместо постоянной работы в этом стиле я перешёл на периодические затягивания из шаблона отдельных обновлений через вышеупомянутый cherry-pick. Но чтобы это нормально работало крайне желательно очень аккуратно делать коммиты в репо шаблона, держа в уме что кто-то может этот отдельный коммит попытаться использовать для обновления своего сервиса.

А патчи git в этом случае не лучше подходят? не надо дополнительных remote создавать

А разве git remote add … && git fetch … не проще, чем создавать и распространять патчи?

так если есть шаблон, то и накатывать надо не один раз, а сразу на несколько сервисов

Это в теории. А на практике — у каждого микросервиса сейчас свои собственные задачи, и обновление "до последнего писка моды по шаблону" может не вписываться в его приоритеты. Равно как и возможности отложить все задачи и срочно обновить все микросервисы тоже может не быть. Поэтому — каждый микросервис обновляется в своём темпе и выборочно.

Один из реальных способов поддерживать общий стиль и единообразие между микросервисами — держать их в одном репо. В частности, если проект не требует реально независимого деплоя отдельных микросервисов и разделение на микросервисы преследует скорее задачу упрощения разработки нежели обеспечения бесконечного масштабирования — имеет смысл использовать "монолит со встроенными микросервисами".


При этом подходе бинарник у нас один, но общий для всех встроенных микросервисов main.go минимальный, все микросервисы по-прежнему полностью изолированы друг от друга (у каждого своя БД, каждый раздаёт API на отдельном порту, код каждого в его собственном internal/, etc.) и, при реальной необходимости, довольно легко выносятся из монолита в отдельное репо. Тем не менее, это даёт реальную возможность "наводить порядок" сразу во всех микросервисах одним PR-ом, дешевле рефакторить внутренние API между этими микросервисами, и, в целом, заметно ускоряет разработку если у нас небольшая команда (когда 4 человека бегают между 50 репо с 50 микросервисами — это несколько утомляет).

У нас так. Но компания выросла, и теперь ты пол дня ждешь что бы замержить в мастер. Монорипо стало боттлнеком.

Выпиливаем сейчас в repository per domain. Пишем на ноде, но тут это не важно.

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

Вначале это тоже был монолит. Потом его разбили на сервисы, воркеры и т.д. А теперь еще и на майкро. Вот и получилось в конце что все в монорипо.

Один из реальных способов поддерживать общий стиль и единообразие между микросервисами — держать их в одном репо.

Это не про монорепо?

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

Не всё так страшно. Достаточно проконтролировать, чтобы встроенные микросервисы не использовали никаких глобальных объектов (вроде http.DefaultServeMux, prometheus.DefaultRegistrer и глобального хранилища миграций goose) плюс разрулить получение ими своих частей общей конфигурации (флаги/переменные окружения/etc монолита). Т.е. как только они стартанули (конфигурация, миграции) и если они не используют после этого никаких глобальных переменных — в целом всё будет гладко.


Дальше могут быть проблемы если какой-то встроенный микросервис будет слишком уж жрать общие ресурсы (память, CPU, файловые дескрипторы) или у него очень специфический профиль работы (типа, он при запуске первые 15 минут разогревает кеши и в это время не может обслуживать запросы) — но вот на этом месте, если по-простому проблема не решается (напр. ограничением потребляемых ресурсов через пул горутин), то пора конкретно эти микросервисы деплоить отдельно (либо вынеся их из монолита, либо просто запуская ещё один инстанс этого монолита с флагом "запусти только вот эти встроенные микросервисы из всех имеющихся").


P.S. Монорепо != монолит. Я говорил про монолит. А монорепо (в котором разные не связанные проекты) я и сам готовить не умею пока — раздельное тестирование и выкат этих проектов на CI/CD в зависимости от того, какой код затронул текущий PR — это большая боль, по крайней мере во всех вариантах, которые я рассматривал.

А как вы решаете такие инфраструктурные вещи?

1. Service discovery
2. Graceful shutdown
3. Logging
4. Metrics

В нашей компании мы пишем на ноде. Но есть вещи которые работали бы намного эффективнее на GO. Но сейчас внедрить GO это проблема, потому сервисы на ноде основываются на обертке, которая и занимается теми вещами которые я перечислил. И что бы втиснуть в экосистему бинарник на GO, мне нужно будет обеспечить работу с консулом и т.д. А это уже выходит за рамки того что я как разработчик сервиса хочу делать. Обидно. А хочется просто кинуть бинарник, и что б работало =)
Наш генератор кода, который мы ласково зовем gogi генерирует по OpenAPI спецификации в том числе и серверный код, в котором есть код отвечающий за инициализацию логера, передачу метрик и т.д. То есть в вашем случае, возможно вам стоит начать с обертки которая будет заниматься всеми вещами что вы перечислили.

Насчет инфраструктурных вещей. Для метрик у нас применяется Prometheus, для логов у нас применяется Elastic+Kibana, Graceful shutdown можно сказать встроен в Kubernetes, трафик внутри Kubernetes мы направляем через ingress либо напрямую между сервисами.

Consul, насколько мне известно, мы сейчас не применяем.

Graceful shutdown лучше поддерживать из коробки внутри микросервисов. Потому что поддержка его докером (и, полагаю, кубом) сводится к тому, что он присылает сначала SIGTERM, а через 10 секунд SIGKILL. И вот неплохо бы на этот SIGTERM штатно отреагировать самому микросервису, аккуратно закрыв текущие подключения клиентов, остановив все фоновые процессы/горутины, и отключившись от используемых им самим БД/сервисов (иногда это прям критично, как в случае необходимости отключения от NATS Streaming, да и nats.Drain() неплохо бы сделать перед выходом).


Помимо этого полноценная поддержка graceful shutdown (в виде context.Context, который получают все работающие горутины, и который будет отменён в момент начала graceful shutdown) позволяет инициировать его изнутри, если какой-то "вечный" процесс внутри микросервиса внезапно завершится с ошибкой (напр. подключение к consul отвалится и не сможет восстановиться, а продолжать работать без него слишком опасно).

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

1. Получается, что все сервисы шарят общий базовый код? Ведь в литературе о микросервисах мы повсеместно находим что-то вроде этого:
Microservices should adhere to a “shared nothing” approach where microservices do not possess shared code. Microservices should instead accept code redundancy and resist the urge to reuse code in order to avoid a close organizational link.

“Don’t repeat yourself” isn’t a golden rule Microservices allow you to violate the DRY (don’t repeat yourself) principle safely. Traditional software design recommends that you generalize repetitive code so that you don’t end up maintaining many copies of slightly different code. Microservice design is exactly the opposite: each microservice is allowed to go its own way


2. Если я ничего не упустил, у вас получается голый кубер, рассматривали ли какие-нибудь сервис меши для него? Чем закончились результаты рассмотрения, если были?
3. Вопрос авторизации и аутентификации в микросервисной архитектуре: долго ли мучались, на чем остановились?
4. Для чего вы используете в названиях пакетов несколько слов и underscores ?)
1. Получается, что все сервисы шарят общий базовый код? Ведь в литературе о микросервисах мы повсеместно находим что-то вроде этого:

В микросервисах написанных на python у нас была общая библиотека, которая шарилась между сервисами и мы на этом обожглись. Обслуживать эту библиотеку стало сложно, нужно постоянно следить за совместимостью версий, обновлять библиотеку в разных сервисах и при этом следить чтобы ничего не сломалось. Поэтому сейчас, в микросервисах на go у нас нет расшаренного кода. Весь код генерируется из OpenAPI спецификаций и сервисы независимы между собой. Шаблон микросервиса — это скорее история про эталон к которому должны подтягиваться проекты чтобы не сильно отличаться между собой. Он отрабатывает только на этапе генерации микросервиса и больше не является зависимостью. Можно сказать мы осознанно идем против принципа DRY здесь, чтобы не увеличивать связность между сервисами.
Если я ничего не упустил, у вас получается голый кубер, рассматривали ли какие-нибудь сервис меши для него? Чем закончились результаты рассмотрения, если были?

Очень хотим внедрить меши и скорее всего начнем какие-то движения в эту сторону уже в этом году. Насколько мне известно, наши DevOPS экспериментируют с Istio и Linkerd.
Вопрос авторизации и аутентификации в микросервисной архитектуре: долго ли мучались, на чем остановились?

У нас есть один сервис который занимается аутентификацией, к нему в основном обращаются 2 сервиса которые принимают трафик извне: наше мобильное API и сайт. Далее запросы от этих сервисов к микросервисам уже идут без повторной аутентификации. Авторизация при этом конечно остается. Мы обязательно проверяем, что пользователь запросил именно свои заказы, а не соседа) Насколько мне известно ощутимых болей при внедрении такого подхода никто особо не испытывал.
Для чего вы используете в названиях пакетов несколько слов и underscores ?)

Если речь про go пакеты, то подавляющее количество названий у нас — это отдельное слово. Названия в несколько слов встречаются, но редко. Обычно обусловлено предметной областью, например, «ограничители правил скидок по лояльности» тяжело впихнуть в одно слово.

подскажите, как решается вопрос с многофазной транзакцией в бд? Когда несколько микросервисов должны в транзакции записать/изменить данные, а потом при необходимости откатить их назад.

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

Данными, которые необходимо изменять в одной транзакции, должен владеть один сервис

Это теория. А практика такова, что не всегда это возможно. Я догадываюсь, что ответ будет — если нужна такая мулька, то отправьте свои сервисы на рефакторинг, но такое себе


Ну, и да — распределенные транзакции — это всегда больно, но мне казалось, что паттерны типа saga отчасти решают эту боль

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


Я всё понимаю про практику и теорию. Тем не менее, в большинстве случаев перепроектировать будет дешевле, чем внедрять саги.

Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.
Информация
Дата основания

5 апреля 2011

Местоположение

Россия

Численность

5 001–10 000 человек

Дата регистрации

20 сентября 2018

Блог на Хабре