Облачное хранилище: обновление API

Спешим сообщить новость: мы переписали API облачного хранилища. Теперь всё работает гораздо стабильнее и быстрее благодаря новой платформе — Hummingbird, которая по сути представляет собой реализацию некоторых компонентов OpenStack Swift на Go. О том, как мы внедряли Hummingbird и какие проблемы нам удалось решить с его помощью, мы расскажем в этой статье.

Модель объектного хранилища OpenStack Swift

Модель объектного хранилища OpenStack Swift включает в себя несколько интегрированных сущностей:

  • Прокси-сервер, который получает от конечного пользователя определенный набор данных, выполняет служебные запросы к прочим компонентам хранилища и, наконец, формирует и отправляет правильный ответ.
  • Серверы аккаунтов и контейнеров. Здесь мы объединяем эти два сервиса, т.к. они очень похожи по принципу работы – каждый из них сохраняет метаданные, а также списки контейнеров (сервер аккаунтов) и файлов (сервер контейнеров) в отдельных sqlite-базах и выдает эти данные по запросам.
  • Серверы объектов, которые, собственно, хранят пользовательские файлы (и метаданные уровня файлов в расширенных атрибутах). Это самый примитивный уровень абстракции – здесь существуют только объекты (в виде файлов), записываемые в определенные партиции.

Каждый из этих компонентов представляет собой отдельный демон. Все демоны объединены в кластер с помощью так называемого кольца — хэш-таблицы для определения места размещения объектов внутри кластера. Кольцо создается отдельным процессом (ring builder) и разносится по всем узлам кластера. Оно задаёт количество реплик объектов в кластере для обеспечения отказоустойчивости (обычно рекомендуется хранить три копии каждого объекта на разных серверах), количество партиций (внутренняя структура swift), распределение устройств по зонам и регионам. Также кольцо предоставляет список так называемых handoff-устройств, на которые будут заливаться данные в случае недоступности основных устройств.
Рассмотрим все компоненты хранилища более подробно.

Proxy

Первоначально мы использовали стандартный swift-proxy, затем, когда нагрузка увеличилась, а нашего собственного кода стало больше, — перевели все это на gevent и gunicorn, позже заменили gunicorn на uwsgi ввиду того, что последний лучше работает под большими нагрузками. Все эти решения были не особо эффективны, время ожидания, связанное с прокси, было достаточно большим и приходилось использовать все больше серверов для обработки авторизованного трафика, т.к. Python cам по себе работает очень медленно. В итоге весь этот трафик пришлось обрабатывать на 12 машинах (сейчас весь трафик — и публичный, и приватный, — обрабатывается всего на 3 серверах).

После всех этих паллиативных действий мы переписали прокси-сервер на go. В качестве основы был взят прототип из проекта Hummingbird, далее, мы дописали middleware, которые реализуют вcю нашу пользовательскую функциональность – это авторизация, квоты, прослойки для работы со статическими сайтами, символические ссылки, большие сегментированные объекты (динамические и статические), дополнительные домены, версионирование и т.д. Кроме того у нас реализованы отдельные эндпоинты для работы некоторых наших специальных функций – это настройка доменов, ssl-сертификатов, подпользователей. В качестве средства для формирования цепочек middleware мы используем justinas/alice, для хранения глобальных переменных в контексте запроса — gorilla/context.

Для отправки запросов к сервисам OpenStack Swift используется компонент directclient, имеющий полный доступ ко всем компонентам хранилища. Помимо прочего мы активно используем кэширование метаданных уровней аккаунта, контейнера и объекта. Эти метаданные будут включаться в контекст запроса; они нужны для принятия решения о дальнейшей его обработке. Чтобы не выполнять слишком много служебных запросов к хранилищу, мы держим эти данные в кэше memcache. Таким образом прокси-сервер получает запрос, формирует его контекст и пропускает через различные прослойки (middleware), одна из которых должна сказать:“Этот запрос — для меня!”. Именно эта прослойка обработает запрос и вернёт пользователю ответ.

