
Привет! Меня зовут Алексей Баранов, я руковожу направлением Data Storage Systems в облаке MWS. Мы начинаем серию статей, в которой расскажем, как устроены наши системы хранения, почему мы их делаем так и почему именно такие.
Начнём с фундаментальных технологий и сервисов — на их основе построены все системы хранения и обработки данных. Посмотрим, какие были варианты, какие из существующих решений мы выбрали, а где стали реализовывать что-то своё.
Расскажем, какие требования мы предъявляем к системам, и как это влияет на их архитектуру. Поднимаясь по стеку решений вверх, поговорим как про базовые сервисы блочного и объектного хранилищ, так и про системы аналитики и баз данных, которые уже предоставляет или будет предоставлять наше облако.
Одна из причин, почему мы объединили такую широкую область под зонтиком одного направления — это стремление найти синергию между сервисами разных уровней и построить единую платформу для работы с данными. Индустрия идёт в сторону разделения слоёв хранения и обработки (compute-storage separation) — бо́льшую пользу приносит создание гибких и эффективных решений по хранению, которые могут быть базой для технологий обработки и анализа.
В качестве дисклеймера и красной нити сразу скажем, что при принятии всех решений важным фактором будет то, что облако — это, в первую очередь, проект технологический, а задача команды облака — глубоко понимать то, как всё устроено, и владеть технологиями, которые мы предоставляем пользователям. Только так можно с нужным качеством и определённостью устранять проблемы и двигать сервисы вперёд. Поэтому даже при переиспользовании существующих решений нужно закладывать трудозатраты на полноценное «взятие на баланс» команды: сборки, тесты, анализ архитектуры, работу с апстримом и патчи уязвимостей и так далее.
Почему начнём с объектного хранилища
В первой статье мы, что может быть не слишком интуитивно, начнём с объектного хранилища. Казалось бы — это SaaS-ный веб-сервис, он находится на «вершине» пищевой цепочки облака. Но всё не так просто.
Самая очевидная причина — потому что мы его запустили раньше всех. А сделали мы именно так из-за особенного места «объектника» в экосистеме облака: с одной стороны, S3-compatible storage — это уже своего рода инфраструктурный must-have, который ожидается от любого облачного провайдера.
С другой — это один из немногих сервисов, у которого минимум критичных взаимозависимостей с другими компонентами облака. Точнее, зависимости могут быть: например, интеграция с событиями для serverless-функций. Однако они скорее расширяют функциональность, чем являются обязательными для работы. В отличие, скажем, от IaaS или классического PaaS: блочное хранилище, то есть диски для ВМ, не имеет смысла без самого IaaS — ну а куда эти диски втыкать? А IaaS — это и частные сети, и доступ в интернет, и базовая сетевая связность (чтобы пользователь мог попасть на ВМ), и сам по себе сервис виртуальных машин: планировщик, управление гипервизорами и прочее. Или, например, сервис управления базами данных: он работает поверх полноценного IaaS и зависит от продвинутых сетевых функций вроде пиринга частных сетей и сетевых балансировщиков.
Объектное хранилище, напротив, зависит только от нескольких базовых сервисов из области безопасности: от механизма базовой иерархии ресурсов (чтобы было куда приземлить бакет объектного хранилища) и проверки прав доступа. Дополнительно оно может использовать что-то из функций облака для шифрования данных at rest. Всё остальное — внутренняя инфраструктура самого сервиса и общий UI.
При этом даже в таком виде объектное хранилище может закрывать много пользовательских сценариев. Кроме этого, облачная инфраструктура IaaS тоже много где использует S3-compatible хранилище. Например, во многих публичных (и не только) облаках снимки дисков (snapshots) для бэкапов и образы (boot volumes или тома с данными — например, для тренировки ML-алгоритмов) часто хранятся именно в объектном хранилище. Если аккуратно избегать круговых зависимостей, этот же S3 (или поднятый рядом) можно использовать и во внутренней инфраструктуре: например, для холодного хранения логов, бэкапов и так далее.
Как видите, S3 — это отличный кандидат, чтобы быть первым сервисом облака: он полезен и как standalone-продукт, и как базовый строительный блок для других сервисов экосистемы облака.
В какую сторону развивается S3
Последний момент нашего «введения в S3-введение» — это посмотреть, как развивается S3 как технология. Может показаться, что S3-like сервисы — это более-менее устоявшиеся commodity-решения с понятным интерфейсом. Но по мере того, как увеличивается количество сфер применения S3, появляются и новые возможности.
Например, в 2018 году в AWS S3 зарелизили object lock'и — возможность заблокировать объект от удаления, с разными политиками. На первый взгляд, ничего особенного. Но эти механизмы прошли сертификацию по ряду compliance-требований (SEC, FINRA, CFTC), что позволило использовать S3 для хранения чувствительных финансовых записей без лишних прослоек.
Ещё один пример — directory buckets. Это специальный тип бакетов, который решает задачу быстрого (за единицы миллисекунд) доступа к данным. Ради этого они умеют только /
для деления «директорий» (в классическом S3 ключи объектов можно пилить на директории по любому символу в любой момент времени), и данные живут только в одной зоне.
Основная целевая аудитория — latency-critical сервисы (например стриминг) и сценарии ML (быстрый доступ к данным для обучения).
Третий пример — table buckets. Это бакеты особого формата, которые позволяют объектному хранилищу без лишних движений быть Apache Iceberg-compliant бэкендом, к которому тривиально подключаются процессинговые движки типа Spark или Flink. Отличное дополнение к S3-native аналитическому движку Trino.
Наконец, нестандартное, но интересное расширение от одного из облачных провайдеров — поддержка PATCH-операций над объектами. Они позволяют использовать S3 как backend для файловых систем эффективнее. Выглядит так, что система работает сама на положительной обратной связи:
— S3 — это удобно, в нём уже лежит куча данных, его любят разработчики, но жаль, что там нет X.
— Say no more!
И тенденция, я уверен, будет нарастать, и через некоторое время хранение всего, что не является OLTP-like нагрузкой, где-то кроме object storage'ей, будет скорее диковинкой — так уж прост и удобен в итоге оказался S3-интерфейс. Ну а для нас, как облака, эти примеры показывают то, что объектное хранилище — это живая технология, которая растёт и развивается высокими темпами, и, чтобы не отстать, нужно строить решение гибким, как с точки зрения архитектуры, так и с точки зрения масштабирования команды разработки.
В оставшейся части статьи мы поговорим о том, почему мы решили создавать велосипед своё решение. Детали — в следующих публикациях. Мы не первые, кто пишет своё объектное хранилище. И не первые, кто пишет об этом статью на Хабре. Тем более что с высоты птичьего полёта все object storage устроены одинаково.
Какие были альтернативы

