Lakehouse-архитектура: открытые форматы таблиц поверх object storage
Предыдущие модули разбирали Trino как движок: координатор, воркеры, сплиты, операторы. Этот модуль отвечает на вопрос, ради которого Trino чаще всего и разворачивают в продакшене: как заставить распределённый SQL-движок работать поверх петабайтов файлов в object storage так, будто это обычные таблицы базы данных. Ответ — lakehouse, и его сердце — открытый формат таблиц. В этом уроке разберём, какую конкретную проблему он решает и почему «папка с Parquet-файлами» — это ещё не таблица.
Две архитектуры, между которыми втиснулся lakehouse
Исторически аналитика жила в двух разных мирах. Data warehouse (Snowflake, Teradata, классический Redshift) — это система, которая хранит данные в собственном проприетарном формате, управляет ими через свой движок и даёт строгие гарантии: транзакции, согласованность, статистику, оптимизатор. Цена — данные заперты внутри: чтобы их прочитать, нужен этот конкретный движок, а compute и storage обычно связаны или тарифицируются вместе.
Data lake — противоположность. Это просто object storage (S3, GCS, Azure Blob, HDFS) с файлами в открытых форматах: Parquet, ORC, Avro, CSV. Хранение дёшево, форматы открыты, читать может кто угодно — Spark, Trino, pandas, DuckDB. Но lake не даёт почти никаких гарантий. Нет атомарности: если запись на середине упала, читатель увидит половину файлов. Нет согласованного списка файлов: list object storage возвращает то, что есть прямо сейчас. Нет schema evolution: переименовали колонку — старые файлы об этом не знают. Нет статистики для оптимизатора. Это «болото данных» — данные есть, но доверять им как таблице нельзя.
Lakehouse — это архитектура, которая берёт хранение и открытость из lake, а гарантии — из warehouse. Файлы данных по-прежнему лежат в object storage в открытом формате (обычно Parquet). Но поверх них добавляется слой метаданных — открытый формат таблиц. Именно он превращает набор файлов в настоящую таблицу с транзакциями, версионированием и статистикой. Trino — один из движков, которые читают и пишут этот слой. Главных открытых форматов таблиц три: Apache Iceberg, Delta Lake и Apache Hudi. Этот модуль и следующий разбирают, как Trino работает с каждым.
Apache Iceberg: каталог и иерархия метаданных Delta Lake: архитектура Transaction Log Spark: архитектура LakehouseПочему «папка с Parquet» — это не таблица
Возьмём конкретный сценарий. У вас в S3 лежит s3://lake/orders/ с тысячей Parquet-файлов. Чтобы это была таблица, движку нужно ответить на четыре вопроса, и data lake сам по себе не отвечает ни на один.
Какие файлы входят в таблицу прямо сейчас? Кажется, что ответ — «все, что в папке». Но представьте: идёт запись, заливающая 200 новых файлов. Параллельный SELECT делает list по папке. Он увидит, например, 140 из 200 новых файлов — операция list над object storage не атомарна. Запрос прочитает несогласованный срез данных и не упадёт, просто молча вернёт неверный результат. Это самый коварный класс багов в data lake.
Какая у таблицы схема? Файл Parquet хранит свою схему. Но если за три года колонку добавляли, удаляли и переименовывали, разные файлы имеют разные схемы. Без внешнего описания «вот текущая схема таблицы и как поля каждого файла на неё мапятся» движок не сможет прочитать таблицу целостно.
Как сделать запись атомарной? Запросу нужно добавить 200 файлов и удалить 50 старых. Если между этими шагами читатель сделает list, он увидит несогласованное промежуточное состояние. Нужен механизм, при котором изменение видно либо целиком, либо никак.
Какие файлы можно пропустить? Запрос WHERE order_date = DATE '2026-05-01'. В идеале движок не должен открывать файлы, где таких дат заведомо нет. Для этого нужна статистика — min/max каждой колонки по каждому файлу — заранее, до того как файл будет прочитан.
Открытый формат таблиц отвечает на все четыре вопроса одним механизмом: слоем метаданных рядом с файлами данных. Метаданные — это тоже файлы в том же object storage, но они описывают, какие файлы данных образуют таблицу, какая у неё схема, как она партиционирована и какая статистика у каждого файла. Каждое изменение таблицы создаёт новую версию метаданных — снапшот. Снапшот — это согласованный «список файлов на момент времени». Запрос всегда читает один конкретный снапшот, поэтому видит атомарный, целостный срез. Параллельная запись создаёт новый снапшот, не трогая старый, — отсюда атомарность и изоляция.
Ключевая мысль модуля: формат таблиц не хранит данные. Данные — в обычных Parquet-файлах. Формат таблиц хранит метаданные о данных. Поэтому те же файлы могут читать и Trino, и Spark, и Flink — никто никого не блокирует, потому что движки координируются через общий слой метаданных, а не через монопольный доступ к файлам.
Где здесь Trino и почему именно он
Trino в lakehouse-архитектуре — это compute-слой, полностью отделённый от storage. Файлы и метаданные живут в object storage, которое не зависит от Trino. Сам Trino не хранит ничего: упал кластер — данные на месте, поднимаете новый кластер, он подключается к тому же storage. Это и есть разделение compute и storage в чистом виде: масштабируете обработку независимо от объёма данных.
Чтобы Trino понял lakehouse-таблицу, он использует коннектор формата — iceberg, delta_lake или hive. Коннектор знает, как читать слой метаданных конкретного формата: где лежит текущий снапшот, как разобрать манифесты, как достать статистику. Координатор Trino читает метаданные, получает список файлов с их min/max, отбрасывает заведомо ненужные файлы (это pruning — увидим в уроке про партиционирование) и генерирует сплиты только по оставшимся файлам. Воркеры читают Parquet-файлы напрямую из object storage параллельно.
Но коннектору нужно знать ещё одну вещь: где взять указатель на текущую таблицу. Object storage — это плоское хранилище объектов, в нём нет понятия «база данных» или «список таблиц». Должен быть отдельный реестр: «таблица orders в схеме sales — это вот такой файл метаданных по вот такому пути». Этот реестр называется каталог метаданных (metadata catalog) — Hive Metastore, AWS Glue, REST catalog, Nessie, JDBC. Не путайте его с понятием catalog в самом Trino (которое означает «коннектор плюс настройки подключения»). Каталогам метаданных целиком посвящён следующий урок.
-- Каталог Trino с именем iceberg, использующий коннектор iceberg.
-- Файл etc/catalog/iceberg.properties:
-- connector.name=iceberg
-- iceberg.catalog.type=rest
-- iceberg.rest-catalog.uri=http://rest-catalog:8181
-- fs.native-s3.enabled=true
-- s3.endpoint=http://minio:9000
-- Создаём схему. LOCATION задаёт префикс в object storage,
-- куда формат таблиц будет складывать и данные, и метаданные.
CREATE SCHEMA iceberg.sales
WITH (location = 's3://lake/sales');
-- Создаём настоящую таблицу формата Iceberg.
CREATE TABLE iceberg.sales.orders (
order_id BIGINT,
customer_id BIGINT,
order_date DATE,
amount DECIMAL(12,2)
)
WITH (format = 'PARQUET');
-- Загружаем данные. Это атомарная операция: создаётся новый снапшот.
INSERT INTO iceberg.sales.orders VALUES
(1, 100, DATE '2026-05-01', DECIMAL '250.00'),
(2, 101, DATE '2026-05-01', DECIMAL '99.90'),
(3, 100, DATE '2026-05-02', DECIMAL '12.50');
-- INSERT: 3 rows
После такого INSERT в s3://lake/sales/orders/ появятся и Parquet-файлы с данными, и файлы метаданных. Любой движок, умеющий читать Iceberg, увидит ровно эти три строки — потому что они зафиксированы одним снапшотом. SELECT во время следующего INSERT увидит старый снапшот целиком, без половинчатых данных.
-- Запрос читает один согласованный снапшот таблицы.
SELECT order_date, count(*) AS orders, sum(amount) AS revenue
FROM iceberg.sales.orders
GROUP BY order_date
ORDER BY order_date;
-- order_date | orders | revenue
-- ------------+--------+---------
-- 2026-05-01 | 2 | 349.90
-- 2026-05-02 | 1 | 12.50
Чем lakehouse отличается от warehouse и lake — итог
| Свойство | Data warehouse | Data lake | Lakehouse (Iceberg/Delta) |
|---|---|---|---|
| Формат хранения | Проприетарный | Открытый (Parquet/ORC) | Открытый (Parquet/ORC) |
| Транзакции, атомарность | Есть | Нет | Есть (через снапшоты) |
| Согласованный список файлов | Есть | Нет (list не атомарен) | Есть (манифесты снапшота) |
| Schema evolution | Есть | Нет | Есть |
| Time travel | Иногда | Нет | Есть |
| Несколько движков на одних данных | Нет | Да, но без гарантий | Да, с гарантиями |
| Compute и storage разделены | Часто нет | Да | Да |
| Статистика для оптимизатора | Есть | Нет | Есть (min/max в манифестах) |
Lakehouse не «лучше всех по всем осям» — у зрелого warehouse транзакции отлажены десятилетиями. Но lakehouse даёт то, чего не даёт ни warehouse, ни lake: открытые данные, на которые при этом можно положиться как на таблицу, с разделением compute и storage. Для дата-инженерии 2026 года это базовая архитектура, и Trino — один из её основных compute-движков.
Попробуй сам
Зайдите на trino.io/docs/current/connector/iceberg.html и найдите раздел про требования коннектора. Ответьте на три вопроса. Первый: какие типы каталога метаданных перечислены для iceberg.catalog.type и почему коннектору вообще нужен отдельный каталог, а не просто путь в S3. Второй: коннектор требует настроенную нативную файловую систему (fs.native-s3.enabled и аналоги) — почему хранение и метаданные нельзя «зашить» в сам Trino. Третий, концептуальный: ваша команда хранит логи в s3://lake/raw-logs/ тысячами Parquet-файлов и читает их Trino через Hive-таблицу с LOCATION. Какие из четырёх проблем data lake (несогласованный list, schema evolution, атомарность записи, statistics-based pruning) сейчас бьют по вам и какие исчезнут после миграции этой таблицы в формат Iceberg? Запишите ответ своими словами — следующие уроки модуля будут опираться на это понимание.