Все неавторизованные запросы к хранилищу сначала пропускаются через кэширующий прокси, в качестве которого нами был выбран Apache Trafficserver.
Так как стандартные политики кэширования подразумевают довольно долгое нахождение объекта в кэше (иначе кэш бесполезен) — мы сделали отдельный демон для очистки кэша. Он принимает события PUT-запросов из прокси и очищает кэш для всех имён изменившегося объекта (у каждого объекта в хранилище есть как минимум 2 имени: userId.selcdn.ru и userId.selcdn.com; ещё пользователи могут прикреплять к контейнерам свои домены, для которых тоже требуется чистить кэш).

Аккаунты и контейнеры

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

С контейнерами дело обстоит по-другому. С технической точки зрения они представляют собой точно такие же sqlite-базы, однако эти базы содержат уже не крошечные списки контейнеров одного пользователя, а метаданные определенного контейнера и список файлов в нем. Некоторые из наших клиентов хранят в одном контейнере до 100 миллионов файлов. Запросы к таким большим sqlite-базам выполняются медленнее, и с репликацией (учитывая наличие асинхронных записей) дело обстоит гораздо сложнее.

Конечно, sqlite-хранилище для контейнеров можно было бы на что-то заменить — но на что? Мы делали тестовые варианты серверов аккаунтов и контейнеров на базе MongoDB и Cassandra, но все подобного рода решения, “завязанные” на централизованную базу вряд ли можно назвать удачными с точки зрения горизонтального масштабирования. Клиентов и файлов со временем становится всё больше, поэтому хранение данных в многочисленных маленьких базах выглядит предпочтительнее, чем использование одной здоровенной базы с миллиардами записей.

Не лишним было бы реализовать автоматический шардинг контейнеров: если бы появилась возможность разбивать огромные контейнеры на несколько sqlite-баз, было бы вообще отлично!
О шардинге подробнее можно прочитать здесь. Как видим, всё пока что в процессе.

Еще одной функцией, непосредственно связанной с сервером контейнеров, является ограничение срока хранения объектов (expiring). Посредством заголовков X-Delete-At, либо X-Delete-After можно задавать период времени, по истечении которого будет удален любой объект объектного хранилища. Эти заголовки могут равно передаваться как при создании объекта (PUT-запрос), так и при изменении метаданных оного (POST-запрос). Однако, нынешняя реализация данного процесса выглядит совсем не так хорошо, как хотелось бы. Дело в том, что первоначально эта фича реализовывалась так, чтобы внести как можно меньше исправлений в имеющуюся инфраструктуру OpenStack Swift. И здесь пошли по самому простому пути — адреса всех объектов с ограниченным сроком хранения решили помещать в специальный аккаунт “.expiring_objects” и периодически просматривать этот аккаунт с помощью отдельного демона под названием object-expirer. После этого у нас появилось две дополнительные проблемы:

  • Первая — служебный аккаунт. Теперь, когда мы делаем PUT/POST запрос к объекту с одним из указанных заголовков- на этом аккаунте создаются контейнеры, имя которых представляет собой временную метку (unix timestamp). В этих контейнерах создаются псевдообъекты с именем в виде временной метки и полного пути к соответствующему реальному объекту. Для этого используется специальная функция, которая обращается к серверу контейнеров и создаёт запись в базе; сервер объектов при этом не задействован вообще. Таким образом, активное использование функции ограничения срока хранения объектов в разы увеличивает нагрузку на сервер контейнеров.
  • Вторая связана с демоном object-expirer. Этот демон периодически проходит по огромному списку псевдообъектов, проверяя временные метки и отправляя запросы на удаление просроченных файлов. Главный его недостаток заключается в крайне низкой скорости работы. Из-за этого часто бывает так, что объект уже фактически удалён, но при этом все равно отображается в списке контейнеров, потому что соответствующая запись в базе контейнеров всё ещё не удалена.

В нашей практике типичной является ситуация, когда в очереди на удаление находятся более 200 миллионов файлов, и оbject-expirer со своими задачами не справляется. Поэтому нам пришлось сделать собственный демон, написанный на Go.

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

