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

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

Модель объектного хранилища 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 (https://github.com/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 (возможно), можете участвовать в разработке — 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, а обо всех нововведениях подробно расскажем в отдельной статье.

Комментарии (0)

© Habrahabr.ru