Комментарии 65
Как пример — django.
В Django мы ловим те же самые проблемы. Собственно там мы чаще всего и ловим) Там же тоже внутри каждой миграции описывается от какой миграции она зависит. И вот когда два разработчика завязываются на одну базовую миграцию — возникает конфликт и Django точно так же не дает их накатить.
Я могу, чисто теоретически, представить конфликт — но для этого два разработчика должны параллельно работать с одной и той же таблицей, и производить над ней несовместимые изменения. Пока ни разу конфликтов не было, при том что ситуация когда три бэкендера параллельно пишут миграции каждый в своей ветке, а потом они сливаются в произвольном порядке — случается регулярно.
Почитайте https://github.com/pressly/goose/issues/63#issuecomment-428681694 — проблема вполне реальная. Порядковая нумерация безопаснее.
А ничего, что накат некоторых миграций не в том порядке может давать разные результаты?
Ну и, мне на самом деле сложно представить такой пример. Как правило, большинство миграций это альтеры на создание новых полей, или создание таблиц. Реже — изменение старых полей. Так же редко — инсерты справочных данных.
Вы можете привести последовательность из четырёх запросов, которые нормально работают попарно — каждый в своей ветке — так же нормально работают вместе в одной последовательности(сначала первая пара, потом вторая) но ломаются если их применить вперемешку(не меняя частные зависимости последовательности в паре)?
Конечно, могу. Равно как и Вы можете эти примеры опровергнуть, сказав что они высосаны из пальца или что так делать не надо было и что есть другие варианты решения проблемы… но ведь на практике разработчики далеко не всегда делают всё правильно, и рассказывать постфактум о том, что существовало лучшее решение которое бы не привело к проблемам — бессмысленно.
Поэтому, на мой взгляд, важно стараться не создавать лишних проблем — а идея что миграции можно на разных серверах накатывать в разном порядке — определённо проблемная. Помимо прочего существует ещё задача отката миграций, нужда в которой возникает хоть и редко, но, тем не менее, возникает — и тут тоже понадобится одинаковый порядок, иначе при откате можно "зацепить" лишнюю миграцию.
Из того, что у меня недавно было на проекте — одна миграция это UPDATE который нормализует существующие записи, а вторая это INSERT который добавляет в справочник запись "по-старинке", ещё не нормализованную. В зависимости от порядка применения либо все записи в БД будут нормализованы, либо все кроме одной. При этом UPDATE — это часть срочного багфикса, INSERT — часть новой фичи. Ну т.е. один разработчик начал делать фичу, создал миграцию с INSERT, но процесс несколько затянулся, как обычно. А в это время нашли баг, и другой разработчик быстро пофиксил его UPDATE-ом. Багфикс смержили до фичи, хотя разработка багфикса началась позднее.
Нетушки. На то есть ревью. Плюс вменяемый разработчик посмотрит сначала на 100_update.sql, и подумает, как эти изменения должны сказаться на его миграции 101.
Но самое главное вовсе не это. Баг создать можно, протупить с миграцией и ревью тоже можно — это всё нормально. Главное, что в результате везде (локально у всех разработчиков, на стейдже, на проде, etc.) будет этот баг с некорректной записью в БД. БД будет везде одинаковая, не будет проблем с откатом последнего PR или можно надёжно (на всех площадках) пофиксить этот баг в 102_update.sql.
откатить миграции
откатить, лол, попробуйте откатить DELETE/UPDATE/ALTER на столбец. Скажем так — существуют миграции, которые возможно откатить, есть правила хорошего тона (писать чтобы все было откатываемым), но это совершенно не означает, что в реальном кейсе не случится какой-то факап.
Погоди, а почему у нас при порядковой нумерации ревью есть. а без нее нету?
Потому что без неё оба PR могут спокойно пройти ревью, потому что по отдельности они оба корректные. Проблемы начинаются в момент, когда один из PR-ов смержили первым, но второй к этому моменту уже может быть аппрувнут. Ну и в целом такая ситуация усложняет процесс ревью, потому что нужно смотреть не только текущий PR, но и все остальные открытые, и не только в момент ревью, но и непосредственно перед мержем… в результате продолбать это на ревью становится намного проще, чем при порядковой нумерации.
Работает очень просто. Очень небольшое количество PR-ов затрагивает миграции, поэтому необходимость переименовывать их возникает редко, и, на моей памяти, ещё не разу это не затрагивало более одного PR. Даже если переименование продолбали — вторая миграция просто не накатится, проблему обнаружат на стейдже, и следом выкатят фикс переименующий миграцию.
Иными словами, оба варианта подразумевают конфликтующие миграции, оба случаются достаточно редко (и хотя в моём варианте это "редко" в теории будет случаться чаще, всё-равно речь о временных интервалах порядка месяца-двух, так что это не принципиально), но в моём проблема быстро обнаруживается, легко исправляется, и гарантированно даёт одинаковый результат на всех площадках, а в вашем… всё значительно менее очевидно.
Пользователь 1 создает миграцию 100_insert.sql
пользователь 2 создает миграцию 100_update.sql
как минимум это так не работает. Вы те же миграции Алхимии под пайтон видели?
goose, основное отличие только в том что миграции «вверх» и «вниз» описываются в одном файле.
На самом деле основное отличие в том, что goose поддерживает миграции в виде функций на Go, что важно, потому что далеко не всегда возможно описать требуемую миграцию на чистом SQL. И в каком проекте и на каком этапе это понадобится — заранее предсказать невозможно, поэтому лучше иметь такую возможность изначально, и пусть лучше не пригодится, чем наоборот.
Вот рабочий вариант миграций с атомарностью и раздельным хранением.
Не так популярно, как go-migrate, но мне тож так больше нравится. (не только верхняя версия, а весь список примененных миграций к конкретной БД и когда они были выполнены.)
Из плюшек — миграции вшиваются в бинарник, и деплой базы упирается в указание, какая из нод должна накатить изменения. Требования обратной совместимости и не мигрировать всеми разворачиваемыми нодами одновременно естественно на релиз инженере.
github.com/lancer-kit/service-scaffold/tree/master/dbschema — пример интеграции.
(надеюсь, что не обидел автора топика разместив ссылки в комментариях)
Иначе как понять состояние базы, если база считает, что была миграция, а кодовая база не знает что в ней было, следовательно не понимает в каком состоянии база, и совпадает ли это состояние с кодом.
Там есть возможность перенакатить базу с форсом. Если это имеет смысл и локальный стейт не содержит нужных данных, или есть подготовленные скрипты наполнения базы случайными данными.
У мигрейта есть схожая функциональность. Можно указать на сколько шагов нужно откатить базу. Но при наложении миграций, или мерже «вчерашних» — он не сообщает о проблемах, что создает дополнительные риски при релизе.
Миграцию выполнять не нужно. Проблемы нет.
Вариант 2 — во второй ветке есть невыполненная миграция. При ее выполнении будет создано новое состояние базы, которое не будет воспроизведено нигде. (результат мержа этих двух веток не всегда будет совпадать с результатом работы каждой из них отдельно выполненной в произвольном порядке)
Если обе меняют схему в БД — откатили и накатили другую.
Если нет — можно и так. По быстрому.
Если же по нормальному — нам нужно работать с тем же состоянием и не создавать себе проблемы с хранением в голове веток, их миграций, взаимосвязей в коде и в базе. Лучше эти ресурсы потратить на разработку самой фичи, и концентрацию на ней.
Для сравнения — на довольно старом (ruby) проекте 100 миграций испольняются секунд за 7. Поэтому я сразу делаю себе алиас а баше, который дропает базу, потом создает ее, и накатывает все миграции. Все! Нет больше никаких страданий «а тут мы написали неправильный down для миграции и все рассыпалось». Проще накатить начисто при переключении ветки.
Если говорить про локальную машину — то в нашем случае если в разных ветках разные миграции и ребейзиться прямо вот сейчас не хочется, то никто не мешает убить контейнер с базой, а потом поднять его снова чистым и накатить миграции. Если посмотреть на docker-compose файл в статье, то мы делаем
make dev-down
make dev-server
и живем дальше.
Интересно, а как вы решаете вопрос обновлений общего шаблона? Вот например был неплохой шаблон, его за полгода использовали для создания 10 новых микросервисов. Всё получилось замечательно и унифицировано. А через полгода осознали, что в шаблон надо бы добавить еще одну общую для всех микросервисов ручку для мониторинга. А может и не через полгода, а через месяц. Как вы поступаете в таких случаях?
Я обычно в таких случаях временно подключаю репо с шаблоном к репо с уже существующим сервисом (да, для 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, мне нужно будет обеспечить работу с консулом и т.д. А это уже выходит за рамки того что я как разработчик сервиса хочу делать. Обидно. А хочется просто кинуть бинарник, и что б работало =)
Насчет инфраструктурных вещей. Для метрик у нас применяется 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 отвалится и не сможет восстановиться, а продолжать работать без него слишком опасно).
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 отчасти решают эту боль
Сага не решает боль. Сага просто даёт этой боли имя и направление, в котором нужно отправить больного. Но от того, что мы теперь знаем, как это называть — меньше болеть не будет.
Я всё понимаю про практику и теорию. Тем не менее, в большинстве случаев перепроектировать будет дешевле, чем внедрять саги.
Унифицируй это: как Lamoda делает единообразными свои Go сервисы