Виртуальные диски MWS Cloud Platform: храним данные легко и блочно

от автора

Мы все привыкли, что виртуальные машины в облаках запускаются быстро и легко с любой ОС на наш выбор и без проблем мигрируют между физическими серверами. Живя в облачной инфраструктуре, мы практически забыли и том, что данные на дисках ВМ могут испортиться сами по себе — только если мы сами к этому приложим руку. А если данные надо восстановить — это занимает считанные секунды или минуты. Но за этим удобством скрывается одна из самых сложных и интересных задач: надёжное и безопасное хранение данных, при этом обеспечивающее минимальные задержки чтения и записи.

Меня зовут Алексей Баранов, я — руководитель направления Compute в MWS Cloud Platform, и в этой статье я расскажу, как строим блочное хранилище в нашем облаке, с какими вызовами встречаемся и как их решаем.

Как облако создаёт диски виртуальных машин

Архитектура нашего облака включает два основных типа серверов. На одних запускаются виртуальные машины пользователей — Compute, на других работают SDS (Software Defined Storage), где физически хранятся данные.

Между собой они связаны высокоскоростной сетью ЦОДа, обеспечивающей стабильный обмен данными с минимальными задержками.

Такую архитектуру называют конвергентной: сервера выполняющие разные роли могут иметь разные конфигурации. В рамках одной роли железо стараются унифицировать чтобы сократить накладные расходы на эксплуатацию.

Есть и другой подход — гиперконвергентный, при котором стремятся унифицировать все железо. У него есть свои плюсы и минусы, но в нашем случае разделение ролей даёт более предсказуемую производительность и удобство эксплуатации.

Давайте посмотрим, что происходит, когда нужно запустить виртуальную машину.

Всё начинается с Compute Control Plane — оркестратора, который управляет жизненным циклом ВМ. Он обращается к другим сервисам, в том числе:

  • к VPC, чтобы создать внутренний адрес, прописать DNS-запись и при необходимости публичный IP;

  • к Storage Control Plane, чтобы запросить создание одного или нескольких дисков.

Проще говоря, Compute координирует работу всех сервисов: сеть, хранилище, шифрование и выполняет оркестрацию процесса создания виртуальной машины, подробнее о сервисе можно почитать в этой статье.

Сервис Storage выполняет внутренние операции, подготавливает виртуальный диск и сообщает Compute, что он готов к подключению.

Связь между виртуальной машиной и хранилищем организует специальный агент на хосте Compute, который взаимодействует с SDS и обеспечивает обмен данными на уровне блоков.

Перед тем как диск будет подключён, Storage должен обратиться к KMS (Key Management Service), чтобы получить материал для ключа шифрования. На нашей платформе каждый диск шифруется индивидуальным ключом, и этот ключ никогда не покидает доверенную среду в открытом виде. После подготовки ключ передаётся на агент, чтобы тот мог шифровать и расшифровывать данные на лету.

После того как все компоненты готовы, Compute запускает виртуальную машину — с подключённым, зашифрованным и полностью готовым к работе диском.

Бэкапы, образы, снэпшоты

Чтобы виртуальная машина могла запуститься, на одном из дисков должна быть операционная система. Другими словами, нужны данные, которые будут записаны ещё до старта виртуальной машины. Можно выделить несколько сценариев, в которых диски создаются непустыми.

1. Развёртывание

Платформа предоставляет готовые образы популярных операционных систем — например, Ubuntu, Debian или CentOS. Это позволяет пользователю быстро развернуть ВМ без установки ОС и глубокой настройки. Со временем продвинутые пользователи начинают создавать собственные образы с предустановленным ПО и настройками под конкретные задачи. Бывает, что используют развёртывание сервисов через Infrastructure as a Code с помощью образов виртуальных машин — то же самое, что делается в контейнерном мире, в Kubernetes можно делать и на уровне облачного IaaS, и многие так и поступают.

2. Бэкапы

