Обучающий курс

Вступительная статья

Кто я?

Меня зовут Олег Бунин, и я организатор крупнейших русскоязычных IT-конференций: HighLoad++, TeamLead Conf, DevOpsConf, PHP Russia и других. Несколько лет руководил разработкой web-проектов в компании Рамблер.

А потом я создал собственную компанию, которая консультировала крупные проекты Рунета (Вконтакте, Sports.ru, ИТАР-ТАСС и многие другие). Сейчас я веду курс «Проектирование высоконагруженных систем» в МФТИ.

Консультируя крупные веб-проекты, после добрых сотен встреч, я заметил, а затем и структурировал подход, который команда консультантов применяла во всех без исключения случаях. Во всех! Без разницы, что за проект перед тобой — социальная сеть, сайт знакомств, сайт госучреждения или RTB-сеть — во всех случаях алгоритм наших действий был одинаковым.

Вот этим алгоритмом я сейчас с вами и поделюсь!

Нет универсального решения

Немного философии о подходе к проектированию, который нередко помогал и выводил из самых сложных ситуаций и который поможет нам сейчас лучше понять друг друга.

Нет серебряной пули! Мне не известен способ автоматического масштабирования проекта, нет такой базы данных, которая сама умеет кластеризоваться и шардиться. Облака не решают проблему высоких нагрузок. Почему? Потому что ваше приложение, ваш сайт должен уметь работать в облаке. А для того, чтобы написать приложение, которое можно запустить в облаке, вам нужно проделать всё тоже самое, полностью пройти тот путь, о котором мы будем говорить. Никто за вас его не пройдёт.

Второе, на самом деле это тот же пункт, но переформулированный. Нет языков, технологий или баз данных, которые лучше или хуже подходят для высоконагруженного проекта. Вопрос - на чём лучше писать highload-проект - на Ruby или Python'е лишён смысла и говорит о низкой технической грамотности вопрошающего.

Высокие нагрузки, отказоустойчивость - это не про технологии, это про АРХИТЕКТУРУ!

От того, как именно вы спроектируете вашу архитектуру и будет зависеть, как будет работать ваш проект, сколько посетителей он выдержит, можно ли будет его масштабировать и как именно это сделать.

Ну что же, теперь приступим непосредственно к алгоритму проектирования высоконагруженной системы!

Начинаем проектирование

Нам нужно выяснить всё о данных в вашем проекте. Всё - это вообще всё.

  • Какие есть данные?
  • Как они связаны друг с другом?
  • Сколько данных каждого типа?
  • Какие минимальные, максимальные и средние размеры у данных каждого типа?
  • Сколько актуальных данных?
  • Как определяется актуальность данных?
  • Как быстро растёт объём данных?
  • Как часто данные читаются?

Какие у нас есть данные?

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

  • Какого размера может быть пост? Минимальный, максимальный и средний размер? / Кстати, ограничениедолжно быть, ограничения есть всегда и это не спроста /
  • Сколько постов может быть у одного пользователя?
  • Сколько в среднем у пользователя постов?
  • Как часто пользователь может публиковать посты?
  • Сколько постов публикуется в день?
  • Сколько всего постов в системе?
  • Как долго должны храниться посты?
  • Можно ли прочитать архивные посты?
  • Сколько всего пользователей в системе?
  • Сколько в среднем у пользователя друзей?
  • Сколько максимум у пользователя друзей?
  • Как быстро пользователь должен увидеть свой пост?
  • Как быстро пользователь должен увидеть пост друга в своей френдленте?
  • Как сортируются посты в френдленте друзей?

В сухом остатке, вы должны точно понимать, что у вас есть за данные, какие эти данные, как эти данные изменяются.

Как данные будут использоваться?

Знать всё о своих данных, предполагает также знание о том, как эти данные будут использоваться. Причём обо всех вариантах использования. Написать такую систему хранения, которая позволит любое использование и обработку данных в высоконагруженной системе, строго говоря, невозможно.

Показ данных пользователю - это тоже использование, причём использование, которое обладает одним критичным свойством - в случае, если мы проектируем с вами веб-систему, то показ данных пользователю должен быть максимально быстрым. Пользователь не будет ждать. Мы должны придумать такую схему хранения данных, которая позволит максимально быстро обработать ЛЮБОЙ запрос пользователя.

Одни данные, разное использование?

Может ли возникнуть такая ситуация, когда для разных схем использования одних и тех же данных их лучше хранить по-разному? Может. Например, переписку между двумя пользователями удобно хранить в каком-то одном месте - можно даже в файле. Эта переписка демонстрируется только этим двум пользователям и демонстрируется сразу вся.

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

Как быть?

Правильный ответ - да, необходимо ввести избыточность! Избыточность - это один из архитектурных паттернов, применяемых в разработке высоконагруженных систем.

Архитектурные паттерны

