Горизонтальная масштабируемость. Вертикальное и горизонтальное масштабирование, scaling для web

Модель доверенная подсистема (или доверенный сервер)

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

Модель с применением множества доверенных удостоверений сервиса

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

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

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

горизонтальное (больше блоков).

При вертикальном масштабировании поддержка повышенной нагрузки обеспечивается через введение в существующие серверы дополнительного оборудования, такого как процессоры, оперативная память и сетевые интерфейсные платы (network interface cards, NIC). Такой простой вариант не добавляет затрат на обслуживание и поддержку, но может быть экономически выгодным лишь до определенного момента. Однако всегда сохраняется вероятность сбоя, что является риском. Кроме того, введение дополнительного оборудования в существующие серверы обеспечивает желаемые результаты не бесконечно, и получение последних 10% расчетной производительности путем наращивания мощностей одного компьютера может быть очень дорогим удовольствием.

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

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

Вопросы вертикального масштабирования

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

Проектирование с поддержкой горизонтального масштабирования

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

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

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

Компромиссы и последствия их принятия

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

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

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

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

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

Компоненты без сохранения состояния

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

Секционирование данных и базы данных

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

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

Принимаемые в сценариях развертывания решения о секционировании хранилища данных во многом определяются типом данных. Рассмотрим значимые факторы:

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

тиражирование требует применения механизмов обеспечения синхронизации системы.

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

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

Данные с отложенной синхронизацией. Некоторые используемые в приложениях данные не требуют немедленной синхронизации или синхронизации вообще. Отличный пример – такие данные онлайн-магазинов, как «С товаром Х часто покупают Y и Z». Эти данные извлекаются из основных данных, но не требуют обновления в режиме реального времени. Проектирование стратегий, обеспечивающих перевод данных из основных в секционируемые (динамические) и затем в статические, является ключевым фактором в построении высокомасштабируемых приложений.

Более подробно схемы перемещения и тиражирования данных рассматриваются в статье «Data Movement Patterns » (Шаблоны передачи данных) по адресу http://msdn.microsoft.com/en-us/library/ms998449.aspx .

|

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

Однако это поправимо. И если сейчас вы подумали о масштабировании – вы на правильном пути.

В двух словах, масштабируемость – это способность системы обрабатывать большой объем трафика и приспособляться к его росту, сохраняя при этом необходимый UX. Существует два метода масштабирования:

  • Вертикальное (также называется scaling up): увеличение системных ресурсов, например, добавление памяти и вычислительной мощности. Этот метод позволяет быстро устранить проблемы с обработкой трафика, но его ресурсы могут быстро себя исчерпать.
  • Горизонтальное (или scaling out): добавление серверов в кластер. Рассмотрим этот метод подробнее.

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

Проще говоря, кластер – это группа серверов. Балансировщик нагрузки – это сервер, распределяющий рабочую нагрузку между серверами в кластере. В любой момент в существующий кластер можно добавить веб-сервер для обработки большего объёма трафика. В этом и есть суть горизонтального масштабирования.

Балансировщик нагрузки отвечает только за то, какой сервер из кластера будет обрабатывать полученный запрос. в основном, он работает как обратный прокси-сервер.

Горизонтальное масштабирование – несомненно, более надёжный метод увеличения производительности приложения, однако оно сложнее в настройке, чем вертикальное масштабирование. Главная и самая сложная задача в этом случае – постоянно поддерживать все ноды приложения обновленными и синхронизированными. Предположим, пользователь А отправляет запрос сайту mydomain.com, после чего балансировщик передаёт запрос на сервер 1. Тогда запрос пользователя Б будет обрабатываться сервером 2.

Что произойдёт, если пользователь А внесёт изменения в приложение (например, выгрузит какой-нибудь файл или обновит содержимое БД)? Как передать это изменение остальным серверам кластера?

Ответ на эти и другие вопросы можно найти в этой статье.

Разделение серверов

Подготовка системы к масштабированию требует разделения серверов; при этом очень важно, чтобы серверы с меньшим объёмом ресурсов имели меньше обязанностей, чем более объёмные серверы. Кроме того, разделение приложения на такие «части» позволит быстро определить его критические элементы.

