Доставка оновлень з БД MySQL в додаток за допомогою клієнта реплікації libslave

  
 
При написанні будь-якого досить великого проекту завжди встають більш-менш схожі проблеми. Одна з них — проблема швидкості отримання оновлень системи. Відносно легко можна налагодити швидке отримання невеликих оновлень. Досить просто зрідка отримувати оновлення великого обсягу. Але що якщо треба швидко оновлювати великий масив даних?
 
Для Таргету Mail.Ru, як і для всякої рекламної системи, швидкий облік змін важливий з наступних причин:
• можливість швидкого відключення показу кампанії, якщо рекламодавець зупинив її в інтерфейсі або якщо у нього скінчилися гроші, а значить, ми не будемо показувати її безплатно;
• зручність для рекламодавця: він може поміняти ціну банера в інтерфейсі, і вже через кілька секунд його банери почнуть показуватися за новою вартістю;
• швидке реагування на зміну ситуації: зміна CTR, надходження нових даних для навчання математичних моделей. Все це дозволяє коригувати стратегію показу реклами, чуйно реагуючи на зовнішні чинники.
 
У цій статті я розповім про оновлення даних, що лежать у великих таблицях в БД MySQL, фокусуючись на швидкості і консистентності — адже не хотілося б вже отримати новий заведений банер, але при цьому не отримати дану рекламну кампанію.
 
Як ми могли б це робити, використовуючи стандартні засоби MySQL? Можна було б періодично перечитувати всі таблиці цілком. Це самий неоптимальний варіант, адже разом з новими даними буде переганятися багато старих, вже відомих даних, гігантська навантаження ляже на мережу і на MySQL-сервер. Інший варіант — відповідним чином підготувати схему зберігання даних: ввести в усі таблиці час останнього оновлення і робити селектів за потрібні проміжки часу. Правда, якщо таблиць багато, і машин, де треба зберігати копію даних, теж багато, то і селектів буде дуже-дуже багато, а навантаження на MySQL-сервер, знову-таки, виходить великий. Крім того, доведеться подбати про консистентності отриманих оновлень.
 
Зручний спосіб вирішення проблеми пропонує нам libslave:
• дані приходять до нас з БД «самі» — немає необхідності Поллі базу, якщо оновлень в даний момент немає;
• поновлення приходять в тому порядку, в якому виконувалися на майстра, нам видно вся історія змін;
• не треба спеціально готувати таблиці, вводити таймстемпи, непотрібні з точки зору бізнес-логіки;
• видні границі транзакцій — тобто точки консистентності даних;
• низьке навантаження на майстер: він не виконує запити, що не навантажує процесор — він просто шле файли.
 
Про те, як ми використовуємо libslave для наших цілей, і буде ця стаття. Я коротенько розповім, як влаштована реплікація даних в MySQL (думаю, все собі це добре уявляють, але все ж), як влаштована сама libslave, як нею користуватися, наведу результати бенчмарків і деякий порівняльний аналіз існуючих реалізацій.
 
 

Пристрій реплікації

Майстер-база записує кожен запит, що змінює дані або схему даних, в спеціальний файл — так званий binary-log . Коли бінарний лог досягає певного розміру, запис переходить до наступного файла. Існує спеціальний індекс цих бінарних логів, а також певний набір команд для управління ними (наприклад, для видалення старих бінлогов).
 
Слейв отримує по мережі ці файли в сирому вигляді (що спрощує реалізацію майстри) і застосовує до своїх даних. Слейв пам'ятає позицію, до якої він дочитав бінлогі, і тому при перезапуску просить майстер продовжити відправляти йому логи починаючи з потрібній позиції.
 
Існує два режими реплікації — STATEMENT і ROW. У першому режимі майстер записує в бінлог вихідні запити, які він виконував для зміни даних (UPDATE / INSERT / DELETE / ALTER TABLE / ...). На слейв всі ці запити виконуються так само, як вони виконувалися б на майстра.
 
У ROW-режимі, доступному починаючи з MySQL версії 5.1, в бінлог пишуться не запити, а вже змінені цими запитами дані (втім, запити, що змінюють схеми даних (DDL), все одно пишуться як є). Подія в ROW-режимі являє собою:
• один рядок даних — для команд INSERT і DELETE. Відповідно, для INSERT пишеться вставлена ​​рядок, для DELETE — дистанційна
• два рядки — BEFORE і AFTER — для UPDATE.
 
При якихось масових зміни даних такі бінлогі виходять значно більше, ніж якщо б ми записали сам запит, але це окремий випадок. Для використання в своєму демона ROW-реплікація нам дуже зручна тим, що не треба вміти виконувати запити і ми отримуємо відразу те, що потрібно — змінені рядки.
 
До речі, зовсім необов'язково включати ROW-реплікацію на своєму самій-важливому-майстер-сервері. Майстер-сервер може годувати кілька слейв за звичайною STATEMENT реплікації (резерви, бекапи), а частина з цих слейв можуть писати ROW-логи, і вже до них звертатимуться за даними демона.
 
 

Як це працює у нас

Отримання даних з бази відбувається в два етапи:
1. Початкова завантаження даних при старті демона
2. Отримання оновлень
 
Початкова завантаження даних, в принципі, може бути полегшена наявністю дампів — демон може скидати свій стан на диск і писати позиції бінлога в той же дамп. Тоді при старті можна завантажити дані в пам'ять з дампа і продовжити читати бінлог з останньої позиції. Але коли дампа немає, треба щось робити. І це щось, зрозуміло, Select.
 
Вибрати дані треба так, щоб у пам'яті вони були консистентними. Для цього можна вибирати дані з усіх таблиць в одній транзакції. Але після цього нам треба почати читати оновлення по libslave і вирішити, з якої позиції ми будемо читати. Взяти поточну позицію після селектів не можна, т. к. під час селектів в базу могли бути записані нові дані, які в селект не потрапили, але позицію бінлога посунули. Брати позицію перед початком селектів теж не можна, оскільки від моменту, коли ми візьмемо поточну позицію, до моменту початку селекта можуть прийти нові дані. Почати транзакцію командою BEGIN, а потім отримати поточну позицію бінлога, знову-таки, чи не вийде — між ними немає ніякої синхронізації, і якщо один клієнт зробив BEGIN, то інші клієнти могли в цей час записати дані, а позиція бінлога, відповідно, могла зміститися.
 
Всі ці роздуми підводять нас до тієї думки, що забезпечувати консистентность читання буде частково завданням демона. Дані у нас в пам'яті влаштовані так, що якщо до нас приходить неконсістентний об'єкт (у тому плані, що ми можемо виявити його неконсістентность — наприклад, йому не вистачає необхідних зв'язків), то він буде просто викинутий з пам'яті демона; однак, якщо пізніше він прийде консистентним, то буде вставлений в пам'ять. Якщо він прийде консистентним два рази, то в пам'яті залишиться його останній стан. Якщо він був у пам'яті консистентним, а прийде неконсістентним, то неконсістентное стан ми не застосуємо, і об'єкт в пам'яті демона не зміниться.
 
Виходячи з усього цього, правильна, з нашої точки зору, модель початкового завантаження виглядає так:
1. Отримуємо поточну позицію бінлога на майстра p1 — робимо це в довільний момент часу.
2. Робимо все селектів.
3. Отримуємо нову поточну позицію бінлога на майстра p2.
4. За допомогою libslave читаємо всі події між p1 і p2. При цьому в демон можуть прийти як нові об'єкти, які утворилися під час селекта, так і змінені старі, які вже є в пам'яті.
5. Після цього у демона є несуперечлива копія даних, демон готовий до роботи — можемо відповідати на запити і приймати оновлення за допомогою libslave, починаючи з позиції p2.
 
Я особливо підкреслю, що, раз консистентность даних у нас підтримується в демона, то нам немає необхідності робити в п. 2 селектів в одній транзакції. Розглянемо, для прикладу, таку складну послідовність подій:
1. Отримуємо поточну позицію бінлога.
2. Почали читати таблицю кампаній, в якій є кампанія campaign1.
3. Створився новий баннер banner1 в стані 1 в існуючій кампанії campaign1.
4. Створилася нова кампанія campaign2, що не потрапляє в результат селекта.
5. Створився новий баннер banner2, який прив'язаний до кампанії campaign2.
6. Banner1 перейшов в стан 2.
7. Закінчили читати таблицю кампаній, перейшли до читання таблиці банерів, і в це читання потрапить banner1 в стані 2 і banner2.
8. Дочитали таблицю банерів.
9. Створився новий баннер banner3 в кампанії campaign2.
10. Отримали нову поточну позицію бінлога p2.
 
І тепер подивимося, що буде відбуватися в демона:
1. Демон селектів всі кампанії і запам'ятовує їх у пам'ять (можливо, перевіряючи якісь критерії — може бути, нам потрібні в пам'яті не всі кампанії).
2. Демон перейшов до селектів банерів. Він прочитає banner1 відразу в стані 2 і запам'ятає його, прив'язавши його до вже прочитаної кампанії campaign1.
3. Він прочитає banner2 й відкине його, т. к. кампанії campaign2 для нього в пам'яті немає — вона не потрапила в результати селекта.
4. На цьому селект закінчився. Переходимо до читання змін від позиції p1 до позиції p2.
5. Зустрічаємо створення банера banner1 в стані 1. Нагадаю, в пам'яті демона він вже є в своєму останньому стані 2, але, проте ж, ми застосуємо це оновлення, і переведемо банер в стан 1 — це не страшно, т. к. дані будуть використовуватися для роботи тільки після того, як ми дочитаємо до позиції p2, а до цієї позиції ми отримаємо зміни цього банера ще раз.
6. Прочитали створення нової кампанії campaign2 — запам'ятали її.
7. Прочитали створення прив'язаного до неї банера banner2 — тепер ми його запам'ятаємо, т. к. для нього є відповідна кампанія, а дані Консистентне.
8. Прочитали переклад banner1 в стан 2 — застосували, тепер і тут Консистентне.
9. Прочитали створення banner3 в кампанію campaign2 — вставили в пам'ять.
10. Дійшли до позиції p2, зупинилися — всі дані завантажені Консистентне, можемо віддавати їх користувачеві і читати поновлення далі в штатному режимі.
 
Зауважимо, що на етапі початкової вибірки даних їх неконсістентность не говорить про те, що помилки присутні в базі — просто це така особливість процесу завантаження даних. Така неконсістентность буде виправлена ​​далі демоном при дочітиваніі даних. А от якщо після цього в них є якісь невідповідності — тоді вже так, тоді це база.
 
Код виглядає приблизно так:
  
slave::MasterInfo sMasterInfo;	// заполняем опции коннекта к базе
    Slave sSlave(sMasterInfo);		// создаем объект для чтения данных
    // запоминаем последнюю позицию бинлога
    const slave::Slave::binlog_pos_t sInitialBinlogPos = sSlave.getLastBinlog();
    select();	// селектим данные из базы
    // получаем новую последнюю позицию бинлога — изменилась за время селекта
    const slave::Slave::binlog_pos_t sCurBinlogPos = sSlave.getLastBinlog();
    // теперь нам надо дочитать данные из слейва до этой позиции
    init_slave();	// здесь добавляются колбеки на таблицы, вызываются Slave::init и Slave::createDatabaseStructure
    sMasterInfo.master_log_name = sInitialBinlogPos.first;
    sMasterInfo.master_log_pos = sInitialBinlogPos.second;
    sSlave.setMasterInfo(sMasterInfo);
    sSlave.get_remote_binlog(CheckBinlogPos(sSlave, sCurBinlogPos));

Функтор CheckBinlogPos викличе завершення читання даних з бінлога по досягненні позиції sCurBinlogPos. Після цього відбувається первинна підготовка даних для використання і запускається читання даних з слейв з останньої позиції вже без всяких функторів.
 
 

Пристрій libslave

Розглянемо докладніше, що таке libslave. Моє опис засноване на найбільш популярною реалізації . Нижче я порівняю кілька ФОРКОМ і зовсім іншу реалізацію.
 
Libslave — це бібліотека на C + +, яка може бути використана у вашому додатку для отримання оновлень з MySQL. Libslave не пов'язана на рівні кодів з MySQL-сервером; вона збирається і линкуется тільки з клієнтом — libmysqlclient. Працездатність бібліотеки перевірялася на майстрах версії від 5.1.23 до 5.5.34 (не на всіх! Тільки на тих, що під руку попалися).
 
Для роботи нам потрібен MySQL-сервер з включеною записом бінлогов в режимі ROW. Для цього у нього в конфіги повинні бути наступні рядки:
 
[mysqld]
log-bin = mysqld-bin
server-id = 1
binlog-format = ROW

Користувачеві, під яким буде ходити libslave, будуть потрібні права доступу REPLICATION SLAVE і REPLICATION CLIENT, а також SELECT на ті таблиці, які він буде обробляти (на ті, які будуть в бінлогах, але які він буде пропускати, SELECT не потрібний). Право SELECT потрібно для отримання схеми таблиці.
 
У libslave вбудований мікро-класик nanomysql :: Connection для виконання звичайних SQL-запитів на сервері. У житті ми його використовуємо не тільки як частина libslave, але і як клієнт для MySQL взагалі (не хотілося використовувати mysqlpp, mysql-connector та інші штуки).
 
Основний клас називається Slave. Перед початком роботи ми задаємо користувальницькі колбек для подій від таблиць, за якими будемо стежити. У колбек передається інформація про подію в структурі RecordSet: його тип (Write / Update / Delete) і дані (вставлена ​​/ оновлена ​​/ дистанційна запис, у випадку Update — її попередній стан).
 
При ініціалізації бібліотеки параметрами майстра відбуваються наступні перевірки:
1. Перевіряємо можливість з'єднатися з сервером.
2. Робимо SELECT VERSION () — переконуємося, що версія не менше, ніж 5.1.23.
3. Робимо SHOW GLOBAL VARIABLES LIKE 'binlog_format' — переконуємося, що формат бінлогов ROW.
4. Читаємо збережену позицію останнього прочитаного повідомлення через користувача функцію. Якщо для користувача функція нічого не повернула (пусте ім'я бінлога, нульова позиція), то читаємо поточне положення бінлога в майстрові через запит SHOW MASTER STATUS.
5. Прочитуємо структуру бази для таблиць, за якими будемо стежити.
6. Генеруємо slave_id — такий, щоб не збігався ні з одному із запитів SHOW SLAVE HOSTS.
7. Реєструємо слейв на майстра виконанням simple_command (COM_REGISTER_SLAVE).
8. Запитувані передачу дампа командою simple_command (COM_BINLOG_DUMP).
9. Запускаємо цикл обробки вхідних пакетів — їх парсинг, виклик потрібних колбек, обробку помилок.
 
Окремо варто згадати про п'ятий пункт — зчитування структури бази даних. У разі справжнього MySQL-slave, ми завжди знаємо правильне пристрій табличок, тому що почали з якогось SQL-дампа і продовжили читати таблиці з відповідної позиції бінлога, дотримуючись всіх DDL-statement. Libslave ж у загальному випадку стартує з тієї позиції бінлога, яку їй надасть користувач (наприклад, з тією, на якій ми збереглися в минулий раз, або з поточної позиції майстра). Минулих знань про структуру бази даних у загальному випадку у неї немає, тому схему таблиці вона отримує Парс виводу результатів запиту SHOW FULL COLUMNS FROM. І саме звідти береться інформація про те, які поля, яких типів і в якому порядку парсити з бінлога. З цим може бути така проблема: опис таблиць ми отримуємо поточні, а бінлогі можемо почати читати попередні, коли таблиця ще виглядала по-іншому. У цьому випадку libslave, швидше за все, вийде з помилкою, що дані неконсістентни. Доведеться починати читати з поточної позиції майстра.
 
Не страшно міняти опис таблиць в процесі роботи libslave. Вона розпізнає запити ALTER TABLE і CREATE TABLE, і відразу після їх отримання перечитує структури таблиць. Звичайно, і тут можливі проблеми. Припустимо, що ми швидко два рази поміняли структуру таблиці, між цими подіями записавши туди якісь дані. Якщо libslave отримає запис про перший Альтері, тільки коли вже буде завершено другий, то через SHOW FULL COLUMNS FROM отримає відразу ж другий стан БД. Тоді подія на оновлення таблиці, яке буде відповідати ще першого опису, має шанси зупинити реплікацію. Втім, на практиці таке буває вкрай рідко (у нас не було жодного разу), і в разі чого лікується перезапуском демона з чистого аркуша.
 
За допомогою libslave можна відстежувати кордону транзакцій. Незважаючи на те, що, поки транзакція не завершиться, жодна її запис не потрапить в бінлог, розрізняти транзакції все ж може бути важливо: якщо у вас є якісь два пов'язаних зміни в різних таблицях, то ви можете не захотіти використовувати тільки одну оновлену, поки не оновиться і друга. У бінлог не потрапляють події BEGIN — при початку транзакції йдуть відразу змінені рядки, які завершуються COMMIT'ом. Тобто транзакції відслідковуються не по BEGIN / COMMIT, а по двох послідовних COMMIT'ам.
 
 

Якщо майстер зник

Основний цикл роботи libslave, що викликається функцією get_remote_binlog , отримує як параметр користувальницький функтор, який перевіряється перед читанням кожного нового пакета. Якщо функтор поверне true, то цикл завершиться.
 
При виникненні будь-яких помилок відбувається виведення помилки в лог і цикл триває. Зокрема, якщо сервер перезавантажують, то libslave намагатиметься переконнектіться з сервером до переможного кінця, після чого продовжить читати дані. Можливі й вічні залипнув — наприклад, якщо з-під майстра потягли логи, які читає libslave, то libslave буде вічно "плакатися» в циклі, що потрібних логів немає.
 
На випадок обриву мережі з'єднання налаштовується за таймаут в 60 секунд. Якщо за 60 секунд не прийшло жодного нового пакета даних (що може бути і у випадку, якщо просто немає оновлень), то буде проведений реконнект до бази даних, і читання потоку повідомлень продовжиться з останньою прочитаної позиції.
 
В принципі, завершити цикл можна і раніше, не чекаючи закінчення таймаута. Припустимо, що ви хочете мати можливість швидко, але коректно завершити додаток по Ctrl + C, не чекаючи можливих 60 секунд при розриві мережі або відсутності оновлень. Тоді в обробнику сигналу досить виставити прапор, який змусить наступний виклик користувальницького функтора повернути true, і викликати функцію Slave :: close , яка примусово закриє сокет MySQL. Через це виклик читання пакета завершиться з помилкою, і при перевірці відповіді від користувальницького функтора відбудеться вихід з циклу.
 
 Статистика
В бібліотеці є абстрактний клас ExtStateIface , якому з libslave передається різна інформація: число реконнекта, час останнього евент, статус з'єднання з БД. Цей же клас відповідає за збереження і завантаження поточної позиції бінлога в яке-небудь постійне сховище. Существует дефолтная реализация этого класса DefaultExtState , работающая через мьютекс (т. к. устанавливать статистику может slave в одном потоке, а читать ее — кто-то другой в другом). Грустная новость заключается в том, что правильная реализация этого класса необходима для корректной работы libslave, т. е. это не просто объект статистики — это объект, который может управлять работой библиотеки.
 
 

Бенчмарки


Бенчмарки проводились на двух комплектах машин.
 
Перший комплект представляв собою одну машину, на якій була встановлена ​​БД, і на ній же виконувався тест. Конфігурація:
• CPU: Intel ® Core (TM) i7-4770K CPU@3.50GHz
• mem: 32 GB 1666 MHz
• MB: Asus Z87M-PLUS
• HDD: SSD OCZ-VERTEX3
• OS Gentoo Linux, ядро ​​3.12.13-gentoo x86_64
Налаштування БД були за замовчуванням. Чесно кажучи, не думаю, що вони мають велике значення для майстра, який фактично просто «ллє» файл по мережі.

Другий комплект представляв собою дві машини. Перша машина з БД:
• CPU: 2 x Intel ® Xeon ® CPU E5620@2.40GHz
• mem: 7 x 8192 MB TS1GKR72V3N 800 MHz (1.2ns), 1 x 8192 MB Kingston 9965434-063.A00LF 800 MHz (1.2ns), 4 x Empty
• MB: ETegro Technologies ETRS370G3
• HDD: 14 ​​x 300 GB HUS156030VLS600, 2 x 250 GB WDC WD2500BEVT-0
• PCI: LSI Logic / Symbios Logic SAS2116 PCI-Express Fusion-MPT SAS-2 [Meteor], Intel Corporation 82801JI (ICH10 Family) SATA AHCI Controller
• OS CentOS release 6.5 (Final) ядро ​​2.6.32-220.13.1.el6.x86_64

Машинка з тестом:
• CPU: 2 x Intel ® Xeon ® CPU E5-2620 0@2.00GHz
• mem: 15 x 8192 MB Micron 36KSF1G72PZ-1G4M1 1333 MHz (0.8ns), 1 x 8192 MB Micron 36KSF1G72PZ-1G6M1 1333 MHz (0.8ns)
• MB: ETegro Technologies ETRS125G4
• HDD: 2 x 2000 GB Hitachi HUA72302, 2 x 250 GB ST250DM000-1BD14
• PCI: Intel Corporation C602 chipset 4-Port SATA Storage Control Unit, Intel Corporation C600/X79 series chipset 6-Port SATA AHCI Controller
• OS CentOS release 6.5 (Final) ядро ​​2.6.32-358.23.2.el6.x86_64

Мережа між машинами 1 Гбіт / с.

Слід зазначити, що в обох тестах БД не зверталася до диску, тобто бінлогі були закешовану в пам'яті, і в передачу даних тест не упирався. Завантаження CPU у всіх тестах була 100%. Це говорить про те, що упиралися ми в саму бібліотеку libslave, тобто досліджували її продуктивність.

Для тесту було створено дві таблиці — маленька й велика:

<code class="html">CREATE TABLE short_table (
    id int NOT NULL auto_increment,

    field1 int NOT NULL,
    field2 int NOT NULL,

    PRIMARY KEY (id)
);

CREATE TABLE long_table (
    id int NOT NULL auto_increment,

    field1 timestamp NOT NULL DEFAULT 0,
    field2 timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    field3 enum('a','b','c') NOT NULL DEFAULT 'a',
    field4 enum('a','b','c') NOT NULL DEFAULT 'a',

    field5 varchar(255) NOT NULL,
    field6 int NOT NULL,
    field7 int NOT NULL,
    field8 text NOT NULL,
    field9 set('a','b','c') NOT NULL DEFAULT 'a',
    field10 int unsigned NOT NULL DEFAULT 2,
    field11 double NOT NULL DEFAULT 1.0,
    field12 double NOT NULL DEFAULT 0.0,
    field13 int NOT NULL,
    field14 int NOT NULL,
    field15 int NOT NULL,
    field16 text NOT NULL,

    field17 varchar(255) NOT NULL,
    field18 varchar(255) NOT NULL,
    field19 enum('a','b','c') NOT NULL DEFAULT 'a',
    field20 int NOT NULL DEFAULT 10,

    field21 double NOT NULL,
    field22 double NOT NULL DEFAULT 1.0,
    field23 double NOT NULL DEFAULT 1.0,

    field24 double NOT NULL DEFAULT 1.0,

    field25 text NOT NULL DEFAULT "",

    PRIMARY KEY (id)
);
</code>
Кожна таблиця містила по одному мільйону записів. При вставці даних одне текстове поле заповнювалося короткою рядком. Всі інші рядки були фактично порожніми, поля заповнювалися значеннями за замовчуванням. Тоб такий метод вставки дозволяв отримати повноцінні бінлогі, але більшість строкових полів були порожніми: 
INSERT INTO short_table (field1, field2) values ​​(1, 2); 
INSERT INTO long_table (field5, field25) values ​​(«short_string», «another_short_string»); 

Кожна з цих таблиць були спочатку вставлена ​​в БД, після чого повністю оновлена ​​запитами: 
UPDATE short_table SET field1 = 12; 
UPDATE long_table SET field6 = 12; 

Таким чином, вдалося отримати набір бінлогов типу INSERT і набір бінлогов типу UPDATE (які рази в два більше, оскільки містять крім зміненої рядки її попередній стан). Перед кожною операцією запам'ятовували позицію бінлога, тобто отримали таким чином 4 інтервалу бінлогов: 
інсерти короткою таблиці (5429180 - 18977180 = &gt; 13548000) 
апдейти короткою таблиці (18977180 - 45766831 = &gt; 26789651) 
інсерти довгої таблиці (45768421 - 183563421 = &gt; 137795000) 
апдейти довгої таблиці (183563421 - 461563664 = &gt; 278000243). 

Тест був зібраний компілятором gcc-4.8.2 з прапорами-O2-fomit-frame-pointer-funroll-loops. Кожен тест проганяли три рази, в якості результату бралися показники третього тесту. 

А тепер трохи таблиць і графіків. Але спочатку нотація: 
• «без колбек» означає, що ми попросив libslave прочитати певний набір бінлогов, що не навішуючи ніяких колбек на таблицю, тобто і запису RecordSet не створювались. 
• «З бенчмарк-колбек» означає, що були повішені колбек, що вимірюють посекундну продуктивність, намагаючись мінімально впливати на загальний час виконання тесту (потрібно для побудови графіків). Більше вони нічого не робили - вся робота на libslave була тільки в тому, щоб розпарсити запис, створити об'єкт (-и) <b>RecordSet </b>і передати їх в користувача функцію за посиланням. 
• «З lockfree-malloc» означає, що в тесті використовувався <a href="https://github.com/Begun/lockfree-malloc">аллокатор </a>. 

«Час 1» і «Час 2» - час виконання тесту для набору машин 1 і 2, відповідно. 

<table>
<tr>
<th>Тест </th>
<th>Час 1, сек. </th>
<th>Час 2, сек. </th>
</tr>
<tr>
<td>Інсерти в маленьку таблицю без колбек </td>
<td>00,0299 </td>
<td>00,1595 </td>
</tr>
<tr>
<td>Інсерти в маленьку таблицю з бенчмарк колбек </td>
<td>02,4092 </td>
<td>03,8958 </td>
</tr>
<tr>
<td>Апдейти в маленьку таблицю без колбек </td>
<td>00,0500 </td>
<td>00,2336 </td>
</tr>
<tr>
<td>Апдейти в маленьку таблицю з бенчмарк-колбек </td>
<td>04,8499 </td>
<td>07,4892 </td>
</tr>
<tr>
<td>Інсерти у велику таблицю без колбек </td>
<td>00,2627 </td>
<td>01,1842 </td>
</tr>
<tr>
<td>Інсерти у велику таблицю з бенчмарк-колбек </td>
<td>20,2901 </td>
<td>33,9604 </td>
</tr>
<tr>
<td>Інсерти у велику таблицю з бенчмарк-колбек з lockfree-malloc </td>
<td>19,0906 </td>
<td>34,5743 </td>
</tr>
<tr>
<td>Апдейти у велику таблицю без колбек </td>
<td>00,6225 </td>
<td>02,3860 </td>
</tr>
<tr>
<td>Апдейти у велику таблицю з бенчмарк-колбек </td>
<td>40,4330 </td>
<td>70,7851 </td>
</tr>
<tr>
<td>Апдейти у велику таблицю з бенчмарк-колбек з lockfree-malloc </td>
<td>37,9637 </td>
<td>68,3616 </td>
</tr>
<tr>
<td>Інсерти і апдейти в обидві таблиці без колбек </td>
<td>00,9499 </td>
<td>03,9179 </td>
</tr>
<tr>
<td>Інсерти і апдейти в обидві таблиці з бенчмарк-колбек на коротку </td>
<td>08,0445 </td>
<td>14,8126 </td>
</tr>
<tr>
<td>Інсерти і апдейти в обидві таблиці з бенчмарк-колбек на довгу </td>
<td>62,8213 </td>
<td>100,9520 </td>
</tr>
<tr>
<td>Інсерти і апдейти в обидві таблиці з бенчмарк-колбек на обидві </td>
<td>67,8092 </td>
<td>118,3860 </td>
</tr>
<tr>
<td>Інсерти і апдейти в обидві таблиці з бенчмарк-колбек на обидві з lockfree-malloc </td>
<td>64,5951 </td>
<td>113,3920 </td>
</tr>
</table>

Нижче наведено графік швидкості читання за останнім бенчмарку з обох машин. Графік поступово спадає: найшвидше читаються маленькі інсерти, далі йдуть маленькі апдейти, далі - великі інсерти, і найповільніше обробляються маленькі апдейти. Можна приблизно уявити швидкість обробки кожного типу бінлогов. 

<img src="http://habrastorage.org/getpro/habr/post_images/5dc/402/070/5dc402070f5946f186faba035e919a28.png"/>

Я не досліджував в даному бенчмарке швидкість обробки подій DELETE, але підозрюю, що вона ідентична швидкості INSERT, оскільки в балці з'являється повідомлення такої ж довжини, що і при вставці. 

<h4>Різні реалізації </h4>
На даний момент мені відомі тільки дві реалізації libslave. Одна з них - це вже <a href="https://github.com/Begun/libslave">згадана </a>, свого часу її відкрила компанія «Бігун», і про це багато де було написано (наприклад, на <a href="http://www.opennet.ru/opennews/art.shtml?num=30308">OpenNet </a>). Саме ця реалізація використовується в портах FreeBSD. 

У Mail.Ru Group використовується форк Бігуна, який я іноді <a href="https://github.com/vozbu/libslave">підпилює </a>. Частина змін в ній була також внесена в втікацький форк. З невнесених: випилювання невикористаного коду, зменшення включення хедерів, більше тестів (тести на BIGINT, на довгі SET'и), перевірка версії формату кожного бінлога, підтримка mysql-5.5, типу decimal (повертається як double - зрозуміло, використовується не в биллинге, а там, де досить зразкового уявлення про балансах), підтримка бітових полів (запозичена з <a href="https://github.com/alheio/libslave">форка </a>, який зараз практично в тому ж стані, що і мій). 

Друга реалізація, яка мені відома - від вищезазначених <a href="https://github.com/dimarik/mysqlslave">Піаніста і Димарик </a>. Що вона собою являє архітектурно і в плані продуктивності, мені ще належить з'ясувати. 

<h4>Приклади коду </h4>
Приклади коду є і в самій бібліотеці, але я дам кілька коментарів. 

Файл types.h: через typedef'и показує маппінг між типами MySQL і типами C + +. Можна помітити, що всі строкові типи, включаючи BLOB, являють собою просто std :: string, а все цілочисельні типи - беззнакові. Тоб навіть якщо у визначенні таблиці написаний просто int (Не unsigned), то бібліотека повертатиме тип uint32_t. 

У цьому ж файлі містяться дві зручні функції з перекладу типів DATE і DATETIME, що надаються libslave, в звичайний time_t. Ці дві функції є зовнішніми (не викликаються всередині libslave) з історичних причин: спочатку libslave повертав дивні закодовані числа для цих дат, і я не став це міняти. 

Файл recordset.h містить визначення структури <b>RecordSet </b>, що представляє собою одну запис бінлога. У ній міститься тип повідомлення, його час, ім'я бази даних і таблиці, до яких він належить, а також два рядки - нова і попередня (для update). 

Рядок являє собою асоціативний масив з імені стовпця в об'єкт типу boost :: any, який міститиме в собі тип, описаний в types.h і відповідний полю. 

Основний об'єкт Slave описаний у файлі Slave.h. 

Найпростіший код для запуску читання реплікації виглядає так: 
<pre><code class="html">void callback(const slave::RecordSet& event) {

    switch (event.type_event) {
    case slave::RecordSet::Update: std::cout << "UPDATE"; break;
    case slave::RecordSet::Delete: std::cout << "DELETE"; break;
    case slave::RecordSet::Write:  std::cout << "INSERT"; break;
    default: break;
    }
}
    slave::MasterInfo masterinfo;

    masterinfo.host = host;
    masterinfo.port = port;
    masterinfo.user = user;
    masterinfo.password = password;

    slave::Slave slave(masterinfo);
    slave.setCallback(«database», «table», callback);
    slave.init();
    slave.createDatabaseStructure();
    slave.get_remote_binlog();
</code>
<b>Приклад сесії з тестовим клієнтом test_client </b>
Для наочності розглянемо невеликий приклад сесії: створення користувача, бази, таблиці, наповнення її даними і відповідний висновок test_client. Тобто приклад готового клієнта реплікації, що міститься у вихідних кодах libslave. 
Виклик test_client виглядає так: 
<code>Usage: ./test_client -h <mysql host> -u <mysql user> -p <mysql password> -d <mysql database> <table name> <table name> …</code>

Спробуємо: 
mysql-u root 

<pre><code class="html">mysql> create user 'slave_test_user'@'localhost';
Query OK, 0 rows affected (0.00 sec)

mysql> create database slave_test_db;
Query OK, 1 row affected (0.00 sec)</code>

Запускаємо клієнт: 
<pre><code class="html">./test_client -h localhost -u slave_test_user -d slave_test_db
Reading binlogs...
Error in reading binlogs: mysql_query() failed: Access denied; you need (at least one of) the REPLICATION SLAVE privilege(s) for this operation : 1227 : [SHOW SLAVE HOSTS]</code>
Бачимо в циклі появляющуюся помилку про неможливість виконати запит. Помилка повторюється в циклі, тому що get_remote_binlog викликається в циклі, сам же він кидає виняток у відповідь на помилку. 

Даємо права: 
<pre><code class="html">mysql> grant REPLICATION SLAVE on *.* to 'slave_test_user'@'localhost';
Query OK, 0 rows affected (0.00 sec)</code>

Повторюємо запуск клієнта: 
<pre><code class="html">./test_client -h localhost -u slave_test_user -d slave_test_db
Reading binlogs...
Error in reading binlogs: mysql_query() failed: Access denied; you need (at least one of) the SUPER,REPLICATION CLIENT privilege(s) for this operation : 1227 : [SHOW MASTER STATUS]</code>

Прав все одно не вистачає, на цей раз REPLICATION CLIENT. Видаємо. 
<pre><code class="html">mysql> grant REPLICATION CLIENT on *.* to 'slave_test_user'@'localhost';
Query OK, 0 rows affected (0.00 sec)</code>

<pre><code class="html">> ./test/test_client -h localhost -u slave_test_user -d slave_test_db
Creating client, setting callbacks...
Initializing client...
Reading database structure...
Reading binlogs...</code>

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

<pre><code class="html">mysql> create table simple (id int auto_increment, word varchar(32), primary key (id));
Query OK, 0 rows affected (0.01 sec)</code>

Додаємо в командний рядок ім'я таблиці: 
<pre><code class="html">> ./test/test_client -h localhost -u slave_test_user -d slave_test_db simple
Creating client, setting callbacks...
Initializing client...
Reading database structure...
Error in initializing slave: mysql_query() failed: SELECT command denied to user 'slave_test_user'@'localhost' for table 'simple' : 1142 : [SHOW FULL COLUMNS FROM simple IN slave_test_db]</code>

Ось і вимога прав на SELECT - даємо. 

<pre><code class="html">mysql> grant SELECT on slave_test_db.* to 'slave_test_user'@'localhost';
Query OK, 0 rows affected (0.00 sec)</code>

Підсумкові права виглядають так: 
<pre><code class="html">mysql> show grants for 'slave_test_user'@'localhost';
+-------------------------------------------------------------------------------------+
| Grants for slave_test_user@localhost                                                |
+-------------------------------------------------------------------------------------+
| GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'slave_test_user'@'localhost' |
| GRANT SELECT ON `slave_test_db`.* TO 'slave_test_user'@'localhost'                  |
+-------------------------------------------------------------------------------------+
2 rows in set (0.00 sec)</code>

Повторюємо спробу: 
<pre><code class="html">> ./test/test_client -h localhost -u slave_test_user -d slave_test_db simple
Creating client, setting callbacks...
Initializing client...
Reading database structure...
Reading binlogs...</code>

Подивимося тепер, як читаються дані і як клієнт бачить кордону транзакцій. В MySQL робимо: 

<pre><code class="html">mysql> insert into simple set word='aaa';
Query OK, 1 row affected (0.00 sec)

mysql> insert into simple (word) values ('bbb'), ('ccc');
Query OK, 2 rows affected (0.00 sec)
Records: 2  Duplicates: 0  Warnings: 0

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into simple set word='ddd';
Query OK, 1 row affected (0.00 sec)

mysql> insert into simple set word='eee';
Query OK, 1 row affected (0.00 sec)

mysql> commit;
Query OK, 0 rows affected (0.00 sec)</code>

Клієнт видає: 
<pre><code class="html">
> ./test/test_client -h localhost -u slave_test_user -d slave_test_db simple
Creating client, setting callbacks...
Initializing client...
Reading database structure...
Reading binlogs...
INSERT slave_test_db.simple
  id : int(11) -> 1
  word : varchar(32) -> 'aaa'
  @ts = 1394604531
  @server_id = 1

COMMIT @server_id = 1

INSERT slave_test_db.simple
  id : int(11) -> 2
  word : varchar(32) -> 'bbb'
  @ts = 1394604567
  @server_id = 1

INSERT slave_test_db.simple
  id : int(11) -> 3
  word : varchar(32) -> 'ccc'
  @ts = 1394604567
  @server_id = 1

COMMIT @server_id = 1

INSERT slave_test_db.simple
  id : int(11) -> 4
  word : varchar(32) -> 'ddd'
  @ts = 1394604643
  @server_id = 1

INSERT slave_test_db.simple
  id : int(11) -> 5
  word : varchar(32) -> 'eee'
  @ts = 1394604646
  @server_id = 1

COMMIT @server_id = 1
</code>
Тоб стає зрозуміло, як бачити межі транзакцій, і можна помітити, що ніякі дані з транзакції в лог не пишуться, поки не буде викликаний COMMIT. 

<h4>Висновок </h4>
На закінчення хочу сказати, що libslave - це відмінний спосіб організувати прийом поновлення даних, що лежать в БД в MySQL, в ваш демон. Ми його експлуатуємо і в хвіст і в гриву, за винятком деяких маленьких конфігураційних таблиць, які за кодом простіше селектів цілком з певною періодичністю. 

До речі, однією з причин публікації цієї статті стало те, що в російськомовному інтернеті мені вдалося знайти лише одну історію успіху - це доповідь на Highload + + 2011 Олександра «Піаніста» Панкова і Дмитра «Димарик» Самірова «<a href="http://profyclub.ru/docs/159">Нестандартне використання реплікації Mysql </a>». Якихось інших обговорень libslave знайти не вийшло. Тому, якщо вам такі публікації відомі, то прошу поділитися посиланнями в коментарях. 

І корисне посилання на сторінку з описом формату бінлогів наостанок: <hh user="https://dev.mysql.com/doc/internals/en/event-data-for-specific-event-types.html"/><a href="https://dev.mysql.com/doc/internals/en/event-data-for-specific-event-types.html">dev.mysql.com / doc / internals / en / event-data-for-specific-event-types.html </a>
  
Джерело: <a href=http://habrahabr.ru/company/mailru/blog/219015/>Хабрахабр</a>

1 коментар

avatar
Оригинал статьи на русском языке: habrahabr.ru/company/mailru/blog/219015/
Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.