Если с данными что-то случается, важно иметь возможность восстановиться из предыдущего состояния. Для этого существуют снимки или бэкапы диска — когда состояние диска сохраняется почти мгновенно, без заметного простоя виртуальной машины. Эти снимки позволяют вернуться к прошлой версии данных буквально за секунды.

3. Большие объёмы данных с высокой скоростью доступа

Рассмотрим, например, запуск распределённого обучения, когда нескольким инстансам тренирующих ВМ нужно предоставить доступ до большого датасета. Можно обойтись объектным хранилищем или сетевой файловой системой (если она есть), но блочное хранилище даст наилучшую скорость доступа к данным.

В нашем облаке (да и не в нашем часто ситуация аналогичная) мы решаем эти задачи с помощью двух сущностей — образов дисков и бэкапов дисков, или снэпшотов. Под капотом и образы, и бэкапы работают на общих механизмах, просто имеют разные сценарии использования. Ключевое отличие снэпшотов — для одного диска они инкрементальные. Первый снимок диска содержит полную копию данных, а все последующие хранят только дельты — изменения, произошедшие с момента последнего снимка.

Если данные на диске не изменились, новый снэпшот практически не занимает места. Это делает хранение снимков экономичным и быстрым, особенно при регулярных бэкапах.

Что касается образов, пользователи могут загружать их в разных форматах, например QCOW2, VMDK или RAW. 

Формат, в котором пользователь импортирует образ, не влияет на то, как он хранится и используется внутри блочного хранилища: система конвертирует его во внутреннее представление, оптимизированное под эффективность хранения.

Что влияет на скорость создания диска

Казалось бы, двадцати образов самых популярных операционных систем вполне достаточно. Но каждый из них нужно регулярно обновлять — примерно раз в неделю, когда выходят новые версии и патчи безопасности. Добавьте сюда пользовательские образы и регулярные бэкапы, и со временем в системе накапливаются терабайты данных.

Поэтому первое, о чём стоит подумать при проектировании хранилища, — эффективность хранения. Нужно экономить место, но при этом обеспечивать быстрый доступ к любому образу и возможность мгновенно создать из него новый диск.

Однако эффективность — лишь половина задачи. Другая половина — скорость создания диска, от которой напрямую зависит время запуска виртуальной машины.

На подготовку сетевых адресов уходят секунды, а вот создание дисков часто становится самым долгим этапом запуска ВМ. Чем больше объём контента, который должен оказаться на диске, тем дольше может быть этот процесс. Чтобы сократить задержку, применяются разные методы оптимизации, например, prefetching и warm-up — предварительная подготовка образов и кеширование часто используемых блоков. Благодаря этому новые ВМ могут создаваться значительно быстрее.

Иногда образы и снэпшоты хранят непосредственно в SDS, использующейся для обслуживания дискового IO, чтобы сократить путь до данных. Но у такого подхода есть несколько недостатков: SDS обычно зональная, тогда как образы и снапшоты должны быть доступны во всех регионах облака. Хранение независимых копий в каждой зоне доступности возможно, но значительно повышает стоимость хранения. Плюс устройство SDS для блочных устройств часто заточено больше на производительность IO-операций, чем на эффективность хранения. Тем не менее такой подход тоже имеет место, когда скорость восстановления важнее стоимости хранения.

В индустрии сложились несколько подходов к задаче ускорения «наливания» дисков:

  • Lazy Init — диск создаётся мгновенно, но данные подтягиваются по мере обращения. Так делают, например, в AWS. Минус — требуется «прогрев», иначе первые операции ввода-вывода будут заметно медленнее.

  • Eager Init — данные полностью копируются из холодного хранилища в SDS перед запуском. Дольше создаётся, но работает предсказуемо и стабильно с первого обращения.

  • Instant Snapshots — данные уже находятся в SDS, поддерживающей copy-on-write. Диск создаётся почти мгновенно, но такие снэпшоты живут только в одной зоне доступности и при этом могут стоить дополнительных денег..

Между эффективностью хранения и скоростью создания всегда нужно искать баланс. И чем сложнее система, тем труднее его удержать.