Предположим, у вас есть PHP-приложение, позволяющее проходить аутентификацию и выкладывать фотографии. Приложение основано на стеке LAMP. Фотографии сохраняются на диске, а ссылки на них – в базе данных. Задача здесь заключается в поддержке синхронизации между несколькими серверами приложений, которые совместно используют эти данные (загруженные файлы и сессии пользователя).

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

В дальнейшем можно настроить балансировку нагрузки; об этом можно прочесть в руководстве « »

Сессионная согласованность

Разделив веб-сервер и базу данных, нужно сосредоточиться на обработке пользовательских сессий.

Реляционные базы данных и сетевые файловые системы

Данные сессий часто хранят в реляционных базах данных (таких как MySQL), потому что это такие базы легко настроить.

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

Сетевые файловые системы – ещё один простой способ хранения данных; при этом не требуется вносить изменения в базу исходных текстов, однако сетевые системы очень медленно обрабатывают I/O операции, а это может оказать негативное влияние на производительность приложения.

Липкие сессии

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

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

Серверы Memcached и Redis

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

Заключительные действия

Горизонтальное масштабирование приложения сначала кажется очень сложным и запутанным решением, однако оно помогает устранить серьёзные проблемы с трафиком. Главное – научиться работать с балансировщиком нагрузки, чтобы понимать, какие из компонентов требуют дополнительной настройки.

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

Tags: ,

Александр Макаров - разработчик популярного фреймворка Yii расскажет про масштабирование веб-проектов.

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

Масштабирование различают двух типов: вертикальное (больше памяти, диска, лучше процессор) и горизонтальное (больше серверов в кластере).

  • Зачем оно нужно, если и так всё работает?
  • Когда? Мониторинг, необдуманные решения, оптимизация и жизнь с одним сервером.
  • Типичная схема.
  • Балансировка нагрузки.
  • Какие, вообще, проблемы на стороне приложения?
  • Почему PHP так хорош для масштабирования.
  • Сессии.
  • База данных.
  • Файлы.
  • Как быть со статистикой?

Александр Макаров (Yii, Stay.com)

Здравствуйте! Я Александр Макаров, и вы можете меня знать по фреймворку «Yii» — я один из его разработчиков. У меня также есть full-time работа — и это уже не стартап — Stay.com, который занимается путешествиями.

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

Что такое масштабирование, вообще? Это возможность увеличить производительность проекта за минимальное время путем добавления ресурсов.

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

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

Самый классный вопрос, который задают, — а зачем оно надо, если у меня все и на одном сервере прекрасно работает? На самом-то деле, надо проверить, что будет. Т.е., сейчас оно работает, но что будет потом? Есть две замечательные утилиты — ab и siege, которые как бы нагоняют тучу пользователей конкурента, которые начинают долбить сервер, пытаются запросить странички, послать какие-то запросы. Вы должны указать, что им делать, а утилиты формируют такие вот отчеты:

Главные два параметра: n — количество запросов, которые надо сделать, с — количество одновременных запросов. Таким образом они проверяют конкурентность.

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

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

Бывает, что на самом деле пока и не надо заботиться о масштабировании — идем на сервер, обновляем PHP, получаем 40% прироста производительности и все круто. Далее настраиваем Opcache, тюним его. Opcache, кстати, тюнится так же, как и APC, скриптом, который можно найти в репозитории у Расмуса Лердорфа и который показывает хиты и мисы, где хиты — это сколько раз PHP пошел в кэш, а мисы — сколько раз он пошел в файловую систему доставать файлики. Если прогнать весь сайт, либо запустить туда какой-то краулер по ссылкам, либо вручную потыкать, то у нас будет статистика по этим хитам и мисам. Если хитов 100%, а мисов — 0%, значит, все нормально, а если есть мисы, то надо выделить больше памяти, чтобы весь наш код влез в Opcache. Это частая ошибка, которую допускают — вроде Opcache есть, но что-то не работает...

Еще часто начинают масштабировать, но не смотрят, вообще, из-за чего все работает медленно. Чаще всего лезем в базу, смотрим — индексов нет, ставим индексы — все сразу залетало, еще на 2 года хватит, красота!

