Загрузка данных
 
Логин:   Пароль:      
Регистрация   Забыли пароль?

15 горячих:

Закрыть
Загрузить:
Указать:
Выравнивание:
Альт

Пример построения высоконагруженной системы на основе открытого решения

Что предстоит сделать?


Openx На основе открытого решения Openx нам предстоит реализовать масштабируемую систему управления рекламой, которая бы выдерживала нагрузку порядка 20М показов баннеров в сутки, что равнозначно более 40М запросов к web-серверу.

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

В моем случае было использовано 5 серверов для создания http-кластера и 2 сервера для БД.

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

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

Как это работает?


Изучив внутренности Openx, я задумался, почему все настолько сложно написано, ведь аналогичный функционал можно было реализовать гораздо проще. Хотя, наверное, такова судьба всех открытых проектов.
Система управления рекламой состоит из двух частей:
  • Административной части (backoffice) – отвечает за управление рекламными материалами
  • Системы доставки рекламы (delivery engine) – отвечает за выборку нужного баннера, а также за ведение статистики.
Административная чать в оптимизации не нуждается, пока не нуждается ;-)), так как 99% нагрузки приходится на систему доставки рекламы.

Система доставки устроена так, что при показе одного баннера к серверу обращается минимум 3 запроса.

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

Третий запрос к web-серверу - это запрос на получение самого баннера.

Вызов баннеров происходит с помощью JavaScript-кода, поскольку это наиболее безопасный способ доставки рекламы, при котором рекламная система на 100% отделена от основного ресурса и никак не может повлиять на его работоспособность.

Возникающие проблемы


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

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

Архитектура системы



  1. Запросы пользователей к системе
  2. Балансирующий реверсивный прокси сервер (reverse-proxy)
  3. Сервера, обрабатывающие запросы пользователей (application servers)
  4. Сервера базы данных (database servers)


Используемое ПО


На всех серверах стоит OC FreeBSD.

Балансировщики


Для распределения нагрузки между backend серверами (3) используется машина с установленным на нее бесплатным httpd-сервером Nginx, который выполняет роль балансировщика нагрузки (2) и реверсивного прокси-сервера.

Машины, на которых стоит nginx, настроены для обслуживания очень большого количества соединений.

Более подробно о настройках сервера можно прочитать здесь.

Для обеспечения надежности системы используется две машины, которые “делят” между собой один IP-адрес (при помощи протокола CARP).

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

Более подробную информацию о CARP можно прочитать здесь http://ru.wikipedia.org/wiki/CARP.

Application сервера


Типичная конфигурация сервера, обрабатывающего запросы пользователей:
  1.   CPU Athlon 64x2
  2.   RAM 4Gb
  3.   HDD 750 Gb SATA 100
  4.  
  5.   OS FeeBSD
  6.   Apache 1.3 +mod_php+eAccelerator


Cистема строилась год назад, сейчас я бы рекомендовал использовать nginx+PHPFPM+xcache.

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


Сервер базы данных


Типичная конфигурация сервера БД:
  1. CPU Athlon 64x2
  2.   RAM 4Gb
  3.   HDD 2x750 Gb SATA 100 RAID 0
  4.   OS FeeBSD
  5.   MySQL: 5.1


Два сервера используются только для увеличения надежности системы, зеркалирование данных осуществляется при помощи репликации.

Профайлинг - поиск узких мест


Обработка поступающих в систему запросов


Входящие запросы от пользователей (1) делятся на два типа:
  1. Запросы на отдачу статических файлов: jpg, gif, swf
  2. Запросы на отдачу динамического содержимого: генерируемая реклама, логирование показов, логирование кликов.

Статические файлы раздаются с помощью nginx (2) – легкого httpd-сервера нового поколения.

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

Ставим диагноз


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

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

Для web-ресурсов нужно производить мониторинг:
  • http-сервера - для nginx это модуль stub_status, для Apache это mod_status
  • RAM - использование памяти (свободная память, память, выделенная приложению, и т.д.)
  • CPU - load average
  • HDD - загрузка диска, дисковые операции (особенно актуально для серверов баз данных)
Числовые данные сами по себе практически не несут никакой смысловой нагрузки - здесь важна динамика и изменение этих данных во времени.

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

Вот графики, котрые были получены при мониторинге работы нашей системы

Мониторинг системы - определение допустимой нагрузки

Мониторинг системы - определение допустимой нагрузки


