Кластеризация веб-приложений на хостинге Amazon Web Services

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

Как известно, есть 2 пути масштабирования приложения:

  1. Вертикальное — это увеличение производительности каждого компонента системы (процессор, оперативная память, прочие компоненты).
  2. Горизонтальное – когда соединяют несколько элементов воедино, а система в целом состоит из множества вычислительных узлов, решающих общую задачу, тем самым увеличивая общую надежность и доступность системы. Увеличение производительности достигается добавлением в систему дополнительных узлов.

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

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

Типичная архитектура веб-приложения

Рис.1 Типичная архитектура веб-приложения

  1. Первым пользователя «встречает» веб-сервер, на его плечи возлагаются задачи отдачи статических ресурсов и передачи запросов приложению;
  2. Далее эстафета передается приложению, где протекает вся бизнес-логика и взаимодействие с базой данных.

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

  • язык программирования и фреймворк: java 7 и rest jersey;
  • сервер приложения: tomcat 7;
  • база данных: MongoDB (NoSQL);
  • система кэширования: memcached.

Как это было, или через тернии к высокой нагрузке

Шаг 1: Разделяй и властвуй

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

  • сервер приложения;
  • сервер базы данных;
  • сервер со статическими ресурсами.

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

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

Сервер баз данных. Что есть работа БД? Это многочисленные дисковые операции ввода-вывода (запись и чтение данных). Тут самым лучшим вариантом будет сервер с наиболее быстрым жестким диском (например ССД). И снова Амазон рад стараться и предлагает нам Storage Optimized Instances, но также подойдет и сервер из линейки General Purpose (large or xlarge), так как в дальнейшем я собираюсь их масштабировать.

Статические ресурсы. Для статических ресурсов не нужен ни мощный процессор, ни большой объем оперативной памяти, тут выбор падает на сервис статических ресурсов Amazon Simple Storage Service.

Разделив приложение, я привел его к схеме, отображенной на рис. 1.

Плюсы разделения:

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

Но само приложение по-прежнему осталось некластеризуемо, также отсутствуют сервера кэша и репликации сессий.

Шаг 2: Эксперименты

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

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

Итак, эксперименты показали, что полученной производительности по-прежнему не хватает, а сервер с приложением был нагружен на 100% (слабым звеном оказался код разработанного приложения). Распараллеливаю. В игру вводятся 2 новых элемента:

  • балансировщик нагрузки;
  • сервер сессий.

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

В качестве балансировщика нагрузки можно завести еще один сервер с широким каналом и настроить специальное программное обеспечение (haproxy[], nginx[], DNS[]), но так как производится работа в инфраструктуре Амазон, то будет использован существующий там сервис ELB (Elastic Load Balancer). Он очень прост в настройке и имеет неплохие показатели производительности. Первым делом необходимо клонировать существующую машину с приложением для последующего добавления пары машин в балансировищик. Клонирование осуществляется средствами Amazon AMI. ELB автоматически осуществляет контроль за состоянием добавленных в рассылку машин, для этого в приложении следует реализовать простейший пинг ресурс, который будет отвечать 200 кодом на запросы, именно его указывают балансировщику.

Итак, после конфигурирации балансировщика на работу с уже двумя существующими серверами с приложением, настраиваю DNS на работу через балансировщик.

Репликация сессий. Этот пункт можно пропускать, если в приложении не возлагается на HTTP сессии дополнительной работы, или же если реализуется простой REST сервис. В противном случае необходимо, чтобы все приложения, участвующие в балансировке, имели доступ к общему хранилищу сессий. Для хранения сессий запускается еще один large instance и настраивается на нем ram memcached хранилище. Репликация сессий возлагается на модуль к томкату: memcached-session-manager [5]

Теперь система выглядит следующим образом (сервер статики опущен для упрощения схемы):

Схема системы после кластеризации

Рис.2 Вид системы после кластеризации приложения

Результаты кластеризации приложения:

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

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

Шаг 4: Оптимизация работы с БД

Итак, снова проводится нагрузочное тестирование с Apache Jmeter и на этот раз все упирается в производительность базы данных. Оптимизируя работу с БД, применяю два подхода: кэширование данных запросов и репликация базы для read запросов.

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

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

Кластеризация БД для чтения

Рис. 3 Кластеризация БД для чтения

Финал

В результате трудов мы достигли желаемого: система обрабатывает 200 000 запросов в минуту.