Приложение называется масштабируемым если. Горизонтальное масштабирование PHP-приложений

30.03.2019

Масштабируемость - способность устройства увеличивать свои
возможности
путем наращивания числа функциональных блоков,
выполняющих одни и
те же задачи.
Глоссарий.ru

Обычно о масштабировании начинают думать тогда, когда один
сервер не справляется с возложенной на него работой. С чем именно он не
справляется? Работа любого web-сервера по большому счету сводится к основному
занятию компьютеров - обработке данных. Ответ на HTTP (или любой другой) запрос
подразумевает проведение некоторых операций над некими данными. Соответственно,
у нас есть две основные сущности - это данные (характеризуемые своим объемом) и
вычисления (характеризуемые сложностью). Сервер может не справляться со своей
работой по причине большого объема данных (они могут физически не помещаться на
сервере), либо по причине большой вычислительной нагрузки. Речь здесь идет,
конечно, о суммарной нагрузке - сложность обработки одного запроса может быть
невелика, но большое их количество может «завалить» сервер.

В основном мы будем говорить о масштабировании на примере
типичного растущего web-проекта, однако описанные здесь принципы подходят и для
других областей применения. Сначала мы рассмотрим архитектуру проекта и простое
распределение ее составных частей на несколько серверов, а затем поговорим о
масштабировании вычислений и данных.

Типичная архитектура сайта

Жизнь типичного сайта начинается с очень простой архитектуры
- это один web-сервер (обычно в его роли выступает Apache),
который занимается всей работой по обслуживанию HTTP-запросов,
поступающих от посетителей. Он отдает клиентам так называемую «статику», то
есть файлы, лежащие на диске сервера и не требующие обработки: картинки (gif,
jpg, png), листы стилей (css), клиентские скрипты (js, swf). Тот же сервер
отвечает на запросы, требующие вычислений - обычно это формирование
html-страниц, хотя иногда «на лету» создаются и изображения и другие документы.
Чаще всего ответы на такие запросы формируются скриптами, написанными на php,
perl или других языках.

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

Решение этой проблемы - распределение работы по обработке
запросов между двумя разными программами - т.е. разделение на frontend и
backend. Легкий frontend-сервер выполняет задачи по отдаче статики, а остальные
запросы перенаправляет (проксирует) на backend, где выполняется формирование
страниц. Ожидание медленных клиентов также берет на себя frontend, и если он использует
мультиплексирование (когда один процесс обслуживает нескольких клиентов - так
работают, например, nginx или lighttpd), то ожидание практически ничего не
стоит.

Из других компонент сайта следует отметить базу данных, в
которой обычно хранятся основные данные системы - тут наиболее популярны
бесплатные СУБД MySQL и PostgreSQL. Часто отдельно выделяется хранилище
бинарных файлов, где содержатся картинки (например, иллюстрации к статьям
сайта, аватары и фотографии пользователей) или другие файлы.

Таким образом, мы получили схему архитектуры, состоящую из
нескольких компонент.

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

Пути дальнейшего разделения компонент тоже понятны - например, можно вынести frontend на отдельный сервер. Но обычно frontend
требует мало системных ресурсов и на этом этапе его вынос не даст существенного
прироста производительности. Чаще всего сайт упирается в производительность
скриптов - формирование ответа (html-страницы) занимает слишком долгое время.
Поэтому следующим шагом обычно является масштабирование backend-сервера.

Распределение вычислений

Типичная ситуация для растущего сайта - база данных уже
вынесена на отдельную машину, разделение на frontend и backend выполнено,
однако посещаемость продолжает увеличиваться и backend не успевает обрабатывать
запросы. Это значит, что нам необходимо распределить вычисления на несколько
серверов. Сделать это просто - достаточно купить второй сервер и поставить на
него программы и скрипты, необходимые для работы backend.
После этого надо сделать так, чтобы запросы пользователей распределялись
(балансировались) между полученными серверами. О разных способах балансировки
будет сказано ниже, пока же отметим, что обычно этим занимается frontend,
который настраивают так, чтобы он равномерно распределял запросы между
серверами.

Важно, чтобы все backend-серверы были способны правильно
отвечать на запросы. Обычно для этого необходимо, чтобы каждый из них работал с
одним и тем же актуальным набором данных. Если мы храним всю информацию в единой
базе данных, то СУБД сама обеспечит совместный доступ и согласованность данных.
Если же некоторые данные хранятся локально на сервере (например, php-сессии
клиента), то стоит подумать о переносе их в общее хранилище, либо о более
сложном алгоритме распределения запросов.

Распределить по нескольким серверам можно не только работу
скриптов, но и вычисления, производимые базой данных. Если СУБД выполняет много
сложных запросов, занимая процессорное время сервера, можно создать несколько
копий базы данных на разных серверах. При этом возникает вопрос синхронизации
данных при изменениях, и здесь применимы несколько подходов.

  • Синхронизация на уровне приложения . В этом случае наши
    скрипты самостоятельно записывают изменения на все копии базы данных (и сами несут
    ответственность за правильность данных). Это не лучший вариант, поскольку он
    требует осторожности при реализации и весьма неустойчив к ошибкам.
  • Репликация - то есть автоматическое тиражирование
    изменений, сделанных на одном сервере, на все остальные сервера. Обычно при
    использовании репликации изменения записываются всегда на один и тот же сервер - его называют master, а остальные копии - slave. В большинстве СУБД есть
    встроенные или внешние средства для организации репликации. Различают
    синхронную репликацию - в этом случае запрос на изменение данных будет ожидать,
    пока данные будут скопированы на все сервера, и лишь потом завершится успешно - и асинхронную - в этом случае изменения копируются на slave-сервера с
    задержкой, зато запрос на запись завершается быстрее.
  • Multi-master репликация. Этот подход аналогичен
    предыдущему, однако тут мы можем производить изменение данных, обращаясь не к
    одному определенному серверу, а к любой копии базы. При этом изменения
    синхронно или асинхронно попадут на другие копии. Иногда такую схему называют
    термином «кластер базы данных».

Возможны разные варианты распределения системы по серверам.
Например, у нас может быть один сервер базы данных и несколько backend (весьма
типичная схема), или наоборот - один backend и несколько БД. А если мы масштабируем
и backend-сервера, и базу данных, то можно объединить backend и копию базы на
одной машине. В любом случае, как только у нас появляется несколько экземпляров
какого-либо сервера, возникает вопрос, как правильно распределить между ними
нагрузку.

Методы балансировки

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

  • Балансирующий узел . В этом случае клиент шлет запрос на один
    фиксированный, известный ему сервер, а тот уже перенаправляет запрос на один из
    рабочих серверов. Типичный пример - сайт с одним frontend и несколькими
    backend-серверами, на которые проксируются запросы. Однако «клиент» может
    находиться и внутри нашей системы - например, скрипт может слать запрос к
    прокси-серверу базы данных, который передаст запрос одному из серверов СУБД.
    Сам балансирующий узел может работать как на отдельном сервере, так и на одном
    из рабочих серверов.

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

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


Разумеется, существуют и комбинации этих подходов. Например,
такой известный способ распределения нагрузки, как DNS-балансировка, основан на
том, что при определении IP-адреса сайта клиенту выдается
адрес одного из нескольких одинаковых серверов. Таким образом, DNS выступает в
роли балансирующего узла, от которого клиент получает «распределение». Однако
сама структура DNS-серверов предполагает отсутствие точки отказа за счет
дублирования - то есть сочетаются достоинства двух подходов. Конечно, у такого
способа балансировки есть и минусы - например, такую систему сложно динамически
перестраивать.