А пока вернёмся в прошлое, в котором команда систем хранения данных MWS размышляет, каким должно быть наше объектное хранилище.
Для начала — какие вообще есть альтернативы «велосипеду»? Рассматривать будем с трёх сторон:
поддерживаемость,
лицензия,
наша уверенность в зрелости решения.
Если tldr, то реально выбирали из одного финалиста — Ceph RGW. Думали ещё про MinIO, но он проиграл без вариантов из-за лицензии AGPLv3 — да-да, той самой «улучшенной» GPL, которая особенно усложняет жизнь SaaS-провайдерам. Покупка коммерческой лицензии MinIO сейчас — задача не из простых.
Поддерживаемость для нас — это в первую очередь технологический мэтч: более-менее мейнстримный стек технологий (ЯП, развёртывание), поддерживаемый платформой разработки (java/kotlin, golang, C++ или C) внутри MWS. Это чтобы можно было без боли нанимать специалистов, при необходимости форкать, патчить, собирать. Поэтому вся экзотика вроде Erlang, Node.js или Rust (прости, Rust, не в этот раз) была исключена из рассмотрения.
Теоретически можно было бы подумать про Swift — S3 из OpenStack. Но он, как минимум, на Python, который мы используем, но не для highload-бэкендов. А если копнуть глубже, то у проекта есть проблемы с масштабированием, и он никогда не претендовал на полноценную реализацию S3 API.
Если у вас есть свой кандидат — напишите в комментариях.
Поэтому вопрос стоял так — использовать Ceph RGW или делать своё? RGW — зрелое, надёжное решение с богатой функциональностью.
Ceph RGW (RADOS Gateway)
Ceph RGW (RADOS Gateway) — это компонент распределённой системы хранения Ceph, предоставляющий объектный интерфейс для работы с данными по Amazon S3-совместимому протоколу.
Более того, в инфраструктуре МТС уже были инсталляции Ceph в разных конфигурациях: с репликацией между ДЦ и без, большие и маленькие — то есть, у нас уже был накопленный опыт эксплуатации.
Ceph RGW — хорошая технология для своих юзкейсов (об этом ниже), и в хороших руках на ней можно сделать много. Но в наших сценариях мы столкнулись с рядом проблем, о которых расскажем ниже.
Эти проблемы не критические или их невозможно решить, но заставляют задумываться в тех местах, где в публичном object storage решения должны быть заложены в ДНК архитектуры.
Деконструируем Ceph
Масштабирование
Несмотря на то, что в мире существуют инсталляции Ceph размером в 10 тысяч OSD (демонов хранения; а один демон хранения, напомним, примерно соответствует одному физическому диску), на практике уже при размере кластера свыше 1000 OSD (которые держат full mesh по связности) возрастает риск отказов из-за увеличивающегося объёма пиринговой информации между узлами и вероятности выхода из строя одного узла, а вслед за ними растёт вероятность каскадных отказов и скорости восстановления после них (буквальная иллюстрация термина «чем больше шкаф, тем громче падает»).

