Pull to refresh

Делаем правильную платформу или Как повторить Google

Reading time 5 min
Views 2.6K
Введение
Сегодня я расскажу о проектировании высоко-нагруженных отказоустойчивых систем. Акцент будет поставлен практическую разработку и жареные факты, а не на сухую теорию. После прочтения вы не испугаетесь разработки сервиса с миллиардом пользователей, если у вас будет достаточное количество серверов. Тема весьма обширна, но я постараюсь быть кратким и лаконичным.

Что мы хотим получить?
Начнем с тезисной постановки задачи и требований к системе, их немного.
  • Система должна автоматически разделять задачи между доступными ей серверами.
  • Одновременный физический отказ (выключение) любых двух серверов никак не повлияет на целостность системы.
  • Физический отказ любого оборудования не влечет за собой отказ целой системы.
  • Включение нового сервера не требует никакой ручной настройки.
  • Клиент может взаимодействовать с любым frontend-сервером равнозначно.
Два пути.
Существуют два пути. Первый — путь Microsoft — каждый сервис разрабатывается независимо, при этом каждая группа разработчиков делает собственные решения.
Второй путь — путь Google — существует единая платформа и единая система, в которой живут приложения (поисковик, почтовик, группы, и т.д.).
Второй путь заметно выигрывает, поскольку при оптимизации любой части системы, улучшается работа сразу всех сервисов, и при разработке не нужно думать о том, как всё это будет в действительности работать. Первый же очень напоминает басню Крылова про лебедя, рака, и щуку.

Основные компоненты платформы.
На каждом сервере может быть запущен любой набор компонентов, количество серверов для каждого компонента определяется автоматически.
  • Хранилище данных. Состояние системы хранится только в нем.
  • Кеш подсистема. Используется для временного хранения горячих данных в памяти.
  • Сервис блокировок. Используется для предотвращения одновременного вызова последовательных процедур.
  • Сервис приложений. В нем живут все конечные сервисы.
  • Сервис взаимодействия с клиентом. Включает в себя Веб-сервер и DNS-сервер.
Хранилище данных.
Состояние системы хранится только в нем, никакие другие хранилища для приложений недоступны. Я выбираю MongoDB. Для того чтобы понять как к ней подойти, рекомендую ознакомиться с моей предыдущей статьей — MongoDB — варим хороший кофе, уделите кластеризации особое внимание. Также важен MapReduce.

Кеш подсистема.
Необходима для снижения нагрузки на базу данных как на простой выборке объектов, так и на сложных выборках. Я выбираю memcached. Про то как подружить MongoDB и memcached сказано в статье о MongoDB, советую обратить внимание.

Сервис блокировок.
Используется для предотвращения одновременного вызова последовательных процедур. Перед тем как выполнить процедуру, которая требует последовательности действий, приложение отправляет запрос сервису блокировок. В ответ оно получает либо согласие на выполнение, либо отказ. Я выбираю phpDaemon::LockServer. В случае падения одного узла, все ведомые им клиенты переключаются на другой узел.

Сервис приложений.
В нем живут конечные сервисы. В качестве сервиса приложений может выступать OpenVZ. Приложения также могут быть асинхронными модулями phpDaemon. При разработке приложений нужно перекладывать в очередь любые тяжелые операции.

Сервис взаимодействия с клиентом.
Включает в себя Веб-сервер (nginx), DNS-сервер (bind9) и firewall, через который идут запросы напрямую к приложениям. Отказоустойчивость и распределение нагрузки обеспечивается либо через DNS round-robin, либо по BGP с получением статуса AS.

Хранение файлов.
Файлы хранятся в базе данных по методологии GridFS, существует реализация в виде FUSE-модуля.

Пример использования. Описание работы сервиса Веб-поиска.
Данный сервис делится на несколько составляющих:
1. Хранение информации о документах.
Для этого используем коллекцию documents с документами содержащими свойства url, host, type, size, mtime, atime, title, text, description, indexed и др. Шардинг полю по url.

2. Crawler'ы, сохраняющие содержимое страниц в базу.
Crawler запрашивает documents.find({host: ..., indexed: 0}), получая блокировку на этот host. Запрашивает каждый документ по сети и складывает его в базу, выставляя при этом indexed: 1.