Что означают эти данные?


  • Reading - сколько соединений находится в состоянии чтения.
  • Writing - сколько соединений находится в состоянии записи.
  • Waiting - keep-alive соединения или же в состоянии обработки запроса.
На мой взгляд, смысл данных показателей заключается в следующем:

Writing - насколько хорошо клиент (application server) отдает данные проксирующему серверу. Если график начинает резко расти вверх, обычно это означает замедление работы сервера, который обслуживает поступающие запросы, либо сервер очень долго обрабатывает поступающие запросы.

Reading - насколько хорошо и быстро клиент (пользователь), пославший запрос, читает данные, которые ему отдает балансировщик. Если график с этим показателем резко растет вверх, это означает, что у нас возникают проблемы с каналом (канал слишком узкий). Обычно этот показатель сильно возрастает при DDOS-атаках.

Теперь посмотрим, что мы видим на графиках:

до 10:00 система работала нормально
после 10:00 резко начинает расти writing
где-то в 11:40 резко возрастает количество запросов (от 180 запросов в секунду до 260 запросов в секунду), application сервера напряглись, но выдержали пиковую нагрузку

После этого нагрузка начинает плавно увеличиваться, а вместе с ней начинает "тормозить" рекламный кластер.

В 15:45 нагрузка на рекламу максимальная, application сервера перегружены и уже почти не способны обрабатывать новые поступающие запросы - состояние "жуткий тормоз".

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

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

Ищем бутылочное горлышко


В своей работе раздатчик у нас использует два файла
  • ajs.php - формирует JavaScript-код баннера
  • lg.php - пишет лог запросов (показы, клики)
После профайлинга этих двух файов выяснилось, что php-код хоть и достаточно громоздкий, но выполняется доволно быстро.

Хороший прирост быстройдействия достигается использованием акселератора, а основным "тормозом" является работа с MySQL.

Самая болшая проблема Openx на больших нагрузках - это соединение и запись статистических данных в БД MySQL.

При нагрузке в 2 000 000 показов в сутки количество запросов к MySQL-серверу будет примерно 90 000 000.
При этом стоит учесть, что сама операция записи данных в таблицу, а также соединение с сервером БД требует довольно много ресурсов.

Если при каких-то условиях БД не была доступна, то на серверах резко возрастало количество процессов Apache httpd и резко повышалась нагрузка на сервера, что могло привести к полной остановке системы раздачи рекламы.

Избавляемся от MySQL при логировании запросов


Как говорится, все гениальное - просто! Вместо того, чтобы писать данные в базу, мы будем писать данные в лог-файлы, которые периодически (например раз в 5 минут) будут вставляться в базу.

Этим действием решаем сразу две проблемы:
  • ускоряем работу наших application серверов;
  • разгружаем MySQL-сервер (ему теперь не нужно делать единичные вставки, вставки делаются большими пачками).
Процесс записи лог-файлов возложим на наш http-сервер, что даст нам максимальное быстродействие для выполнения этой операции.

Для того, чтобы реализовать такой функционал, надо подкорректировать файл

  1. \lib\OA\Dal\Delivery\mysql.php (~690 строка) функция OA_Dal_Delivery_logAction


