Слабая или сильная изоляция транзакций в базах данных
Сегодня я решил затронуть больную и холиварную для многих тему. :)
Слабая изоляция
Использование более слабого уровня изоляции транзакций не всегда означает компромисс между согласованностью данных и производительностью выполнения запросов. Всё зависит от данных и выполняемых операций. Действительно, в некоторых случаях слабый уровень изоляции может бескомпромиссно обеспечить лучшую производительность.
Наряду с этим считается, что более слабая изоляция подходит для большинства ситуаций, а разработчики приложений должны быть достаточно прозорливыми, чтобы выявлять случаи, когда требуется более сильная изоляция, и использовать её по мере необходимости.
На практике я не видел примеры удачного жонглирования уровнями изоляции. В лучшем случае запросы на чтение идут с одним уровнем, а на запись — с другим. Чаще всего ультимативно используется что-то типа Read Committed, а прозорливость начинает просыпаться только в момент, когда не проходит интеграционный тест или кто-то находит ошибку.
Данная ситуация усугубляется в том числе разночтениями SQL-стандарта при реализации уровней изоляции. Мартин Клеппманн также подмечает, что это создает неопределенность для разработчиков в понимании того, есть ли в коде приложения ошибки.
В свою очередь, гонки по данным — одна из самых противных ошибок. Её сложно обнаружить и воспроизвести. И зачастую совсем непросто понять, что пошло не так, чтобы исправить. Пока что не созданы надёжные и удобные инструменты для обнаружения подобных ошибок. А из этого можно сделать вывод, что слабый уровень изоляции — это очень большой кредит, который берет на себя разработчик.
Сильная изоляция
Сильные уровни изоляции транзакций исключают многие аномалии. Возможные баги в большинстве своём будут связаны с бизнес-логикой, а не со спецификой работы базы данных. Такие ошибки хорошо отлавливаются на этапе автоматизированного тестирования. Сам же код становится более простым, не содержащим различных ухищрений, нацеленных на борьбу с аномалиями.
Да, старые версии реляционных баз данных приучили нас, что сильные уровни изоляции, такие как Serializable, ставят крест на производительности. Но время не стоит на месте, и технологии хранения развиваются. Например, с появлением SSI, MVCC, Calvin и т.п. реализация Serializable стала намного производительней, чем на базе блокировок.
Также не стоит забывать, что концепция ACID-транзакций не подразумевает компромиссов относительно изоляции. Уровни изоляции — это компромисс между согласованностью и производительностью, на который разработчик должен пойти осознанно. Но зачем идти на компромиссы до того, как будет сделано первое нагрузочное тестирование и собраны первые метрики? Как мне кажется, это резонный вопрос.
Подведём итоги
- Начинать разработку с использованием слабого уровня изоляции транзакции больше похоже на преждевременную оптимизацию. Во-первых, лучше начать с написания более простого и понятного кода (KISS), подкрепить его нагрузочным тестированием, и только потом осознанно переходить к понижению уровня изоляции. Во-вторых, а часто ли бывает так, что база данных становится узким местом? Если и так, по моим наблюдениям, это следствие плохо написанного кода, неудачной модели хранения или неподходящей базы.
- Использование слабых уровней изоляции — источник уязвимостей. От разработчика требуется значительный опыт, отличное знание и понимание уровней изоляции. При реализации сложной бизнес-логики — это высокая когнитивная нагрузка. Даже если вы справитесь, вы построите карточный домик, который с легкостью развалит менее опытный специалист. И, скорей всего, вы ничего не заметите по началу.
- Жонглирование уровнями изоляции часто не самая удачная идея. В общем случае транзакции с разным уровнем изоляции не сочетаются, т.е. результат их выполнения будет непредсказуем. Реализуя новый бизнес-процесс с особенным уровнем изоляции транзакции, вы постоянно будете пересматривать всё, на что он может повлиять.
- Историй и публикаций о багах, вызванных слабыми уровнями изоляции, гораздо больше, чем случаев, когда более сильные уровни изоляции приводили к непрактично низкой производительности.
Естественно, существуют разные данные, разные профили нагрузок, и нет единственно верного решения. Решая задачу, мы также принимаем во внимание свой опыт, без этого никуда. Поэтому вышенаписанное — это не призыв к использованию сильных уровней изоляции транзакций по умолчанию, это, скорей, повод задуматься.
Поделюсь историей о слабой изоляции...
Задача: система автоматической проверки задач по программированию (что-то типа LeetCode). Пользователь решает задачи. Решение - это программный код, который проходит ряд тестов и получает какую-то оценку. Можно отправить несколько вариантов решения одной и той же задачи, в зачёт пойдёт лучшее.
Как было сделано. При сохранении очередного решения оно сравнивалось с последним наилучшим. Если новое было более удачным, оно помечалось как наилучшее и шло в итоговый зачёт.
Всё работало отлично до тех пор, пока не наступила необходимость повторной проверки решений. Такое бывает, если постфактум меняются условия проверки решений. Например, у задачи изменяется набор тестов.
Повторная проверка производится в потоковом режиме (через очередь) с распараллеливанием (в разных потоках). Последнее, в свою очередь, иногда создаёт конкуренцию на доступ к решениям одной и той же задачи одного и того же автора. Проверять решения последовательно мы не можем, т.к. это существенно снижает пропускную способность процесса повторной проверки.
В итоге в таких условиях из-за слабой изоляции мы получили гонку по данным и признак лучшего решения доставался не самому удачному. Об этом мы (совсем не сразу) узнали от пользователей и достаточно долго не могли ни понять, ни воспроизвести проблему. Весь код сохранения был покрыт тестами (и модульными, и интеграционными). На понимание проблемы натолкнуло знание, что ситуация повторяется только в процессе повторной проверки, а она отличается от обычной только параллельным исполнением.
В этой истории есть нюанс в пользу слабой изоляции. Она "подсветила" то, что, как минимум, подход к хранению лучшего решения не самый удачный и для продолжения использования слабой изоляции его нужно пересмотреть. Но это уже другая история... Тут больше хочется сказать, что в идеале не хотелось бы узнавать о подобных проблемах от пользователей, а выявлять заранее. (Вопрос к наблюдаемости.)
Выбранная модель хранения в общем-то и никого никогда не беспокоила, не создавала проблем с производительностью и т.п. Не самая приоритетная задача, в общем. Даже если бы мы чуть-чуть медленнее сохраняли результаты ответа (например, из-за Serializable), это было бы не так критично. А вот момент, когда мы неправильно делали оценку решений, это уже репутационные издержки. И это было больно.
P.s. Если вам интересна данная тематика, присоединяйтесь к моей новостной ленте в Telegram или здесь. Буду рад поделиться опытом. ;-)