Ну, еще надо включить кэш, заменить apache на nginx и php-fpm, чтобы сэкономить память. Будет все классно.

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

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

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

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

На что нужно обращать внимание прямо сейчас при мониторинге? Это:

  1. доступность, т.е. жив сервер, вообще, или нет;
  2. нехватка ресурсов диска, процессора и т.д.;
  3. ошибки.

Как это все мониторить?

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

  • Monit — http://mmonit.com/monit/
  • Zabbix — http://www.zabbix.com/
  • Munin — http://munin-monitoring.org/
  • Nagios — http://www.nagios.org/
  • ServerDensity — https://www.serverdensity.com/

Первые 4 инструмента можно поставить на сервер, они мощные, классные. А ServerDensity хостится у кого-то, т.е. мы за нее платим деньги, и она может собирать с серверов все данные и отображать их для анализа.

Для мониторинга ошибок есть два хороших сервиса:

  • Rollbar — https://rollbar.com/
  • Sentry — https://getsentry.com/

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

Sentry можно поставить к себе на сервер, есть исходник, а Rollbar — нет, но Rollbar лучше, потому что он берет деньги за количество ошибок, т.е. стимулирует их исправлять.

Про нотификации повторю, что спамить не стоит, теряется внимание.

Что, вообще, надо анализировать?

RPS и Responce time — если у нас начинает время ответа падать, то надо что-то делать.

Количество процессов, потоков и размеры очередей — если это все начинает плодиться, забиваться и т.д., то что-то здесь опять не так, надо анализировать более детально и как-то менять инфраструктуру.

Также стоит смотреть на бизнес-анализ. Google Analytics для сайтовых типов отлично подходит, а mixpanel — для логирования ивентов, он работает на десктопных приложениях, на мобильных, на веб. Можно и на основе каких-то своих данных писать, но я бы советовал готовые сервисы. Смысл в том, что наш мониторинг может показывать, что сервис жив, что все работает, что общий Responce time нормальный, но когда мы, допустим, регистрацию в mixpanel"е начинаем трекать, он показывает, что их как-то маловато. В этом случае надо смотреть, насколько быстро отрабатывают определенные ивенты, страницы, и в чем состоят проблемы. Проект всегда должен быть "обвешан" анализом, чтобы всегда знать, что происходит, а не работать вслепую.

Нагрузка, вообще, возникает или запланировано, или нет, может возникать постепенно, может не постепенно:

Как бороться с нагрузкой? Решает все бизнес, и важна только цена вопроса. Важно:

  1. чтобы сервис работал,
  2. чтобы это было не сильно дорого, не разорило компанию.

Остальное не очень важно.

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

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

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

Вообще, PHP — штука отличная для масштабирования, потому что он следует принципу Share nothing по умолчанию. Это означает, что если мы возьмем, допустим, Java для веба, то там приложение запускается, читает весь код, записывает по максимуму данных в память программы, все там крутится, работает, на request уходит очень мало времени, очень мало дополнительных ресурсов. Однако есть засада — т.к. приложение написано так, что оно должно на одном инстансе работать, кэшироваться, читать из своей же памяти, то ничего хорошего у нас при масштабировании не получится. А в PHP по умолчанию ничего общего нет, и это хорошо. Все, что мы хотим сделать общим, мы это помещаем в memcaсhed, а memcaсhed можно читать с нескольких серверов, поэтому все замечательно. Т.е. достигается слабая связанность для слоя серверов приложений. Это прекрасно.

Чем, вообще, балансировать нагрузку?

Чаще всего это делали Squid"ом или HAProxy, но это раньше. Сейчас же автор nginx взял и партировал из nginx+ балансировщик в nginx, так что теперь он может делать все то, что раньше делали Squid"ом или HAProxy. Если оно начинает не выдерживать, можно поставить какой-нибудь крутой дорогой аппаратный балансировщик.

Проблемы, которые решает балансировщик — это как выбрать сервер и как хранить сессии? Вторая проблема — чисто PHP"шная, а сервер может выбираться либо по очереди из списка, либо по географии каких-то IP"шников, либо по какой-то статистике (nginx поддерживает least-connected, т.е. к какому серверу меньше коннектов, на него он и будет перекидывать). Можем написать для балансировщика какой-то код, который будет выбирать, как ему работать.

