Создание высоконагруженной системы доставки рекламы на базе OpenX

This post is also available in: Английский

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

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

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

Измерение показателей производительности

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

Кроме того, мы рекомендуем в дополнение к последней версии 2.4 скачать набор плагинов для JMeter, из которых весьма полезными могут быть следующие: Ultimate Thread Group для гибкого задания динамики количества одновременных соединений в ходе тестирования и Response Times vs Threads и Transaction Throughput vs Threads для построения различных графиков по результатам теста.


Если вы работаете с системой, которая уже введена в эксплуатацию, очень полезным будет анализ графиков загрузки серверов системы, строящихся в различных системах мониторинга серверов типа Zabbix, Cacti и т.п. Сами разработчики OpenX рекомендуют использовать Ganglia.

Производительность базовой установки OpenX на одном среднем сервере по информации из различных источников составляет от 30 до 100 запросов в секунду в зависимости от используемых настроек. Исходя из этого необходимо решить, какое количество серверов и сколько ядер на каждом из них будет обрабатывать ваши запросы. Очевидно, что разброс от 30 до 100 достаточно велик и только от сделанных вами улучшений будут зависеть конкретные цифры.


Серверная архитектура

Первым делом рассмотрим архитектуру серверов нашей системы. Естественно, что для использования такой относительно медленной системы как OpenX, производительность которой фундаментально ограничена необходимостью выполнения PHP-скриптов для доставки рекламы и логирования информации о показах, необходимо изначально закладывать в архитектуру возможность горизонтального масштабирования, то есть легкого добавления дополнительных серверов с целью повышения производительности. Такая возможность изначально заложена в OpenX. Этому способствует как открытость самого продукта, так и использование широко распространенных открытых технологий, а также то, что его легко можно разделить на три основные части по их функциональному назначению: веб-интерфейс управления рекламой и статистики, движок доставки рекламы (он принимает решение о выдаче рекламы и осуществляет логирование), движок обработки статистики и вычисления приоритетов кампаний.

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

Здесь функции интерфейса управления кампаниями и обработки сырых статистических данных выносятся на отдельный сервер, а функции доставки распределяются на необходимое количество серверов, так как именно на них ложится основная нагрузка в ходе работы системы. Каждый узел доставки работает с собственным сервером MySQL, настроеным на репликацию данных с главного сервера базы данных. При этом сырые данные логирования хранятся локально и периодически обобщаются скриптом сбора распределенной статистики, который запускается на главном сервере и пишет обработанную статистику в основной сервер БД. Таким образом удается снизить нагрузку на основной сервер БД и распределить запросы к скриптам доставки на любое количество серверов. Подробнее об этом варианте архитектуры можно прочитать в документации OpenX и на других ресурсах [1, 2, 3].

Мы немного видоизменили данную схему, принимая во внимание имеющееся у нас в распоряжение оборудование и информацию, собранную при выполнении нагрузочных тестов. Все поступающие запросы обрабатывает один фронтенде, который берет на себя распределение запросов на доставку между PHP-бэкендами и обработку запросов админского интерфейса. Движок обработки статистики и вычисления приоритетов, а также сервер Memcached для кэширования информации, получаемой из БД, также выполняются на этом сервере. Все эти задачи требуют относительно малой вычислительной мощности. Основная же нагрузка ложится на несколько серверов PHP-бэкенда. База данных реплицируется с master-сервера на два slave-сервера с целью оптимизации производительности и обеспечения сохранности данных. Таким образом мы улучшили предыдущую схему, освободив серверы PHP-бэкенда от несвойственных ему функций (сервер БД), и можем масштабировать различные аспекты системы (серверы БД, серверы PHP-бэкенда) независимо друг от друга.

Несколько слов о настройке программного обеспечения на серверах. Все серверы работают под управлением ОС Linux. На фронтенде установлен высокопроизводительный HTTP-сервер Nginx, который отдает рекламные креативы и прочую статику и проксирует запросы на выполнение скриптов PHP. На бэкендах установлена связка spawn-fcgi + PHP 5.1. В версии PHP 5.3 появилось несколько улучшений в плане производительности, в частности интегрирован модуль php-fpm, однако поддержка работы OpenX с этой версией пока реализована не до конца.

При редактировании конфигов используемого ПО следует исключить все факторы, которые могут ограничивать возможное число одновременных соединений: максимальное число соединений и количество рабочих процессов в Nginx, лимиты Linux на количество открытых соединений и файлов (не забыть выставить на машине, с которой проводятся нагрузочные тесты), лимиты Memcached и MySQL на количество одновременных соединений и т.д.

