Apache Iceberg. ACID‑транзакции

Конкуренция — двигатель прогресса

|

Давайте поговорим о чём‑то, что кажется невозможным: получение настоящих ACID‑транзакций на файлах, лежащих в S3. Каким‑то образом они вместе ведут себя как таблица базы данных.

Iceberg справляется с этой задачей, и понимание того, как он это делает, изменит ваше представление о data lakes.

Проблема: объектное хранилище не предназначено для транзакций

На момент записи этого курса S3, оригинальному объектному хранилищу, уже почти 20 лет. Оно само по себе развивалось, добавляя новые функции, но всё равно не было спроектировано как слой данных в транзакционной БД вроде Postgres.

Начнём с того, что файлы данных неизменяемы. Вы не можете обновить их на месте — можно только записать новые. Во‑вторых, в S3 нет механизмов блокировок или координации. S3 не знает, пишут ли два процесса в одну и ту же таблицу.

Но мы‑то привередливы! Мы хотим тех же мощных гарантий для нашего data lake, которые десятилетиями были в транзакционных базах данных. Иными словами — ACID‑гарантий.

Основы ACID

Давайте убедимся, что мы одинаково понимаем, что означают эти четыре буквы.

Атомарность (Atomicity)

Транзакция выполняется по принципу «всё или ничего». Она либо полностью записывается, либо не происходит вообще. Вы не получите ситуацию «из вашего расчётного счета списали $500, но перевод на сберегательный счет не прошёл». Всё должно завершиться успехом или полностью откатиться.

Консистентность (Consistency)

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

Изоляция (Isolation)

Транзакции не мешают друг другу. Когда один писатель обновляет набор записей, другой читатель не видит этих изменений, пока они не закоммичены. Нет «грязного чтения», нет утечки частичных результатов.

Изоляция — сложная штука. Существует множество уровней изоляции и способов её реализации, которые зависят от конкретной СУБД. Для простоты скажем, что читатель не видит писателя, пока тот находится в процессе записи.

Долговечность (Durability)

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

Как Iceberg обеспечивает ACID поверх blob‑хранилища?

Ответ — умная инженерия. Iceberg принял два важных архитектурных решения:

  1. Неизменяемые структуры данных — всегда выигрышный подход, если удаётся его применить.
  2. Атомарные операции с метаданными.

Вместе они делают ACID возможным.

Атомарность: атомарное обновление указателя на метаданные

Вот что происходит:

  • Все файлы — данные, манифесты, списки манифестов — записываются в объектное хранилище без какой‑либо координации. Никаких распределённых блокировок, никакого контроллера транзакций. Просто пишем в S3.
  • После записи всех файлов данных происходит коммит. Этот коммит — единственная атомарная операция compare‑and‑swap.

compare‑and‑swap — это как сказать «поменяй этот файл на новый, но только если он до сих пор тот же, что и минуту назад». В объектных хранилищах это делается через условные запросы с проверкой ETag.

Iceberg использует операцию compare‑and‑swap для атомарного обновления указателя на метаданные. Писатель запоминает текущую версию метаданных, подготавливает новые файлы, а затем пытается заменить указатель на новую версию. Если за это время другой писатель не изменил указатель — операция успешна, и все изменения становятся видимыми. Если же указатель уже обновлён — коммит откатывается, и ничего не происходит.

flowchart TD
    A[Писатель читает текущий указатель] --> B[Запоминает версию V1]
    B --> C[Подготавливает новые файлы]
    C --> D{Пытается заменить указатель<br>compare‑and‑swap V1 → V2}
    D -->|Указатель всё ещё V1| E[Успех: указатель обновлён]
    D -->|Указатель уже не V1| F[Неудача: откат]
    E --> G[Изменения видны читателям]
    F --> H[Ничего не изменилось]

На диаграмме показан поток операций compare‑and‑swap: успешный и неудачный сценарии.

Вся эта логика опирается на поддержку compare‑and‑swap в самом blob‑хранилище. Например, S3 предоставляет такую возможность.

Консистентность: снапшоты неизменяемы

Снапшоты, будучи созданными, не могут стать неконсистентными. Если создаётся неконсистентный снапшот — это баг в реализации айсберга у конкретного используемого движка.

Если движок следует спецификации Iceberg, вы получаете консистентные снапшоты. Каждый из них представляет валидное состояние таблицы: схема корректна, спецификации партицирования в порядке, все ссылки ведут на реальные файлы.

Валидация схемы происходит во время записи, поэтому некорректные данные никогда не попадают в снапшот. Если вдруг записана невалидная схема — это баг движка, а не Iceberg.

Конкурентные записи: изоляция на двух уровнях

Читатели получают изоляцию на уровне снапшота. Вы выбираете снапшот и читаете точное состояние таблицы на тот момент. Вы защищены от любых параллельных записей, которые происходят в то же время.

