Насколько много маркетинга в ACID?

    Всем привет. На связи Владислав Родин. В настоящее время я являюсь руководителем курса «Архитектор высоких нагрузок» в OTUS, а также преподаю на курсах, посвященных архитектуре ПО.

    Помимо преподавания, как вы могли заметить, я занимаюсь написанием авторского материала для блога OTUS на хабре и сегодняшнюю статью хочу приурочить к запуску курса «Базы данных», на который прямо сейчас открыт набор.




    Предисловие


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

    Период времени, в течении которого происходили данные события, характеризовался отсутствием высоких нагрузок, Интернета и проблем с производительностью, для решения которых можно было обойтись лишь методами вертикального масштабирования. Впоследствии, в начале 2000-ых возник тренд на NoSQL базы данных, появилась аббревиатура BASE, которая фактически противопоставлялась классическому ACID (ACID — кислота, BASE — щелочь). Сейчас возникает обратный тренд на ACID. Даже NoSQL-ая MongoDB стала поддерживать ACID.

    Давайте разберемся с тем, что означает данная аббревиатура и насколько много маркетинга в ней.

    ACID представляет 4 свойства:

    A = atomicity (атомарность)
    C = consistency (консистентность или целостность)
    I = isolation (изоляция)
    D = durability (надежность)

    А теперь давайте поговорим о каждом свойстве отдельно.

    A = atomicity (атомарность)


    Атомарность является перегруженным термином и в контексте транзакций в базе данных может быть сформулирована в виде принципа «все или ничего». Если ваша транзакция содержит в себе 10 операций insert, то будут выполнены либо все 10 (будет осуществлен commit транзакции), либо ни один из них (будет осуществлен rollback транзакции).

    Каким образом обеспечивается данное свойство? Дело в том, что когда в базу приходит транзакция, содержащая те самые 10 insert'ов, данные не начинают изменяться. Транзакция перед накатом непосредственно на данные пишется в журнал, в котором фиксируются изменения, вносимые ею. Данный журнал может быть использован также и для репликации данных, а может быть совершенно с ней не связан, о чем рассказано мною, например, здесь. Благодаря этому журналу может быть осуществлен commit транзакции непосредственно на данные, либо, в случае чего, rollback ее же. Правило, согласно которому транзакцию следует закрывать как можно скорее, следует непосредственно из понимания принципов реализации данного свойства: зачастую базы запрещают чистить данный журнал пока какая-либо транзакция открыта, поэтому он может забиваться, что, в свою очередь, приводит к весьма неприятным последствиям.

    C = consistency (консистентность или целостность)


    В терминах ACID консиситентность обозначает не тоже самое, что в терминах CAP-теоремы (в теории распределенных систем существует достаточно много степеней этой консистентности). Под консистентностью мы понимаем следующее: некоторые заранее определенные инварианты системы должны выполняться как до, так и после commit'а транзакции. В качестве примеров инвариантов системы могут быть представлены: дебит сходится с кредитом, суммарная ЗП сотрудников не превосходит бюджет, количество сотрудников в компании равна количеству открытых ранее вакансий и т. д. На языке БД это означает всего лишь выполнение всех constraint'ов.

    Вопрос про необходимость наличия консистентности на уровне БД является достаточно спорным, ведь наличие constraint'ов может говорить о том, что часть бизнес-логики переехала из приложения на уровень БД, что не является общепризнанно хорошей практикой. В конце концов, приложение может само принимать решение просто не commit'ить невалидные данные. Бытует мнение о том, что консистентность была добавлена лишь для того, чтобы аббревиатура получилась красивой (маркетинг).

    I = isolation (изоляция)


    Изоляция — это свойство базы данных, позволяющее выполнять параллельные транзакции как последовательные. Ведь никто не запрещает базе данных выполнять одновременно несколько транзакций, надо сделать так, чтобы они не влияли друг на друга, чтобы не возникали аномалии вида race condition. Фактически, именно изоляция решает проблему доступа к данным в конкурентной среде.

    Раскрытие понятия изоляции из-за своего объема заслуживает отдельной статьи, потому как именно изоляцию можно назвать сердцем ACID. Цена транзакционности часто сводится к цене обеспечения изоляции, именно поэтому изоляция обладает различными уровнями, каждый из которых предоставляет свой уровень защиты от race condition'ов и несет тот или иной overhead. В полной мере изоляция обеспечивается лишь на уровне serializable, который очень тяжело реализовать и который редко когда нужен.

    D = durability (надежность)


    Durability говорит нам о том, что если транзакция была применена, то она ни в коем случае не должна пропасть. Фактически это означает следующее: если БД ответила, что был совершен commit транзакции, то транзакция была зафиксирована в энергонезависимой памяти. Это означает, что произошел системный вызов fsync, т. е. буфферы были сброшены на жесткий диск, и он ответил ok'ом.

    Durability тоже является маркетинговым термином, потому как в полной мере он обеспечен быть не может. Даже если мы отбросим искусственные сценарии «сожжения Земли инопланетянами», после которого вообще не останется никаких баз данных и транзакций, более вероятные, но экстремальные сценарии физического уничтожения конкретного жесткого диска, на котором была зафиксирована транзакция, мы можем вспомнить, что системный вызов fsync гарантирует попадание в контроллер жесткого диска, в котором, в свою очередь, все-таки находится энергозависимый буффер. Время пребывания в нем мало, но не равно 0. Как следствие, если выключить электричество ровно в «нужный» момент, то транзакция все-таки может быть потеряна!

    Хоть проблема с fsync'ом и решена в дорогостоящей базе от Oracle, ничто не может защитить нас от проблем, связанных с физическим уничтожением машины. Мы можем всего лишь увеличивать гарантии с использованием бэкапов и репликации.

    Выводы


    Несмотря на то, что ACID предоставляет достаточно интересные свойства, такие как атомарность и изоляция, некоторые из данных свойств являются всего лишь маркетингом, и даже известный своей строгостью по сравнению с BASE ACID не обеспечивает в полной мере отсутствие возможностей потерь транзакции, а также влияния результатов выполнения одновременных транзакций друг на друга (изоляцию надо еще настраивать!).


    Приглашаем всех желающих на бесплатный урок по теме: «Модель работы с данными в PostgreSQL».


    OTUS. Онлайн-образование
    Цифровые навыки от ведущих экспертов

    Комментарии 8

      0
      Можете показать статьи или репорты об утере данных из-за fsynс, который не сохранил данные на диск?
        0
        Вообще-то похоже что автор еще собрался решать проблемы fsync репликацией. Ну, у него так написано. А это точно работает? Ну в смысле, если fsync реально может не сохранить данные, и мы их потеряем — с какой стати два fsync на разных машинах нам что-то гарантируют? В лучшем случае — они повысят вероятность. Не до 100%.
          0
          да это известный факт. например, из blog.httrack.com/blog/2013/11/15/everything-you-always-wanted-to-know-about-fsync
          * Linux/ext3: If the underlying hard disk has write caching enabled, then the data may not really be on permanent storage when fsync() return
          * Linux/ext4: The fsync() implementations in older kernels and lesser used filesystems does not know how to flush disk caches
          * OSX: For applications that require tighter guarantees about the integrity of their data, Mac OS X provides the F_FULLFSYNC fcntl. The F_FULLFSYNC fcntl asks the drive to flush all buffered data to permanent storage

          то есть, есть детали, но это не магические недостатки fsync-а, а баги, которые надо исправлять (использовать F_FULLFSYNC, чинить ext4, кстати, уже починили, и так далее). Всё как обычно, баги случаются.
          0
          Спасибо за статью, есть вопросы про durability.

          энергозависимый буффер. Время пребывания в нем мало, но не равно 0.

          Если диск не игнорирует flush, то есть ли разница, сколько времени, сколько времени данные лежат в кэше записи? К концу flush всё, что надо, будет записано. А если игнорирует, то такие диски противоказано использовать с СУБД.

          Как следствие, если выключить электричество ровно в «нужный» момент, то транзакция все-таки может быть потеряна!

          Если под «нужным моментом» подразумевается промежуток выполнения flush, то не важно, запишутся данные физически или нет? СУБД не дождётся окончания fsync, транзакция не будет считаться завершённой, и после восстановления при включении питания эти изменения физически откатятся.

          Как мне видится, те тактики, которые применяют СУБД для durability (WAL, Checkpoint), достаточны для достижения этой характеристики.
          Подробнее про них можно прочитать в статье на Хабре
          Как устроены базы данных

          Уточню, что мой опыт тестирования durability в нормальных условиях (когда не сдыхает железо) доказывают, что в Postgresql как раз всё отлично. Всё, что БД возвращает сразу после commit, доступно также при восстановлении после сбоя. И после сбоя не появляется ничего лишнего, чего СУБД не вернула после commit.
            0
            Попробую догадаться какую проблему скорее всего имел ввиду автор. Дело в том, что применение самого commit-а, на низком уровне, может состоять из нескольких flush() в разрозненных областях диска. Порядок этих flush для логики commit-а — важен. Например: сначала мы обновляем новый служебный блок, делаем ему flush(), потом меняем индекс блока в B-дереве и делаем окончательный flush() в какой-нибудь таблице. Каждый flush() может вернуть управление прежде чем контроллер запишет свои буфера физически на диск. И вот тут тонкость — при нормальной работе (в том числе закрытия процесса) логический порядок всегда правильный, т.е. если читать данные, изменения увидятся в правильном порядке, но вот если произойдет сбой по питанию — здесь все очень сильно будет зависеть от порядка сброса энергонезависимых буферов контроллера. В каком порядке будет запись и будет ли записана вся очередь не смежных блоков в этом случае, это вопрос. Понятно что для обеспечения 100% durability порядок должен строго сохраняться. Все это усложняется ситуацией когда мы имеем сложные слои и комбинации дисковых носителей, разные файловые системы или RAID.
              0

              Спасибо за ответ. В общем случае это действительно так. Не читав исходники, не знаешь на 100%, но логично во время commit не делать fsync нигде, кроме wal. А он строго с последовательной записью. За консистентный перенос данных в файлы таблиц Postgresql отвечает checkpoint, он уже может как угодно сложно делать свою работу, чтобы любые прерывания на полпути были обратимыми после аварии.

                0
                С логом проще, хотя и там порядок записи блоков очень важен. Я рассматриваю стадию накатывания изменений непосредственно самой базы, например из того же лога. Не очень представляю как алгоритмически можно поменять сложную древовидную структуру за один flush(). Durability возможна, безусловно, но важно что она зависит не только от алгоритмов и API применяемых самой базой данных. Важно, еще, на какой системной конфигурации и на каком оборудовании все это развертывается, и вот тут могут быть нюансы.
                  0
                  Ок, давайте фантазировать, как происходит checkpoint.
                  1) пишет в wal, что checkpoint начался, и делает fsync
                  2) копирует изменения, которые произошли с момента последнего checkpoint, в файлы баз данных. Часть этой работы уже сделали ранее другие процессы (напр. bgwriter или сами воркеры запросов).
                  3) в каком-то порядке делает fsync к файлам базы и дожидается окончания fsync.
                  4) пишёт в wal номер транзакции начала шага №3 и факт окончания checkpoin, делает fsync.
                  Следующий chechpoint будет работать с изменениями с момента только что записанного номера транзакции.

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

          Самое читаемое