Что, если мы упремся в балансировщик?

Есть такая штука как DNS Round robin — это замечательный трюк, который позволяет нам не тратиться на аппаратный балансировщик. Что мы делаем? Берем DNS-сервер (обычно DNS-сервера у себя никто не хостит, это дорого, несильно надежно, если он выйдет из строя, то ничего хорошего не получится, все пользуются какими-то компаниями), в А-записи прописываем не один сервер, а несколько. Это будут А-записи разных балансировщиков. Когда браузер туда идет (гарантий, на самом деле, нет, но все современные браузеры так действуют), он выбирает по очереди какой-нибудь IP-адрес из А-записей и попадает либо на один балансировщик, либо на второй. Нагрузка, конечно, может размазываться не равномерно, но, по крайней мере, она размазывается, и балансировщик может выдержать немного больше.

Что делать с сессиями?

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

Возникает очевидное желание сделать общую файловую систему, подключить NFS. Но делать так не надо — она до жути медленная.

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

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

Redis — нормальное решение. Смысл в том, что Redis у нас на отдельном сервере, и все наши фронтенды ломятся туда и начинают с Redis"а свои сессии считывать. Но Redis однопоточный и рано или поздно можем хорошенько упереться. Тогда делают sticky-сессии. Ставится тот же nginx и сообщается ему, что нужно сделать сессии так, чтобы юзер, когда он пришел на сервер и ему выдалась сессионная cookie, чтобы он впоследствии попадал только на этот сервер. Чаще всего это делают по IP-хэшу. Получается, что если Redis на каждом инстансе, соответственно, сессии там свои, и пропускная способность чтения-записи будет гораздо лучше.

Как насчет cookies? Можно писать в cookies, никаких хранилищ не будет, все хорошо, но, во-первых, у нас все еще куда-то надо девать данные о сессии, а если мы начнем писать в cookies, она может разрастись и не влезть в хранилище, а, во-вторых, можно хранить в cookies только ID, и нам все равно придется обращаться к БД за какими-то сессионными данными. В принципе, это нормально, решает проблему.

Есть классная штука — прокси для memcached и Redis:

Они, вроде как, поддерживают распараллеливание из коробки, но делается это, я не сказал бы, что очень оптимально. А вот эта штука — twemproxy — она работает примерно как nginx с PHP, т.е. как только ответ получен, он сразу отправляет данные и в фоне закрывает соединение, получается быстрее, меньше ресурсов потребляет. Очень хорошая штука.

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

В PHP есть такой механизм как session handler, т.е. мы можем поставить свой handler и писать в cookies, в БД, в Redis — куда угодно, и все это будет работать со стандартными session start и т.д.

Сессии надо закрывать вот этим замечательным методом.

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

Что делать с файлами?

С ними можно справляться двумя способами:

  1. какое-то специализированное решение, которое дает абстракцию, и мы работаем с файлами как с файловой системой. Это что-то вроде NFS, но NFS не надо.
  2. "шардирование" средствами PHP.

Специализированные решения из того, что действительно работает, — это GlusterFS. Это то, что можно поставить себе. Оно работает, оно быстрое, дает тот же интерфейс, что NFS, только работает с нормальной терпимой скоростью.

И Amazon S3 — это, если вы в облаке Amazon"а, — тоже хорошая файловая система.

Если вы реализуете со стороны PHP, есть замечательная библиотека Flysystem, покрытая отличными тестами, ее можно использовать для работы со всякими файловыми системами, что очень удобно. Если вы сразу напишете всю работу с файлами с этой библиотекой, то потом перенести с локальной файловой системы на Amazon S3 или др. будет просто — в конфиге строчку переписать.

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

Лучше всего раздавать файлы nginx"ом или Varnish"ем, но лучше все делать nginx"ом, т.к. мы все его любим и используем — он справится, он хороший.

Что у нас происходит с базой данных?

Если у вас все уперлось в код PHP, мы делаем кучу фронтендов и все еще обращаемся к одной БД — она справится достаточно долгое время. Если нагрузка не страшная, то БД живет хорошо. Например, мы делали JOIN"ы по 160 млн. строк в таблице, и все было замечательно, все бегало хорошо, но там, правда, оперативки надо больше выделить на буферы, на кэш...