В чем оно заключается? В схеме базы контейнеров появятся дополнительные поля, которые позволят репликатору контейнеров удалять просроченные файлы. Это решит проблемы с наличием в базе контейнеров записей об уже удаленных объектах + object auditor будет удалять файлы истекших объектов. Это позволит также полностью отказаться от object-expirer’а, который на данный момент является таким же атавизмом, как многососковость.

Объекты

Уровень объектов — самая простая часть OpenStack Swift. На этом уровне существуют только наборы байт объектов (файлов) и определенный набор операций над этими файлами (запись,чтение, удаление). Для работы с объектами используется стандартный набор демонов:

  • сервер объектов (object-server) — принимает запросы от прокси-сервера и размещает/отдаёт объекты реальной файловой системы;
  • репликатор объектов (object-replicator) — реализует логику репликации поверх rsync;
  • аудитор объектов (object-auditor) — проверяет целостность объектов и их атрибутов и помещает повреждённые объекты в карантин, чтобы репликатор мог восстановить верную копию из другого источника;
  • корректор (object-updater) — предназначен для выполнения отложенных операций обновления баз данных аккаунтов и контейнеров. Такие операции могут появиться, например, из-за таймаутов, связанных с блокировкой sqlite-баз.

Выглядит довольно просто, не так ли? Но у этого слоя есть несколько значительных проблем, переходящих из релиза в релиз:

  1. Медленная (очень медленная) репликация объектов поверх rsync. Если в небольшом кластере с этим можно смириться, то после достижения пары миллиардов объектов всё выглядит совсем печально. Rsync предполагает push-модель репликации: сервис object-replicator просматривает файлы на своей ноде и пытается запушить эти файлы на все прочие ноды, где этот файл должен быть. Сейчас это уже не такой простой процесс – в частности, для увеличения быстродействия используются дополнительные хэши партиций. Подробнее обо всём этом можно прочитать здесь.
  2. Периодические проблемы с сервером объектов, который запросто может блокироваться при зависании операций ввода-вывода на одном из дисков. Это проявляется в резких скачках времени ответа на запрос. Почему так происходит? В больших кластерах не всегда удается достичь идеальной ситуации, когда абсолютно все сервера запущены, все диски примонтированы, и все файловые системы доступны. Диски в серверах объектов периодически “подвисают” (для объектов используются обычные hdd с небольшим лимитом по iops’ам), в случае с OpenStack Swift это часто ведет к временной недоступности всей object-ноды, т.к. в стандартном object-сервере нет механизма изоляции операций к одному диску. Это ведет, в частности, к большому количеству таймаутов.

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

Hummingbird как попытка решения проблем Swift

Совсем недавно компания Rackspace начала работу по переработке OpenStack Swift и переписыванию его на Go. Сейчас это практически готовый к использованию слой объектов, включающий сервер, репликатор и аудитор. Пока что нет поддержки политик хранения (storage policies), но в нашем хранилище они и не используются. Из демонов, связанных с объектами, нет только корректора (object-updater).

В целом, hummingbird — это feature branch в официальном репозитории OpenStack, проект этот сейчас активно развивается и в скором времени будет включен в master (возможно), можете участвовать в разработке — https://github.com/openstack/swift/tree/feature/hummingbird

Чем Hummingbird лучше Swift?

Во-первых, в Hummingbird изменилась логика репликации. Репликатор обходит все файлы в локальной файловой системе и отправляет всем нодам запросы: “Вам это нужно?” (для упрощения роутинга таких запросов используется метод REPCONN). Если в ответе сообщается, что где-то есть файл более новой версии — локальный файл удаляется. Если данный файл где-то отсутствует — создаётся недостающая копия. Если файл уже был удалён и где-то обнаружится tombstone-файл с более новой временной меткой, локальный файл тотчас же будет удалён.

Здесь необходимо пояснить, что такое tombstone-файл. Это пустой файл, который помещается на место объекта при его удалении.