К этому добавляется ещё один фактор — шифрование. Когда все диски зашифрованы, встают дополнительные вопросы: зашифрованы ли исходные образы, нужно ли использовать тот же ключ, как обработать инкрементальные снапшоты с разными ключами и прочее. Каждый из этих нюансов увеличивает время подготовки и усложняет модель взаимодействия между сервисами.

Пока мы выбрали относительно простой и надёжный путь: все образы и снэпшоты хранятся в S3-совместимом объектном хранилище. Это решает задачу высокой доступности, ведь Object Storage развёрнуто во всех зонах доступности. В этой статье рассказали, как устроено Object Storage в MWS Cloud Platform. 

Перед сохранением RAW-образы сжимаются и загружаются в S3, а при создании диска система просто перекачивает нужные данные из S3 в SDS. Реализация управляемого prefetch или warm-up уже есть в планах.

Представим, что в облаке одновременно запускаются сто виртуальных машин, каждая с 50-гигабайтным диском с данными. Чтобы запустить их «в лоб», нужно перелить около 5 терабайт данных из одного хранилища в другое и сделать это так, чтобы они не мешали друг другу и не перегрузили сеть.

Именно такие сценарии проверяют архитектуру на прочность и показывают, насколько эффективно выстроены внутренние процессы блочного хранилища. Как видите, мы ещё не дошли непосредственно до работы ВМ с дисками, а уже обсуждаем сервисы, которые должны уметь эффективно работать с высокими неравномерными нагрузками. И не только на низком уровне, но и в среде «классических» веб-сервисов, которые занимаются подготовкой данных для виртуальных дисков.

Как диск подключается к виртуальной машине

Теперь разберёмся, как диск подключается к виртуальной машине и что происходит под капотом.

Важно понимать, что ВМ не имеет доступа ни к хостовой сети, чтобы напрямую обращаться в SDS, ни к физическим устройствам хоста, чтобы работать с локальными дисками. Для взаимодействия с хранилищем ей нужны виртуальные устройства и виртуальные драйверы.

Если бы речь шла о классической «полной» виртуализации, нам потребовался бы драйвер, полностью эмулирующий физическое устройство. Но такой подход сегодня практически не используется — он слишком тяжёлый по накладным расходам.

Мы используем QEMU, который реализует паравиртуализацию. Это значит, что операционная система внутри ВМ знает, что она работает в виртуальной среде, и имеет специальные драйверы для взаимодействия с виртуализированными устройствами.

Когда виртуальная машина хочет выполнить операцию ввода-вывода, она договаривается с хостом об области общей памяти (shared memory), через которую и передаются запросы.

Virtio — стандарт паравиртуализации

Самый распространённый паравиртуализированный драйвер в Linux — virtio. У него есть разные реализации:

  • Virtio-net — для сетевых интерфейсов;

  • Virtio-blk и Virtio-scsi — для блочных устройств.

Мы выбрали Virtio-scsi. Он немного медленнее, чем virtio-blk, но зато надёжнее при управлении устройствами: SCSI-диск можно «отцепить» или «подцепить» без активного участия гостевой ОС, поэтому даже если внутри ВМ что-то пошло не так, диски всё равно можно подключать и отключать. Это особенно важно при автоматизации и работе CSI-драйверов в Kubernetes.

Эволюция Virtio

В классической схеме Virtio виртуальная машина обменивается данными с внешним миром через гипервизор QEMU: он выступает посредником, с которым ВМ делит общую память и через которого идут все операции ввода-вывода. Это просто и удобно, но неэффективно: QEMU становится лишним хопом, через который проходят все запросы, а его потоки ввода-вывода (I/O threads) нужно задавать заранее, что мешает гибко масштабировать производительность.

Чтобы ускорить работу, появилась схема vhost. Внутри виртуальной машины ничего не меняется — те же virtio-драйверы, но общение идёт не с QEMU, а напрямую с ядром хостовой операционной системы. ВМ и ядро обмениваются данными через разделённую память и специальные дескрипторы.

