Кратко (TL;DR): Content Addressed Storage (CAS), появившееся в качестве экспериментальной функции в Antalya 26.6, представляет собой расширение модели хранения ClickHouse, которое позволяет хранить данные MergeTree в общем объектном хранилище. Самое главное, оно позволяет масштабировать хранилище независимо от вычислений.
«Долго бьешься головой о стену — глядишь, что-нибудь и получится». — Английская поговорка
Разделение вычислений и хранилища — важнейшая проблема в современных аналитических системах. Аналитические СУБД следуют примеру Snowflake и размещают единственную копию данных таблицы в общем объектном хранилище. ClickHouse двигался в этом направлении в течение нескольких лет, но до сих пор версии с открытым исходным кодом не предлагали کاملного решения.
Altinity Antalya 26.6 представляет новое расширение модели хранения ClickHouse — Content Addressed Storage (CAS), которое предлагает простой способ внедрения общего объектного хранилища для таблиц MergeTree. Пользователи могут включить его, обновившись до последней сборки Antalya и выполнив простое изменение конфигурации. Оно работает со всеми движками таблиц MergeTree.
Большинство баз данных спроектированы для одноузловой архитектуры. Чтобы масштабировать такие архитектуры вверх, необходимо увеличить объем памяти, дисков и количество процессоров в системе. Но существует предел тому, насколько это можно увеличить в рамках одного сервера.
Именно здесь на сцену выходит архитектура без разделения ресурсов (shared-nothing). В ней каждый узел обладает собственной вычислительной мощностью и выделенным срезом или копией данных. Это обеспечивает колоссальный параллелизм, распределяя данные и обработку по нескольким узлам. Базы данных приняли эту архитектуру под названием массово-параллельной обработки (MPP).
Идеальным примером MPP-базы данных является с открытым исходным кодом ClickHouse. Каждый сервер поставляется с процессорами и собственным локальным хранилищем. Несколько серверов в кластере повышают вычислительную мощность за счет репликации и шардинга. Каждый узел имеет собственное локальное хранилище.
Но у архитектуры shared-nothing есть одно критическое ограничение: она не может масштабировать хранилище и вычисления независимо друг от друга.
Пользователи и разработчики баз данных искали архитектуру, в которой единственная копия данных могла бы быть подключена к нескольким узлам базы данных, распределяющим рабочую нагрузку запросов. Snowflake была пионером этого подхода. DataBricks последовала тому же паттерну несколько лет спустя. В таких архитектурах существует пул данных, обычно размещаемый в объектном хранилище, и несколько узлов обработки запросов, которые извлекают данные из этого пула для выполнения запросов. Узлы обработки можно добавлять или удалять по требованию. Хранилище существует само по себе.
Источник изображения — блог Snowflake
В 2020 году ClickHouse начал переход на использование объектного хранилища в качестве варианта хранения для таблиц MergeTree под названием S3 disks. Процесс прошел через множество итераций. Объектное хранилище имеет характеристики, отличные от блочного хранилища. В частности, задержка (latency) здесь намного выше, что требует иных методов организации и выборки данных. С годами поддержка объектного хранилища в ClickHouse стремительно эволюционировала. Компактные парты (parts), множество кэшей, префетч и другие хитрости сокращали разрыв в производительности между локальным хранилищем и объектным. Но одна проблема долгое время оставалась нерешенной — совместное использование данных. Несмотря на использование объектного хранилища, ClickHouse продолжал использовать подход shared-nothing, при котором каждый реплика и шард используют собственную копию данных!
Проблема была очевидна, и разработчики ClickHouse отреагировали на нее репликацией с нулевым копированием (zero-copy replication). Репликация с нулевым копированием означает, что как только одна реплика ClickHouse размещает данные на диске S3, их не нужно реплицировать на другие реплики ClickHouse. Несколько реплик подключаются к одним и тем же общим данным на S3. Проблема решена? Ну, не совсем.
К сожалению, лежащая в основе конструкция диска ClickHouse S3 делает репликацию с нулевым копированием ненадежной. Если вкратце, дисковое хранилище ClickHouse S3 состоит из трех систем, которые должны согласовываться транзакционным образом: ссылки на данные хранятся на локальном хранилище и реплицируются как обычная таблица ClickHouse, сами данные хранятся в объектном хранилище, а состояние репликации отслеживается в Keeper отдельно для каждой реплики. Более подробно вы можете узнать в статье об архитектуре MergeTree поверх S3. Удерживать эти три источника данных в синхронном состоянии в реальной жизни практически невозможно. Большинство пользователей, по-видимому, страдали молча, поэтому практические проблемы широко не известки. Наши друзья из Tinybird опубликовали захватывающую историю о своем опыте. Самое главное, что эта конструкция не удаляет сиротские файлы должным образом. Отсутствие эффективной сборки мусора является ключевым недостатком подхода с нулевым копированием.
Проблемы репликации с нулевым копированием заставили основную команду ClickHouse объявить ее устаревшей (deprecated) в 2025 году (#82250). Код на месте, но все тесты удалены.
Появление CAS
Потребовалось время, чтобы прийти к лучшему подходу к проблеме разделения вычислений и хранилища в ClickHouse. Мы начали с RFC 54644, пытаясь исправить репликацию с нулевым копированием, но столкнулись с многочисленными препятствиями. Год спустя мы запустили Project Antalya. Project Antalya добавляет Apache Iceberg в качестве уровня хранения в ClickHouse, а бессерверные узлы swarm — в качестве масштабируемых вычислений. Это позволяет совместно использовать данные между узлами ClickHouse и внешними системами через Iceberg и, следовательно, использовать Iceberg в качестве единой копии данных не только для ClickHouse, но и для других внешних систем. Это отличная архитектура, которая обеспечивает доступ к данным из потенциально тысяч сервисов запросов.
Antalya работает действительно хорошо, но она также добавляет сложности взамен на обеспечение широкой доступности хранилища для других приложений. Нам нужно было более простое решение, предназначенное только для ClickHouse. Михаил Филимонов, главный инженер Altinity и один из лучших разработчиков ПО, с которыми мне доводилось работать, много ломал голову над этой проблемой. Он заперся в комнате на месяц с изобилием инструментов искусственного интеллекта. Так родился CAS.
Концепция контентно-адресуемого хранилища (content addressed storage) не нова. Она работает за счет ссылки на файлы с использованием хэша содержимого вместо обычного имени файла. Так работает, например, Git. ClickHouse уже использовал CAS для некоторых внутренних сценариев использования, таких как дедупликация блоков. Мы просто перенесли это на уровень файлов. Мы создали это как расширение существующей модели политики хранения самым простым из возможных способов.
Основная идея заключается в том, что на каждый файл данных таблицы MergeTree ссылается хэш его содержимого. Множество серверов используют один пул объектного хранилища. Здесь нет дублирования байтов, нет учета zero-copy в Keeper, нет локального дискового состояния ссылок для каждой реплики, которое растет вместе с объемом данных, и нет изменяемого счетчика ссылок (refcount) для каждого блоба. Только хэш. Карта между хэшами и содержимым хранится в неизменяемых манифестных файлах и таблице ссылок в памяти (reftable). Ничего не хранится локально, все находится в объектном хранилище.
Архитектура напоминает Iceberg, и на то есть веские причины. Хранение всей информации «на диске» создает единственный источник истины и упрощает такие задачи, как восстановление. Это масштабируемое и проверенное решение.
Вместо замены MergeTree новым движком мы добавили другой тип метаданных CAS для дисков объектного хранилища. Это существенно упрощает как внедрение, так и миграцию.
Но хватит теории. Позвольте мне показать, как это работает, а затем мы вернемся к техническим деталям.
CAS в действии
CAS расширяет архитектуру ClickHouse, ничего не удаляя, поэтому его можно применять в работающих системах ClickHouse без какого-либо редизайна. Чтобы начать его использовать, просто обновитесь до сборок Antalya версии 26.6 и выше. В версии 26.6.4 произошли изменения формата, поэтому используйте последнюю сборку.
После обновления до сборки, поддерживающей CAS, ничего не происходит мгновенно. Чтобы начать его использование, диск CAS должен быть определен в конфигурации хранилища ClickHouse:
Как может заметить опытный пользователь ClickHouse, эта конфигурация мало отличается от обычной конфигурации диска S3. Главное отличие заключается в новом типе metadata_type. ClickHouse уже поддерживает несколько типов метаданных, поэтому мы добавили новый — cas. Кроме того, ему нужно различать разных «клиентов» CAS, и они подписываются с помощью cas_server_root_id, который должен уникально идентифицировать узел кластера. Это похоже на {replica} в таблицах ReplicatedMergeTree. (Вместо макроса {replica} можно также использовать {server_uuid}.)
Поскольку CAS является диском, его нужно добавить в политики хранения, чтобы использовать. Например, вот политика, которая хранит данные только на CAS:
Или многоуровневая политика, которая добавляет CAS в качестве второго уровня:
Все функции ClickHouse поддерживаются в полном объеме. Мы можем создавать таблицы на дисках CAS, перемещать куски и партиции между локальными дисками, обычным S3 и CAS, или использовать диск CAS в качестве холодного уровня с выражениями TTL таблицы.
Для целей этой статьи я загружу набор данных «ontime» на диск CAS в одноузловой ClickHouse в Altinity.Cloud. Позже я масштабирую его на большее количество узлов и проведу другие эксперименты. Чтобы загрузить тестовый набор данных, можно использовать функцию IMPORT DATASET в Altinity.Cloud или запустить ее на обычном SQL следующим образом:
Обратите внимание, что движок — ReplicatedMergeTree, но репликация используется только для координации. Данные записываются только один раз.
Загрузка 200 млн строк занимает менее 4 минут, и таблица готова к выполнению запросов. Никакой магии, это просто работает.
Чтобы посмотреть, как данные хранятся на самом деле, мы можем использовать системные таблицы. Мы можем начать с system.parts, чтобы убедиться, что все данные находятся на CAS:
Также появились три новые таблицы, специфичные для CAS:
- cas_mounts — напоминает system.replicas, но на уровне дисков. Показывает серверы и диски, смонтированные в пулы CAS
- cas_log и cas_gc_log — полезны для отслеживания того, что происходит внутри CAS
Например, cas_log, сгруппированный по event_type, может дать вам представление о том, что происходило внутри CAS:
Давайте проверим добавление новой реплики, чтобы убедиться, что мы можем легко масштабировать вычислительные узлы. Altinity Operator для ClickHouse и Altinity.Cloud делают добавление реплик очень простым. При обычном хранилище ReplicatedMergeTree ClickHouse должен был бы реплицировать данные по сети на новую реплику, что занимает довольно много времени. С CAS инициализация таблицы ontime на новой реплике заняла всего 14 секунд. Это видно из cas_log после того, как реплика переходит в онлайн:
На больших наборах данных это может занять больше времени. Нам еще предстоит измерить, как это работает на данных размером в несколько терабайт.
Основные особенности архитектуры
Под капотом CAS представляет собой бэкенд MetadataStorage, который хранит каждый файл куска MergeTree один раз, используя его хэш содержимого в качестве ключа объекта. Узлы ClickHouse совместно используют один и тот же пул объектного хранилища и публикуют ссылки на неизменяемые манифесты. Данные координации CAS находятся в бакете, а не в Keeper.
Позвольте мне объяснить основные архитектурные решения более подробно.
Как CAS хранит кусок?
Начнем с базовой объектной модели. Кусок MergeTree представляет собой каталог, содержащий множество файлов. CAS вычисляет хэш содержимого для каждого файла и сохраняет его под ключом, полученным из этого хэша. Если другой кусок содержит тот же файл, он разрешается в тот же объект, поэтому нет необходимости сохранять еще одну копию.
Имя куска не указывает напрямую на эти блобы. Вместо этого оно указывает на изменяемую ссылку (mutable ref), которая указывает на неизменяемый манифест (immutable manifest). В манифесте перечислены файлы, принадлежащие куску, и места хранения их содержимого:
имя куска → изменяемая ссылка → неизменяемый манифест → неизменяемые хешированные блобы
Небольшие файлы метаданных, такие как count.txt или columns.txt, могут быть встроены непосредственно в манифест. Более крупные файлы хранятся как отдельные блобы с адресацией по содержимому.
Как публикуется кусок?
Итак, как новый кусок становится видимым? ClickHouse не может сначала опубликовать ссылку, а затем загрузить данные. Сбой между этими действиями приведет к тому, что видимый кусок будет указывать на отсутствующие файлы.
CAS использует тщательно упорядоченную последовательность публикации. Сначала запись создает неизменяемый манифест. Затем она записывает долговечную предварительную ссылку (precommit ref) перед загрузкой или принятием необходимых блобов. Как только все блобы становятся доступны, предварительная фиксация повышается до зафиксированной ссылки (committed ref), и кусок становится видимым для обычных читателей.
Условная запись объектов обеспечивает координацию. Если два сервера пытаются создать один и тот же блоб, успешной оказывается только одна запись. Другой сервер находит существующий блоб и принимает его вместо загрузки еще одной копии. В этом решении не участвует отдельная служба метаданных. Координация происходит полностью внутри объектного хранилища.
Чтение следует по цепочке в обратном направлении. ClickHouse разрешает ссылку на кусок, проверяет манифест, а затем считывает необходимые диапазоны блобов из объектного хранилища. Файлы, внедренные в манифест, не требуют дополнительного запроса объекта.
Объектное хранилище как единственный источник правды
CAS возлагает на объектное хранилище больше ответственности, чем обычный диск на базе S3. В бакете хранятся не только данные, но и ссылки, манифесты, арендные договоры на монтирование, состояние ограждения (fencing state) и метаданные сборщика мусора.
Это не исключает Keeper из ReplicatedMergeTree. Keeper по-прежнему координирует журнал репликации и консенсус набора кусков, как описано ниже.
Каждый сервер имеет собственную идентификацию, эпоху записи и возобновляемую аренду монтирования. Если сервер теряет аренду, локальное ограждение мешает ему выполнять дальнейшие записи. Неоднозначные операции завершаются ошибкой (fail closed): ClickHouse повторяет их попытки, вместо того чтобы предполагать, что неопределенная запись не произошла.
Для этого требуется реальная поддержка условного создания и замены объектов, чтения диапазонов, возобновляемого листинга, стабильных токенов объектов и удаления по точному токену. Недостаточно того, чтобы S3-совместимая реализация принимала соответствующие заголовки — она должна правильно реализовывать логику.
AWS S3 и Google Cloud Storage поддерживают необходимые операции. Azure пока не проверялась.
Поведение репликации
CAS не заменяет репликацию ClickHouse. ReplicatedMergeTree по-прежнему использует Keeper для журнала репликации и консенсуса набора кусков — именно так другие реплики узнают о прибытии нового куска. CAS меняет то, как принимающая реплика получает данные куска.
Чем это отличается от репликации ClickHouse без копирования (zero-copy)? Цель аналогична, но модель владения принципиально отличается. Репликация без копирования говорит:
«Реплика 2 должна ссылаться на тот же удаленный объект, который уже использует Реплика 1».
CAS говорит:
«Хэш файла идентифицирует объект. Любая реплика может независимо опубликовать ссылку на него».
При репликации без копирования (zero-copy replication) реплики используют общие пути к удаленным объектам и применяют журнал Keeper для координации владения. В CAS вместо этого используются общие идентификаторы контента.
Выборка частей без копирования позволяет избежать передачи байтов, предоставляя принимающей реплике метаданные, которые указывают на существующие удаленные файлы. Самая сложная задача — доказать, что эти файлы остаются активными в процессе передачи прав владения между репликами.
CAS справляется с этим с помощью трехэтапного процесса перелинковки:
- Принимающая реплика фиксирует свое намерение использовать общие файлы.
- Она проверяет, по-прежнему ли отправляющая реплика владеет той же частью.
- После подтверждения она публикует часть локально.
Если результат какого-либо этапа неопределен, CAS повторяет попытку, вместо того чтобы гадать, что произошло. Она скачивает байты части только тогда, когда может подтвердить, что перелинковка не завершилась. Это предотвращает ситуацию, когда реплика публикует часть с недостающими данными или публикует одну и ту же часть дважды.
Сборка мусора
Это, пожалуй, самое крупное архитектурное отличие, которое решает проблему «осиротевших» файлов, с которыми пользователи сталкивались при промышленном использовании репликации без копирования.
При использовании zero-copy необходимо отслеживать, какие реплики все еще ссылаются на удаленный объект. В RFC 62936 (одной из многочисленных попыток исправить репликацию без копирования) описываются WAL, очереди ZooKeeper и общие снимки состояния (snapshots), призванные сделать такое отслеживание более безопасным.
CAS применяет другой подход. Она определяет, что все еще необходимо, прослеживая цепочку достижимости:
- Зафиксированная или находящаяся в процессе выполнения ссылка (ref) поддерживает жизнь своего манифеста.
- Живой манифест поддерживает жизнь всех своих блобов.
- Когда ссылка удаляется, ее блобы могут стать кандидатами на очичку, если на них больше никто не ссылается.
- Сборка мусора помечает неиспользуемый блоб и проверяет его снова в следующем цикле.
- Она удаляет только ту конкретную версию объекта, которая была помечена, поэтому более новая заменяющая версия остается в безопасности.
Если уверенности в безопасности нет, CAS откладывает удаление и выполняет проверку снова в последующих циклах сборки мусора. Очистка происходит автоматически, как только будет доказано, что объект не используется.
Важное примечание по разработке ПО: Михаил доказал эффективность этого алгоритма с помощью TLA+ при содействии Claude. Этот подход представляет собой качественный скачок в разработке надежных алгоритмов, связанных с управлением распределенным хранилищем. В будущем мы еще расскажем о процессе проектирования CAS.
Подсказки и советы
Мы все еще собираем опыт эксплуатации дисков CAS. Позвольте поделиться несколькими подсказками.
CAS может страдать от тех же проблем, что и любое MergeTree на базе S3. Запросы стоят дорого, поэтому если записывать много данных напрямую в CAS, AWS может применить ограничение частоты запросов (rate limiting). Вы увидите следующее сообщение в логах, а также общее замедление:
2026.09.04 07:21:28.037323 [ 872 ] {} <Error> AWSClient: Response status: 503, Slow Down
Вот несколько советов, которые могут сделать ее эффективнее:
- Не выполняйте частую запись на диски CAS. Вместо этого используйте локальное хранилище для высокочастотных вставок и слияний, а затем перемещайте менее фрагментированные данные в CAS с помощью правил TTL.
- Агрессивнее используйте компактные части. Значение по умолчанию min_bytes_for_wide_part составляет всего 10 МБ, его можно безопасно увеличить до 100 МБ или более. Также можно установить min_level_for_wide_part в значение 3 или 4, чтобы все части ранних поколений оставались в компактном формате.
- Шардируйте пул для крупных развертываний. Если шардирование кластера фиксированное, это можно сделать, добавив макрос {shard} в эндпоинт. Кроме того, разные таблицы могут находиться в разных пулах.
Другие конфигурационные трюки см. также во встроенной документации.
До того как CAS достигнет статуса GA (общедоступной версии), мы планируем создать подробное руководство пользователя, а также применить дополнительные оптимизации сервера, чтобы работа с ним была плавной. Она также будет интегрирована в Altinity.Cloud.
Статус и планы на будущее
Диски CAS являются экспериментальной функцией начиная с релиза Antalya 26.6. Она была всесторонне протестирована самыми разными способами. Она проходит все стандартные тесты ClickHouse при использовании CAS вместо обычного S3. Мы также разработали собственный набор тестов для функций, специфичных для CAS. Реализация функционально завершена для AWS S3 и GCP GCS. Ее все еще необходимо адаптировать для работы с хранилищем блобов Azure.
Наш следующий фокус — производительность. Мы протестировали производительность вставки и запросов и поделимся результатами в отдельной статье в блоге. Функция отлично работает для запросов, но пока не обладает полной производительностью для вставок. Поэтому ее можно использовать для холодного яруса, но не в качестве единственной модели хранения.
По ходу дела мы также представим сравнения между хранением данных в CAS, которые читаются только через ClickHouse, и размещением данных таблиц в Iceberg, который общедоступен для широкого спектра движков запросов. Они решают разные задачи и имеют разные компромиссы. Большое преимущество CAS заключается в том, что это готовое расширение для существующих приложений ClickHouse.
Мы продолжим экспериментировать с CAS внутри компании и работать над улучшениями. Мы приглашаем всех попробовать ее в деле и сообщать о проблемах. Это открытый исходный код, и так будет всегда! Мы с нетерпением ждем появления версии GA для разделения вычислений и хранения в OSS ClickHouse к концу 2026 года!










