Как-то я привык, что если хочешь писать SQL, тебе нужна СУБД. А оказывается, давно вперёд шагнули технологии. Про iceberg слышали, про duckdb слышали, про мета каталоги слышали.. слышали ведь?
Надо закинуть всё в кастрюльку, потрястись под музыку и посмотреть, что получится.
Iceberg
Для одного из клиентов 💚 у нас развернут солидный кластер ClickHouse. Данные в него постоянно прибывают, а деньги.. в общем возник вопрос, как подешевле обслуживать, да и бесконечно наращивать диски нельзя же.
Некоторую часть данных мы просто храним в S3 паркетами, но это долго.
Для объемных таблиц организовали гибридное хранение, когда данные по мере устаревания автоматически отправляются с SSD на HDD, а оттуда на S3. Но из-за репликации они дублируются в объектном хранилище, что кажется напрасным.
Поискав в инете, выяснил, что есть открытые форматы таблиц. Люди придумали не просто складывать паркеты, а докидывать к ним метаинформацию и всякие вспомогательные расчеты.
Nessie
Но это всё равно какие-то паркеты в облаке! Как с ними одновременно-то работать? И файлы с метаинфорацией плодятся с огромной скоростью, едва тронешь данные. Ещё непонятно, что там с доступами и ролями.
Вот так появились каталоги – сервисы с постгресом, которые решают эти проблемы.
Я потыкал несколько, и успехов за разумное время добился только с Nessie. Хоть это чудовище у меня и ломалось с каждым релизом, альтернативы оказались еще страшнее.
Trino и DuckDB
Ну и кто-то должен взять на себя грязную работу - исполнять мой SQL. Trino имеет кучу коннекторов к разным базам и форматам, может масштабироваться вдоль, и многие облака сделали у себя его managed версии. А вот suckdb меня привлек тем, что ему не нужен отдельный сервис, может запускаться любым процессом.
Более того, у него есть wasm вариант - исполняется прямо в браузере.
Cooking
Пришло время варить!
Чтобы сложить данные в айсберг, нужны эти самые данные, поэтому поднимаю Trino.
# ./docker-compose.yaml
services:
trino:
image: trinodb/trino
container_name: trino
restart: unless-stopped
networks:
iceberg-nessie-net:
ports:
- "8080:8080"
volumes:
- "./catalog:/etc/trino/catalog"
- "./config/log.properties:/etc/trino/log.properties"
env_file:
- .env
networks:
iceberg-nessie-net:
И знакомлю его с имеющимся у меня ClickHouse
# ./catalog/clickhouse.properties
connector.name=clickhouse
connection-url=${ENV:CH_URL}
connection-user=${ENV:CH_USER}
connection-password=${ENV:CH_PASSWORD}
clickhouse.map-string-as-varchar=true
Уже можно поселектить КХ через триню в дибивере. Но эти данные мне же надо перелить в S3.. в ведро 🤣
Я завёл бакет в яндексе и выписал сервисный аккаунт к нему со статичным ключом. Пришло время призвать из глубин чудовище.
# docker-compose.yaml
services:
catalog:
image: ghcr.io/projectnessie/nessie:0.105.5
container_name: nessie
restart: unless-stopped
ports:
- "19120:19120"
environment:
# общие
- nessie.server.default-branch=main
- nessie.server.authentication.enabled=false
- write.object-storage.enabled=true
- nessie.version.store.type=JDBC2
- nessie.version.store.persist.jdbc.datasource=postgresql
- quarkus.datasource.postgresql.jdbc.url=${DB_URL}
- quarkus.datasource.postgresql.username=${DB_USERNAME}
- quarkus.datasource.postgresql.password=${DB_PASSWORD}
# хранилища
- nessie.catalog.default-warehouse=yandex
- nessie.catalog.warehouses.yandex.location=s3://lakehouse
# s3 яндекс
- nessie.catalog.service.s3.buckets.lakehouse.endpoint=${S3_YANDEX_ENDPOINT}
- nessie.catalog.service.s3.buckets.lakehouse.region=${S3_YANDEX_REGION}
- nessie.catalog.service.s3.buckets.lakehouse.path-style-access=false
# обязательно передавать через секрет
- nessie.catalog.service.s3.buckets.lakehouse.access-key=urn:nessie-secret:quarkus:yandex-secrets
- yandex-secrets.name=${S3_YANDEX_ACCESS_KEY}
- yandex-secrets.secret=${S3_YANDEX_SECRET_KEY}
env_file:
- .env
networks:
iceberg-nessie-net:
В этом конфиге создаётся warehouse под названием yandex, в которое будут складываться паркеты. Обратите внимание, что название хранилища потом используется в пути настроек для него. Аналогично с названием бакета.
Ах да, мне всё-таки нужна СУБД 😅
Я указываю креды managed постгресса в том же яндексе. Несси сделает в нём буквально пару табличек в отдельной базе, ничего критичного.
Познакомим с новым зверьком триню.
# ./catalog/nessie.properties
connector.name=iceberg
fs.native-s3.enabled=true
iceberg.catalog.type=rest
iceberg.rest-catalog.uri=http://catalog:19120/iceberg
iceberg.rest-catalog.warehouse=yandex
s3.endpoint=${ENV:S3_YANDEX_ENDPOINT}
s3.region=${ENV:S3_YANDEX_REGION}
s3.path-style-access=false
s3.aws-access-key=${ENV:S3_YANDEX_ACCESS_KEY}
s3.aws-secret-key=${ENV:S3_YANDEX_SECRET_KEY}
Наконец можно покидать данные в ведро 🤣
insert into nessie.some_schema.some_table
select * from clickhouse.some_schema.some_table
where date(date) = date('2025-10-26')
limit 100;
Чуете странное в конфигах? да, я знаю, вас так просто не провести.
Ключ сервисного акка для доступа к бакету у меня указан и в каталоге и в SQL движке. Каталогу он нужен, чтобы видеть файлы, а движку, чтобы работать с ними. Вроде логично.. но как-то тупо. Поэтому придумали научить каталог делиться кредами, и тут есть два варианта:
- движок составляет запрос к s3, отправляет в каталог, тот его подписывает токеном, возвращает, и движок шлёт результат к объектном хранилищу. Это жидкий вариант, мы не будем его показывать
- движок запрашивает временные креды у каталога, сохраняет у себя и использует, пока не протухнут, потом обновляет.
В настройках несси для трини надо внести корректировки
# ./catalog/nessie.properties
# включаем получение кредов у каталога
iceberg.rest-catalog.vended-credentials-enabled=true
# всякие access-key и secret-key можно убрать
Это было просто, а вот чудовище заставило меня повозиться по незнанке. Сперва я потестил выдачу временных прав по инструкции яндекс клауда через cli
aws --endpoint https://sts.yandexcloud.net/ sts assume-role \
--role-arn a1234567891234567890 \
--role-session-name project_id \
--duration-seconds 3600 \
--policy file://policy.json
И когда оно заработало, перенёс в несси.. ну, или наоборот 😅
Конечно же для продакшна надо всё переписать.
# docker-compose.yaml
# отключаем механизм подписывания запросов: медленный и не поддерживается движками
- nessie.catalog.service.s3.buckets.lakehouse.request-signing-enabled=false
# настраиваем выдачу временных кредов
- nessie.catalog.service.s3.buckets.lakehouse.sts-endpoint=https://sts.yandexcloud.net/
- nessie.catalog.service.s3.buckets.lakehouse.client-iam.enabled=true
- nessie.catalog.service.s3.buckets.lakehouse.client-iam.assume-role=a1234567891234567890
- nessie.catalog.service.s3.buckets.lakehouse.client-iam.role-session-name=project_id
- nessie.catalog.service.s3.buckets.lakehouse.client-iam.session-duration=3600
- nessie.catalog.service.s3.buckets.lakehouse.client-iam.policy={"Version":"2012-10-17","Statement":{"Sid":"all","Effect":"Allow","Principal":"*","Action":"*","Resource":"arn:aws:s3:::lakehouse/*"}}
А что там с уткой? Зацените, как легко теперь подключить этот каталог в dickdb
ATTACH '' AS lakehouse (
TYPE iceberg,
AUTHORIZATION_TYPE 'none',
ENDPOINT 'http://localhost:19120/iceberg/'
);
По логике надо было бы указать имя warehouse, но тогда некоторые команды не работают из-за наличия yandex|main в пути.
Эту и кучу других проблем вам удалось скипнуть, потому что только дураки учатся на своих ошибках.