Удаляем там все, что связанно с SQL-запросом, и добавляем такой код:

  1. apache_setenv('ENV_TYPE_'.strtoupper($table),    '1', true);
  2.  
  3.  /** plvs */
  4.  apache_setenv('plv01',          $log_viewerId);
  5.  apache_setenv('plv02',          '');
  6.  apache_setenv('plv03',          $dateFunc('Y-m-d H:i:s'));
  7.  apache_setenv('plv04',          $adId);
  8.  apache_setenv('plv05',          $creativeId);
  9.  apache_setenv('plv06',          $zoneId);
  10.  apache_setenv('plv07',          isset($_GET['source']) ? MAX_commonDecrypt($_GET['source']) : '' );
  11.  apache_setenv('plv08',          isset($zoneInfo['channel_ids']) ? $zoneInfo['channel_ids'] : '' );
  12.  apache_setenv('plv09',          isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) ? substr($_SERVER['HTTP_ACCEPT_LANGUAGE'], 0, 32) : '');
  13.  apache_setenv('plv10',          $_SERVER['REMOTE_ADDR']);
  14.  apache_setenv('plv11',          $_SERVER['REMOTE_HOST']);
  15.  apache_setenv('plv12',          isset($geotargeting['country_code']) ? $geotargeting['country_code'] : '');
  16.  apache_setenv('plv13',          isset($zoneInfo['scheme']) ? $zoneInfo['scheme'] : '');
  17.  apache_setenv('plv14',          isset($zoneInfo['host']) ? $zoneInfo['host'] : '');
  18.  apache_setenv('plv15',          isset($zoneInfo['path']) ? $zoneInfo['path'] : '');
  19.  apache_setenv('plv16',          isset($zoneInfo['query']) ?  $zoneInfo['query'] : '');
  20.  apache_setenv('plv17',          isset($_GET['referer']) ? $_GET['referer'] : '');
  21.  apache_setenv('plv18',          isset($_SERVER['HTTP_USER_AGENT']) ? substr($_SERVER['HTTP_USER_AGENT'], 0, 255) : '');
  22.  apache_setenv('plv19',          isset($userAgentInfo['os']) ? $userAgentInfo['os'] : '');
  23.  apache_setenv('plv20',          isset($userAgentInfo['browser']) ? $userAgentInfo['browser'] : '');
  24.  apache_setenv('plv21',          isset($geotargeting['region']) ? $geotargeting['region'] : '');
  25.  apache_setenv('plv22',          isset($geotargeting['city']) ? $geotargeting['city'] : '');
  26.  apache_setenv('plv23',          isset($geotargeting['postal_code']) ? $geotargeting['postal_code'] : '');
  27.  apache_setenv('plv24',          isset($geotargeting['latitude']) ? $geotargeting['latitude'] : '');
  28.  apache_setenv('plv25',          isset($geotargeting['longitude']) ? $geotargeting['longitude'] : '');
  29.  apache_setenv('plv26',          isset($geotargeting['dma_code']) ? $geotargeting['dma_code'] : '');
  30.  apache_setenv('plv27',          isset($geotargeting['area_code']) ? $geotargeting['area_code'] : '');
  31.  apache_setenv('plv28',          isset($geotargeting['organisation']) ? $geotargeting['organisation'] : '');
  32.  apache_setenv('plv29',          isset($geotargeting['netspeed']) ? $geotargeting['netspeed'] : '');
  33.  apache_setenv('plv30',          isset($geotargeting['continent']) ? $geotargeting['continent'] : '');


Это позволит установить в сервере Apache значения серверных переменных, которые будут потом записаны в файл.

Дальше делаем изменения в конфигурационном файле httpd-сервера Apache.

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

  1. # The following directives define some format nicknames for use with
  2.  # a CustomLog directive (see below).
  3.  #
  4.  
  5.  LogFormat "%{plv01}e\t%{plv02}e\t%{plv03}e\t%{plv04}e\t%{plv05}e\t%{plv06}e\t%{plv07}e\t%{plv08}e\t
  6. %{plv09}e\t%{plv10}e\t%{plv11}e\t%{plv12}e\t%{plv13}e\t%{plv14}e\t%{plv15}e\t%{plv16}e\t%{plv17}e\t
  7. %{plv18}e\t%{plv19}e\t%{plv20}e\t%{plv21}e\t%{plv22}e\t%{plv23}e\t%{plv24}e\t%{plv25}e\t%{plv26}e\t
  8. %{plv27}e\t%{plv28}e\t%{plv29}e\t%{plv30}e" PHP_LOG
  9.  
  10.  #
  11.  # The location and format of the access logfile (Common Logfile Format).
  12.  # If you do not define any access logfiles within a <VirtualHost>
  13.  # container, they will be logged here.  Contrariwise, if you *do*
  14.  # define per-<VirtualHost> access logfiles, transactions will be
  15.  # logged therein and *not* in this file.
  16.  #
  17.  CustomLog "| /usr/local/sbin/rotatelogs /var/log/apache/click.log 300" PHP_LOG env=ENV_TYPE_OA_DATA_RAW_AD_CLICK
  18.  CustomLog "| /usr/local/sbin/rotatelogs /var/log/apache/impr.log 300" PHP_LOG env=ENV_TYPE_OA_DATA_RAW_AD_IMPRESSION


Для ротирования логов используем стандартный Apache rotatelogs.

После ротации файлы можно брать и использовать.

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

Запрос для импортирования будет иметь приблизительно такой вид

  1. $sql = "LOAD DATA LOCAL INFILE '".LOG_DIR.$file."' INTO TABLE ".$table."  FIELDS TERMINATED BY '\t' LINES TERMINATED BY '\n'";


