Облачное хранилище: обновление API
![Тирекс](https://selectel.ru/blog/wp-content/uploads/2023/05/T-Rex_Autor-3-400x400.png)
Спешим сообщить новость: мы переписали API облачного хранилища. Теперь всё работает гораздо стабильнее и быстрее благодаря новой платформе — Hummingbird, которая по сути представляет собой реализацию некоторых компонентов OpenStack Swift на Go. О том, как мы внедряли Hummingbird и какие проблемы нам удалось решить с его помощью, мы расскажем в этой статье. Модель объектного хранилища […]
![Изображение записи](https://selectel.ru/blog/wp-content/uploads/2016/06/PR-2345_preview.png)
Спешим сообщить новость: мы переписали 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-баз.
Выглядит довольно просто, не так ли? Но у этого слоя есть несколько значительных проблем, переходящих из релиза в релиз:
- Медленная (очень медленная) репликация объектов поверх rsync. Если в небольшом кластере с этим можно смириться, то после достижения пары миллиардов объектов всё выглядит совсем печально. Rsync предполагает push-модель репликации: сервис object-replicator просматривает файлы на своей ноде и пытается запушить эти файлы на все прочие ноды, где этот файл должен быть. Сейчас это уже не такой простой процесс – в частности, для увеличения быстродействия используются дополнительные хэши партиций. Подробнее обо всём этом можно прочитать здесь.
- Периодические проблемы с сервером объектов, который запросто может блокироваться при зависании операций ввода-вывода на одном из дисков. Это проявляется в резких скачках времени ответа на запрос. Почему так происходит? В больших кластерах не всегда удается достичь идеальной ситуации, когда абсолютно все сервера запущены, все диски примонтированы, и все файловые системы доступны. Диски в серверах объектов периодически “подвисают” (для объектов используются обычные 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, а обо всех нововведениях подробно расскажем в отдельной статье.