Привет! На этот раз я затрону тему небезопасных миграций в более значительных проектах. Если у вас относительно небольшой проект, то нет ничего плохого в том, чтобы внести некоторые изменения. Масштаб меняет все в работе с базами данных. Но давайте начнем с нуля — смело переходите к интересующей вас теме.
Что такое транзакция базы данных?
Предположим, что мы играем в игру с коробками. Вы можете купить у меня упаковку, но вы не можете видеть, что внутри, понятно? Я готовлю все для того, чтобы эти коробки были сгруппированы, и только когда коробка будет готова, вы увидите ее в игре. Если это произойдет в процессе упаковки — вы не узнаете об этой коробке. После этого, если упаковка соответствует вашим ожиданиям (размер, цвет и т.д.), вы должны ее купить. В этот момент я передаю эту коробку вам, и вы можете сказать мне, все ли в порядке или нет. Если, например, в коробке окажется та же футболка, которая у вас уже есть, вы можете вернуть посылку, но не этот элемент коробки. Все или ничего — таково правило!
И… ну, теперь давайте сравним эту дерьмовую аналогию с базами данных. Коробка — это транзакция, которая может включать в себя множество различных элементов (SQL-операторы, такие как INSERT
, DELETE
или UPDATE
). Таким образом, покупка коробки — это аналогия фиксации (сохранения, персистирования) транзакций в базе данных. Эти транзакции ACID
(atomicity, consistency, isolation, and durability), поэтому вы можете договориться о фиксации всего или ничего. Если вы не согласны с некоторыми элементами коробки → вы можете отбросить весь пакет — точно так же, как база данных делает откат ваших операций.
Что такое миграция базы данных?
Итак, пришло время объяснить, что такое миграция базы данных, если вы знаете, что такое транзакция базы данных. При работе с приложениями нам необходимо вносить изменения в наши базы данных. Как сказал Гераклит (греческий философ): «изменение — единственная константа в жизни» — то же самое происходит и в наших приложениях. Если вы создаете какое-либо профессиональное программное обеспечение, вам следует использовать миграции, чтобы избежать ручных изменений в ваших базах данных и сохранить историю изменений в хорошем виде. Большинство (если не все) фреймворков имеют встроенные инструменты для управления структурой базы данных из кода. Благодаря этим инструментам мы можем записывать каждое изменение структуры в виде строки SQL-запросов или, как во многих технологиях — использовать предоставленные ими DSL для описания того, что меняется. Например, эти транзакции могут выглядеть как этот пример PHP Doctrine:
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use DoctrineDBALSchemaSchema;
use DoctrineMigrationsAbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20220530204833 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE blog.posts ADD identifier CHAR(10)');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE blog.posts DROP identifier');
}
}
Или как этот пример Elixir Ecto:
defmodule PostRepo.Migrations.AddIdentifierToPostsTable do
use Ecto.Migration
def up do
alter table("posts") do
add :identifier, :string, length: 10
end
end
def down do
alter table("posts") do
remove :identifier
end
end
end
Это выглядит намного иначе, но это связано с использованием прямых SQL-запросов (как в PHP Doctrine) или DSL, предоставляемого фреймворком (Elixir’s Ecto). Как видите, оба содержат секции up
и down
— первая используется, когда мы развертываем наши изменения, а вторая — для отката изменений, когда что-то происходит и нам нужно вернуть изменения. Эти миграции могут быть автоматически сгенерированы фреймворком или созданы вручную, но эффект один и тот же — изменения базы данных описаны в коде и хранятся в одном git-репозитории.
Что такое единица развертывания?
Итак, если мы разобрались с тем, что такое транзакции и миграции — тогда мы можем затронуть последнюю важную тему, необходимую для понимания проблем с миграциями в больших масштабах. Эта тема называется единицей развертывания. При развертывании наших изменений в производственной среде они обычно закрываются вместе как один образ Docker. Этот образ может содержать код миграций с изменениями базы данных, которые вы планируете внедрить. Развертывание может состоять как минимум из одного объединенного pull request. Иногда запросов на слияние бывает больше — особенно если вы работаете над гораздо более крупным проектом или не занимаетесь непрерывной поставкой, а выпускаете релизы по требованию. Стоит добавить, что миграции сначала попадают в продакшн; после этого изменения вашего бизнес-кода становятся доступны пользователям. Это имеет некоторые недостатки, которые вы увидите в следующей части этого поста.
Ошибки
Сейчас я расскажу о некоторых возможных ошибках, когда вы работаете над большим проектом с более значительным трафиком. Я разделю ошибки на несколько групп, например, общие ошибки — независимо от того, какую систему баз данных вы используете — это проблема, и ошибки PostgreSQL. Итак, давайте двигаться дальше и рассмотрим все каверзные вещи.
Общие ошибки
-
Добавление столбца со значением по умолчанию
Если вы используете PostgreSQL версии ниже 11, MariaDB ниже 10.3 или MySQL ниже 8.0.12, вам следует быть осторожным при добавлении нового столбца со значением по умолчанию. Причина в том, что выполнение операции ALTER TABLE и добавление еще одного столбца приведет к перезаписи всей таблицы. Это происходит потому, что все изменения будут выполняться на копии таблицы. Затем, после внесения всех изменений, она применит их к оригиналу. Поэтому эта операция может повредить более обширным таблицам, так как мы блокируем входящие запросы. Однако алгоритм
INSTANT
в более новых версиях движков баз данных не будет останавливать входящие SQL-запросы, так что это не так уж плохо. -
Изменение или удаление столбцов в той же единице развертывания с вашим изменением кода
Как вы уже знаете из этого пункта о единицах развертывания — миграции выполняются до развертывания кода из конкретной единицы развертывания. Из-за этого вы можете столкнуться с простоем, если вы развертываете миграцию, которая внесет некоторые изменения в структуру базы данных и код, который использует новую версию структуры, потому что — после выполнения миграций старый код все еще будет искать старую структуру, ой!
-
Изменение типа столбца
Эта проблема настолько распространена, что может стать королевой бала. Вы можете спросить — почему? Ситуация усложняется тем, что — во-первых, мы развертываем миграции (как вы уже могли знать из предыдущих параграфов), поэтому весь старый код будет пытаться использовать древнюю структуру (до миграции). Во-вторых… этот
ALTER
заставит всю таблицу быть пересозданной (скопированной в память, измененной, примененной) в памяти базы данных. Это заблокирует все операции чтения и записи в таких движках, как PostgreSQL, а для MySQL и MariaDB мы остановим все записи. Это ужасно, не так ли? Так, например, если у вас большая таблица базы данных с миллионами записей, изменение типа столбцов будет очень опасно для вас с точки зрения длительного простоя. Помните: надежность — важнейшая характеристика любого приложения, потому что неработающее приложение просто бесполезно. Поэтому вместо того, чтобы менять тип столбца, добавьте новый столбец с нужным типом столбца, записывайте данные в оба столбца, заполните старые данные из старых записей и запретите запись в старый столбец. Затем вы можете отказаться от столбца. Это звучит как путешествие, но это не песочница, если вы заботитесь об обширной таблице базы данных. Делайте это медленнее, но без простоев; это победа! -
Переименование столбцов базы данных, таблиц
Это похоже на предыдущий случай с изменением типа столбца. Однако переименование может вызвать множество ошибок в вашей системе, поэтому гораздо лучше следовать последней инструкции — создать новую таблицу/столбец, записать в оба пробела и итеративно избавиться от старой с некорректным именем.
Ошибки PostgreSQL
-
Неиспользование параллельно создаваемых индексов.
PostgreSQL имеет функциональность для одновременного добавления индексов. Помните — по умолчанию миграции являются транзакционными, поэтому при добавлении индекса к существующей таблице лучше отключить транзакцию для конкретной миграции и выполнять ее параллельно. Благодаря этому PostgreSQL не будет блокировать мутации (
INSERT
,UPDATE
,DELETE
), как при не параллельной операцииCREATE INDEX
. Однако это более дорогостоящая операция, поскольку движок должен выполнить два сканирования таблицы и т.д. Дополнительная информация. -
Не устанавливайте NOT NULL для существующего столбца.
Если вы работаете над более обширной базой данных с высокой пропускной способностью, добавление
NOT NULL
к столбцу может быть не лучшим вариантом в PostgreSQL. Почему? Потому что эта операция блокирует, чтение и запись в вашу таблицу, пока каждая строка не будет проверена на возможность примененияNOT NULL
. Вместо этого лучше добавить проверочное ограничение, которое будет гарантировать, что вы готовы установитьNOT NULL
. Конечно, эту проверку пока не следует подтверждать. Вместо этого вы можете подтвердить это ограничение в последующей миграции (лучшая единица развертывания) и применить классическиNOT NULL
. Это гораздо более безопасный способ.
Все имеет значение, когда дело доходит до масштаба
Да, все эти правила и советы применимы при борьбе с большим масштабом. Для небольших приложений с небольшими базами данных и не очень большим трафиком — не имеет значения, будете ли вы действовать по правилам или просто коротким путем. Но когда вы делаете все возможное, чтобы приложение работало, у вас огромная нагрузка на систему — вам следует учесть эти ошибки и постараться избежать подобных ситуаций. Конечно, вы потратите больше времени на внесение изменений, связанных с базой данных, но благодаря этому вы сэкономите деньги и доверие клиента. Это очень важно — сохраните свою систему надежной, и вы будете спать спокойнее. Я не Мэтью Уокер (автор феноменальной книги «Почему мы спим»), чтобы рассуждать о природе сна; просто поверьте мне — лучше вносить изменения постепенно, если речь идет о базах данных.