Плюс — мы избавились от посредника QEMU. Минус — теперь в процессе участвует ядро и, чтобы подключить собственную систему хранения, нужно писать модуль ядра — сложный и менее безопасный путь.

Следующим шагом стала архитектура vhost-user. Здесь принцип тот же, но вместо ядра используется отдельный пользовательский процесс на хосте — vhost-backend.

ВМ и vhost-backend обмениваются управляющими сигналами через unix-socket, а данные ходят через общую память.

Так мы получаем лучшее из двух миров:

  • QEMU не участвует в каждом I/O-запросе;

  • ядро остаётся в стороне, а значит, нет риска паник и проблем с обновлением модулей.

При этом всё работает в user space, что делает систему более гибкой, безопасной и управляемой.

Как устроены наши диски

Чтобы виртуальная машина могла выполнять операции чтения и записи, на стороне хоста нужен компонент, который будет принимать IO-запросы и передавать их в систему хранения. Эту роль выполняет vhost-демон, и здесь нам очень помог проект SPDK (Storage Performance Development Kit) — набор высокопроизводительных библиотек и компонентов для организации ввода-вывода в пользовательском пространстве. Кстати, писали статью о SPDK, почитать можно по ссылке.

На каждом хосте, где работают виртуальные машины, запущен vhost-демон на основе SPDK и управляющий агент. «Вход» для дискового ввода/вывода — обработчик в vhost-демоне, который реализует SCSI-протокол поверх vhost-user, с которым гостевая ОС взаимодействует через общую память.

Из виртуального SCSI-контроллера IO-операции попадают в единый для всех типов дисков конвейер обработки, о котором поговорим дальше, и в конце — на конкретный backend хранения (например, SDS или локальные SSD).

Конвейер обработки IO-операций виртуальной машины

Есть две важные задачи, которые мы хотим единообразно решать для всех дисков.

1. Quality of Service (QoS)

Ни одна виртуальная машина не должна быть способна «выбить» хранилище за пределы допустимой нагрузки.

Ограничения выставляются на двух уровнях:

  • по ВМ — лимиты зависят от числа vCPU и характеристик виртуальной машины;

  • по диску — например, максимальное количество IOPS может определяться размером тома или его классом производительности.

QoS защищает SDS от перегрузки и обеспечивает предсказуемость: даже если на одном хосте десятки машин одновременно генерируют нагрузку, их операции не мешают друг другу.

2. Шифрование (Encryption)

Мы стремимся шифровать данные как можно ближе к виртуальной машине. Это значит, что агент шифрует блоки до того, как они покидают хост — ещё до отправки в сеть дата-центра. Так данные никогда не путешествуют в открытом виде и лучше защищены от редких, но теоретически возможных исключительных ситуаций.

Почему управляющая логика вынесена в отдельный агент

SPDK написан на C, и, хотя он очень производителен, работать с ним нужно аккуратно. Он не предназначен для того, чтобы на нём строить сложную бизнес-логику. Поэтому мы поместили эту логику в отдельный агент на Go, который:

  • общается с Control Plane по HTTP;

  • конфигурирует vhost-демон под конкретные диски — создаёт SCSI-контроллеры, наполняет их таргетами — дисками виртуальных машин, настраивает QoS, шифрование и связь с хранилищами;

  • агрегирует метрики;

  • координирует передачу активных SCSI-контроллеров новой версии vhost-демона при релизах.

В такой схеме SPDK отвечает за скорость, а Go-агент — за управление, логику, конфигурацию и интеграцию с облаком.

Что пользователю нужно от дисков

Прежде чем говорить о классах производительности, важно вспомнить, какое вообще бывает железо.

  • HDD — традиционные «крутящиеся» диски. Latency записи у них — от 1 мс до 10+ мс, и это физическое ограничение механики: головку нужно переместить, пластину — прокрутить.

  • SSD — твердотельные накопители. Latency чтения начинается примерно от 80 мкс, запись — около 20 мкс. Разница по сравнению с HDD — на порядки.

  • Сетевая задержка внутри дата-центра менее 100 мкс для обычных сетей и менее 10 мкс для RDMA/InfiniBand/ROCE-класса.

Эти цифры важны, потому что облачное блочное хранилище всегда работает через сеть. И если вы хотите дать пользователю «почти SSD-скорость», вам нужно учитывать не только диски, но и сетевой стек, протоколы, виртуализацию и особенности SDS.

Классы производительности, которые мы выделили

Мы пришли к трём классам, которые покрывают разные сценарии использования.

Non-critical latency (~ 1–8 мс)

Это самый универсальный класс. Сюда попадает:

  • большинство сценариев работы операционной системы;

  • базы данных, где важнее объём, чем минимальная задержка;

  • файловые системы и стандартные сервисные нагрузки.

Задержка в единицы миллисекунд тут некритична. Мы уже реализовали этот класс на основе Ceph, он даёт хорошую надёжность, масштабируемость и предсказуемую производительность.

Latency-critical (< 500 мкс)

Этот класс нужен системам, где важна минимальная задержка одиночных операций:

  • журналы транзакций в реляционных СУБД;

  • очереди сообщений;

  • сервисы, чувствительные к tail-latency.

Требование здесь простое: latency должна укладываться в сотни микросекунд, желательно 200–300 мкс.

Готовых решений на рынке практически нет: Ceph слишком тяжёл для таких задержек, а проприетарные решения облачных гигантов недоступны. Поэтому мы разрабатываем собственную SDS, ориентированную на низкую задержку и стабильный tail-latency.

Bare SSD

Максимальная возможная скорость, практически «как у железа». Используется в сценариях:

  • CI — сборка и компиляция больших проектов;

  • ML-нагрузки — быстрый доступ к данным обучения или кешам для инференса;

  • Распределенные системы хранения со сверхвысокими требованиями по зедержкам IO, которые сами решают задачи избыточности хранения;

Здесь допускается меньшая надёжность, потому что главное в таких сценариях — скорость. Такие диски у нас тоже есть — они становятся хорошим дополнением к вычислительным VM с GPU или большим числом vCPU. Локальные диски также используются сервисами управляемых баз данных, потому что избыточность хранения там реализована на уровне высокодоступных конфигураций самих баз данных.

Как построить Storage мечты

Прежде чем разбирать архитектуру, посмотрим, чем в целом занимается любая современная SDS.

Основные задачи системы хранения данных

Репликация

Данные должны храниться в нескольких экземплярах, чтобы выход из строя одного или даже нескольких дисков не приводил к потере информации. Есть разные схемы:

  • Полная репликация x3 — три независимые копии данных. Избыточность 3×, высокая устойчивость к отказам и минимальная сложность записи.

  • Erasure Coding (например, 4+2) — данные делятся на 4 части + 2 части избыточности, размещаются на 6 разных узлах. Избыточность 1,5×, экономия места, но сложнее запись и восстановление.

Несмотря на экономичность кодов избыточности, высокопроизводительные SDS часто используют полную репликацию. Она проще, стабильнее под нагрузкой и лучше переносит деградированные состояния, когда узлы или диски временно недоступны.

Балансировка нагрузки

Кластер хранения — это десятки или сотни серверов, на каждом — множество дисков. Если данные распределены неравномерно, возникают проблемы:

  • одни диски изнашиваются быстрее;

  • другие простаивают;

  • исчезает предсказуемость в latency и пропускной способности.

Поэтому SDS постоянно перераспределяет данные между дисками, чтобы нагрузка была ровной.

Scrubbing (проверка целостности)

Данные физически «стареют»: битовые ошибки, деградация носителей, нарушение целостности. Scrubbing — это периодическая проверка, когда SDS:

  • читает данные;

  • сверяет их с контрольными суммами;

  • при необходимости выполняет восстановление.

Для больших объёмов хранения scrubbing может давать существенную нагрузку, но это фундаментальный компонент надёжности.