Работа с сайтом обычно не ограничивается одним запросом.
Поэтому при проектировании важно понять, могут ли последовательные запросы
клиента быть корректно обработаны разными серверами, или клиент должен быть
привязан к одному серверу на время работы с сайтом. Это особенно важно, если на
сайте сохраняется временная информация о сессии работы пользователя (в этом
случае тоже возможно свободное распределение - однако тогда необходимо хранить
сессии в общем для всех серверов хранилище). «Привязать» посетителя к
конкретному серверу можно по его IP-адресу (который, однако, может меняться),
или по cookie (в которую заранее записан идентификатор сервера), или даже
просто перенаправив его на нужный домен.

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

Распределение данных

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

  • Вертикальное распределение (vertical partitioning) - в простейшем случае
    представляет собой вынесение отдельных таблиц базы данных на другой сервер. При
    этом нам потребуется изменить скрипты, чтобы обращаться к разным серверам за
    разными данными. В пределе мы можем хранить каждую таблицу на отдельном сервере
    (хотя на практике это вряд ли будет выгодно). Очевидно, что при таком
    распределении мы теряем возможность делать SQL-запросы, объединяющие данные из
    двух таблиц, находящихся на разных серверах. При необходимости можно реализовать
    логику объединения в приложении, но это будет не столь эффективно, как в СУБД.
    Поэтому при разбиении базы данных нужно проанализировать связи между таблицами,
    чтобы разносить максимально независимые таблицы.

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

  • Горизонтальное распределение (horizontal partitioning) - заключается в
    распределении данных одной таблицы по нескольким серверам. Фактически, на
    каждом сервере создается таблица такой же структуры, и в ней хранится
    определенная порция данных. Распределять данные по серверам можно по разным
    критериям: по диапазону (записи с id < 100000 идут на сервер А, остальные - на сервер Б), по списку значений (записи типа «ЗАО» и «ОАО» сохраняем на сервер
    А, остальные - на сервер Б) или по значению хэш-функции от некоторого поля
    записи. Горизонтальное разбиение данных позволяет хранить неограниченное
    количество записей, однако усложняет выборку. Наиболее эффективно можно выбирать
    записи только когда известно, на каком сервере они хранятся.

Для выбора правильной схемы распределения данных необходимо
внимательно проанализировать структуру базы. Существующие таблицы (и, возможно,
отдельные поля) можно классифицировать по частоте доступа к записям, по частоте
обновления и по взаимосвязям (необходимости делать выборки из нескольких
таблиц).

Как упоминалось выше, кроме базы данных сайту часто требуется
хранилище для бинарных файлов. Распределенные системы хранения файлов
(фактически, файловые системы) можно разделить на два класса.

  • Работающие на уровне операционной системы . При этом для
    приложения работа с файлами в такой системе не отличается от обычной работы с
    файлами. Обмен информацией между серверами берет на себя операционная система.
    В качестве примеров таких файловых систем можно привести давно известное
    семейство NFS или менее известную, но более современную систему Lustre.
  • Реализованные на уровне приложения распределенные
    хранилища подразумевают, что работу по обмену информацией производит само
    приложение. Обычно функции работы с хранилищем для удобства вынесены в
    отдельную библиотеку. Один из ярких примеров такого хранилища - MogileFS, разработанная
    создателями LiveJournal. Другой распространенный пример - использование
    протокола WebDAV и поддерживающего его хранилища.

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

Выводы

Подводя итог сказанному, сформулируем выводы в виде кратких тезисов.

  • Две основные (и связанные между собой) задачи масштабирования - это распределение вычислений и распределение данных
  • Типичная архитектура сайта подразумевает разделение ролей и
    включает frontend, backend, базу данных и иногда хранилище файлов
  • При небольших объемах данных и больших нагрузках применяют
    зеркалирование базы данных - синхронную или асинхронную репликацию
  • При больших объемах данных необходимо распределить базу данных - разделить
    ее вертикально или горизонтально
  • Бинарные файлы хранятся в распределенных файловых системах
    (реализованных на уровне ОС или в приложении)
  • Балансировка (распределение запросов) может быть равномерная или
    с разделением по функционалу; с балансирующим узлом, либо на стороне клиента
  • Правильное сочетание методов позволит держать любые нагрузки;)

Ссылки

Продолжить изучение этой темы можно на интересных англоязычных сайтах и блогах.

Масштабирование приложений ASP.NET

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

Далее вы развертываете приложение, вводите его в эксплуатацию и оно работает замечательно в течение нескольких первых недель, но с увеличением количества пользователей увеличивается и количество запросов, которые требуется обработать вашему серверу, и вдруг, ваш сервер начинает захлебываться. Сначала это может проявляться в увеличении времени обработки запросов, затем рабочий процесс начинает использовать все больше памяти и вычислительных ресурсов и в конечном итоге веб сервер просто перестает успевать обрабатывать все запросы и в файлах журналов начинают все чаще появляться сообщения HTTP 500 («Internal Server Error»).

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

Горизонтальное масштабирование (scaling out) веб-приложений - это естественный процесс, начинающийся в определенный момент в жизни веб-приложений. Один сервер может одновременно обслуживать десятки, сотни и даже тысячи пользователей, но он не в состоянии достаточно долго выдерживать пиковые нагрузки. Память начинает заполняться информацией о сеансах, обработка новых запросов приостанавливается из-за отсутствия свободных потоков выполнения, и переключения контекста начинают выполняться слишком часто, что ведет к увеличению задержек и снижению пропускной способности сервера.

Горизонтальное масштабирование

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

Одной из основных проблем горизонтального масштабирования, с которыми сталкиваются разработчики - как реализовать привязку к серверу. Например, когда работает единственный веб-сервер, информация о состоянии сеансов пользователей хранится в памяти. Если добавить еще один сервер, как обеспечить для него доступ к объектам сеансов? Как синхронизировать сеансы между серверами?

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

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

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

Механизмы масштабирования в ASP.NET

Горизонтальное масштабирование требует хранения информации о состоянии за пределами процессов. В ASP.NET имеется два механизма, обеспечивающих такой способ хранения данных:

Служба управления состоянием (State Service)

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

SQL Server

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

Для нужд кеширования в большинстве случаев можно с успехом использовать один из механизмов распределенного кеширования, таких как Microsoft AppFabric Cache , NCache или Memcached , последний из которых является открытой реализацией распределенного кеша.

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

Некоторые распределенные кеши, такие как AppFabric Cache и Memcached, также имеют собственные реализации службы управления состоянием и провайдеров кеша для ASP.NET.

Ловушки горизонтального масштабирования

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

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

В случае с единственным сервером это не вызывает никаких проблем, но когда веб-приложение выполняется на нескольких серверах, это может превратиться в проблему, так как каждый сервер будет иметь свой собственный уникальный ключ. Представьте такую ситуацию: клиент посылает запрос серверу A и получает в ответ cookie, подписанный уникальным ключом сервера A, затем клиент посылает новый запрос с принятым cookie, который попадает на сервер B. Поскольку сервер B имеет иной уникальный ключ, содержимое cookie признается недействительным и клиенту возвращается сообщение об ошибке.

Управлять генерацией этих ключей в ASP.NET можно путем настройки параметров в разделе machineKey, в файле web.config. Когда веб-приложение выполняется на нескольких серверах, вам необходимо настроить все серверы так, чтобы они использовали один и тот же предварительно сгенерированный ключ.

