0,0
рейтинг
17 февраля 2011 в 20:44

Разработка → Масштабирование нагрузки web-приложений

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

Оптимизация


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

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

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


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

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

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


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



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

Узкое место


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

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

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


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

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

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

Об обоих решениях можно почитать в замечательной книге по настройке PostgreSQL.

Однако, для хранения больших объёмов данных лучшим решением будет «шардинг» (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-приложения. Каждый из них обладает своими достоинствами и недостатками. Не существует некоторого рецепта, как сделать всё хорошо и сразу – для каждой задачи найдётся множество решений со своими плюсами и минусами. Какой из них выбрать – решать вам.
Михаил Крестьянинов @krestjaninoff
карма
127,2
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (38)

  • 0
    спасибо за статью и ссылки, особенно про postgresql
    • 0
      Репликация — это не совсем решение, а если речь идет не о единицах серверов, а о хотя бы десятках — вообще не решение.

      Более того, то, что шардинг в NoSQL сильно проще, чем в РСУБД, это несколько не соответствует действительности. Чисто технически — почти одинаково. Дело в другом: при работе с РСУБД надо перестраивать мозг, а на key-value все «ложится» практически сразу и само.
  • 0
    Полезно. Обзорно.
  • 0
    Спасибо. Всегда хотел знать, как же такие штуки устроены.
    • +4
      Каждый High-load разный.

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

      • 0
        Общий подход, мне кажется, вполне одинаковый.
      • 0
        Согласен, что вся соль именно в деталях, но целью данной статьи был именно поверхностный обзор. Каждая приведённая выше веха по хорошему заслуживает отдельной статьи.
  • +2
    Отличная статья. Продолжение будет?
  • +1
    Секционирование есть и в mysql начиная с 5.1
    • 0
      Есть, да не совсем. Отсекционируйте-ка мне этим секционированием табличку на 5 000 000 000 записей по сотенке серверов ;)
      • 0
        А в чем проблема собственно?
        • 0
          Ну а как вы себе это представляете?
      • 0
        Вообще-то эта задча решается скорее клиентской библиотекой, чем СУБД.
  • 0
    Один вопрос, на который я не могу найти ответ:

    как при шардинге базы добавляются новые сервера в кластер базы?
    Если например у нас есть 3 сервера (3 базы), то при добавлении 4-го нам ведь надо все данные
    с этих трех переворошить и распределить на 4 части (по новой хеш функции),
    а это совсем нетривиальная задача при миллионах записей.

    • 0
      Не надо ничего ворошить ;)

      Шардится всё всегда по какому-то сиквенсу, например user_id — вот и хранится карта соответствий

      [ { user_id_range1 => server_id_1 }, { user_id_range2 => server_id_2 },… ]

      Ее несложно сделать общедоступной: любой самый простейший key value сервер, либо просто разложить на все аппликейшен-ноды конфиг — не так уж это и часто надо.
      • +1
        Не всё так просто на самом деле. Шардить приходится по разным критериям, далеко не всегда есть смысл делать «первый миллион идёт на сервер1, второй на сервер2 и т.д.». Иногда приходится шардить по странам, иногда по именам. А потом оказывается что Джонов гораздо больше чем Зоуи, и на шард J-L нужно добавить ещё серверов. И решардинга не избежать.
        По сути это не такая уж и страшная операция, если её делать вовремя. Никто не держит сервера загруженными на 100%, всегда должен быть запас ресурсов, которые можно отдать под решардинг. На старых серверах немного возрастает чтение, на новых соответственно запись. Но если проспать нужный момент (как например произошло с Foursquare), тогда действительно проблем не избежать.
        • 0
          Не спорю, кейсы бывают разные, но в целом, если рассматривать не конкретный кейс, а среднее по больнице, я против такой архитектуры, когда на шарды завязана какая-то там логика, искусственно вводимая для избежания кросс-шард запросов, это очень сложно суппортить и чревато неожиданностями. Проще создать внешние индексы в какой-то шустрой искалке, типа sphinx RT, а шарды воспринимать как key-value, которые вне рамок PK просто ничего не умеют. Стоимость дисков возрастает линейно, а стоимость суппорта таких выкрутасов растет в лучшем случае геометрически =)
        • 0
          И да, я читал доклад по архитектуре 4sq, это ж убиться веником. Сами себе проблемы придумали.
      • 0
        Ну вариант с диапазонами id понятен ( просто с какого то момента все новые пользователи
        отправляются на новые сервера). Но в этом случае уже не будет равномерного красивого распределения данных. Или это нормальная практика?
        • 0
          Дык тут все просто — на первых серверах экспериментально определяем, сколько разумно засунуть на один сервер (заранее все равно не угадаешь характер нагрузки, распределение популярности и активности), тут можно и мелкими диапазонами. А дальше уже знаем сколько выделять. Учитывая что на одном сервере пользователей все равно получится дофига, будет примерно равномерно :)
        • 0
          Ну и кстати надо заметить, что шардить может оказаться полезным не только по серверам, но и по таблицам (тут профит очевиден; можно заюзать тот же встроенный партишенинг, а можно и не заюзать =)), и даже по базам/схемам (зависит от конкретной СУБД и специфики ее реализации).
          • 0
            У нас postgres, сейчас 8 баз на 4 серверах с запасом в 3-4 раза.
            Кластеризацию осуществляем через plproxy, делим данные по user_id равномерно.
            В нагруженных таблицах где то по 20-40 мл записей.
            Оптимистично мы ожидаем прирост пользователей через год на 200-500%.

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

            Сейчас избавляемся от plproxy, переводя всю логику в основной код (java)
            Также рассматриваем частичный переход на noSQL

  • +1
    partitioning к сожалению в postgresql крайне зачаточный, не в пример mysql 5.5, там все гораздо удобнее. Успешно использую данный механизм оптимизации в своем приложении, работает :)
    • 0
      Использую partitioning (postgres 8.4) для таблицы в 50 млн. записей. Кроме необходимости ручного создания новых таблиц никаких затруднений не ощущаю.
      • 0
        А наследование индексов в наследуемых таблицах там сделали? Или до сих пор ручками? Помню, что было в TODO давно еще…
        • 0
          На 9.0 индексы для наследуемых таблиц приходится создавать, к сожалению, явно. Тем не менее, с учётом того, что создание дочерний таблицы легко оборачивается в функцию, — это не большая проблема.
          • 0
            Вот это я и имел ввиду, когда говорил, что в mysql удобнее, в postgresql все ручками нужно делать.
  • НЛО прилетело и опубликовало эту надпись здесь
    • 0
      Я, честно говоря, вообще считаю, что кластерные ФС это вынужденная мера, когда какому-то уже написанному приложению надо подсунуть нечто, о чем оно будет думать, как о локальной posix FS. Как и вообще считаю, что любой кластер (в полноценном понимании слова) это такой workaround, чтобы смасштабировать немасштабируемое.

      А если заранее делаем, грубо говоря, фейсбук, а не страничку Васи Пупкина, которая ВНЕЗАПНО стала превращаться в фейсбук, разумно все предусмотреть на уровне приложения. С точки зрения производительности — никогда не будет ничего быстрее тупой отдачи файла с обычной фс обычного диска, с точки зрения управляемости — на апплевеле всегда более управляемо.

      Для типичных вебдванольных задач (фоточки-видюшки) решается все очень просто, пара классов по сотне строк на любимом скриптовом ЯП.
      • НЛО прилетело и опубликовало эту надпись здесь
        • +2
          Ваша правда. Но когда таки пошло, разумно, поддерживая костылями что есть, параллельно начинать делать по уму и разрабатывать план миграции. Иначе есть немалый риск, что стоимость владения будет прогрессировать с каждым новым пользователем и с каждой новой фичей до бесконечности.
  • +1
    «Кроме того, этот подход (вертикальное масштабирование) не требует никаких доработок приложения.»
    Строго говоря это не всегда так. Хорошо вертикально масштабируются только или компоненты в которых нет конкурентного доступа к разделяемым ресурсам (типа нитей выполнения php/perl/вашлюбимыйсерверныйязык) либо достаточно простые вещи.
    Сложные части вроде СУБД могут плохо масштабироваться вертикально. Особенно если преобладает write-нагрузка. Увеличение количества ядер и памяти сервера с некоторого момента не оказывает влияния.
    И гораздо разумней этот многоядерный многогигабайтный сервер нарезать на виртуальные машинки по 2-4 ядра (в зависимости от того что туда хотите ставить) и делать необходимые доработки в архитектуре для переходя к горизонтальному масштабированию (пусть и внутри пока одного физического сервера)

    Ещё в копилку ключевых слов:
    Механизм с поднятием нескольких экземпляров одной базы на разных серверах, master/slave репликацией и распределением операций записи на master, чтений на slave'ы — называется read/write split — и работает опять же только если у нас преобладает read-нагрузка. И имеет очевидное ограничение до которого так можно масштабироваться — количество запросов на изменение данных которое может переварить 1 сервер.
  • 0
    Спасибо за статью. В избранное.
  • 0
    habrahabr.ru/company/kanobu/blog/114488/
    Вот еще интересная статья про partitioning, federation and sharding.
    • 0
      lethargy.org/~jesus/writes/partitioning-vs.-federation-vs.-sharding
      Упс, промазал :)
  • 0
    Файлы статики можно смонтировать с некого общего файлового хранилища по NFS/CIFS или использовать распределённую ФС (HDFS, GlusterFS, Ceph).
    глупее совета не встречал.
    Если у Вас нагруженная система то ваша NFS гавкать будет, как резанный кролик. Если вы хотите сделать распределенное хранилище файлов — то почитайте что такое шардинг. Обычный шардинг контента на несколько серверов. Задача не сложнее шардинга БД, кстати про который здесь было упомянуто как-то вскользь. Поспрашивайте у ребят из Мамбы, Баду или Сонетика — как все это сделать.
  • 0
    Отдельно стоит отметить проблему деплоймента на несколько хостов. Как сделать так, что бы пользователь, нажимая «Обновить», не видел разные версии приложения? Самым простым решением, на мой взгляд, будет исключение из конфига балансировщика нагрузки (web-сервера) не обновлённых хостов, и последовательного их включения по мере обновления. Так же можно привязать пользователей к конкретным хостам по cookie или IP. Если же обновление требует значимых изменений в БД, проще всего, вообще временно закрыть проект.
    все решается тривиально просто:
    есть конфиг версий, есть index.php который подгружает классы из соответствующей папки: папка — номер ревизии свн.
    есь скрипт, который одновременно заливает на n серверов новый конфиг.
    Как только мы развернули на n серверах новую версию в нужной папочке — используя scp/tar а не svn co
    мы запускаем скрипт инициализации, который модифицирует БД, и после чеего производит централизованное переключение верси.
    Всегда просто сделать Откат, запустив скрипт, который лишь изменит (на всех серверах одновременно) одну строчечку номера версии на предыдущую.
    все как дважды два!!!
  • 0
    Спасибо.
    Еще полезные моменты про шардинг, репликацию и партиционирование.

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