Принципы дизайна нашей SDS

Метаданные в хранилище на основе консенсуса (Paxos/Raft)

Метаданные — это «единый источник истины» о том, где данные каждого виртуального диска, какие версии актуальны, как вести восстановление и репликацию. Для этого подходят системы со строгой консистентностью, основанные на консенсусе:

  • Paxos (например в Ceph Mon);

  • Raft (например, etcd).

Это обеспечивает ACID-семантику и предсказуемость поведения кластера.

Разделение Control plane и Data plane

Максимальная производительность требуется только при обработке дискового IO, а, например, от системы управления кластером требуется корректность и удобство разработки и эксплуатации. Поэтому Control Plane и Data Plane реализуются на разных технологиях — Golang и C++ соответственно.

Репликация на уровне клиента

Классически (в том же Ceph) клиент отправляет данные на один из узлов SDS, который берёт на себя роль «лидера», рассылая данные на несколько других узлов для репликации. Это упрощает жизнь клиента, но добавляет +1 сетевой RTT в любую IO-операцию. «Толстый» клиент может сам управлять репликацией, параллельно отправляя данные в несколько узлов хранения, убирая лишние сетевые задержки. При этом увеличивается требуемая “толщина” канала от серверов compute в сторону SDS, но к сожалению физику не обмануть и нужно выбирать одно из двух зол, и для разных профилей производительности дисков решение будет разным.

Максимально использовать kernel-bypass

Мы хотим минимизировать переходы в ядро при работе с сетью и дисками. Для этого используется:

  • SPDK — kernel bypass для дискового ввода-вывода на NVMe-диски;

  • RDMA/ROCE — kernel bypass для сети. Если есть хорошая сеть в дата-центры, TCP тоже даёт приемлемые для целевых значений задержки, поэтому мы целимся как в использование RDMA, так и в TCP, но на основе linux uring.

В Ceph, к слову, тоже экспериментально реализованы эти подходы, но преимущества теряются на фоне других накладных расходов.

CRDT в Data Plane

CRDT (Conflict-Free Replicated Data Types) — структуры данных, которые позволяют разрешать конфликты репликации без централизованной координации. Виртуальный диск — это набор CDRT-регистров. Идея в том, что:

  • каждому блоку (группе блоков) присваивается монотонно растущая версия;

  • если есть конфликт (несколько копий блока на разных узлах), система выбирает самую «старшую» версию.

Восстановление даже активно  изменяющихся данных в такой системе может происходить в фоновом режиме и не влияет на производительность пользовательских операций

Redirect-Write

Данные и метаданные на диск всегда пишутся в новое место. Благодаря этому снапшоты получаются из коробки — достаточно не удалять предыдущие версии блоков, а атомарность достигается без сложных алгоритмов. Однако это требует реализации garbage collection. Но и плюсы огромны: мгновенные снапшоты и отсутствие порчи старых данных при сбоях и преимущественно последовательная запись на устройство. Ceph в значительной степени использует похожий принцип в BlueStore и ещё больше развивает этот подход в новом бэкенде Seastore.

И что в итоге

На первом этапе развития мы оптимизируем нашу SDS на сценарии быстрого блочного ввода-вывода — максимальную утилизацию дисков по iops/полосе и максимум один RTT и одну дисковую IO-операцию на один iops виртуального диска — без учёта репликации, конечно. При этом мы хотим сохранить все привычные для пользователей облачных дисков удобства — снэпшоты, гибкость управления размером и производительностью. Мы ищем баланс между производительностью конечной системы и гибкостью и скоростью разработки там, где это возможно.

Смотря на то, что получается, уже есть мысли, как система может быть адаптирована и под другие сценарии, помимо максимально быстрого дискового IO. Но про это мы расскажем в следующих сериях 🙂

А пока приходите тестировать наше облако MWS Cloud Platform, а обратной связью можно поделиться в сообществе MWS Cloud Platform — на все вопросы отвечает команда разработки.

ссылка на оригинал статьи https://habr.com/ru/articles/1053842/