Другой проблемой, связанной с горизонтальным масштабированием и уникальными ключами, является возможность шифрования разделов в файлах web.config. Закрытая информация в файлах web.config часто шифруется, когда приложение развертывается на серверах. Например, раздел connectionString можно зашифровать, чтобы предотвратить утечку имени пользователя и пароля к базе данных. Вместо того, чтобы шифровать файл web.config на каждом сервере отдельно, усложняя процесс развертывания, можно сгенерировать один зашифрованный файл web.config и развернуть его на всех серверах. Для этого следует создать RSA-контейнер ключей и импортировать его на все веб-серверы.

Более полную информацию о создании уникальных ключей и включения их в настройки приложений можно получить в базе знаний Microsoft Knowledge Base . За дополнительной информацией о создании RSA-контейнера ключей обращайтесь к статье «Импорт и экспорт защищенных контейнеров ключей RSA для конфигурации» на сайте MSDN.

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

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

  • Функциональное разделение
  • Классическое горизонтальное масштабирование
    • Концепции Shared Nothing и Stateless
    • Критика концепций Shared Nothing и Stateless
    • Связность кода и данных
  • Кеширование
    • Проблема инвалидации кеша
    • Проблема старта с непрогретым кешем

Начнем наш третий урок, посвященный бизнес-логике проекта. Это самая главная составляющая в обработке любого запроса. Для таких вычислений требуются бэкенды - тяжелые серверы с большими вычислительными мощностями. Если фронтенд не может отдать клиенту что-то самостоятельно (а как мы выяснили в прошлом номере, он без проблем можем сам отдать, к примеру, картинки), то он делает запрос бекенду. На бэкенде отрабатывается бизнес-логика, то есть формируются и обрабатываются данные, при этом данные хранятся в другом слое - сетевом хранилище, базе данных или файловой системе. Хранение данных - это тема следующего урока, а сегодня мы сосредоточимся на масштабировании бекенда.

Сразу предупредим: масштабирование вычисляющих бэкендов - одна из самых сложных тем, в которой существует множество мифов. Облачные вычисления решают проблему производительности - уверены многие. Однако это верно не до конца: для того чтобы вам действительно могли помочь облачные сервисы, вы должны правильно подготовить ваш программный код. Вы можете поднять сколько угодно серверов, скажем, в Amazon EC2, но какой с них толк, если код не умеет использовать мощности каждого из них. Итак, как масштабировать бэкенд?

Функциональное разделение

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

Несмотря на простоту, о подобном подходе многие забывают. Например, мы очень часто встречаем веб-проекты, где используется только одна база MySQL под совершенно различные типы данных. В одной базе лежат и статьи, и баннеры, и статистика, хотя по-хорошему это должны быть разные экземпляры MySQL. Если у вас есть функционально не связанные данные (как в этом примере), то их целесообразно разносить в разные экземпляры баз данных или даже физические серверы. Посмотрим на это с другой стороны. Если у вас есть в одном проекте и встроенная интегрированная баннерокрутилка, и сервис, который показывает посты пользователей, то разумное решение - сразу осознать, что эти данные никак не связаны между собой и поэтому должны жить в самом простом варианте в двух разных запущенных MySQL. Это относится и к вычисляющим бэкендам - они тоже могут быть разными. С совершенно разными настройками, с разными используемыми технологиями и написанные на разных языках программирования. Возвращаясь к примеру: для показа постов вы можете использовать в качестве бэкенда самый обычный PHP, а для баннерной системы вы можете запустить модуль к nginx’у. Соответственно, для постов вы можете выделить сервер с большим количеством памяти (ну PHP все-таки), при этом для баннерной системы память может быть не так важна, как процессорная емкость.

Сделаем выводы: функциональное разбиение бэкенда целесообразно использовать в качестве простейшего метода масштабирования. Группируйте сходные функции и запускайте их обработчики на разных физических серверах. Обратимся к следующему подходу.

От авторов

Основным направлением деятельности нашей компании является решение проблем, связанных с высокой нагрузкой, консультирование, проектирование масштабируемых архитектур, проведение нагрузочных тестирований и оптимизация сайтов. В число наших клиентов входят инвесторы из России и со всего мира, а также проекты «ВКонтакте», «Эльдорадо», «Имхонет», Photosight.ru и другие. Во время консультаций мы часто сталкиваемся с тем, что многие не знают самых основ - что такое масштабирование и каким оно бывает, какие инструменты и для чего используются. Эта публикация продолжает серию статей «Учебник по высоким нагрузкам». В этих статьях мы постараемся последовательно рассказать обо всех инструментах, которые используются при построении архитектуры высоконагруженных систем.

Классическое горизонтальное масштабирование

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

Концепции Shared Nothing и Stateless

Рассмотрим две концепции - Shared Nothing и Stateless, которые могут обеспечить возможность горизонтального масштабирования.

Подход Shared Nothing означает, что каждый узел является независимым, самодостаточным и нет какой-то единой точки отказа. Это, конечно, не всегда возможно, но в любом случае количество таких точек находится под жестким контролем архитектора. Под точкой отказа мы понимаем некие данные или вычисления, которые являются общими для всех бэкендов. Например, какой-нибудь диспетчер состояний или идентификаторов. Другой пример - использование сетевых файловых систем. Это прямой путь получить на определенном этапе роста проекта узкое место в архитектуре. Если каждый узел является независимым, то мы легко можем добавить еще несколько - по росту нагрузки.

Концепция Stateless означает, что процесс программы не хранит свое состояние. Пользователь пришел и попал на этот конкретный сервер, и нет никакой разницы, попал пользователь на этот сервер или на другой. После того как запрос будет обработан, этот сервер полностью забудет информацию об этом пользователе. Пользователь вовсе не обязан все свои следующие запросы отправлять на этот же сервер, не должен второй раз приходить на него же. Таким образом, мы можем динамически менять количество серверов и не заботиться о том, чтобы роутить пользователя на нужный сервак. Наверное, это одна из серьезных причин, почему веб так быстро развивается. В нем гораздо проще делать приложения, чем писать классические офлайновые программы. Концепция «ответ - запрос» и тот факт, что ваша программа живет 200 миллисекунд или максимум одну секунду (после чего она полностью уничтожается), привели к тому, что в таких распространенных языках программирования, как PHP, до сих пор нет сборщика мусора.

Описанный подход является классическим: он простой и надежный, как скала. Однако в последнее время нам все чаще и чаще приходится отказываться от него.

Критика концепций Shared Nothing и Stateless

Сегодня перед вебом возникают новые задачи, которые ставят новые проблемы. Когда мы говорим про Stateless, это означает, что каждые данные каждому пользователю мы заново тащим из хранилища, а это подчас бывает очень дорого. Возникает резонное желание положить какие-то данные в память, сделать не совсем Stateless. Это связано с тем, что сегодня веб становится все более и более интерактивным. Если вчера человек заходил в веб-почту и нажимал на кнопку «Reload», чтобы проверить новые сообщения, то сегодня этим уже занимается сервер. Он ему говорит: «О, чувак, пока ты сидел на этой страничке, тебе пришли новые сообщения».

Возникают новые задачи, которые приводят к тому, что подход с Shared Nothing и отсутствием состояния в памяти иногда не является обязательным. Мы уже сталкивались неоднократно с ситуациями наших клиентов, которым мы говорим: «От этого откажитесь, положите данные в память» и наоборот «Направляйте людей на один и тот же сервер». Например, когда возникает открытая чат-комната, людей имеет смысл роутить на один и тот же сервер, чтобы это все работало быстрее.