Поэтому для большей надёжности ceph-кластеров в рамках одного виртуально-бесконечного сервиса объектного хранилища со временем будет несколько. Но проблема в том, что с точки зрения RGW — даже в multisite-конфигурации — это отдельные кластеры с отдельными экземплярами RGW. А чтобы превратить всё это в единый консистентный сервис с адресом вида storage.mws.ru, который выглядит как один S3 API в регионе с 2–3 дата-центрами и несколькими (2+) Ceph-кластерами (и RGW-инстансами) внутри, нужен умный stateful-фронтенд — фасад, умеющий всё это собирать в единое целое. Дальше мы поговорим о сложностях, с которыми столкнулись, эксплуатируя multisite-конфигурации RGW.
Disclaimer: описанные ниже проблемы могут быть решены в новых версиях Ceph — your mileage may vary. Но нам нужно было принимать решение в конкретной точке времени, и ниже мы ещё поговорим про возможность понимать и направлять развитие апстрима.
Многозональные конфигурации
Начнём с очевидного — как устроена multisite-конфигурация. Есть realm — объект, описывающий весь сетап, в нём живут zone groups и zones. Зоны бывают master и secondary, но ещё active или passive (readonly), при этом secondary может быть как active, так и passive. У realm есть period, который описывает конкретную версию конфигурации realm. (К слову, обновление period ведёт к кратковременному даунтайму, пока зоны не получат обновлённый period.)
С зонами внутри zone group всё более-менее понятно — все данные реплицируются между зонами одной группы, и таким образом мы получаем отказоустойчивость. На самом деле в рамках zone group репликация может быть сделана как угодно гибко — вплоть до object-level настроек. Однако в публичных S3 принято, чтобы сервис продолжал работать как будто ничего не произошло при выходе из строя одной из зон доступности. Поэтому, например, если дата-центра всего два, то иначе чем полной репликацией данных между зонами такого не добиться. С тремя ДЦ становится интереснее, но всё равно гибкость схем репликации не нужна — нужна одна конкретная схема, которая будет применяться для всех бакетов всех пользователей.
Zone group в Ceph RGW — это логическая единица, которая исторически играет роль «региона». Она не реплицирует данные с другими zone groups, но всё равно сохраняет уникальность имён бакетов и пользователей внутри всей системы (realm). Это делает zone group удобным логическим эквивалентом «региона». Но в эту схему плохо вписывается необходимость пошардировать ceph'ы в рамках одного «региона», поскольку один бакет может быть только в одной zone group, то есть объём данных в бакете ограничивается одним ceph-кластером.
Хотя кажется, что один кластер способен вместить огромное количество данных (петабайты), в публичных облаках количество бакетов и клиентов велико. Невозможно заранее предсказать, какие из них вырастут до таких объёмов. Рано или поздно может случиться ситуация, когда на одном кластере появляется несколько крупных бакетов и, даже если технически в инсталляции хватает ресурсов, будут конкретные пользователи и бакеты, которым не повезло и для них место кончится. И из этой ситуации нет простого выхода средствами обычного Ceph — придётся долго и мучительно расселять или двигать бакеты между кластерами или просить пользователей завести несколько бакетов, что может сильно влиять на логику приложений, которые привыкли работать с одним. Или всё-таки растить физически объём конкретного кластера Ceph, но тогда мы возвращаемся к проблеме стабильности больших кластеров.
Избыточность хранения
Отдельно стоит поговорить и про избыточность хранения данных, и как архитектура Ceph rgw это ограничивает.
Сначала давайте посмотрим на целевые показатели. Если мы хотим, чтобы наши данные переживали выход из строя одного из 3 дата-центров, избыточность хранения не может быть меньше чем x1,5 (теряется ⅓ данных, остаётся x1 — как будто просто одна копия). Но поскольку мы всё ещё должны уметь переживать выход из строя отдельных дисков и серверов в 2 работающих ДЦ, на практике избыточность делается чуть больше. Например, AWS говорит, что хранит данные в 3 AZ с избыточностью 1,8 (EC 9 + 5 с дополнительным поселением части кусочков в разные AZ).
Возвращаемся в Ceph. Если помните, то данные реплицируются между зонами одной группы, а это значит, что избыточность данных не может быть меньше числа зон в группе. Если мы посмотрим типичное облако, то в одном регионе обычно 3 зоны доступности. При накладывании этого на принятый в multisite rgw подход получается, что избыточность данных не может быть меньше 3. Больше трёх она будет, потому что каждый кластер поддерживает корректность данных внутри себя независимо и требует дополнительной избыточности. На практике это выглядит так, что каждая зона в группе хранит данные с erasure coding (дефолтное — 4 + 2, но можно и 10 + 2 или любую другую схему). Для двух ДЦ избыточность хранения будет x3 в первом случае и x2,4 во втором. Если такое масштабировать в 3 ДЦ, то получается 4,5 или 3,6 соответственно (Карл) — в 2+ раза хуже, чем у AWS. Есть два выхода из ситуации: растянуть Ceph на несколько дата-центров или делать «шашечные» группы зон, где каждая группа живёт и реплицируется в 2 из 3 ДЦ. Делать растянутый на несколько ДЦ Ceph всё ещё считается challenging-занятием с точки зрения эксплуатации. Шашечки же всё равно не дают возможность сделать избыточность меньше двух, но ещё усложняют равномерную утилизацию железа.
В завершение темы сложностей с multisite скажем, что с репликацией нам тоже не повезло попасть на один неприятный баг rgw, при котором часть объектов не реплицировалась между зонами доступности из-за orphaned-шардов лога репликации (об этом мы, может быть, расскажем чуть позже в отдельной статье).
Большие бакеты
Вторая проблема — это количество объектов в бакетах. Публичные S3 никак не ограничивают их количество и строят архитектуру так, чтобы хранение метаданных об объектах в бакете было горизонтально масштабируемо с минимальной деградацией. Ceph хранит индексы бакетов в отдельных объектах, ему можно сконфигурировать количество шардов. Однако решардирование происходит с продолжительной недоступностью на запись (Ceph v17). Альтернатива — сразу задавать такое количество шардов для каждого бакета, чтобы «точно хватило всем», но всё равно бывают те, кому не хватает :) Автомасштабирование bucket index'ов в более новых версиях Сeph есть, но на момент написания статьи фича всё ещё слишком свежая.
Delete-штормы
Есть в S3 одна базовая batch-ручка — delete, которая позволяет посылать в удаление по 1000 объектов за раз. И есть клиенты, которые любят ночью запустить удаление миллионов объектов таким образом. Для метаданных объектов в Сeph, а точнее — для операций в RocksDB (LSM-based базе данных), это значит большой рейт операций обновления объектов (тех же bucket-index'ов).
Для LSM-баз жизни не бывает без регулярных compaction — операций переноса данных между слоями хранения, которые сильно грузят диск и CPU на bookkeeping-активности. На практике удаление миллионов объектов приводило к распуханию RocksDB и её деградации, а следующий compaction запускался настолько долго и сложно, что OSD-демоны не успевали восстанавливаться за отведённое на старт время. И конечно, всё это происходило каскадно. LSM-compaction знаменит тем, что очень много и часто рандомно обращается к диску, что максимально противопоказано HDD-дискам. В худшие дни OSD в момент compaction'ов совсем переставали отвечать на любые запросы данных.
Да, в новых версиях поведение compaction дотюнили, плюс помогло то, что знает каждый, кто имел с цефом дело, — перенос метабаз на ssd🙂. Но осадочек остался.
Немного теории
Целеполагание и архитектура
проект, который люди разворачивают в своих on-prem инсталляциях как основу своей инфраструктуры хранения.
В такой модели важно, чтобы система:
разумно конфигурировалась «из коробки»;
легко разворачивалась;
не потребляла много ресурсов вхолостую;
имела минимум внешних зависимостей (например, хранит всё в себе).
Обычно когда в on-prem инсталляции Ceph возникает потребность добавить ресурсов или подключить большого клиента, это решается просто — поднимается новый кластер Ceph и RGW под конкретную нагрузку.
В публичном облаке всё строго наоборот. Для клиента есть один общий сервис object storage (или по одному на регион), в котором можно хранить виртуально бесконечное количество данных. Количество «инсталляций» такого сервиса можно пересчитать по пальцам — не считая стендов для разработки. При таком подходе не слишком важно, один или несколько (десятков) микросервисов в приложении, и есть ли дополнительные технологии хранения — особенно если в компании есть специалисты, которые умеют с ними работать.
Как пример, Сeph RGW хранит в Сeph всё, включая метаданные и, например, очередь репликации. А Ceph by design — это KV-хранилище с довольно ограниченной транзакционной семантикой. Да, это удобно в смысле, что «всё в себе», но это значит, что поверх механизма расширения Ceph'a (extclass) были написаны базовая FIFO queue и какое-то подобие транзакционной базы данных со связями объектов.
Получается много сложного кода, в котором пришлось решать (и где-то до сих пор не решить) массу проблем — консистентность, масштабирование, которые были преодолены в других специализированных для этого системах: тех же RDBMS (или NewSQL) и очередях сообщения (Kafka). И там, где для on-prem решения Kafka — это лишняя зависимость, сложность развёртывания, увеличение футпринта ресурсов, усложнение порога входа и т. п., для большого SaaS-сервиса — это right tool for the right job — для очереди меж-ДЦ репликации на миллионы объектов.
Если вы следите за индустрией, то могли увидеть доклады коллег из других компаний, которые тоже писали своё S3-совместимое хранилище. И почти всегда их ключевая задача — построение масштабируемого и поддерживаемого слоя управления метаданными. Потому что слой хранения blob'ов (файлов) масштабируется и шардируется довольно просто. Всё упирается в метаслой — он задаёт пределы системе.

Contribution challenges
Ceph — это зрелый open source проект с историей и сообществом. Из пунктов выше, надеюсь, становится понятно, что для того, чтобы эффективно жить на кодовой базе Ceph, потребовалось бы делать много довольно фундаментальных изменений (не столько самого Сeph, сколько RGW, который по объёму кода уже не уступает core Ceph). Не просто «поработать с кодом», а годы работы, общения, ревью без чётких сроков и гарантий принятия изменений. А значительные изменения в апстриме большого open source проекта — это человеко-годы работы, причём скорее общения с людьми, чем написания кода, с неопределённой длиной пинга и негарантированным результатом. И дело не в zeitgeist, плохих процессах или личных качествах мейнтейнеров, а в том, что цель Сeph RGW, как проекта (основным мейнтейнером которого является Red Hat), — быть хорошим S3 для on-prem инсталляций, а не основой для мультитенантного SaaS-решения. А это, как говорилось выше, две большие разницы и разное целеполагание, ведущее к разной архитектуре.
Если же говорить непосредственно про код, то, пожалуй, лучшее слово для описания кодовой базы Ceph — фрагментированная. Весь код в дереве, который не участвует в сборке основных дистрибуций, может находиться в любом виде: «вообще не собирается», «не работает», «падает», «делает нерабочим всё остальное» и «вроде работает». Чаще всего причина в том, что код так и не вышел из фазы эксперимента и устарел, но узнать это можно разве что из переписки с мейнтейнерами, где пинги легко могут измеряться неделями (где-то в этот момент заплакал котёнок, привыкший к прозрачности Kubernetes Enhancement Process 🐱).
Поэтому если и строить своё решение на основе Ceph RGW, то с полным осознанием, что это будет форк, который со временем сильно разойдётся с апстримом. И тогда ответ на вопрос: что дешевле — дорабатывать Ceph или написать своё с нуля — уже не так очевиден.
Добавим к этому ещё:
Ceph написан на C++. Это язык со сложной судьбой, на котором скорость разработки в среднем ниже, чем, например, на Golang. Рынок C++-разработчиков для бэкенда устроен несколько сложнее, чем для более мейнстримных языков бэкенд-разработки.
Обширная и наиболее «живая» с точки зрения эволюции кодовой базы часть объектного хранилища — это вполне классический веб-сервис: в БД сходи, в проверку прав сходи, в очередь положи, и так далее. В таких задачах Golang показывает себя в текущий момент времени лучше всего: close-to-native перфоманс сетевого стека, незаметная и удобная работа с сетью благодаря горутинному движку, и минималистичный язык, который заставляет писать просто и в едином стиле.
Что мы решили
В завершение — назовём своими именами то, как мы в итоге реализуем объектное хранилище. Хотя, возможно, внимательный читатель уже всё понял.
Если кратко, наш S3 — это набор сервисов на Golang, которые:
хранят метаданные объектов и бакетов в PostgreSQL;
используют Kafka для фоновых процессов (репликация между ДЦ, GC удалённых объектов, lifecycle management);
хранят сами объекты в Ceph.
Главное достоинство такой архитектуры — модульность и независимое масштабирование компонентов. Любую часть можно отделить, заменить, увеличить в объёме без затрагивания остальных. Единственное нетривиально масштабируемое место — это метабаза в PostgreSQL, поскольку она:
обеспечивает транзакционность работы с объектами;
реализует read-after-write семантику;
должна масштабироваться условно бесконечно по числу сущностей (например, объектов в бакете).
Остальные компоненты масштабируются гораздо проще:
Ceph-кластеры можно добавлять независимо, как для масштабирования, так и для экспериментов — снаружи это не будет заметно.
Kafka отлично растёт за счёт скейлинга числа партиций.
Application layer можно масштабировать на простейших механизмах горизонтального скейлинга в Kubernetes.
Что касается выбора PostgreSQL для метабазы, то мы руководствовались двумя соображениями:
Во-первых, мы точно знаем, что это работает. Вероятно, крупнейшее object storage в России построено на PostgreSQL.
Во-вторых, Postgres — core-технология для нас. Это основная СУБД всех сервисов облака. Управляемый PostgreSQL — часть гигиенического минимума любой облачной платформы данных.
У нас уже есть и будет устойчивая команда экспертов по этой технологии. Поэтому даже если придётся самостоятельно реализовывать, скажем, умное шардирование данных о бакетах — синергия от Postgres everywhere даст больше эффективности, чем развёртывание и сопровождение, например, CockroachDB, с которым нам пришлось бы разбираться с нуля, и вряд ли удалось бы быстро монетизировать экспертизу.
Ceph мы используем исключительно как хранилище блобов, в схеме независимых кластеров по ДЦ с репликацией на уровне приложения. Это позволило быстро запустить продукт, но мы уже смотрим на более эффективные схемы избыточности и более экономичное решение с точки зрения потребления CPU на обслуживание единицы места хранения.
На этом, пожалуй, поставим пока точку (но с запятой) — рассказ и так получился объёмным.
В одной из следующих статей мы расскажем уже более детально про то, как всё устроено, в какие характерные показатели производительности мы целимся в такой архитектуре, и что мы сделали, чтобы их получать. Расскажем, как оптимизируем и шардируем метабазу, как работаем с Kafka и Ceph и с какими узкими местами боролись на пути к желаемым характеристикам.
До скорых встреч!
Зачем мы строим собственное публичное облако? Рассказывает CTO MWS Данила Дюгуров.
Реалити-проект для инженеров про разработку облака. Рассказываем про архитектуру сервисов платформы ещё до релиза.
Подкаст "Расскажите про MWS". Рассказываем про команду новой облачной платформы MWS.