Основные рекомендации по настройке параметров ОС, почерпнутые из различных источников [1, 2, 3] сводятся к следующему:

  • Отключить запуск всех ненужных сервисов и скриптов cron.
  • Задать опцию noatime в /etc/fstab для файловых систем, чтобы при доступе к ФС не тратилось время на обновление информации о времени последнего доступа, связанной с каждым файлом.
  • В файле /etc/sysctl.conf задать следующие параметры:
1
2
3
4
# чтобы некорректно закрытые соединения автоматически отваливались через 10 секунд
net.ipv4.tcp_fin_timeout = 10
# чтобы увеличить число доступных local-портов для исходящих соединений
net.ipv4.ip_local_port_range = 16384 65536
  • В файле /etc/security/limits.conf задать для отмены ограничения на количество открытых файлов:
1
*   soft    nofile          65000

Для spawn-fcgi рекомендуется задать число тредов чуть большее, чем число доступных ядер. Число процессов PHP можно оставить равным 1, но для увеличения надежности можно поставить и чуть побольше. При регулировке этих параметров нужно обращать внимание на значений показателя Load average при полной загрузке системы. Оно должно быть приблизительно равно числу доступных ядер.

При настройке БД — включить репликацию, задать максимальное число соединений. Обязательно включить в конфиге OpenX постоянные соединения с MySQL: в разделе [database] установить persistent=1. Другие параметры MySQL (по материалам 1, 2, 3):

1
2
3
[mysqld]
; максимальное число соединений
max_connections =2048

В конфиге OpenX увеличить буферы сортировки для обработки статистики:

1
2
[databaseMysql]
statisticsSortBufferSize            = 536868864

Для хранения таблиц по-умолчанию используется тип MyISAM, т.к. он быстрее работает. Однако проблема с MyISAM в том, что таблица полностью блокируется на время записи. Обычно это не сильно влияет на производительность, но в случае высоконагруженных систем может повлиять существенно, особенно во время запуска скрипта сбора статистики, поэтому нужно осуществлять его мониторинг для того, чтобы убедиться, что он не выполняется слишком долго. Для установок OpenX, в которых используется выделенный сервер БД с хорошей производительностью рекомендуется использовать InnoDB. Хотя работа с таблицами этого типа в целом и медленнее, но в них реализована построчная блокировка при записи. В общем случае нужно проводить нагрузочные тесты и мониторинг на реальной нагрузке по каждому варианту и оценивать, что подходит больше.

Для Nginx выставлены следующие настройки:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# число рабочих процессов равно числу ядер
worker_processes   16;
# лимит открытых файлов для каждого рабочего процесса
worker_rlimit_nofile  8192;
# задаем максимальное число соединений
events {
    worker_connections 65000;
    use epoll;
}
# включаем сжатие ответов, чтобы ускорить их отдачу и быстрее освобождать соединения
gzip on;
gzip_min_length 1100;
gzip_buffers 64 8k;
gzip_comp_level 3;
gzip_http_version 1.1;
gzip_proxied any;
gzip_types text/plain application/xml application/x-javascript text/css;
# отключаем keep-alive соединения
keepalive_timeout 0;

Оптимизация доставки

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

  1. Запрос рекламных материалов
    для каждой зоны или сразу всех.
  2. Запрос
    рекламного креатива (статическая картинка). Его мы не учитываем, так
    как он дает относительно небольшую нагрузку на серверы и легко
    обрабатывается Nginx-ом.
  3. Отправка логирования показа.
  4. Отправка логирования клика.

На первом этапе производится запрос на чтение к БД для определения набора баннеров пригодных к показу и выбора подходящего на основе приоритетов и настроек кампаний. Это можно закэшировать, но кэш должен периодически обновляться, чтобы подтягивать обновленные после запуска скрипта вычисления приоритетов. На этапах 3 и 4 производится увеличение счетчика показов в таблице сырой статистики запросом вида INSERT ON DUPLICATE UPDATE. Это уже закэшировать нельзя, но и здесь все-таки есть определенные приемы оптимизации.

Итак лимитирующие факторы:

  • необходимость чтения из БД,
  • необходимость записи в БД,
  • скорость выполнения скриптов PHP.

Кэширование запросов на чтение

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

1
2
3
4
5
6
[delivery]
cacheExpire=600
cacheStorePlugin="deliveryCacheStore:oxMemcached:oxMemcached"
[oxMemcached]
memcachedServers="10.2.35.2:11211"
memcachedExpireTime=600

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

Сокращение числа запросов к БД

Хотя это может показаться не актуальным в свете кэширования всего подряд, но все-таки запросы время от времени совершаются, не смотря на кэширование, а, во-вторых, отдельные запросы часто бывают связаны с другими накладными расходами, даже если они закэшированы. Например, если вы пишете свои расширения для OpenX, добавляя в схему БД новые признаки для баннеров или других сущностей системы, то с точки зрения удобства разработки и правильности архитектуры вы можете решить хранить это значение в отдельной таблице со связью по ID сущности и получать его в процессе доставки отдельным запросом, написав для его вызова отдельный плагин. Однако все эти архитектурно правильные решения скажутся на производительности, поэтому оптимальнее будет вносить правки непосредственно в основной код OpenX, добавлять колонки непосредственно к базовым таблицам и т.д. Другая возможность по сокращению числа запросов связана с отключением ненужных плагинов доставки.

