Урок по оптимизации серверной части веб-приложений

a964bf1461da7bcce74ab62b242aa133.png

Привет, Хабр! Меня зовут Алексей Приставко, я директор по веб-проектам в компании DataLine. Моя сегодняшняя статья посвящена тому, как исправить или предотвратить проблемы с производительностью бэк-энда веб-приложений.

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

Терминология


Давайте для начала разберемся в терминологии. Говоря о производительности веб-проектов или веб-систем, я в первую очередь имею в виду back-end и серверную составляющую. То, что происходит при загрузке страниц в браузере — это совершенно другая история, которой, скорее всего, посвящу отдельную статью.

  • Мерилом производительности приложения у нас будет являться количество обрабатываемых запросов в секунду (RPS) и скорость их выполнения (TTFB — Time to First Byte).
  • Соответственно, под масштабируемостью системы мы будем понимать пул возможностей для увеличения RPS.


Теперь о надежности. Здесь обязательно нужно разделять два понятия: отказоустойчивость и катастрофоустойчивость.

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


При этом катастрофоустойчивая система ≠ отказоустойчивая система. Ситуация, в которой катастрофоустойчивая, но не отказоустойчивая система продолжает работать только на одном «плече», вполне нормальна. Но если откажет один из серверов, система также выйдет из строя.

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

С чего начать оптимизацию


1e97edadf840f697e8a995e8a53d7269.png

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

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

9963fbb5a9be1336886b708fb4d071e2.png

«Что? Где? Когда?» — оптимизируем запросы


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

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

  • Присваивайте каждому входящему запросу уникальный идентификатор. В Nginx для этого есть встроенная переменная $request_id. Передавайте идентификатор в заголовках на back-end и пишите во все логи. Так вы сможете удобно трассировать запросы.
  • Логируйте не только конец запроса к внешнему компоненту, но и его начало. Так вы измерите реальную продолжительность отработки внешнего вызова. Она может существенно отличаться от того, что вы видите в удаленной системе, например, из-за проблем с сетью или тормозов DNS.


Итак, данные собраны. Теперь разберем проблемные точки. Определите:

  • Где тратится больше всего времени?
  • Куда приходит наибольшее количество запросов?
  • Куда приходят самые «долгие» запросы?


В итоге вы получите список наиболее интересных для оптимизации участков системы.

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


Кэшируем с умом


Существуют общие правила кеширования, на которые стоит опираться при оптимизации:

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


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

Совет 1: Делайте компонентное кеширование готовой странички на стороне Nginx с помощью Edge Side Includes. Оно хорошо ложится на микросервисную/SOA архитектуру и разгружает систему в целом, значительно улучшая скорость отклика.

Совет 2: Следите за размером объектов в кэше, показателем hit ratio и объемами записи/чтения. Чем больше объект, тем дольше он будет обрабатываться. Если вы пишете в кэш чаще или больше, чем читаете, такой кэш — вам не товарищ. Его стоит или убрать, или подумать над повышением его эффективности.

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


Профили нагрузки


Переходим к профилям нагрузки. Как вы знаете, есть два основных типа: OLAP и OLTP.

  • Для OLAP (Online Analytical Processing) важно количество отработанного трафика в секунду.
  • Для OLTP (Online Transaction Processing) ключевой показатель — скорость отклика, миллисекундные тайминги.


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

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

Базы данных


753b357df47b173bb57ab2de92e02e58.png

Итак, мы закономерно подошли к одному из самых сложных этапов оптимизации —, а именно, к оптимизации базы данных.

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

По возможности храните исторические данные, логи приложения и часто используемые данные в разных базах данных. Еще лучше — разнесите их на разные сервера. Это не только облегчит жизнь основной БД, но и даст больше пространства для дальнейшей оптимизации, к примеру в ряде случаев позволит использовать разные индексы под разную нагрузку. Также «однотипность» нагрузки упрощает жизнь планировщику и оптимизатору запросов сервера БД.

И снова о важности планирования


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

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


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

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


54bdf1be69c1d596c882fc2d04d5c11c.png

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

Теперь мы поговорим о том, как выбрать стратегию масштабирования и повысить отказоустойчивость.

Существует два вида масштабирования системы:

  • вертикальное — рост  объема ресурсов при сохранении количества сущностей;
  • горизонтальное — рост количества сущностей.


Растём в высоту


Начнем с выбора стратегии вертикального масштабирования.

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

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

Если в вашей системе несколько серверов и стоит выбор — увеличить мощность существующих или докупить еще несколько, обратите внимание на финансовую сторону. Например, один мощный сервер может оказаться дороже, чем два на 50% «слабее». Поэтому резонно будет остановиться на втором компромиссном варианте. В то же время, при большом количестве серверов решающее значение имеет соотношение производительности, энергопотребления и стоимости полной стойки.

Растём в ширину


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

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

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


«Подтягиваем» базы данных


Сложнее всего масштабировать базы данных. Для этого есть две основные техники: шардирование и тиражирование. Рассмотрим их.

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

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


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

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

Совет: Если есть необходимость держать более 2-х мастеров баз данных, рекомендую вам рассмотреть NoSQL-решения, так как многие из них имеют встроенные механизмы приведения данных в консистентное состояние.


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

Помните: Любые важные данные необходимо бэкапить и хранить в виде независимой не редактируемой копии.


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


Напоследок — пара советов про производительность при создании резервных копий:

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

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


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

На этом всё. Если у вас остались какие-то вопросы, я с удовольствием отвечу на них в комментариях.

© Habrahabr.ru