СУБД не нужна

/ Всеволод Миронович

Как-то я привык, что если хочешь писать 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 в пути.
Эту и кучу других проблем вам удалось скипнуть, потому что только дураки учатся на своих ошибках.

Полезные ссылки

  1. Apache Iceberg
  2. Project Nessie
  3. DuckDB
  4. Big data is dead
  5. Больше чем просто данные в S3. Iceberg как основа архитектуры Next-Gen КХД