Расскажем про еще один случай, с которым сталкивались. Один наш знакомый разрабатывал на Ruby on Rails игрушку наподобие «Арены» (онлайн драки и бои). Вскоре после запуска он столкнулся с классической проблемой: если несколько человек находятся в рамках одного боя, каждый пользователь постоянно вытаскивает из БД данные, которые во время этого боя возникли. В итоге вся эта конструкция смогла дожить только до 30 тысяч зарегистрированных юзеров, а дальше она просто перестала работать.

Обратная ситуация сложилась у компании Vuga, которая занимается играми для Facebook. Правда, когда они столкнулись с похожей проблемой, у них были другие масштабы: несколько миллиардов SELECT’ов из PostgreSQL в день на одной системе. Они перешли полностью на подход Memory State: данные начали храниться и обслуживаться прямо в оперативной памяти. Итог: ребята практически отказались от базы данных, а пара сотен серверов оказались лишним. Их просто выключили: они стали не нужны.

В принципе, любое масштабирование (в том числе горизонтальное) достижимо на очень многих технологиях. Сейчас очень часто речь идет о том, чтобы при создании сервиса не пришлось платить слишком много за железо. Для этого важно знать, какая технология наиболее соответствует данному профилю нагрузки с минимальными затратами железа. При этом очень часто, когда начинают размышлять о масштабировании, то забывают про финансовый аспект того же горизонтального масштабирования. Некоторые думают, что горизонтальное масштабирование - это реально панацея. Разнесли данные, все разбросали на отдельные серверы - и все стало нормально. Однако эти люди забывают о накладных расходах (оверхедах) - как финансовых (покупка новых серверов), так эксплуатационных. Когда мы разносим все на компоненты, возникают накладные расходы на коммуникацию программных компонентов между собой. Грубо говоря, хопов становится больше. Вспомним уже знакомый тебе пример. Когда мы заходим на страничку Facebook, мощный JavaScript идет на сервер, который долго-долго думает и только через некоторое время начинает отдавать вам ваши данные. Все наблюдали подобную картину: хочется уже посмотреть и бежать дальше пить кофе, а оно все грузится, грузится и грузится. Надо бы хранить данные чуть-чуть «поближе», но у Facebook уже такой возможности нет.

Слоистость кода

Еще пара советов для упрощения горизонтального масштабирования. Первая рекомендация: программируйте так, чтобы ваш код состоял как бы из слоев и каждый слой отвечал за какой-то определенный процесс в цепочке обработки данных. Скажем, если у вас идет работа с базой данных, то она должна осуществляться в одном месте, а не быть разбросанной по всем скриптам. К примеру, мы строим страницу пользователя. Все начинается с того, что ядро запускает модуль бизнес-логики для построения страницы пользователя. Этот модуль запрашивает у нижележащего слоя хранения данных информацию об этом конкретном пользователе. Слою бизнес-логики ничего не известно о том, где лежат данные: закешированы ли они, зашардированы ли (шардинг - это разнесение данных на разные серверы хранения данных, о чем мы будем говорить в будущих уроках), или с ними сделали еще что-нибудь нехорошее. Модуль просто запрашивает информацию, вызывая соответствующую функцию. Функция чтения информации о пользователе расположена в слое хранения данных. В свою очередь, слой хранения данных по типу запроса определяет, в каком именно хранилище хранится пользователь. В кеше? В базе данных? В файловой системе? И далее вызывает соответствующую функцию нижележащего слоя.

Что дает такая слоистая схема? Она дает возможность переписывать, выкидывать или добавлять целые слои. Например, решили вы добавить кеширование для пользователей. Сделать это в слоистой схеме очень просто: надо допилить только одно место – слой хранения данных. Или вы добавляете шардирование, и теперь пользователи могут лежать в разных базах данных. В обычной схеме вам придется перелопатить весь сайт и везде вставить соответствующие проверки. В слоистой схеме нужно лишь исправить логику одного слоя, одного конкретного модуля.

Связность кода и данных

Следующая важная задача, которую необходимо решить, чтобы избежать проблем при горизонтальном масштабировании, - минимизировать связность как кода, так и данных. Например, если у вас в SQL-запросах используются JOIN’ы, у вас уже есть потенциальная проблема. Сделать JOIN в рамках одной базы данных можно. А в рамках двух баз данных, разнесенных по разным серверам, уже невозможно. Общая рекомендация: старайтесь общаться с хранилищем минимально простыми запросами, итерациями, шагами.

Что делать, если без JOIN’а не обойтись? Сделайте его сами: сделали два запроса, перемножили в PHP - в этом нет ничего страшного. Для примера рассмотрим классическую задачу построения френдленты. Вам нужно поднять всех друзей пользователя, для них запросить все последние записи, для всех записей собрать количество комментариев - вот где соблазн сделать это одним запросом (с некоторым количеством вложенных JOIN’ов) особенно велик. Всего один запрос - и вы получаете всю нужную вам информацию. Но что вы будете делать, когда пользователей и записей станет много и база данных перестанет справляться? По-хорошему надо бы расшардить пользователей (разнести равномерно на разные серверы баз данных). Понятно, что в этом случае выполнить операцию JOIN уже не получится: данные-то разделены по разным базам. Так что придется делать все вручную. Вывод очевиден: делайте это вручную с самого начала. Сначала запросите из базы данных всех друзей пользователя (первый запрос). Затем заберите последние записи этих пользователей (второй запрос или группа запросов). Затем в памяти произведите сортировку и выберите то, что вам нужно. Фактически вы выполняете операцию JOIN вручную. Да, возможно вы выполните ее не так эффективно, как это сделала бы база данных. Но зато вы никак не ограничены объемом этой базы данных в хранении информации. Вы можете разделять и разносить ваши данные на разные серверы или даже в разные СУБД! Все это совсем не так страшно, как может показаться. В правильно построенной слоистой системе большая часть этих запросов будет закеширована. Они простые и легко кешируются - в отличие от результатов выполнения операции JOIN. Еще один минус варианта с JOIN: при добавлении пользователем новой записи вам нужно сбросить кеши выборок всех его друзей! А при таком раскладе неизвестно, что на самом деле будет работать быстрее.

Кеширование

Следующий важный инструмент, с которым мы сегодня познакомимся, - кеширование. Что такое кеш? Кеш - это такое место, куда можно под каким-то ключом положить данные, которые долго вычисляют. Запомните один из ключевых моментов: кеш должен вам по этому ключу отдать данные быстрее, чем вычислить их заново. Мы неоднократно сталкивались с ситуацией, когда это было не так и люди бессмысленно теряли время. Иногда база данных работает достаточно быстро и проще сходить напрямую к ней. Второй ключевой момент: кеш должен быть единым для всех бэкендов.

Второй важный момент. Кеш - это скорее способ замазать проблему производительности, а не решить ее. Но, безусловно, бывают ситуации, когда решить проблему очень дорого. Поэтому вы говорите: «Хорошо, эту трещину в стене я замажу штукатуркой, и будем думать, что ее здесь нет». Иногда это работает - более того, это работает очень даже часто. Особенно когда вы попадаете в кеш и там уже лежат данные, которые вы хотели показать. Классический пример - счетчик количества друзей. Это счетчик в базе данных, и вместо того, чтобы перебирать всю базу данных в поисках ваших друзей, гораздо проще эти данные закешировать (и не пересчитывать каждый раз).