3. Полнотекстовый поиск.
Для эффективного поиска по тексту нужен специальный механизм, который позволит быстро находить документы по заданным словам.

3.1. Приведение слова к «нормальной» форме. Слова могут записываться в разных словоформах, их все необходимо привести к одной нормальной форме, для этого нужно воспользоваться словарями (см. spelldump из Sphinx), и эвристическими стеммерами для слов отсутствующих в словарях.

3.2. Хранение индекса.
Существует два подхода.

Первый подход чаще используется в поисковых системах со сравнительно небольшим количеством поисковых запросов. Он подразумевает дробление единого поискового индекса на несколько, и параллелизм работы всех узлов при запросе. То есть, когда подается поисковый запрос, опрашиваются все узлы и их ответы склеиваются. В этом подходе для увеличения производительности добавляют зеркала узлов, и балансируют нагрузку между этими узлами-зеркалами. Но это весьма неэффективно, т.к. чем больше индекс, тем больше серверов требуется, при этом вовсе не учитывается популярность слов. Многие слова вообще никогда не будут запрошены.

Второй подход подразумевает распределение по словам, таким образом при запросе «hot girls», мы сначала узнаем какие сервера отвечают за каждое из этих слов, затем запрашиваем их, и склеиваем результаты. Сервера, которые не отвечают за эти слова мы вообще никак не нагружаем. Это дает огромный выигрыш в производительности.

Поэтому, мы выбираем второй вариант. При индексации документ разбивается на слова, они приводятся к нормальной форме, и для каждого слова вызывается words.upsert({word: ...,{"$push": {«docs»: ...}}})

3.3. Поиск.
При простом поиске «hot girls» подается запрос words.find({word: «hot»}) и words.find({word: «girl»), заметьте что «girl», а не «girls». Затем выявляются пересечения между ними и выполняется ранжирование.
Для эффективного ранжирования необходимо хранить расстояния между всеми парами слов в документе. Коллекция wordpairs с объектами вида {docid: ..., word1: «hot», word2: «girl», distance: 1}. Таким образом, можно быстро узнать в каких документах hot и girl расположены ближе всего.

Ссылки на используемое ПО.
  • Nginx — Веб-сервер
  • MongoDB — База данных
  • phpDaemon — сервер включающий в себя сервер блокировок и другие модули
Заключение
В действительности, сделать такую систему вовсе не сложно. Тем более, я работаю над соответствующим фреймворком для простого создания подобных систем.
Очень сложно уместить в статью такой огромный объем информации, но надеюсь мне удалось передать суть. Мне интересно какие аспекты покажутся вам особенно интересными, и я напишу о них в следующий раз.
Главное, поймите и осознайте, если вы сталкиваетесь с проблемой работая с более простым инструментом, а не используя вместо этого готовое решение, позволяющее абстрагироваться от таких проблем — это вовсе не означает, что проблема решена, скорее всего эта проблема решена настолько через одно место, что вы даже себе не представляете. Вообще, я с подозрением отношусь к продуктам, которые не исповедуют принцип Keep It Simple, Stupid. Когда я не могу проконтролировать что будет происходить на элементарном уровне, я понимаю что не удастся сделать всё правильно, поскольку всякие автоматически оптимизаторы (SQL-запросов, кода, и т.д.) очень часто ошибаются.
Например, в комментариях к одной из статей, человек спросил возможно ли в MongoDB выполнить аналог SQL-запроса «SELECT * FROM table ORDER BY RAND()*hosts LIMIT 5». Это конечно здорово и удобно, но с тем же успехом мы можем выбрать всю табличку, а затем в скрипте выбрать что нам нужно, это будет не намного сильнее тормозить, поскольку MySQL в этом случае выполняет RAND()*hosts для каждого ряда, сортирует все эти ряды в памяти, и отрезает 5 первых результатов. Само собой, чем больше табличка, тем больше тормоза. Так что надо всегда думать что в действительности происходит в результате ваших действий, иначе вы никогда не научитесь делать высоко-нагруженные системы.
Спасибо за внимание!
Tags:
Hubs:
+21
Comments 299
Comments Comments 299

Articles