Помимо этого, если в системе используется более одного backend сервера, нужно организовать мониторинг поступления лог-файлов с разных серверов.

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

Запрос на обновление данных в таблице

  1. $sql = "REPLACE INTO oa_parsers SET server='".$hostname."', update_time='".gmdate('U')."'";


Теперь по этим данным можно настроить систему мониторинга – Nagios, написав для нее нужный плагин.

Настройка MySQL



Для быстрой вставки и хранения логов используйте таблицы типа MyISAM.

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

Если вы планируете хранить много данных (объем таблицы более 4Gb), внесите изменения в структуру таблиц (выделено цветом)

  1. CREATE TABLE `oa_data_raw_ad_impression` (
  2. `viewer_id` varchar(32) DEFAULT NULL,
  3. `viewer_session_id` varchar(32) DEFAULT NULL,
  4. `date_time` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
  5. `ad_id` int(10) UNSIGNED NOT NULL DEFAULT '0',
  6. `creative_id` int(10) UNSIGNED NOT NULL DEFAULT '0',
  7. `zone_id` int(10) UNSIGNED NOT NULL DEFAULT '0',
  8. `channel` varchar(255) DEFAULT NULL,
  9. `channel_ids` varchar(64) DEFAULT NULL,
  10. `language` varchar(32) DEFAULT NULL,
  11. `ip_address` varchar(16) DEFAULT NULL,
  12. `host_name` varchar(255) DEFAULT NULL,
  13. `country` varchar(2) DEFAULT NULL,
  14. `https` tinyint(1) DEFAULT NULL,
  15. `domain` varchar(255) DEFAULT NULL,
  16. `page` varchar(255) DEFAULT NULL,
  17. `query` varchar(255) DEFAULT NULL,
  18. `referer` varchar(255) DEFAULT NULL,
  19. `search_term` varchar(255) DEFAULT NULL,
  20. `user_agent` varchar(255) DEFAULT NULL,
  21. `os` varchar(32) DEFAULT NULL,
  22. `browser` varchar(32) DEFAULT NULL,
  23. `max_https` tinyint(1) DEFAULT NULL,
  24. `geo_region` varchar(50) DEFAULT NULL,
  25. `geo_city` varchar(50) DEFAULT NULL,
  26. `geo_postal_code` varchar(10) DEFAULT NULL,
  27. `geo_latitude` decimal(8,4) DEFAULT NULL,
  28. `geo_longitude` decimal(8,4) DEFAULT NULL,
  29. `geo_dma_code` varchar(50) DEFAULT NULL,
  30. `geo_area_code` varchar(50) DEFAULT NULL,
  31. `geo_organisation` varchar(50) DEFAULT NULL,
  32. `geo_netspeed` varchar(20) DEFAULT NULL,
  33. `geo_continent` varchar(13) DEFAULT NULL,
  34. KEY `data_raw_ad_impression_viewer_id` (`viewer_id`),
  35. KEY `data_raw_ad_impression_date_time` (`date_time`),
  36. KEY `data_raw_ad_impression_ad_id` (`ad_id`),
  37. KEY `data_raw_ad_impression_zone_id` (`zone_id`)
  38. ) ENGINE=MyISAM DEFAULT CHARSET=utf8 MAX_ROWS=4294967295 AVG_ROW_LENGTH=50;


Если таблицы уже существуют, можно использовать ALTER TABLE

  1. ALTER TABLE tbl_name MAX_ROWS=1000000000 AVG_ROW_LENGTH= nnn ;


Подробнее о вносимых изменениях можно прочитать здесь http://dev.mysql.com/doc/refman/5.0/en/full-table.html

IP-адрес



При использовании обратного проксирования очень важно, чтобы приложение правильно определяло IP-адрес клиента. Для этого мы настроим nginx так, чтобы реальный IP-адрес передавался с помощью переменной HTTP_X_FORWARDED_FOR.

Для этого вносим в конфигурационный файл nginx следующие настройки

  1. proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;


Далее нужно модифицировать код Openx.

По крайней мере, версия, которая была на момент написания этого материала, неверно определяет IP-адрес клиента.

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

Модифицируем файл

  1. \plugins\geotargeting\GeoIP\GeoIP.delivery.php (83)