Сейчас мы с вами сделаем небольшое отступление, которое позволит пройти дальше. Подобных паттернов много, вот они:

  • Сервисно-ориентированная архитектура;
  • Вертикальное масштабирование;
  • Горизонтальное масштабирование;
  • Отложенные вычисления;
  • Асинхронная обработка;
  • Конвейерная обработка;
  • Использование толстого клиента;
  • Кеширование;
  • Функциональное разделение;
  • Шардинг;
  • Виртуальные шарды;
  • Центральный диспетчер;
  • Репликация;
  • Партиционирование;
  • Кластеризация;
  • Денормализация;
  • Введение избыточности;
  • Параллельное выполнение;

Обо всех них мы будем рассказывать далее в нашей обучающей рассылке. Вообще, о чём этот список рассылки? В нём мы будем с вами идти сразу по трём дорогам.

  1. Изучение архитектурных паттернов, границы применения каждого из них, примеры использования, плюсы и минусы;
  2. Обучающая программа по высоконагруженным инструментам - мы с вами последовательно пройдём от общей архитектуры проекта, через изучение конкретных технологий и до эксплуатации высоконагруженного проекта, включая деплой и даже мониторинг;
  3. Лучшие видео с конференции разработчиков высоконагруженных систем HighLoad++, крупнейшее в России профессиональное мероприятие для специалистов, за последние девять лет.

Переписка между пользователями

Теперь вернёмся к нашим данным. Давайте подробно разберём пример с перепиской между двумя пользователями.

Текстовые сообщения небольшого размера. Текстовых сообщений может быть очень много. Пользователей тоже много, но переписка всегда между парой пользователей.

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

Спроектируем теперь страницу для показа списка переписок. Старая схема с одним контейнером на переписку уже не годится, нам придётся слишком много данных перебирать для формирования одного списка. Не просто достать список переписок из базы данных, но и для каждой из них открыть контейнер и достать оттуда последнее текстовое сообщение.

Можно было бы использовать обычную реляционную таблицу c четырьмя полями:

  1. Идентификатор первого пользователя
  2. Идентификатор второго пользователя
  3. Дата сообщения
  4. Само сообщение

Тогда переписка достаётся выборкой по двум идентификаторам с сортировкой по дате сообщения. А список переписок - выборка по одному идентификатору пользователя с группировкой по-другому. Как доставать последнее сообщение в каждой переписке по-прежнему не очень понятно - либо конструировать сложный запрос, либо делать подзапросы.

Отметём решение "в лоб", оно, похоже, работать не будет.

Используем такой архитектурный паттерн, как ВВЕДЕНИЕ ИЗБЫТОЧНОСТИ.
Во-первых, создадим список переписок. Это отдельный список сущностей, который в нормализованной таблице не должен существовать, он легко достаётся из списка сообщений. Список переписок может храниться отдельно и, так как переписок не так много, как сообщений, мы можем хранить список переписок пользователя в реляционной таблице.

Как же быть с последним сообщением?

А очень просто - мы добавляем ещё одно поле в сущность ПЕРЕПИСКА и помещаем туда последнее сообщение. Таким образом, каждый раз, когда пользователь отправляет сообщение, мы должны поместить его уже в два места - в список сообщений внутри переписки между двумя пользователями и в поле ПОСЛЕДНЕЕ СООБЩЕНИЕ сущности ПЕРЕПИСКА.

Хорошо, а как быть с поиском по сообщениям?
Какой будет правильный ответ?

Правильно! Поиск - это отдельные индексы и отдельная схема хранения данных. Таким образом, при отправке сообщения оно будет попадать уже в три хранилища - в список сообщений внутри переписки, в поле ПОСЛЕДНЕЕ СООБЩЕНИЕ и в ПОИСКОВЫЙ ИНДЕКС.

Нормально ли это? Абсолютно! Помните - самое главное, быстро обслужить пользователя, отдать ему данные максимально быстро. Это значит, что данные должны быть если уж не в виде готового блюда, но в виде полуфабриката.

Таким образом вы обходите все способы использования каждой из конкретных сущностей на вашем сайте и проектируете для каждого использования схему хранения и обработки.

Уже затем, спроектировав схемы хранения, вы выбираете ИНСТРУМЕНТЫ хранения, ту самую MongoDB или старый добрый MySQL или файловую систему или RabbitMQ.

Алгоритм проектирования

Итак, ещё раз:

  1. Выяснить всё о своих данных;
  2. Составить схему движения и использования данных;
  3. Для каждого использования подобрать архитектурных приём, разработать архитектуру;
  4. Для каждой архитектуры подобрать инструменты и технологии.

Это - вкратце! Очень многое осталось за скобками:

  • Что конкретно означает каждый из архитектурных паттернов?
  • Как передавать данные из хранилища в хранилище? Действительно ли надо писать сразу во все?
  • Что такое и как использовать допустимую деградацию системы;
  • Всегда ли мы упираемся именно в данные? Может быть в CPU?
  • Ну и хотелось бы больше примеров.

На все эти вопросы я тоже дам ответ.
Но не сейчас!

Оставайтесь с нами :)

 
HighLoad++ для начинающих →