Для кеша есть критерий эффективности использования, то есть показатель того, что он работает, - он называется Hit Ratio. Это отношение количества запросов, для которых ответ нашелся в кеше, к общему числу запросов. Если он низкий (50–60%), значит, у вас есть лишние накладные расходы на поход к кешу. Это означает, что практически на каждой второй странице пользователь, вместо того чтобы получить данные из базы, еще и ходит к кешу: выясняет, что данных для него там нет, после чего идет напрямую к базе. А это лишние две, пять, десять, сорок миллисекунд.

Как обеспечивать хорошее Hit Ratio? В тех местах, где у вас база данных тормозит, и в тех местах, где данные можно перевычислять достаточно долго, там вы втыкаете Memcache, Redis или аналогичный инструмент, который будет выполнять функцию быстрого кеша, - и это начинает вас спасать. По крайней мере, временно.

Олег Бунин

Известный специалист по Highload-проектам. Его компания «Лаборатория Олега Бунина» специализируется на консалтинге, разработке и тестировании высоконагруженных веб-проектов. Сейчас является организатором конференции HighLoad++ (www.highload.ru). Это конференция, посвященная высоким нагрузкам, которая ежегодно собирает лучших в мире специалистов по разработке крупных проектов. Благодаря этой конференции знаком со всеми ведущими специалистами мира высоконагруженных систем.

Константин Осипов

Специалист по базам данных, который долгое время работал в MySQL, где отвечал как раз за высоконагруженный сектор. Быстрота MySQL - в большой степени заслуга именно Кости Осипова. В свое время он занимался масштабируемостью MySQL 5.5. Сейчас отвечает в Mail.Ru за кластерную NoSQL базу данных Tarantool, которая обслуживает 500–600 тысяч запросов в секунду. Использовать этот Open Source проект может любой желающий.

Максим Лапшин

Решения для организации видеотрансляции, которые существуют в мире на данный момент, можно пересчитать по пальцам. Макс разработал одно из них - Erlyvideo (erlyvideo.org). Это серверное приложение, которое занимается потоковым видео. При создании подобных инструментов возникает целая куча сложнейших проблем со скоростью. У Максима также есть некоторый опыт, связанный с масштабированием средних сайтов (не таких крупных, как Mail.Ru). Под средними мы подразумеваем такие сайты, количество обращений к которым достигает около 60 миллионов в сутки.

Константин Машуков

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

Проблема инвалидации кеша

Но с использованием кеша вы бонусом получаете проблему инвалидации кеша. В чем суть? Вы положили данные в кеш и берете их из кеша, однако к этому моменту оригинальные данные уже поменялись. Например, Машенька поменяла подпись под своей картинкой, а вы зачем-то положили одну строчку в кеш вместо того, чтобы тянуть каждый раз из базы данных. В результате вы показываете старые данные - это и есть проблема инвалидации кеша. В общем случае она не имеет решения, потому что эта проблема связана с использованием данных вашего бизнес-приложения. Основной вопрос: когда обновлять кеш? Ответить на него подчас непросто. Например, пользователь публикует в социальной сети новый пост - допустим, в этот момент мы пытаемся избавиться от всех инвалидных данных. Получается, нужно сбросить и обновить все кеши, которые имеют отношение к этому посту. В худшем случае, если человек делает пост, вы сбрасываете кеш с его ленты постов, сбрасываете все кеши с ленты постов его друзей, сбрасываете все кеши с ленты людей, у которых в друзьях есть те, кто в этом сообществе, и так далее. В итоге вы сбрасываете половину кешей в системе. Когда Цукерберг публикует пост для своих одиннадцати с половиной миллионов подписчиков, мы что - должны сбросить одиннадцать с половиной миллионов кешей френдлент у всех этих subscriber’ов? Как быть с такой ситуацией? Нет, мы пойдем другим путем и будем обновлять кеш при запросе на френдленту, где есть этот новый пост. Система обнаруживает, что кеша нет, идет и вычисляет заново. Подход простой и надежный, как скала. Однако есть и минусы: если сбросился кеш у популярной страницы, вы рискуете получить так называемые race-condition (состояние гонок), то есть ситуацию, когда этот самый кеш будет одновременно вычисляться несколькими процессами (несколько пользователей решили обратиться к новым данным). В итоге ваша система занимается довольно пустой деятельностью - одновременным вычислением n-го количества одинаковых данных.

Один из выходов - одновременное использование нескольких подходов. Вы не просто стираете устаревшее значение из кеша, а только помечаете его как устаревшее и одновременно ставите задачу в очередь на пересчет нового значения. Пока задание в очереди обрабатывается, пользователю отдается устаревшее значение. Это называется деградация функциональности: вы сознательно идете на то, что некоторые из пользователей получат не самые свежие данные. Большинство систем с продуманной бизнес-логикой имеют в арсенале подобный подход.

Проблема старта с непрогретым кешем

Еще одна проблема - старт с непрогретым (то есть незаполненным) кешем. Такая ситуация наглядно иллюстрирует утверждение о том, что кеш не может решить проблему медленной базы данных. Предположим, что вам нужно показать пользователям 20 самых хороших постов за какой-либо период. Эта информация была у вас в кеше, но к моменту запуска системы кеш был очищен. Соответственно, все пользователи обращаются к базе данных, которой для построения индекса нужно, скажем, 500 миллисекунд. В итоге все начинает медленно работать, и вы сами себе сделали DoS (Denial-of-service). Сайт не работает. Отсюда вывод: не занимайтесь кешированием, пока у вас не решены другие проблемы. Сделайте, чтобы база быстро работала, и вам не нужно будет вообще возиться с кешированием. Тем не менее даже у проблемы старта с незаполненным кешем есть решения:

  1. Использовать кеш-хранилище с записью на диск (теряем в скорости);
  2. Вручную заполнять кеш перед стартом (пользователи ждут и негодуют);
  3. Пускать пользователей на сайт партиями (пользователи все так же ждут и негодуют).

Как видите, любой способ плох, поэтому лишь повторимся: старайтесь сделать так, чтобы ваша система работала и без кеширования.

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

Приступим?

Для начала имеет смысл определиться с тем, о чем мы вообще будем говорить. В данном контексте перед веб-приложением ставятся три основные цели:

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

Основной темой разговора будет, как не трудно догадаться, масштабируемость, но и остальные цели не думаю, что останутся в стороне. Сразу хочется сказать пару слов про доступность, чтобы не возвращаться к этому позднее, подразумевая как "само собой разумеется": любой сайт так или иначе стремится к тому, чтобы функционировать максимально стабильно, то есть быть доступным абсолютно всем своим потенциальным посетителям в абсолютно каждый момент времени, но порой случаются всякие непредвиденные ситуации, которые могут стать причиной временной недоступности. Для минимизации потенциального ущерба доступности приложения необходимо избегать наличия компонентов в системе, потенциальный сбой в которых привел бы к недоступности какой-либо функциональности или данных (или хотябы сайта в целом). Таким образом каждый сервер или любой другой компонент системы должен иметь хотябы одного дублера (не важно в каком режиме они будут работать: параллельно или один "подстраховывает" другой, находясь при этом в пассивном режиме), а данные должны быть реплицированы как минимум в двух экземплярах (причем желательно не на уровне RAID, а на разных физических машинах). Хранение нескольких резервных копий данных где-то отдельно от основной системы (например на специальных сервисах или на отдельном кластере) также поможет избежать многих проблем, если что-то пойдет не так. Не стоит забывать и о финансовой стороне вопроса: подстраховка на случай сбоев требует дополнительных существенных вложений в оборудование, которые имеет смысл стараться минимизировать.

Масштабируемость принято разделять на два направления:

Вертикальная масштабируемость Увеличение производительности каждого компонента системы c целью повышения общей производительности. Горизонтальная масштабируемость Разбиение системы на более мелкие структурные компоненты и разнесение их по отдельным физическим машинам (или их группам) и/или увеличение количества серверов параллельно выполняющих одну и ту же функцию.

Так или иначе, при разработке стратегии роста системы приходится искать компромис между ценой, временем разработки, итоговой производительность, стабильностью и еще массой других критериев. С финансовой точки зрения вертикальная масштабируемость является далеко не самым привлекательным решением, ведь цены на сервера с большим количеством процессоров всегда растут практически экспоненциально относительно количества процессоров. Именно по-этому наиболее интересен горизонтальный подход, так как именно он используется в большинстве случаев. Но и вертикальная масштабируемость порой имеет право на существование, особенно в ситуациях, когда основную роль играет время и скорость решения задачи, а не финансовый вопрос: ведь купить БОЛЬШОЙ сервер существенно быстрее, чем практически заново разрабатывать приложения, адаптируя его к работе на большом количестве параллельно работающих серверов.

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

Серверы приложений

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

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

Балансировка нагрузки

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

Оборудование Сетевое оборудование, позволяющее распределять нагрузку между несколькими серверами, обычно стоит достаточно внушительные суммы, но среди прочих вариантов обычно именно этот подход предлагает наивысшую производительность и стабильность (в основном благодаря качеству, плюс такое оборудование иногда поставляется парами, работающими по принципу ). В этой индустрии достаточно много серьезных брендов, предлагающих свои решения - есть из чего выбрать: Cisco , Foundry , NetScalar и многие другие. Программное обеспечение В этой области еще большее разнообразие возможных вариантов. Получить программно производительность сопоставимую с аппаратными решениями не так-то просто, да и HeartBeat придется обеспечивать программно, но зато оборудование для функционирования такого решения представляет собой обычный сервер (возможно не один). Таких программных продуктов достаточно много, обычно они представляют собой просто HTTP-серверы, перенаправляющие запросы своим коллегам на других серверах вместо отправки напрямую на обработку интерпретатору языка программирования. Для примера можно упомянуть, скажем, с mod_proxy . Помимо этого имеют место более экзотические варианты, основанные на DNS, то есть в процессе определения клиентом IP-адреса сервера с необходимым ему интернет-ресурсов адрес выдается с учетом нагрузки на доступные сервера, а также некоторых географических соображений.

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

Ресурсоемкие вычисления

Во многих приложениях используются какие-либо сложные механизмы, это может быть конвертирование видео, изображений, звука, или просто выполнение каких-либо ресурсоемких вычислений. Такие задачи требует отдельного внимания если мы говорим о Сети, так как пользователь интернет-ресурса врядли будет счастлив наблюдать за загружающейся несколько минут страницей в ожидании лишь для того, чтобы увидеть сообщение вроде: "Операция завершена успешно!".

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

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

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

Сессии

Практически все веб-приложения каким-либо образом взаимодействуют со своими посетителями и в подавляющем большинстве случаев в них присутствует необходимость отслеживать перемещения пользователей по страницам сайта. Для решения этой задачи обычно используется механизм сессий , который заключается в присвоении каждому посетителю уникального идентификационного номера, который ему передается для хранения в cookies или, в случае их отсутствия, для постоянного "таскания" за собой через GET. Получив от пользователя некий ID вместе с очередным HTTP-запросом сервер может посмотреть в список уже выданных номеров и однозначно определить кто его отправил. С каждым ID может ассоциироваться некий набор данных, который веб-приложение может использовать по своему усмотрению, эти данные обычно по-умолчанию хранятся в файле во временной директории на сервере.

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

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

Централизованное хранение сессий Идея проста: создать для всех серверов общую "копилку", куда они смогут складывать выданные ими сессии и узнавать о сессиях посетителей других серверов. В роли такой "копилки" теоретически может выступать и просто примонтированная по сети файловая система, но по некоторым причинам более перспективным выглядит использование какой-либо СУБД, так как это избавляет от массы проблем, связанных с хранением сессионных данных в файлах. Но в варианте с общей базой данных не стоит забывать, что нагрузка на него будет неуклонно расти с ростом количества посетителей, а также стоит заранее предусмотреть варианты выхода из проблематичных ситуаций, связанных с потенциальными сбоями в работе сервера с этой СУБД. Децентрализованное хранение сессий Наглядный пример - хранение сессий в , изначально расчитанная на распределенное хранение данных в оперативной памяти система позволит получать всем серверам быстрый доступ к любым сессионным данным, но при этом (в отличии от предыдущего способа) какой-либо единый центр их хранения будет отсутствовать. Это позволит избежать узких мест с точек зрения производительности и стабильности в периоды повышенных нагрузок.

В качестве альтернативы сессиям иногда используют похожие по предназначению механизмы, построенные на cookies, то есть все необходимые приложению данные о пользователе хранятся на клиентской стороне (вероятно в зашифрованном виде) и запрашиваются по мере необходимости. Но помимо очевидных преимуществ, связанных с отсутствием необходимости хранить лишние данные на сервере, возникает ряд проблем с безопасностью. Данные, хранимые на стороне клиента даже в зашифрованном виде, представляют собой потенциальную угрозу для функционирования многих приложений, так как любой желающий может попытаться модифицировать их в своих интересах или с целью навредить приложению. Такой подход хорош только если есть уверенность, что абсолютно любые манипуляции с хранимые у пользователей данными безопасны. Но можно ли быть уверенными на 100%?

Статический контент

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

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

Возможно такой вариант по каким-либо причинам будет нереализуем, тогда придется "изобретать велосипед" для реализации на уровне приложения принципов схожих с сегментированием данных в отношении СУБД, о которых я еще упомяну далее. Этот вариант также вполне эффективен, но требует модификации логики приложения, а значит и выполнение дополнительной работы разработчиками.

Альтернативой этим подходам выступает использование так называемых Content Delievery Network - внешних сервисов, обеспечивающих доступность Вашего контента пользователям за определенное материальное вознаграждение сервису. Преимущество очевидно - нет необходимости организовывать собственную инфраструктуру для решения этой задачи, но зато появляется другая дополнительная статья расходов. Список таких сервисов приводить не буду, если кому-нибудь понадобится - найти будет не трудно.

Кэширование

Кэширование имеет смысл проводить на всех этапах обработки данных, но в разных типах приложений наиболее эффективными являются лишь некоторые методы кэширования.