В файл добавляем функцию Get_realIP - исходный код есть в том же Openx
  1. /**
  2. *
  3. * Get real ip addres from enviroment variables
  4. *
  5. *
  6. */
  7.   if (!function_exists('Get_realIP')) {
  8.  
  9.  function Get_realIP(){
  10.  
  11.  $aHeaders = array(
  12.  'HTTP_FORWARDED',
  13.  'HTTP_FORWARDED_FOR',
  14.  'HTTP_X_FORWARDED',
  15.  'HTTP_X_FORWARDED_FOR',
  16.  'HTTP_CLIENT_IP'
  17.  );
  18.  
  19.  foreach ($aHeaders as $header) {
  20.  if (!empty($_SERVER[$header])) {
  21.  $ip = $_SERVER[$header];
  22.  break;
  23.  }
  24.  }
  25.  
  26.  if (!empty($ip)) {
  27.  // The "remote IP" may be a list, ensure that
  28.  // only the last item is used in that case
  29.  $ip = explode(',', $ip);
  30.  $ip = trim($ip[count($ip) - 1]);
  31.  
  32.  // If the found address is not unknown or a private network address
  33.  if (($ip != 'unknown') && (!MAX_remotehostPrivateAddress($ip))) {
  34.  // Set the "real" remote IP address, and unset
  35.  // the remote host (as it will be wrong for the
  36.  // newly found IP address) and HTTP_VIA header
  37.  // (so that we don't accidently do this twice)
  38.  $_SERVER['REMOTE_ADDR'] = $ip;
  39.  $_SERVER['REMOTE_HOST'] = '';
  40.  $_SERVER['HTTP_VIA']    = '';
  41.  }
  42.  }
  43.  
  44.  return $ip;
  45.  }
  46.  }


Далее модифицируем функцию MAX_Geo_GeoIP_getInfo, вставив в нее кусок, выделенный жирным шрифтом.

  1. function MAX_Geo_GeoIP_getInfo()
  2.  {
  3.  $conf = $GLOBALS['_MAX']['CONF'];
  4.  if (isset($GLOBALS['_MAX']['GEO_IP'])) {
  5.  $ip   = $GLOBALS['_MAX']['GEO_IP'];
  6.  }elseif(isset($_SERVER['HTTP_X_FORWARDED_FOR'])){
  7.  $ip = Get_realIP();
  8.  } else {
  9.  $ip = $_SERVER['REMOTE_ADDR'];
  10.  }


Это все изменения, которые мы внесли в исходный код продукта.

Теперь осталось проверить, что же у нас получилось.

Подводим итоги


Итак, что мы получили в результате, покажет наша система мониторинга

День первый


Мониторинг системы - после внесения изменений

Мониторинг системы - после внесения изменений


Увеличение нагрузки (макс. 500 запросов в секунду) почти в три раза от того, что есть на исходном графике, - система работает стабильно.

День второй


Мониторинг системы - после внесения изменений

Мониторинг системы - после внесения изменений - день 1


780 запросов в секунду - все работает стабильно.


День третий


Мониторинг системы - после внесения изменений - день 2

Мониторинг системы - после внесения изменений - день 3


800 запросов в секунду - стабильная работа.

При такой нагрузке application сервера обрабатывают где-то 120-160 запросов в секунду, в сутки доставляется 13М баннеров.

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

При увеличении нагрузки на систему необходимо просто добавить application сервер.

За год эксплуатации такой системы было показанно более 2,5 миллиарда баннеров и сделано более 15 миллионов кликов.

Данная статья так же доступна в виде документа.

Благодарности


Особую благодарность выражаю Алексею Рыбаку за то, что направил в правильное русло мысли о том, как побороть возникшую проблему.
Sych 30 сентября 2008 14:32 комментариев: 2
:) 2 :(

Комментарии:
Спасибо за идею использования apache_setenv для логирования данных.
Никогда раньше не встречал подобной затеи, но выглядит очень красиво.
Kirill   3 октября 2008 20:17 Комментировать может только авторизованный пользователь
:) 1 :( #
Идея хорошая - но после того как попробывал использовать nginx+PHPfpm(FCGI) то апач для новых проектов умер ;-)
Sych Sych   8 октября 2008 12:04 Комментировать может только авторизованный пользователь
:) 1 :( #
Только зарегистрированные пользователи могут оставлять комментарии.
© 2008 | О сайте | Инструкции | Обратная связь
© Powered by BigStreet

Работа с БД:
 Время - 0.003
 Запросов - 7
Работа с кэшем:
 Время - 0.0028
 Записей - 2
 Прочтений - 5
Общее время:
 0.0374