Что делать с БД, если мы уперлись в нее? Есть такие техники как репликация. Обычно делается репликация мастер-слэйв, есть репликация мастер-мастер. Можно делать репликацию вручную, можно делать шардирование и можно делать партицирование.

Что такое мастер-слэйв?

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

В случае типичного проекта большое количество слэйвов позволяет разгрузить как мастер, так и, вообще, увеличить скорость чтения.

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

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

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

Есть такая штука как read/write split.Делается 2 пула серверов — мастер, слэйв, соединение по требованию, и логика выбора соединения варьируется. Смысл в том, что если мы будем всегда читать со слэйвов, а писать всегда в мастер, то будет небольшая засада:

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

Есть два типа выборок:

  1. для чтения или для вывода;
  2. для записи, т.е., когда мы что-то выбрали и потом это что-то надо изменить и записать обратно.

Если выборка для записи, то мы можем либо всегда читать с мастера и писать на мастер, либо мы можем выполнить "SHOW SLAVE STATUS" и посмотреть там Seconds_Behind_Master (для PostgreSQL тоже супер-запрос есть на картинке) — он покажет нам число. Если это 0 (нуль), значит, все у нас уже реплицировалось, можно смело читать со слэйва. Если число больше нуля, то надо смотреть значение — либо нам стоит подождать немного и тогда прочитать со слэйва, либо сразу читать с мастера. Если у нас NULL, значит еще не реплицировали, что-то застряло, и надо смотреть логи.

Причины подобного лага — это либо медленная сеть, либо не справляется реплика, либо слишком много слэйвов (больше 20 на 1 мастер). Если медленная сеть, то понятно, ее надо как-то ускорять, собирать в единые дата-центры и т.д. Если не справляется реплика, значит надо добавить реплик. Если же слишком много слэйвов, то надо уже придумывать что-то интересное, скорее всего, делать какую-то иерархию.

Что такое мастер-мастер?

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

Можно делать репликацию всегда руками, т.е. организовать пару соединений и писать сразу в 2, в 3, либо что-то делать в фоне.

Что такое шардирование?

Фактически это размазывание данных по нескольким серверам. Шардировать можно отдельные таблицы. Берем, допустим, таблицу фото, таблицу юзеров и др., растаскиваем их на отдельные сервера. Если таблицы были большие, то все становится меньше, памяти ест меньше, все хорошо, только нельзя JOIN"ить и приходится делать запросы типа WHERE IN, т.е. сначала выбираем кучу ID"шников, потом все эти ID"шники подставляем запросу, но уже к другому коннекту, к другому серверу.

Можно шардировать часть одних и тех же данных, т.е., например, мы берем и делаем несколько БД с юзерами.

Можно достаточно просто выбрать сервер — остаток от деления на количество серверов. Альтернатива — завести карту, т.е. для каждой записи держать в каком-нибудь Redis"е или т.п. ключ значения, т.е. где какая запись лежит.

Есть вариант проще:

Сложнее — это когда не удается сгруппировать данные. Надо знать ID данных, чтобы их достать. Никаких JOIN, ORDER и т.д. Фактически мы сводим наш MySQL или PostgreSQL к key-valuе хранилищу, потому что мы с ними ничего делать не можем.

Обычные задачи становятся необычными:

  • Выбрать TOP 10.
  • Постраничная разбивка.
  • Выбрать с наименьшей стоимостью.
  • Выбрать посты юзера X.

Если мы зашардировали так, что все разлетелось по всем серверам, это уже начинает решаться очень нетривиально. В этой ситуации возникает вопрос — а зачем нам, вообще SQL? Не писать ли нам в Redis сразу? А правильно ли мы выбрали хранилище?

Из коробки шардинг поддерживается такими штуками как:

  • memcache;
  • Redis;
  • Cassandra (но она, говорят, с какого-то момента не справляется и начинает падать).

Как быть со статистикой?

Часто статистику любят считать с основного сервера — с единственного сервера БД. Это прекрасно, но запросы в статистике обычно жуткие, многостраничные и т.д., поэтому считать статистику по основным данным — это большая ошибка. Для статистики в большинстве случаев realtime не нужен, так что мы можем настроить мастер-слэйв репликацию и на слэйве эту статистику уже посчитать. Или мы можем взять что-нибудь готовое — Mixpanel, Google Analytics или подобное.

