Мы начали работать над Облаком Mail.Ru в июне 2012. За полтора года мы прошли долгий и тернистый путь от первого прототипа до публичного сервиса, выдерживающего нагрузки свыше 60 Гбит/с. Сегодня мы хотим поделиться с вами рассказом о том, как это было.
Ты помнишь, как все начиналось
Начиналось все с одной девелоперской виртуальной машины, на которой запускался первый прообраз демона хранения метаданных. Тогда все было кристально просто. Мы хранили логи действий каждого пользователя (создание директории, создание файла, удаление файла и т.д.), которые приводили к финальному состоянию его виртуальной файловой системы. Тела файлов хранились в общей для всех пользователей директории. Эта система, написанная на коленке за две недели, проработала несколько месяцев.
Сервер метаданных
Прототип приблизился к продуктовой версии, когда мы написали свой сервер для хранения метаданных. Его можно назвать продвинутым key-value storage, где ключом является двенадцатибайтный идентификатор дерева, а значением — иерархия файлов и каталогов отдельного пользователя. С помощью этого дерева эмулируется файловая система (добавляются и удаляются файлы и каталоги). Для общения с сервисом первый клиент использовал бинарный сессионно-ориентированный протокол: устанавливалось TCP-соединение, в него сваливались нотификации об изменениях, чтобы два клиента могли синхронизироваться, и клиенты писали команды в это поддерживаемое соединение. Этот прототип проработал еще пару месяцев.
Разумеется, мы прекрасно понимали, что клиенты не могут позволить себе держать постоянное TCP-соединение с сервером. Более того, на тот момент у нас не было никакой аутентификации: мы верили клиенту. Это было хорошо для разработки, но совершенно неприемлемо для продакшна.
HTTP-обертка
Следующим шагом мы обернули отдельные пакеты, из которых состоял наш бинарный протокол, HTTP-заголовками, и стали общаться с сервером по HTTP, сообщая в строке запроса, с каким деревом мы хотим работать. Эта обертка на данный момент реализована с помощью модуля Nginx, который поддерживает TCP-соединение с сервером метаданных; сервер ничего не знает про HTTP.
Такое решение, конечно, все еще не давало нам проверку авторизации, но клиент уже мог работать в условиях, приближенных к реальным: например, при краткосрочном разрыве соединения все не шло прахом.
Также на этом этапе мы столкнулись с необходимостью стороннего HTTP-нотификатора. Мы взяли Nginx HTTP Push-Module — это Comet-сервер для нотификации — и стали в него слать нотификации из нашего сервера метаданных.
Когда это не просто заработало, а стало нормально переживать падения и рестарты Comet-сервера, мы приступили к следующему этапу.
Авторизация
Следующим этапом стало внедрение авторизации как на сервере, так и на клиенте. Вооружившись Nginx, исходниками модуля, производящего похожие действия, и мануалом по работе с нашим внутренним интерфейсом авторизации и проверки сессии, мы написали модуль авторизации. Реализованный модуль на входе получал куку пользователя, а на выходе вычислял идентификатор этого пользователя, получал его email и производил другие полезные действия.
Мы получили сразу две приятные плюшки: авторизация заработала одновременно и при работе с сервером метаданных, и при работе с сервером нотификаций.
Loader
На загрузку файлов работал DAV-модуль Nginx, который получал файлы с хэшами и складывал их локально в одну директорию. На тот момент ничего не бэкапилось, все работало до первого вылета диска. Поскольку тогда было далеко даже до закрытого внутреннего тестирования, и системой пользовались только сами разработчики, такой вариант был приемлем. Конечно, в продакшн мы вышли с полностью фолт-толерантной схемой, сегодня каждый файл пользователя хранится в двух копиях на разных серверах в разных дата-центрах (ДЦ).
К ноябрю 2012 года настал день, когда мы уже не могли отворачиваться от того, что важна не только иерархия директорий пользователя, но и контент, который он хранит.
Мы хотели сделать загрузчик надежным и функциональным, но в то же время достаточно простым, поэтому не стали строить витиеватых переплетений между иерархией директорий и телами файлов. Мы решили ограничить круг задач загрузчика следующим: принять входящий файл, посчитать его хэш и параллельно записать на два бэкенда. Эту сущность мы назвали Loader, и его концепция не сильно поменялась с тех времен. Первый работающий прототип Loader’а был написан в течение двух недель, а уже через месяц мы полностью перешли на новый загрузчик.
Он умел делать все, что нам было нужно: получать файлы, считать хэш и отправлять на стораджи без сохранения файла на локальном жестком диске фронтенда.
Иногда мы получаем восторженные отзывы о том, что, например, гигабайтный фильм залился в Облако мгновенно. Это происходит благодаря механизму заливки, который поддерживают наши клиентские приложения: перед заливкой они считают хэш файла, а затем «спрашивают» у Loader-а, есть ли у нас в Облаке такой хэш; нет — заливают, есть — просто обновляют метаданные.
Zeppelin: Python vs C
Пока писался загрузчик, мы поняли, что, во-первых, одного сервера метаданных не хватит на всю аудиторию, которую нам хотелось бы охватить. Во-вторых, было бы неплохо, чтобы метаданные также существовали в двух экземплярах. В-третьих, никому из нас не хотелось заниматься роутингом пользователя на нужный сервер метаданных внутри модуля Nginx.
Решение казалось очевидным: написать свой демон на Python. Так делают все, это модно, популярно, а самое главное – быстро. Задача демона — выяснять, на каком сервере метаданных живет пользователь, и перенаправлять все запросы туда. Чтобы не заниматься роутингом пользователей на серверах данных, мы написали прослойку на Twisted (это асинхронный фреймворк для Python). Однако выяснилось, что либо мы не умеем готовить Python, либо Twisted тормозной, либо существуют другие неведомые причины, но больше тысячи запросов в секунду эта штука не держала.
Тогда мы решили, что будем как хардкорные ребята писать на С с использованием Metad-фреймворка, позволяющего писать асинхронный код в синхронной манере (выглядит это примерно как state threads). Первая рабочая версия сишного Zeppelin-а (к выбору такого названия нас привела цепочка ассоциаций Облако — дирижабли — Zeppelin) появилась на тестовых серверах уже через месяц. Результаты были вполне ожидаемыми: имеющиеся полторы-две тысячи запросов в секунду обрабатывались легко и непринужденно при близкой к нулевой загрузке процессора.
Cо временем функционал Zeppelin-а расширился, и сегодня он отвечает за проксирование запросов в Metad, работу с веблинками, согласование данных между Metad-ом и filedb, авторизацию.
Генератор миниатюр (Thumbnailer)
Кроме того, мы хотели научить Облако отображать превью картинок. Мы рассмотрели два варианта — хранить миниатюры или генерировать их на лету и временно кэшировать в оперативной памяти — и остановились на втором. Генератор был написан на Python и использует библиотеку GraphicsMagick.
Для того чтобы обеспечить максимальную скорость, миниатюры генерируются на той машине, где физически лежит файл. Поскольку стораджей очень много, нагрузка по генерации миниатюр «размазана» примерно равномерно, и, по нашим ощущениям, генератор работает достаточно быстро (ну, может, за исключением тех случаев, когда запрашивается файл 16 000 x 16 000 пикселей).
Первый стресс-этап: закрытая бета
Серьезной проверкой наших решений стал выход на закрытое бета-тестирование. Бета началась как у людей – пришло больше народа, чем мы ожидали, они создали больше трафика, чем мы ожидали, нагрузки на диск оказались больше, чем мы ожидали. С этими проблемами мы героически боролись, и за полтора-два месяца побороли многие из них. Мы очень быстро доросли до ста тысяч пользователей и в октябре сняли инвайты.
Второй стресс-этап: публичный релиз
Вторым серьезным тестом стал публичный релиз: вернулись проблемы с дисками, которые мы в очередной раз решили. Со временем ажиотаж начал угасать, но тут подвернулся Новый год, и мы решили раздать по терабайту всем желающим. 20-е числа декабря в связи с этим выдались очень веселыми: трафик превысил все наши ожидания. На тот момент у нас было в районе 100 стораджей, в которых было около 2400 дисков, при этом трафик на отдельные машины превысил гигабит. Общий трафик вышел за отметку 60 Гбит.
Мы такого, конечно, не ждали. Ориентируясь на опыт коллег по рынку, можно было предположить, что в сутки будет заливаться около 10 ТБ. Сейчас, когда пик уже спал, у нас заливается 100 ТБ, а в наиболее напряженные дни доходило до 150 ТБ/сутки. То есть каждые десять дней мы делаем петабайт данных.
Опись имущества
Итак, на данный момент у нас используется:
● Самописная база данных. Зачем нужно было писать свою базу? Изначально мы хотели запуститься на большом количестве дешевых машин, поэтому при разработке мы боролись за каждый байт оперативной памяти, за каждый такт процессора. Делать это на существующих реализациях, вроде MongoDB, которые хранят JSON, пусть даже бинарно упакованный, было бы наивно. Кроме того, нам нужны были специфические операции, такие как юникодный case folding, реализация файловых операций.
Это можно было бы сделать, расширяя существующую noSQL базу данных, но в таком случае логику vfs просто-напросто пришлось бы вписывать в уже существующую солидных объемов кодовую базу, либо расширять серверный функционал через встроенные скриптовые языки, эффективность которых вызывала некоторые сомнения.
В традиционных SQL базах данных хранить древовидные структуры не слишком-то удобно, да и довольно неэффективно — nested sets положат базу на апдейтах.
Можно было бы хранить все пути строками в Tarantool, но это невыгодно по двум причинам. Во-первых, мы явно не сможем уместить всех пользователей в оперативной памяти, а Tarantool хранит данные именно там; во-вторых, если хранить полные пути, мы получим огромные накладные расходы.
В итоге получалось, что все существующие решения требовали интенсивной доработки напильником и / или были довольно прожорливы по части ресурсов. Кроме того, потребовалось бы учиться их администрировать.
Именно поэтому мы решили написать свой велосипед. Он получился достаточно простым и делает ровно то, что нам от него нужно. Благодаря имеющейся архитектуре можно, например, заглянуть в метаданные пользователя на момент времени в прошлом. Помимо этого, можно реализовать версионирование файлов.
● Tarantool key-value storage
● Широко используется Nginx (замечательная российская разработка, идеальная до тех пор, пока не придется писать под нее расширения)
● В качестве рудимента временно применяется старый нотификатор на внутренней разработке Mail.Ru Imagine
● Более трех тысяч пар дисков. Пользовательские файлы и метаданные у нас хранятся в двух экземплярах на разных машинах, в каждой из которых 24 диска емкостью 2 или 4 ТБ (все новые серверы сетапим с дисками на 4ТБ).
Неожиданные открытия
Мы пишем подробные логи на каждый запрос. У нас есть отдельная команда, которая пишет анализатор, строящий различные графики для выявления и анализа проблем. Графики отображаются в Graphite, помимо этого мы используем Radar и другие системы.
Исследуя то, как пользуются нашим Облаком, мы сделали несколько любопытных открытий. Например, самое популярное расширение файлов в Облаке — .jpg. Это и неудивительно — люди любят фоточки. Что интереснее, в топ-10 популярных расширений входят .php и .html. Уж не знаем, что там хранится — кодовая база или бэкапы с хостинга, но факт остается фактом.
Мы замечаем существенные скачки трафика в дни, когда появляются обновленные моды, выпущенные к апдейтам популярных онлайн-игр: кто-то загружает очередной аддон, а потом армия геймеров дружно скачивает его из нашего Облака. Доходило до того, что в отдельные дни до 30% заходов к нам совершали для скачивания этих обновлений.
Вопросы и ответы
Мы подумали о списке вопросов, которые вам, возможно, захочется задать.
Как насчет расширения HTTP?
У нас нигде не расширяется протокол HTTP. Когда мы начинали разработку Облака, все работало на открытых каналах данных без HTTPS (в продакшн мы вышли, разумеется, с полным SSL-шифрованием всего трафика между клиентом и сервером). Поэтому, когда у кого-то возникало желание вытащить что-нибудь в заголовок, угроза дикой прокси, которая порежет все кастомные заголовки, быстро его отбивала. Так что все, что нам нужно, сосредоточено либо в URL, либо в теле.
Что используется в качестве идентификатора файла?
Идентификатором файла у нас служит SHA-1.
Что будет с пользовательскими данными, если «накроется» сервер или дата-центр Облака целиком?
У нас все устроено абсолютно фолт-толерантно, и выход из строя сервера или даже целого ДЦ почти никак не влияет на работу пользователя. Все данные пользователя дублируются на разных серверах в разных ДЦ.
Какие планы на будущее?
В наших ближайших планах развития запуск WebDAV, дальнейшая оптимизация всего и вся, и много интересных фич, о которых вы скоро узнаете.
Если мы что-то упустили, постараемся ответить в комментариях.