Оптимизация записи в БД

С замедлением, связанным с необходимостью записи логов доставки отчасти борется архитектура распределенной статистики, рассмотренная ранее. Другой оригинальный подход предложен в статье http://hi-load.php.com.ua/topic/31/. Он заключается в том, чтобы писать статистику не в БД, а в файл на диске, что должно быть менее затратно. Для этого вносятся изменения в исходный код системы, а именно в функцию OA_Dal_Delivery_logAction в файле lib\OA\Dal\Delivery\mysql.php, которая и вызывается различными плагинами доставки для записи логов. Кроме того авторам удалось избавиться и от необходимости открывать файл для записи при каждом запросе, передавая данные логирования непосредственно в лог веб-сервера, используя функцию PHP apache_setenv и специальные настройки Apache. Далее логи периодически импортируются в БД простым SQL-запросом LOAD DATA INFILE. Очевидным недостатком такого подхода является зависимость от веб-сервера Apache. Принцип, озвученный в этой статье, можно было бы развить и далее, совсем отказавшись от записи сырых данных в БД. Интересно было бы исследовать возможность их хранения и считывания скриптом обработки статистики с использованием одного из множества набирающих популярность в последнее время key-value хранилищ или NoSQL серверов.

Увеличение скорости выполнения PHP

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

1
2
3
4
5
6
; размер кэша в разделяемой памяти в мб
eaccelerator.shm_size = 512
; использовать только кэш в памяти
eaccelerator.shm_only = 1
; не использовать сжатие для закэшированного кода
eaccelerator.compress = 0

По оптимизации кода скриптов доставки разработчики провели значительную работу. Код доставки по большей части независим от остального кода системы и хорошо оптимизирован: не использует дополнительных помимо стандартных библиотек для доступа к БД, не использует классы и объектно-ориентированное программирование вообще, минимизирует количество директив include и require, все дорогостоящие операции кэшируются во внешнем кэше или в глобальных переменных и выполняются максимум 1 раз за время выполнения скрипта. К стандартной поставке OpenX прилагается специальный скрипт для компиляции всех инклюдов в скриптах доставки в один файл. Все эти правила нужно принимать во внимание разработчикам, которые модифицируют код OpenX или пишут собственные плагины. Для определения узких мест в коде рекомендуется использовать информацию о времени выполнения, полученную под реальной нагрузкой с помощью профайлера встроенного в расширение XDebug для PHP. Выходные файлы профайлера можно просматривать с помощью утилиты WinCacheGrind.

Еще один интересный подход к ускорению скриптов доставки для разработчиков, хорошо знакомых с PHP, – использование так называемого реального FastCGI (1, 2) совместно с PHPDaemon. Достоинства этого подхода в сокращении на треть времени выполнения скриптов за счет выполнения кода инициализации только один раз – во время запуска демона. Недостатки связаны с потенциальной необходимостью вносить значительные изменения в скрипты доставки, сложной ситуацией с утечками памяти в PHP (необходимо периодически перезапускать процесс PHP, теряя преимущества реального FastCGI), необходимостью использовать версию PHP 5.3, в которой значительно улучшен сборщик мусора.

Если вы не считаете нужным лезть в код системы, то полезно будет хотя бы выключить все ненужные плагины в конфиге OpenX. В частности, хороший эффект дает отключение различных плагинов логирования дополнительной информации в разделе конфига [deliveryHooks], группы правил Client плагина deliveryLimitations в разделе [pluginGroupComponents] (инициализация ограничений по браузеру пользователя занимает довольно много времени из-за использования внешней библиотеки phpSniff, причем она выполняется даже, если вы не используете эти ограничения). Можно также полностью отключить логирование различных аспектов доставки в разделе [logging].

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

В OpenX по-умолчанию включен механизм выполнения скрипта сбора статистики в ходе выполнения скриптов доставки. Это бывает удобно для небольших систем, когда у администратора нет доступа к крону или просто для облегчения процесса установки, но на высоконагруженной системе это недопустимо, так как это будет замедлять доставку баннеров. Скрипт статистики должен выполняться по крону и желательно на отдельной машине (1, 2). Для этого в конфиге OpenX необходимо задать параметр

1
2
[maintenance]
autoMaintenance=0

И добавить в crontab следующую строку:

1
0 * * * * /path/to/php /path/to/openx/scripts/maintenance/maintenance.php www.mydomain.com

Заключение

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