СУБД Практически все современные СУБД предоставляют встроенные механизмы для кэширования результатов определенных запросов. Этот метод достаточно эффективен, если Ваша система регулярно делает одни и те же выборки данных, но также имеет ряд недостатков, основными из которых является инвалидация кэша всей таблицы при малейшем ее изменении, а также локальное расположение кэша, что неэффективно при наличии нескольких серверов в системе хранения данных. Приложение На уровне приложений обычно производится кэширование объектов любого языка программирования. Этот метод позволяет вовсе избежать существенной части запросов к СУБД, сильно снижая нагрузку на нее. Как и сами приложения такой кэш должен быть независим от конкретного запроса и сервера, на котором он выполняется, то есть быть доступным всем серверам приложений одновременно, а еще лучше - быть распределенным по нескольким машинам для более эффективной утилизации оперативной памяти. Лидером в этом аспекте кэширования по праву можно назвать , о котором я в свое время уже успел . HTTP-сервер Многие веб-серверы имеют модули для кэширования как статического контента, так и результатов работы скриптов. Если страница редко обновляется, то использование этого метода позволяет без каких-либо видимых для пользователя изменений избегать генерации страницы в ответ на достаточно большую часть запросов. Reverse proxy Поставив между пользователем и веб-сервером прозрачный прокси-сервер, можно выдавать пользователю данные из кэша прокси (который может быть как в оперативной памяти, так и дисковым), не доводя запросы даже до HTTP-серверов. В большинстве случаев этот подход актуален только для статического контента, в основном разных форм медиа-данных: изображений, видео и тому подобного. Это позволяет веб-серверам сосредоточиться только на работе с самими страницами.

Кэширование по своей сути практически не требует дополнительных затрат на оборудование, особенно если внимательно наблюдать за использованием оперативной памяти остальными компонентами серверами и утилизировать все доступные "излишки" под наиболее подходящие конкретному приложению формы кэша.

Инвалидация кэша в некоторых случаях может стать нетривиальной задачей, но так или иначе универсального решения всех возможных проблем с ней связанных написать не представляется возможным (по крайней мере лично мне), так что оставим этот вопрос до лучших времен. В общем случае решение этой задачи ложится на само веб-приложение, которое обычно реализует некий механизм инвалидации средствами удаления объекта кэша через определенный период времени после его создания или последнего использования, либо "вручную" при возникновении определенных событий со стороны пользователя или других компонентов системы.

Базы данных

На закуску я оставил самое интересное, ведь этот неотъемлемый компонент любого веб-приложения вызывает больше проблем при росте нагрузок, чем все остальные вместе взятые. Порой даже может показаться, что стоит вообще отказаться от горизонтального масштабирования системы хранения данных в пользу вертикального - просто купить тот самый БОЛЬШОЙ сервер за шести- или семизначную сумму не-рублей и не забивать себе голову лишними проблемами.

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

Путь практически любого веб проекта с точки зрения баз данных начинался с одного простого сервера, на котором работал весь проект целиком. Затем в один прекрасный момент наступает необходимость вынести СУБД на отдельный сервер, но и он со временем начинает не справляться с нагрузкой. Подробно останавливаться на этих двух этапах смысла особого нет - все относительно тривиально.

Следующим шагом обычно бывает master-slave с асинхронной репликацией данных, как работает эта схема уже неоднократно упоминалось в блоге, но, пожалуй, повторюсь: при таком подходе все операции записи выполняются лишь на одном сервере (master), а остальные сервера (slave) получают данные напрямую от "мастера", обрабатывая при этом лишь запросы на чтение данных. Как известно, операции чтения и записи любого веб-проекта всегда растут пропорционально росту нагрузки, при этом сохраняется почти фиксированным соотношение между обоими типами запросов: на каждый запрос на обновление данных обычно приходится в среднем около десятка запросов на чтение. Со временем нагрузка растет, а значит растет и количество операций записи в единицу времени, а сервер-то обрабатывает их всего один, а затем он же еще и обеспечивает создание некоторого количества копий на других серверах. Рано или поздно издержки операций репликации данных станут быть настолько высоки, что этот процесс станет занимать очень большую часть процессорного времени каждого сервера, а каждый slave сможет обрабатывать лишь сравнительно небольшое количество операций чтения, и, как следствие, каждый дополнительный slave-сервер начнет увеличивать суммарную производительность лишь незначительно, тоже занимаясь по большей части лишь поддержанием своих данных в соответствии с "мастером".

Временным решением этой проблемы, возможно, может стать замена master-сервера на более производительный, но так или иначе не выйдет бесконечно откладывать переход на следующий "уровень" развития системы хранения данных: "sharding" , которому я совсем недавно посвятил . Так что позволю себе остановиться на нем лишь вкратце: идея заключается в том, чтобы разделить все данные на части по какому-либо признаку и хранить каждую часть на отдельном сервере или кластере, такую часть данных в совокупности с системой хранения данных, в которой она находится, и называют сегментом или shard ’ом. Такой подход позволяет избежать издержек, связанных с реплицированием данных (или сократить их во много раз), а значит и существенно увеличить общую производительность системы хранения данных. Но, к сожалению, переход к этой схеме организации данных требует массу издержек другого рода. Так как готового решения для ее реализации не существует, приходится модифицировать логику приложения или добавлять дополнительную "прослойку" между приложением и СУБД, причем все это чаще всего реализуется силами разработчиков проекта. Готовые продукты способны лишь облегчить их работу, предоставив некий каркас для построения основной архитектуры системы хранения данных и ее взаимодействия с остальными компонентами приложения.

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

Денормализация Запросы, комбинирующие данные из нескольких таблиц, обычно при прочих равных требуют большего процессорного времени для выполнения, чем запрос, затрагивающий лишь одну таблицу. А производительность, как уже упоминалось в начале повествования, чрезвычайно важна на просторах Сети. Логическое разбиение данных Если какая-то часть данных всегда используется отдельно от основной массы, то иногда имеет смысл выделить ее в отдельную независимую систему хранения данных. Низкоуровневая оптимизация запросов Ведя и анализируя логи запросов, можно определить наиболее медленные из них. Замена найденных запросов на более эффективные с той же функциональностью может помочь более рационально использовать вычислительные мощности.

В этом разделе стоит упомянуть еще один, более специфический, тип интернет-проектов. Такие проекты оперируют данными, не имеющими четко формализованную структуру, в таких ситуациях использование реляционных СУБД в качестве хранилища данных, мягко говоря, нецелесообразно. В этих случаях обычно используют менее строгие базы данных, с более примитивной функциональностью в плане обработки данных, но зато они способны обрабатывать огромные объемы информации не придираясь к его качеству и соответствию формату. В качестве основы для такого хранилища данных может служить кластерная файловая система, а для анализа же данных в таком случае используется механизм под названием , принцип его работы я расскажу лишь вкратце, так как в полном своем масштабе он несколько выходит за рамки данного повествования.

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

Map Основной целью данного этапа является представление произвольных входных данных в виде промежуточных пар ключ-значение, имеющих определенный смысл и формально оформленных. Результаты подвергаются сортировке и группированию по ключу, а после чего передаются на следующий этап. Reduce Полученные после map значения используются для финального вычисления требуемых итоговых данных.

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

Примером готового каркаса для реализации работы с данными по такому принципу служит opensource проект Apache Foundation под названием , о котором я уже неоднократно рассказывал ранее, да и написал в свое время.

Вместо заключения

Если честно, мне с трудом верится, что я смог написать настолько всеобъемлющий пост и сил на подведение итогов уже практически не осталось. Хочется лишь сказать, что в разработке крупных проектов важна каждая деталь, а неучтенная мелочь может стать причиной провала. Именно по-этому в этом деле учиться стоит не на своих ошибках, а на чужих.

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

С ростом популярности web-приложения его поддержка неизбежно начинает требовать всё больших и больших ресурсов. Первое время с нагрузкой можно (и, несомненно, нужно) бороться путём оптимизации алгоритмов и/или архитектуры самого приложения. Однако, что делать, если всё, что можно было оптимизировать, уже оптимизировано, а приложение всё равно не справляется с нагрузкой?

Оптимизация