Это основная идея, которая помогает раскидывать все по разным серверам и масштабировать. Во-первых, от этого сразу виден профит — даже если у вас один сервер и вы начинаете в фоне что-то выполнять, юзер получает ответ гораздо быстрее, но и впоследствии размазывать нагрузку, т.е. мы можем перетащить всю эту обработку на другой сервер, можно обрабатывать даже не на PHP. Например, в Stay.com картинки ресайзятся на Go.

Можно сразу взять Gearman. Это готовая штука для обработки в фоне. Есть под PHP библиотеки, драйвера... А можно использовать очереди, т.е. ActiveMQ, RabbitMQ, но очереди пересылают только сообщения, сами обработчики они не вызывают, не выполняют, и тогда придется что-то придумывать.

Общий смысл всегда один — есть основное ПО, которое помещает в очереди какие-то данные (обычно это "что сделать?" и данные для этого), и какой-то сервис – он либо достает, либо ему прилетают (если очередь умеет активно себя вести) эти данные, он все обрабатывает в фоне.

Перейдем к архитектуре.

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

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

Service-oriented architecture.

Есть 2 подхода разбиения систем на части:

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

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

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

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

Что с доменным слоем?

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

Есть 2 книги про доменный слой, которые всем советую:

  • "Domain-Driven Design: Tackling Complexity in the Heart of Software" от Eric Evans,
  • "Implementing Domain-Driven Design, Implementing Domain-Driven Design".
  • про BoundedContext — http://martinfowler.com/bliki/BoundedContext.html (то, о чем было выше — если у вас две области вроде как пересекаются, но они разные, то стоит некоторые сущности продублировать, такие как модель user);
  • про DDD в общем — — ссылка еще на одну книгу.

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

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

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

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

Еще немного ссылок полезных:

АЛЕКСАНДР КАЛЕНДАРЕВ , РБК Медиа, программист, [email protected]


Проблемы и пути решения

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

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

Уточним терминологию:

  • Производительность (performance) – способность приложения отвечать таким требованиям, как максимальное время реакции, пропускная способность.
  • Пропускная способность (capacity) – максимальная возможность приложения пропустить через себя определенное количество запросов в единицу времени или держать определенное число пользовательских сессий.
  • Масштабируемость (scalability) – это характеристика приложения, показывающая его способность сохранять производительность при увеличении пропускной способности. В свою очередь, масштабирование – это процесс обеспечения роста системы. Масштабирование может быть вертикальным или горизонтальным.
  • Вертикальное масштабирование – это увеличение производительности за счет наращивания мощности железа, объема оперативной памяти и т.д. Рано или поздно вертикальное масштабирование упрется в верхний предел.
  • Горизонтальное масштабирование – это увеличение производительности за счет разделения данных на множество серверов.

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

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

Масштабирование с использованием репликации

Самый простой способ масштабирования, который часто используется для небольших и средних проектов, – использование репликации. Репликация – это механизм синхронизации нескольких копий объекта, таблиц базы данных (см. рис. 2). Master-slave-репликация – это синхронизация данных с основного master-сервера к подчиненным slave-серверам.

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

Множество БД имеет встроенную репликацию, или, как говорят, «решение из коробки». Например, PostgreSQL-репликация может осуществляться следующими утилитами:

  • Slony-I – асинхронная (master to multiple slaves) репликация;
  • pgpool-I/II – синхронный мультимастер репликации;
  • Pgcluster – синхронный мультимастер репликации;
  • Bucardo;
  • Londiste;
  • RubyRep.
  • начиная с версии 9.0, встроенная потоковая репликация.

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

Статью целиком читайте в журнале «Системный администратор», №10 за 2014 г. на страницах 54-62.

PDF-версию данного номера можно приобрести в нашем магазине .


Вконтакте

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

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

  • Функциональное разделение
  • Классическое горизонтальное масштабирование
    • Концепции 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. Пускать пользователей на сайт партиями (пользователи все так же ждут и негодуют).

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



Есть вопросы?

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: