Хабрахабр — Анонсы

индекс
252,17

Баланс

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

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

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


Набор серьёзных средств, которые могут помочь в этом деле вполне известны, например, это средства проксирования и шардинга и супер-кластер: MySQL Proxy, Spock Proxy, MySQL Cluster. Последний, конечно, серьёзно отличается по своей сути от двух первых.

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

Удалось избежать потребности использования внешнего средства оценки состояния сервера: для эксперимента был выбран параметр 'Threads Running' (количество активных тредов) из стандарного статусного отчёта MySQL. Как показали испытания, он вполне объективно отражает уровень нагруженности сервера если речь идёт о двух машинах с идентичными настройками.

В результате вышло такое решение:
  • При каждой инициализации средств работы с базой данных, производится сравнение загруженности серверов из заданного списка, проверяется состояние репликации слейв-серверов, представленных в списке ('Slave_IO_Running', 'Slave_SQL_Running' из 'show slave status'). На основе этого анализа выбирается наиболее подходящий сервер (у кого 'Threads Runnig' меньше, тот и круче).
  • Результаты тестов помещаются в Memcached на короткий период времени (супер-методы smartGet/smartSet нашего же изделия, минимизирующие вероятность образования шквала запросов к базе данных в момент удаления объекта из кеша), так как запрос получения полного статуса и выбор из него поля 'Threads Runnig' довольно ресурсоёмок, что сказывается при частом его выполнении.
  • Если есть сервера, одинаково хорошо подходящие для отработки запросов, случайным образом выбирается только один из них.


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

PID USERNAME    THR PRI NICE   SIZE    RES STATE  C   TIME   WCPU COMMAND
32130 mysql        22  20    0  3401M  3007M kserel 6  30.3H  1.51% mysqld


PID USERNAME    THR PRI NICE   SIZE    RES STATE  C   TIME   WCPU COMMAND
36399 mysql        26  20    0  4697M  3565M kserel 5  22.6H  1.61% mysqld


Чтобы не быть голословным, приведу соответствующий метод из нашего класса Db (класс низкого уровня для работы с базой данных).

Внимание, этот код действительно используется на суперхабре!

<?
/**
* Устанавливает соединение с наименее нагруженным сервером из списка
* @param array $connections список соединений
* @param int $permission типа прав
*/
function connectFree ($connections, $permission = DB_R) {
   if(! is_array($connections)) throw new Exception('connectFree: передан некорректный массив соединений');

   $statuses = array();                 // Массив всех состояний
   $pool = array();                     // Массив для случайного выбора одного из нескольких равнозначных соединений
   $newConnections = array();           // Список установленных соединений
   $minLoad = 0;                        // Показатель минимальной нагруженности
   $avName = '';                        // Название подключения к серверу с минимальной нагрузкой
   $report = '';                        // Отчёт

   $cachePref = 'db_cStat_';            // Префикс ключа memcached
   $cacheTime = 35;                     // Время на которое кешируется состояние подключения
   $cache = Box:: getInstanceOf('CacheMC');   // Кеш

   // Перебор всех предложенных соединений
   foreach($connections as $conName) {
      /**
       * Проверяем закешированный статус соединения 
       */
      $cacheKey = $cachePref. $conName;
      $status = $cache->smartGet($cacheKey);

      // Если закешированного состояния нет
      if(!$status) {
         // Если нужное соединение ещё не открыто
         if(empty($this->connections[$conName])) {
            try {
               $connection = $this->connect($conName);
               $newConnections[] = $conName;
            } catch (Exception $e) {
               err('При проверке свободных серверов произошла ошибка подключения '. $conName);
               $report .= $conName. ': ошибка подключения'. '; ';

               continue;
            }
         } else {
            $connection = $this->connections[$conName];
         }

         // Если это слейв, проверяем статус репликации
         if(! empty($this->conParams[$conName]['slave']) && !$this->checkSlave($connection)) {
            $report .= $conName. ': слейв не прошёл проверку'. '; ';;
            $status = -2;
            $statuses[$conName] = $status;
         }

         // Проверка загруженности сервера по количеству работающих тредов
         // (пока я не придумал другого способа, но и этот довольно толков и информативен)
         if(!$status) {
            $status = $this->query('show status like \'Threads_running\'', $connection);

            // Вынимаем закопанное значение
            if($status && $status->rowCount()) $status = $status->fetchAll();
            if(! empty($status[0]['Value'])) $status = $status[0]['Value']; else $status = -1;
         }

         // Сохренение значения в кеш
         $cache->smartSet($cacheKey, $status, $cacheTime);
      }

      // Обработка состояния
      if($status) {
         // Поиск минимального значения
         if((!$minLoad && $status >= 0) || ($status > 0 && $status < $minLoad)) {
            $minLoad = $status;
            $avName = $conName;
         }

         $statuses[$conName] = $status;
         $report .= $conName. ': '. $status. '; ';
      }
   }

   // Если в итоге пусто
   if(! count($statuses)) throw new Exception('connectFree: не удалось найти ни одного доступного соединения');

   // Выбор равнозначных соединений
   foreach($connections as $conName) {
      if($statuses[$conName] == $minLoad) $pool[] = $conName;
   }

   // Если соединений несколько, выбираем одно случайно
   if(count($pool) > 1) {
      shuffle($pool);
      $avName = $pool[0];
   }

   // Отключиться от всех серверов, к которым производилось подключение, кроме самого доступного
   foreach($newConnections as $cName) {
      if($cName != $avName) $this->disconnect($cName);
   }

   // Подключиться к выбранному соединению
   if(empty($this->connections[$avName])) {
      $this->connect($avName);
   }

   // Установка соединений в зависимости от запрошенных прав
   if($permission == DB_R) {
      if($this->conNameRead != $avName) $this->init($avName);
   } else {
      if($this->conNameWrite != $avName) $this->init(null, $avName);
   }

   // В отладку
   $report .= 'результат: '. $avName;
   debug($report, 'Db:: connectFree');
}
?>
+74
17 сентября 2008, 19:48
85

комментарии (57)

+1
Zelenov #
Развиваемся! И даже чувствуется задел на будущее. А ведь уже скоро настанет тот день, когда понадобится подключать третий сервер. Интересно, какое будет у него название?
+9
rossomachin #
Edgar Hoover
+1
fzn7 #
Bond. James Bond.
+1
juks #
Sega
0
justpro #
офигенное название, что-нибудь так-же назову, можно?)
+3
juks #
Можно конечно :-)

Я вообще хотел написать про это, собрать фольклёр, что ли, но решил что как-то глупо.

Одно из требований к названию это то, что его должны хорошо воспринимать по телефону люди из датацентра :-)
+2
CurlyBrace #
Игорь, мне кажется про фольклор было бы интересно многим )
0
Qwertyfog #
Предлагаю Chucky.
+1
KiNDER #
можно: Чак Норис
+1
Kurimochi #
Билл, Стив, Линус (Билли, Стиви, Линуська(?) )
0
shalomman #
Back-end Сервер баз данных — «Анатолий Вассерман»
0
juks #
у нас есть сервер Gena
0
glader #
И женщина Люба: D
0
void #
а у нас cerf
+4
kilg #
сделайте конкурс на имя нового сервака
0
Zelenov #
Да можно прямо тут провести. Мои предложения (по-аналогии): Godfather, Scarface.
+3
KRen #
Почему-то вспомнились три поросенка =))
+1
workless #
33 Богатыря, с ними дядька суперсервер Черномор
+1
glader #
маршрутизатор
0
egorinsk #
А можно поинтересоваться (не совсем по теме, но все же): применяется ли на Хабре кеширование страниц (например, текста топиков) и эффективно ли оно, если применяется? Или тут его не применить из-за обновления комментариев и прямого эфра?
+2
ur001 #
Если бы не применялось 2 сервера бы не спасло.
Применяется, причём достаточно сильное и многоуровневое
0
egorinsk #
А можно примерно хотя бы узнать, где? Очень любопытно) Если, конечно это не страшная военная хабратайна))
0
ur001 #
Кеширование НЕ применяется только в некоторых случаях при выдаче постов
+6
Aist #
Например, если зайти гостем «на морду», то к базе будет 0 запросов в 99.9% случаев.
0
egorinsk #
o_0 круто)
+3
MajestiC #
А можно узнать механизм smartSet/smartGet?
+2
Aist #
Если говорить именно о механизме, то «умные» запросы к memcached позволяют избежать ситуации, когда данные устарели в кеше и приходит несколько запросов, последовательно пытающихся получить данные и записать их в кеш. Если данные устарели, то лишь первый запрос пойдет их обновлять, а остальные запросы будут получать устаревшие данные, пока ушедший за обновлением запрос не отработает :) Поэтому smartSet/smartGet используется не всюду, а лишь там, где не критично получать «немного старые данные».

ЗЫ Juks, молодец! Этого нам не хватало очень сильно.
0
kabachok #
а где он был? o_0
+3
murr #
Хабратипограф не даст бездумно скопипастить Хабракод :)
+6
Trave #
if(! is_array($connections)) throw new Exception('connectFree: передан некорректный массив соединений');
некорректный или не массив? :)
0
ItGold #
На следующем этапе перестанете изгаляться и поставите нормальный load-balancer.
0
Aist #
Случится это при запуске мегахабра, никак не раньше :) При данных задачах «нормальный load-balancer», пожалуй, избыточен.
0
ItGold #
Будем надеяться что это случится как можно раньше :)
0
juks #
не один нормальный MySQL load-balancer, боюсь, за несколько часов не поставить. У них у большинства имеются свои ограничения на программную часть, как минимум, скорее всего придётся писать свой межсерверный join
+3
magicdream #
// Поиск минимального значения
if((!$minLoad && $status >=) || ($status > && $status < $minLoad))

И эта строчка работает? :)
+4
juks #
куда-то снесло часть значений при покраске
+1
Kigorw #
Fowler Refactoring — код подошел бы для книжного примера.
+1
freehome #
интересно
«Сохренение значения» порадовало )
0
kurokikaze #
Очень интересно. Потом на свежую голову вчитаюсь в код получше.
0
glader #
«проверяем статус репликации»
Смотрите отставание от мастера?
0
juks #
Отставание пока нет, надо конечно, но пока только статус. В деле существенное отставание бывает только когда слейв догоняет мастера после восстановления репликации
0
juks #
уже есть :-)
0
glader #
Извини, туплю. Что ты имеешь в виду под «статусом»? Какую цифру?
0
juks #
mysql> show slave status\G
*************************** 1. row ***************************
Slave_IO_State: Waiting for master to send event
Master_Host: 10.0.0.3
Master_User: replication
Master_Port: 3306
Connect_Retry: 10
Master_Log_File: clyde-relay-bin.000011
Read_Master_Log_Pos: 698473286
Relay_Log_File: bonnie-relay-bin.000022
Relay_Log_Pos: 698473429
Relay_Master_Log_File: clyde-relay-bin.000011
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
Replicate_Do_DB: superhabr
Replicate_Ignore_DB:
Replicate_Do_Table:
Replicate_Ignore_Table:
Replicate_Wild_Do_Table:
Replicate_Wild_Ignore_Table:
Last_Errno: 0
Last_Error:
Skip_Counter: 0
Exec_Master_Log_Pos: 698473286
Relay_Log_Space: 698473429
Until_Condition: None
Until_Log_File:
Until_Log_Pos: 0
Master_SSL_Allowed: No
Master_SSL_CA_File:
Master_SSL_CA_Path:
Master_SSL_Cert:
Master_SSL_Cipher:
Master_SSL_Key:
Seconds_Behind_Master: 0
1 row in set (0.00 sec)
0
glader #
А ларчик просто открывался :)
Спасибо
0
glader #
«При каждой инициализации средств работы с базой данных»
Один раз на запрос? Чаще? Реже?
–4
svetko #
столько строчек кода, только чтобы подключиться к бд… скоро чтобы присвоить значение переменной придётся писать 500 строчек =)
+2
aleks_raiden #
это уже есть :) сеттеры и геттеры уже везде почти :) :) :)
0
juks #
Это базовый класс низкого уровня. В проекте думать про соединение вообще не надо.

0
svetko #
разница базовый не базовый? кол-во опкодов для действия зашкаливает.
0
juks #
$users = new HabrUsers();
$user = $users->getOne(array('login'=>$login));

echo $user['email'];
0
gkirok #
можешь написать более точное описание серверов, железа так сказать, мне нужно начальству показать что нужно чтоб миллион запросов обрабатывать. спасибо.
+1
juks #
Clyde: два Xeon 1.6 по 4 ядра в каждом. 6 гигабайт памяти, два SAS диска по 74 гигабайта. FreeBSD 6.2. Mysql 5.0.67
0
gkirok #
спасибо
0
maghamed #
Скажите, %juks%, а может быть имело смысл вынести такую логику по коннекту к определенному DB серверу и выбору менее загруженного, скажем в отдельный скрипт, и запускать его по крону, чтобы он обновлял статистику в memcached актуальными данными. Ну и поставить интервал запуска скрипта в соответствии с Вашим $cacheTime = 35;. А в рабочих скриптах уже просто выгребать данные из кеша. В которых бы лежали статистика по каждому из серверов. Что Вы думаете по данному поводу?
0
juks #
Я думаю, что это уже на любителя. Я стараюсь делать меньше крон-задач, меньше нагромождения.

Текущая реализация схожа по работоспособности с тем, что вы предлагаете, просто действует «on demand». Как-нибудь, может напишу про smartGet и smartSet, там всё элементарно, но в действительности спасает от лишних запросов к базе.

Если не кешировать запросы вообще, то это добавляет по 20% нагрузки CPU на каждый сервер. С кешированием дополнительная нагрузка незаметна. Как получить отдельно число работающих тредов без сбора всей статистики целиком я так и не нашёл.
0
juks #
Кстати, вспомнил.

У крона ведь дискретность одна минута, у Memcached — секунда. Даже при периоде в 5 секунд, время от времени имеет место некоторая «раскачка» нагрузки от сервера к серверу.

Так что если выносить, то надо делать отдельный демон.

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