В случае масштабных систем не происходит ни откатов, ни запланированных переходов (cut-over) — ваше ПО движется только вперёд.
Примечание: это электронное письмо, взятое из книги, которую я пишу последние три недели. В ней я отражаю сложную идею, которую вынашивал 10 лет. Я постарался сократить его содержание для удобства восприятия и хотел бы услышать ваши мысли по этому поводу.
Данные живут вечно
Данные — это основная причина, по которой ПО движется только вперёд.
Как только вы сохраняете состояние, ваш код должен будет понимать его всегда. Особенно это актуально для состояний, которые покидают вашу систему, становясь распределёнными.
Типичными примерами подобных случаев являются состояние платежа, электронные письма и асинхронные задачи.
Когда пользователь отправляет вам деньги, то ожидает от этой транзакции надёжности. Вы не можете просто потерять его платёж из-за того, что изменили некую деталь реализации, после чего, например, ID чека утратил свою валидность, либо использованный метод оплаты стал недействительным.
Кроме того, состояние платежа часто распространяется в другие системы.
В ходе финансовых операций оно вносится в электронные таблицы и другие замысловатые программы, где в рамках интеграции данных отправляется в хранилища и инструменты BI (Business Intelligence, бизнес-аналитики). Плюс эта информация передаётся в электронных письмах, через сторонние платёжные сервисы, ей обмениваются банки …В общем, данные о платежах распространяются повсюду. Если какая-то из этих систем однажды запросит у вас инвойс ID12345
, то лучше бы вам его иметь.
В большинстве случаев налоговые и бизнес-учреждения ожидают, что вы будете хранить отчёты о продажах в течение 5 лет. И даже если пользователь не станет вспоминать столь давние покупки, зато это может сделать аудитор.
▍ Как именно данные движутся вперёд
Предположим, вы запустили сервис с двумя способами оплаты: через систему Stripe и с помощью купонов.
Вы закладываете в таблицу базы данных поле payment_type
. Для кода это важно, поскольку два этих способа оплаты обрабатываются по-разному: информация запрашивается из разных источников, отображается разный UI и тому подобное. Важно это и для ваших коллег по бизнесу, так как они хотят знать, какие покупки принесли выручку (Stripe), а какие нет (купоны).
Чтобы поддерживать чистоту данных и избегать появления неожиданных значений, вы добавляете в базу данных ограничение ['stripe', 'coupon']
, а в код — типы. Теперь, если вдруг возникнет нечто неожиданное, программа выдаст ошибку. В данном случае ошибка будет лучше, чем выдача пользователю искажённой информации о платеже.
Впоследствии вы добавляете в систему возможность оплаты наличными и кредитными картами.
Теперь ограничение расширяется до ['stripe', 'coupon', 'credit_card', 'cash']
. Вы обновляете типы в коде и выстраиваете в нём новые пути.
В завершение вы тестируете код и выводите его в продакшен. Но тестировать платёжные системы непросто — в этом контексте существует множество пограничных случаев и путей кода, которые невозможно воссоздать в тестовой среде без использования реальных денежных средств. Вскоре после деплоя вы обнаруживаете баг.
Но пока ваш код работал, некоторые пользователи совершили покупки, используя новые методы оплаты. Перед вами возникла дилемма.
Если вы откатите этот деплой, то те покупатели, которые рассчитывались с помощью cash
или credit_card
, при попытке использовать оплаченный сервис получат ошибку. Но это не страшно — база данных не позволит вам откатить ограничение, если имеющиеся данные не будут совпадать.
Как же разрешить эту ситуацию?
Вы можете удалить строки cash
и credit_card
, но тогда потеряете платежи. Можно также изменить эти строки на stripe
или coupon
, но тогда вы не сможете восстановить эту информацию после исправления бага…
Хорошо, значит, базу данных откатить нельзя. Данные живут вечно.
А что насчёт кода? Тут ответ неоднозначный: приемлемо ли для вас, если пользователи будут получать ошибку при попытке кода считать из БД незнакомую для него строку с методом платежа? Есть решение, при котором вы сможете исключить такую возможность, отключив специальный фича-флаг.
Но тогда пользователи могут начать жаловаться, что недавно оплаченный сервис работает некорректно.
Распределённые системы + состояние
Это классическая задача с распределёнными системами и состоянием. Вы можете не рассматривать свой код и базу данных как распределённую систему, но это так и есть. Вот вам тест из одного вопроса:
Могут ли части системы (приложение, база данных и так далее) изменяться независимо?
Если ответ «да», то ваша система распределённая. Вы можете независимо развёртывать, обновлять, запускать, останавливать и иным образом управлять её компонентами (приложением и базой данных). Даже если оба этих компонента выполняются на одной машине, и вы почти всегда обновляете их одновременно.
И такая схема обеспечивает множество плюсов.
Например. Вы можете обновлять код приложения, не прибегая каждый раз к апгрейду системы базы данных до последней версии. Или можете перезагружать приложение, не теряя работоспособности базы данных, на которую опирается команда финансистов. И в некоторых случаях вы можете даже откатить код, не откатывая БД.
Конечная версия этого подхода для больших проектов представляет архитектуру микросервисов, в которой каждая команда реализует собственный набор независимых сервисов, выполняющих конкретные задачи. И даже в самых малых масштабах у вас будут, как минимум:
- сервис кэширования,
- сервис базы данных,
- балансировщик нагрузки,
- основное приложение,
- несколько сервисов для асинхронных задач бэкенда.
Эти распланированные задачи действуют как сервисы, даже если их работа заключается исключительно в вызове API основного приложения. Продолжит ли этот API работать через 3 недели, когда активируется задание? 😉
Старое и новое всегда рядом
Проблема, которую создают распределённые сервисы, заключается в том, что все их части должны утвердить общее определение бизнес-логики.
Если ваша база данных структурирована одним образом, а код ожидает другой, система ломается. Если клиент вызывает API, который не существует, или не может спарсить ответ, система также ломается.
И вы этого не заметите, пока система и её трафик невелики. Вы можете обновить определения базы данных, затем обновить код. Или обновить код, а затем базу данных. Ничего плохого не случится.
Но по мере масштабирования период между обновлением каждой части системы становится важен. Как долго ваша база данных и код могут находиться в асинхроне? А что насчёт клиентов? Сколько запросов вы можете получать в течение этого времени?
Всё это зависит от производимого обновления и размера системы.
Обновление крупной базы данных занимает больше времени. Приложение с несколькими резервными серверами обновляется дольше, чем с одним. Да и кто знает, когда конечный пользователь нажмёт Update
на своём клиенте.
При больших масштабах вы можете предположить, что система всегда выполняет несколько асинхронных версий вашего кода. Запросы никогда не прекращаются.
Представьте, если бы бариста сказал: «Простите, вы озвучивали заказ, как раз когда я составлял новую стопку кофейных чашек, поэтому вас не расслышал». Такое случается, и мы повторяем свой заказ. Нет проблем. Но если с вас уже взяли плату за кофе, потом заказ потеряли, а потом ещё заявили, что вы его не делали… Вас это наверняка расстроит или даже разозлит.
▍ На распространение обновлений требуется время
Самый тяжёлый случай обновления на моей памяти случился, когда к нашим серверам поступил запрос от приложения iOS, версия которого перестала обслуживаться ещё 2 года назад. Произошёл сбой, и система сообщила об этом. Тогда мы просто скрестили пальцы в надежде, что пользователь поймёт, в чём дело, поскольку версия его клиента была старше обновления, в котором мы добавили всплывающее уведомление «Версия вашего приложения устарела».
И даже при отсутствии клиентов конечных пользователей, на распространение обновлений, как правило, уходит несколько минут.
Вы развёртываете новую версию на 1 сервере, убеждаетесь, что всё в порядке, перенаправляете трафик, переходите к следующему серверу и так далее, пока последний код не будет запущен везде (обычно этот процесс автоматизирован). И пока происходят все эти обновления, у вас некоторые серверы работают со старым кодом, а некоторые с новым — и все они в произвольном порядке принимают активный трафик.
Такая схема развёртывания называется Blue-Green Deployment и является стандартным подходом к управлению распределёнными системами. И этот подход не только позволяет до перенаправления трафика убедиться, что ваш код выполняется и проходит все проверки, но также даёт возможность обеспечить отсутствие даунтайма.
Когда у вас есть трафик и распределённая система, то такого понятия, как запланированный переход, не существует. Вы всегда выполняете несколько версий кода.
Несколько советов
Не существует каких-то жёстких правил для обработки программного обеспечения, которое всегда движется вперёд. Всё зависит от применяемого обновления, конкретной природы вашей системы и вашей терпимости к ошибкам.
Вот несколько общих правил, которых придерживаюсь лично я:
- поэтапные (пойдёт в качестве альтернативы «аддитивности»?) изменения вполне допустимы,
- будьте снисходительны к получаемому вводу (вместо выдачи ошибки просто игнорируйте лишнее),
- вместо внесения критических изменений в конечную точку API, создавайте новую
- сначала обновляйте базу данных,
- обновляйте серверы до обновления клиентов,
- если возможно, реализуйте опцию для принудительного обновления клиентов,
- принимайте все версии ввода и вносите данные уже на стороне бэкенда,
- отклоняйте устаревшие версии не спеша,
- придерживайтесь небольших обновлений.
Причём вам не всегда потребуется следовать всем из них.
Обновление платёжной системы подразумевает одни рекомендации, а обновление цвета кнопки — другие. Здесь главное — просто спрашивать себя: «Какой произойдёт сбой, если возникнет нестыковка версий? Как провернуть всё гладко для новых и старых клиентов?», после чего действовать соответственно.
Если вы всё ещё здесь, то благодарю за внимание ❤️ Для электронного письма получилось многовато.
Автор: Bright_Translate