Хабрахабр

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Базы данных

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть