Хмарне сховище: оновлення API

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


Модель об'єктного сховища OpenStack Swift

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

  • проксі-сервер, який отримує від кінцевого користувача певний набір даних, виконує службові запити до інших компонентів сховища і, нарешті, формує і відправляє правильну відповідь.
  • сервери акаунтів і контейнерів. Тут я поєдную ці два сервіси, т. к. вони дуже схожі за принципом роботи – кожен з них зберігає метадані, а також списки контейнерів (сервер акаунтів) і файлів (сервер контейнерів) в окремих sqlite-базах і видає ці дані за запитами.
  • сервери об'єктів, які, власне, зберігають файли користувача (метадані рівня файлів в розширених атрибутів). Це самий примітивний рівень абстракції – тут існують тільки об'єкти (у вигляді файлів), записувані в певні партіціі.


Кожен з цих компонентів являє собою окремий демон. Всі демони об'єднані в кластер з допомогою так званого кільця хеш-таблиці для визначення місця розміщення об'єктів усередині кластера. Кільце створюється окремим процесом (ring builder) і розноситься по всіх вузлах кластера. Воно задає кількість реплік об'єктів у кластері для забезпечення відмовостійкості (зазвичай рекомендується зберігати три копії кожного об'єкта на різних серверах), кількість партіцій (внутрішня структура swift), розподіл пристроїв по зонах і регіонах. Також кільце надає список так званих handoff-пристроїв, на які будуть заливатися дані у випадку недоступності основних пристроїв.

Розглянемо всі компоненти сховища більш докладно.

Proxy

Спочатку ми використовували стандартний swift-proxy, потім, коли навантаження збільшилося, а нашого власного коду стало більше — перевели все це на gevent і gunicorn, пізніше замінили gunicorn на uwsgi зважаючи на те, що останній краще працює під великими навантаженнями. Всі ці рішення були не особливо ефективні, час очікування, пов'язане з проксі, було досить великим і доводилося використовувати все більше серверів для обробки авторизованого трафіку, т. к. Python сам по собі працює дуже повільно. У результаті весь цей трафік довелося обробляти на 12 машинах (зараз весь трафік — і публічний, і приватний, — обробляється всього на 3 серверах).

Після всіх цих паліативних дій я переписав проксі-сервер на go. В якості основи був узятий прототип з проекту Hummingbird, далі, я дописав middleware, які реалізують всю нашу власну функціональність – це авторизація, квоти, прошарку для роботи зі статичними сайтами, символічні посилання, великі сегментовані об'єкти (динамічні та статичні), додаткові домени, версіонування і т. д. Крім того у нас реалізовані окремі эндпоинты для роботи деяких наших спеціальних функцій – це налаштування доменів, 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 весь кластер хмарного сховища. Завдяки цьому знизилося середній час очікування відповіді від сервера об'єкта, і помилок стало набагато менше. Як вже було зазначено вище, ми використовуємо свій проксі-сервер на базі прототипу з Колібрі. Шар об'єктів також замінений на набір демонів з Колібрі.
З компонентів стандартного OpenStack Swift використовуються тільки демони, пов'язані з шарами акаунтів і контейнерів, а також демон-коректор (object-updater).

Висновок

У цій статті ми розповіли про Hummingbird і про те, які проблеми нам вдалося вирішити з його допомогою. Якщо у вас є питання, ми з задоволенням відповімо на них в коментарях.

Завдяки переходу на нову платформу нам вдалося суттєво розширити спектр можливостей API сховища. З'явилися нові функції: управління користувачами, доменами, SSL-сертифікатами і багато іншого. Найближчим часом ми розмістимо в панелі керування документацію до оновленого API, а про всі нововведення докладно розповімо в окремій статті.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.