Писатели используют оптимистичный контроль конкурентности. Они не берут блокировки — они просто пытаются закоммитить.

  • Если два писателя изменяют разные части таблицы (разные файлы данных), оба коммита могут успешно завершиться — конфликта нет.
  • Если они изменяют одни и те же файлы, включается сериализуемая изоляция: один из коммитов выигрывает (кто первый), второй терпит неудачу и должен повторить попытку.

Таким образом Iceberg обеспечивает конкурентные записи без потери корректности.

Долговечность: забота blob‑хранилища

Файлы записываются один раз и никогда не изменяются. Как только данные попали в S3, вы получаете 11 девяток долговечности (или сколько там у вашего провайдера). Большинству из нас можно просто доверять хранилищу.

Поскольку Iceberg хранит несколько версий метаданных, у вас также встроена возможность восстановления на определённый момент времени — ранее записанные данные тоже хранятся долговечно.

Если запись прервалась на полпути — не проблема. Последний консистентный снапшот остаётся нетронутым, а на уровне приложения или движка вы получите уведомление о неудаче и сможете управлять повторами.

Под капотом конкурентных записей

Представьте, что два процесса хотят писать в одну таблицу:

  1. Оба начинают с чтения текущих метаданных таблицы (допустим, версия 5).
  2. Затем они независимо вносят изменения (выполняют вычисления, пишут новые файлы данных, манифесты и т.д.).
  3. Оба пытаются закоммитить примерно в одно время.

Допустим, процесс A немного быстрее и приходит первым. Он выполняет атомарный compare‑and‑swap: «если текущие метаданные всё ещё версия 5, обнови их до версии 6». Поскольку указатель не сдвинулся, операция успешна — процесс A выигрывает гонку.

Процесс B появляется на несколько микросекунд позже и пытается сделать то же самое: «если версия 5, обнови до версии 6». Но теперь метаданные уже версия 6 — коммит процесса B проваливается.

Однако это ещё не конец. Что происходит дальше, зависит от того, конфликтуют ли изменения.

Сценарий без конфликта

Процесс A писал в одну партицию (например, данные по Америке), процесс B — в другую (Азиатско‑Тихоокеанский регион). Они не трогали одни и те же файлы.

Процесс B говорит: «ладно, не проблема». Он читает новое состояние (версия 6), «перебазирует» свои изменения поверх него и коммитит версию 7. Оба изменения попадают в таблицу, все счастливы, и движок может сделать это автоматически.

Сценарий с конфликтом

Оба процесса изменили одни и те же файлы данных (например, оба обновляли продажи в Ëбурге). Теперь автоматическое перебазирование невозможно — изменения перекрываются, и движок не может программно гарантировать корректное последовательное выполнение без глубокого семантического понимания данных (что является задачей приложения).

Коммит процесса B терпит неудачу. Приложение должно решить: откатиться, начать заново с текущего состояния таблицы, пересчитать изменения и т.д.

Ограничение: только одиночные таблицы

Ядро спецификации Iceberg поддерживает транзакции только в пределах одной таблицы.

  • Один SQL‑запрос, одна таблица — атомарный коммит работает идеально.
  • Если нужно несколько операций над одной таблицей — используйте Transaction API в Java/Python или объединяйте их в batch.

Мультитабличные транзакции (BEGIN … COMMIT, затрагивающие несколько таблиц) в Iceberg не поддерживаются даже на уровне API. На конец 2025 года сообщество работает над этой проблемой, но готового решения ещё нет.

Для аналитических нагрузок это обычно не проблема — вы чаще загружаете данные в одну таблицу или делаете upserts в одной таблице. Если же мультитабличные транзакции критичны, можно посмотреть в сторону Project Nessie или Apache Polaris. Классические каталоги вроде Hive, Glue, JDBC такой возможности не дают.

Итог

Iceberg обеспечивает ACID‑гарантии поверх объектного хранилища благодаря двум ключевым идеям:

  • Атомарные свопы метаданных через compare‑and‑swap.
  • Неизменяемые файлы данных, которые гарантируют консистентность снапшотов.

В результате вы можете обращаться с таблицами data lake как с настоящими таблицами базы данных: конкурентные записи работают, консистентность гарантирована, нет corrupted data и partial updates. Именно это и даёт современный табличный формат.

Конкуренция — двигатель прогресса. Iceberg не боится параллелизма, а превращает его в преимущество. Как в хорошем оркестре: каждый музыкант играет свою партию, а дирижёр (метаданные) ensures, что всё звучит гармонично.


Что дальше? В следующем уроке мы разберём Обновления и удаления — как Iceberg выполняет операции UPDATE и DELETE, сохраняя при этом неизменяемость данных и поддерживая time travel.

→ Вперёд к обновлениям и удалениям

← Назад к упражнению «Каталоги»