Первым делом стоит сесть и подумать, а всё ли вам уже удалось оптимизировать:
  • оптимальны ли запросы к БД (анализ EXPLAIN, использование индексов)?
  • правильно ли хранятся данные (SQL vs NoSQL)?
  • используется ли кеширование?
  • нет ли излишних запросов к ФС или БД?
  • оптимальны ли алгоритмы обработки данных?
  • оптимальны ли настройки окружения: Apache/Nginx, MySQL/PostgreSQL, PHP/Python?
О каждом из этих пунктов можно написать отдельную статью, так что детальное их рассмотрение в рамках данной статьи явно избыточно. Важно лишь понимать, что перед тем как приступить к масштабированию приложения, крайне желательно максимально оптимизировать его работу – ведь возможно тогда никакого масштабирования и не потребуется.

Масштабирование

И так, допустим, что оптимизация уже проведена, но приложение всё равно не справляется с нагрузкой. В таком случае решением проблемы, очевидно, может послужить разнесение его по нескольким хостам, с целью увеличения общей производительности приложения за счёт увеличения доступных ресурсов. Такой подход имеет официальное название – «масштабирование» (scale) приложения. Точнее говоря, под «масштабируемостью » (scalability) называется возможность системы увеличивать свою производительность при увеличении количества выделяемых ей ресурсов. Различают два способа масштабирования: вертикальное и горизонтальное. Вертикальное масштабирование подразумевает увеличение производительности приложения при добавлении ресурсов (процессора, памяти, диска) в рамках одного узла (хоста). Горизонтальное масштабирование характерно для распределённых приложений и подразумевает рост производительности приложения при добавлении ещё одного узла (хоста).

Понятно, что самым простым способом будет простое обновление железа (процессора, памяти, диска) – то есть вертикальное масштабирование. Кроме того, этот подход не требует никаких доработок приложения. Однако, вертикальное масштабирование очень быстро достигает своего предела, после чего разработчику и администратору ничего не остаётся кроме как перейти к горизонтальному масштабированию приложения.

Архитектура приложения

Большинство web-приложений априори являются распределёнными, так как в их архитектуре можно выделить минимум три слоя: web-сервер, бизнес-логика (приложение), данные (БД, статика).

Каждый их этих слоёв может быть масштабирован. Поэтому если в вашей системе приложение и БД живут на одном хосте – первым шагом, несомненно, должно стать разнесение их по разным хостам.

Узкое место

Приступая к масштабированию системы, первым делом стоит определить, какой из слоёв является «узким местом» - то есть работает медленнее остальной системы. Для начала можно воспользоваться банальными утилитами типа top (htop) для оценки потребления процессора/памяти и df, iostat для оценки потребления диска. Однако, желательно выделить отдельный хост, с эмуляцией боевой нагрузки (c помощью или JMeter), на котором можно будет профилировать работу приложения с помощью таких утилит как xdebug , и так далее. Для выявления узких запросов к БД можно воспользоваться утилитами типа pgFouine (понятно, что делать это лучше на основе логов с боевого сервера).

Обычно всё зависит от архитектуры приложения, но наиболее вероятными кандидатами на «узкое место» в общем случае являются БД и код. Если ваше приложение работает с большим объёмом пользовательских данных, то «узким местом», соответственно, скорее всего будет хранение статики.

Масштабирование БД

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

Снизить нагрузку на БД можно разнеся её на несколько хостов. При этом остро встаёт проблема синхронизации между ними, решить которую можно путём реализации схемы master/slave с синхронной или асинхронной репликацией. В случае с PostgreSQL реализовать синхронную репликацию можно с помощью Slony-I , асинхронную – PgPool-II или WAL (9.0). Решить проблему разделения запросов чтения и записи, а так же балансировки нагрузку между имеющимися slave’ами, можно с помощью настройки специального слоя доступа к БД (PgPool-II).

Проблему хранения большого объёма данных в случае использования реляционных СУБД можно решить с помощью механизма партицирования (“partitioning” в PostgreSQL), либо разворачивая БД на распределённых ФС типа Hadoop DFS .

Однако, для хранения больших объёмов данных лучшим решением будет «шардинг » (sharding) данных, который является встроенным преимуществом большинства NoSQL БД (например, MongoDB).

Кроме того, NoSQL БД в общем работают быстрее своих SQL-братьев за счёт отсутствия overhead’а на разбор/оптимизацию запроса, проверки целостности структуры данных и т.д. Тема сравнения реляционных и NoSQL БД так же довольно обширна и заслуживает .

Отдельно стоит отметить опыт Facebook, который используют MySQL без JOIN-выборок. Такая стратегия позволяет им значительно легче масштабировать БД, перенося при этом нагрузку с БД на код, который, как будет описано ниже, масштабируется проще БД.

Масштабирование кода

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

Далее необходимо настроить балансировку нагрузки/запросов между этими хостами. Сделать это можно как на уровне TCP (haproxy), так и на HTTP (nginx) или DNS .

Следующим шагом нужно сделать так, что бы файлы статики, cache и сессии web-приложения были доступны на каждом хосте. Для сессий можно использовать сервер, работающий по сети (например, memcached). В качестве сервера кеша вполне разумно использовать тот же memcached, но, естественно, на другом хосте.

Файлы статики можно смонтировать с некого общего файлового хранилища по NFS /CIFS или использовать распределённую ФС (HDFS , GlusterFS , Ceph).

Так же можно хранить файлы в БД (например, Mongo GridFS), решая тем самым проблемы доступности и масштабируемости (с учётом того, что для NoSQL БД проблема масштабируемости решена за счёт шардинга).

Отдельно стоит отметить проблему деплоймента на несколько хостов. Как сделать так, что бы пользователь, нажимая «Обновить», не видел разные версии приложения? Самым простым решением, на мой взгляд, будет исключение из конфига балансировщика нагрузки (web-сервера) не обновлённых хостов, и последовательного их включения по мере обновления. Так же можно привязать пользователей к конкретным хостам по cookie или IP. Если же обновление требует значимых изменений в БД, проще всего, вообще временно закрыть проект.

Масштабирование ФС

При необходимости хранения большого объёма статики можно выделить две проблемы: нехватка места и скорость доступа к данным. Как уже было написано выше, проблему с нехваткой места можно решить как минимум тремя путями: распределённая ФС, хранение данных в БД с поддержкой шардинга и организация шардинга «вручную» на уровне кода.

При этом стоит понимать, что раздача статики тоже не самая простая задача, когда речь идёт о высоких нагрузках. Поэтому в вполне резонно иметь множество серверов предназначенных для раздачи статики. При этом, если мы имеем общее хранилище данных (распределённая ФС или БД), при сохранении файла мы можем сохранять его имя без учёта хоста, а имя хоста подставлять случайным образом при формировании страницы (случайным образом балансирую нагрузку между web-серверами, раздающими статику). В случае, когда шардинг реализуется вручную (то есть, за выбор хоста, на который будут залиты данные, отвечает логика в коде), информация о хосте заливки должна либо вычисляться на основе самого файла, либо генерироваться на основании третьих данных (информация о пользователе, количестве места на дисках-хранилищах) и сохраняться вместе с именем файла в БД.

Мониторинг

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

Заключение

Выше кратко рассмотрено множество вариантов решений проблем масштабирования web-приложения. Каждый из них обладает своими достоинствами и недостатками. Не существует некоторого рецепта, как сделать всё хорошо и сразу – для каждой задачи найдётся множество решений со своими плюсами и минусами. Какой из них выбрать – решать вам.