Зачем такие файлы нужны? В случае с большими распределенными хранилищами мы не можем гарантировать, что при отправке DELETE-запроса сразу же будут удалены абсолютно все копии объекта, потому что для подобного рода операций мы отправляем пользователю ответ об успешном удалении после получения n запросов, где n соответствует кворуму (в случае 3-х копий — мы должны получить два ответа). Это сделано намеренно, так как некоторые устройства могут быть недоступны по различным причинам (например, плановые работы с оборудованием). Естественно, копия файла на этих устройствах не будет удалена.
Более того, после возвращения устройства в кластер, файл будет доступен. Поэтому при удалении объект заменяется на пустой файл с текущей временной меткой в имени. Если в ходе опроса серверов репликатором будут обнаружены два tombstone-файла, да ещё и с более новыми временными метками, и одна копия фала с более старым last-modified — значит, объект был удалён, и оставшаяся копия тоже подлежит удалению.

С сервером объектов то же самое: диски изолированы, есть семафоры, которые ограничивают конкурентные соединения с диском. Если какой-то диск по тем или иным причинам “подвисает” или переполняет очередь запросов к нему, можно просто отправиться на другую ноду.

Проект Hummingbird успешно развивается; будем надеяться, что совсем скоро он будет официально включён в OpenStack.

Мы перевели на Hummingbird весь кластер облачного хранилища. Благодаря этому снизилось среднее время ожидания ответа от сервера объекта, и ошибок стало гораздо меньше. Как уже было отмечено выше, мы используем свой прокси-сервер на базе прототипа из Hummingbird. Слой объектов также заменён на набор демонов из Hummingbird.
Из компонентов стандартного OpenStack Swift используются только демоны, связанные со слоями аккаунтов и контейнеров, а также демон-корректор (object-updater).

Заключение

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

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

Что еще почитать по теме

T-Rex 30 марта 2021

Что такое SMTP-протокол и как он устроен?

SMTP (Simple Mail Transfer Protocol) — протокол передачи почты. Он был представлен еще в 1982 году, но не теряет актуальности до сих пор. В статье разбираемся, какие задачи решает протокол и как он ра…
T-Rex 30 марта 2021
Владимир Туров 1 сентября 2020

Дело совершенно секретного iPod

Это был обычный серый день в конце 2005 года. Я сидел на рабочем месте и писал код для следующей версии iPod. Вдруг без стука ворвался директор ПО для iPod, начальник моего начальника, и закрыл дверь.
Владимир Туров 1 сентября 2020
T-Rex 21 августа 2020

TrendForce: цены на SSD упадут

Эксперты DRAMeXchange предсказывают значительное падение цен на оперативную память и твердотельные накопители в ближайшее время. Причина — сокращение спроса на чипы для NAND и DRAM.
T-Rex 21 августа 2020

Новое в блоге

Михаил Фомин 24 июня 2022

Docker Swarm VS Kubernetes — как бизнес выбирает оркестраторы

Рассказываем, для каких задач бизнесу больше подойдет Docker Swarm, а когда следует выбрать Kubernetes.
Михаил Фомин 24 июня 2022
Ульяна Малышева 30 сентября 2022

«Нулевой» локальный диск. Как мы запустили облако только с сетевыми дисками и приручили Ceph

Чем хороши сетевые диски и почему именно Ceph, рассказал директор по развитию ядра облачной платформы Иван Романько.
Ульяна Малышева 30 сентября 2022
Валентин Тимофеев 30 сентября 2022

Как проходит онбординг сотрудников ИТО? Что нужно, чтобы выйти на смену в дата-центр

Рассказываем, как обучаем новых сотрудников, какие задачи и испытания проходят инженеры прежде, чем выйти на свою первую смену.
Валентин Тимофеев 30 сентября 2022
T-Rex 28 сентября 2022

Книги по SQL: что почитать новичкам и специалистам

Собрали 6 книг, которые помогут на старте изучения SQL и при углублении в тему.
T-Rex 28 сентября 2022