Як влаштована MySQL-реплікація

Андрій Аксьонов
Андрій Аксьонов (Sphinx), shodan
Мій доповідь призначений для тих людей, які знають слово «реплікація», навіть знають, що в MySQL вона є, і, можливо, один раз її налаштували, 15 хвилин витратили і забули. Більше про неї вони не знають нічого.
Ми трохи пройдемося по теорії, спробуємо пояснити, як це все працює всередині, а після цього ви з потрійними силами зможете самі пірнути в документацію.
Що таке реплікація, в принципі? Це копіювання змін. У нас є одна копія БД, ми хочемо з якоюсь метою ще одну копію.
Реплікація буває різних видів. Різні осі порівняння:
  • ступінь синхронізації змін (sync, async, semisync);
  • кількість серверів записи (M/S, M/M);
  • формат змін (statement-based (SBR), row-based (RBR), mixed);
  • теоретично, модель передачі змін (push, pull).
У доповіді не буде:


Все це є в Інтернеті, синтаксис розбирати сенсу немає.

Кумедний факт — якщо трохи замислитися, реплікація нам теоретично допомагає з принципових міркувань скейлить тільки читання. Ось такий дещо неочевидний висновок. Це тому що, якщо у нас на одну і ту ж копію даних треба налити певну кількість змін, і ця певна копія даних обслуговується одним і тим же сервером, то цей сервер здатний витримати певну кількість апдейтів в секунду, і більше туди не залити. Здатний оновити сервер 1000 записів в секунду, а 2000 — не здатен. Що зміниться від того, що ти поставиш до цього сервера репліку, неважливо, в режимі майстер-слэйв або майстер-майстер? Зумієш ти на цю репліку налити другу тисячу апдейтів? Правильна відповідь — ні.
На репліку в режимі майстер-майстер ти, звичайно, зумієш налити додаткових апдейтів, інша справа, що, коли вони не прилетять на перший майстер і спробують на ньому зробити другу тисячу апдейтів, то ємності вже не вистачить. Треба розуміти і не змішувати два майже очевидних моменту, що реплікація, як би, про одне, а те, що дані треба дробити, і якщо треба скейлить не читання, а записи, то доведеться робити щось інше, а реплікація не дуже врятує.
тобто реплікація — це більше про читання.
Про синхронізацію.
Синхронізація — гарантія наявності та доступності. Доступності в тому сенсі, що у нас commit пройшов, транзакція зафіксувалася, все добре, ці дані видно одній або кільком нодам в кластері, вони можуть брати участь в наступних запитах. Наявність — це те, що дані, в принципі, є більш ніж на одному сервері, але, можливо, транзакція не проигралась і не доступна.
Тут немає рефрену «commit закінчився успішно, що це означає?». Синхронний commit означає, що у нас локальний і віддалений (хоча б на одній репліці) закінчився, тобто ми щось закоммитили на машину, якщо у нас синхронний режим реплікації, то ці зміни успішно закоммитились, вони невидимі для подальших запитів на локальній машині, на віддаленій машині (хоча б на одній) теж видно. Це означає, що якщо трапилася стандартна позаштатна ситуація, тобто в один і серверів прилетів лом і пробив все наскрізь — від процесора до самого гвинта, то, незважаючи на це, дані не тільки скопійовані на якийсь віддалений сервер, але ще, до того ж, можуть миттєво, без якихось додаткових затримок, брати участь в наступних транзакцій.
Це все загальна термінологія, ніяк абсолютно не пов'язана з MySQL. У будь-розподіленій системі воно буде влаштовано так.
Асинхронний commit — ніяких додаткових гарантій, як пощастить.
Полусинхронный commit — приємне проміжне рішення, це коли у нас локальний commit пройшов, про віддалений commit нічого не відомо — може, слэйв наздогнав, а, може, і не наздогнав, але, принаймні, нам прийшло підтвердження, що ці дані кудись відлетіли і там прийняті і, напевно, записалися.
Про сервера для запису. Які бувають види реплікації.
Master-slave classic, зміни все ллються на один сервер, після цього копіюються на масу реплік.
Master-master true — коли зміни ллються на купу майстрів одночасно і якимось чином з одного на інший, з іншого на третій і між ними усіма, що породжує і ряд радощів, і ряд автоматичних проблем. Зрозуміло, що коли у тебе є одна «золота копія» і з неї кілька реплік, які повинні (в ідеалі — миттєво) повторювати цю «золоту копію», то все відносно просто з точки зору того, як дані туди-сюди ганяти і що робити на кожній конкретній копії. З master-master починається цікава «головний біль», причому, підкреслюю, не конкретно у випадку MySQL, а суто теоретична. Як же бути, якщо на двох ноди одночасно спробували прогнати одну і ту ж операцію, яка змінює одні і ті ж дані, причому, змінює їх, для простоти прикладу, по-різному. Зрозуміло, що одночасно ці два зміни ми застосувати не можемо. На момент, коли ми на одній ноде починаємо щось змінювати, на другий ноде ще поки нічого немає. Конфлікт. Одну з транзакцій доведеться відкочувати. До того ж починаються окремі «танці» зі звіркою годин і т. п.
Цікавий момент — навіть варіант, коли у вас в кінцевому підсумку всі зміни зі всіх майстрів повинні поступово поширилися скрізь, все одно не допоможе тому самому write bandwidth. Прикро, але ось так.
Приємний варіант — під назвою«Master-slave + routing запитів». Приємний він тим, що всередині програмувати просто, в тебе є одна основна копія, ти її реплицируешь на купу машин. Це набагато простіше, ніж у майстер-майстер середовищі, коли всі рівноправні і т. д., але з точки зору програми все одно виглядає так, ніби у тебе точок запису багато. Ти приходиш на будь-яку ноду, вона знає, куди тебе зароутить, і успішно роутит. Ну, і читання масштабуються — ось воно щастя реплікації. Читати можна з усіх точок всі і завжди.
Тепер ближче до баз даних, «чарівним» форматів statement-based, row-based і т. д. Про формат змін.
Що робити? Можна передавати самі запити, а можна передавати тільки змінені рядки. Підкреслюю — поки ми ще не пірнули в нетрі MySQL, цим може займатися будь-яка СУБД, в якій є запити, породжують велику (або не дуже) кількість змін, тобто оновлюють багато даних. Виникає питання — а що конкретно будемо копіювати? Можна самі запити туди-сюди між нодами ганяти, а можна ганяти тільки змінені дані. Цікаво, що і так і сяк дуже погано! Можна ще намагатися змішувати.
Ще один пункт про те, які бувають реплікації. Про модель поширення. Напевно, десь досі ще не повністю вимерла модель Push-based, коли та нода, яка внесла зміни та зобов'язана їх розсилати всім іншим нодам. З точки зору програмування і відстеження state'ов це та ще морока. Тому рулить Pull-based. Забирати апдейти з тієї чи іншої ноди — це набагато простіше запрограмувати, ніж на одній ноде стежити за хаотичним кластером своїх реплік.
Якісь загальні терміни ввели. Переходимо до того, як зробили в MySQL.
MySQL, сам по собі, це якийсь обман. Є логічний шар під назвою MySQL, який займається всяким загальними та ізольованими від зберігання даних справами — мережа, оптимізатор, кеші і т. д. Конкретний фізичний шар, який відповідає за зберігання даних, лежить на поверх нижче. Є кілька вбудованих, є стояли плагінами. Але навіть вбудовані MyISAM, InnoDB і т. д. живуть на фізичному шарі. Плагинная архітектура — це кльово, можна підчепити новий движок, але миттєво виникає якась неоптимальность. В принципі, транзакційні write-ahead log'і (WAL), які фізичний шар зберігання все одно пише, було б добре використовувати для реплікації, і якщо система знає про те, що є якийсь фізичний рівень, або досить добре пов'язана з цим фізичним рівнем, то можна було б окремий лог на логічному рівні не писати, а використовувати той же самий WAL. Але у MySQL це неможливо концептуально, або, якщо поміняти інтерфейс в PSE так, щоб стало можливо концептуально, то буде дуже багато роботи.
Реплікація реалізована на рівні самого MySQL. В цьому є і хороше — крім одного лода у вигляді глибоко внутрішніх даних движка зберігання, є більш-менш логічний лог, можливо, на рівні statement'ів, який ведеться окремо від цього движка. А це «зайва» безпека і т. д. плюс, оскільки ніяких обмежень всередині немає, можна робити всякий креатив типу підміни движка «на льоту».
У наведених термінах в MySQL 4.1 було реалізовано: master-slave, pull-based, суворо async і строго SBR. Якщо ви застрягли в давній епосі 4.х, то, напевно, у вас все погано. Версіями 5.х вже мало не 10 років — пора б і оновитися.
Цікаво простежувати за версіями, як люди наступали на всілякі граблі і, коли зробити вже нічого було не можна, прикручували до цих граблях нові граблі, щоб життя була не така болюча. Так, у версії 5.1 прикрутили RBR, щоб компенсувати неминучі проблеми з SBR, і прикрутили mixed режим. У версії 5.6 прикрутили ще приємних штук: semi-sync, delayed slave, GTID.
Ще один момент. Оскільки MySQL — це якийсь загальний шар, з одного боку, і купа pluggable движків, з іншого боку, в тому числі, вбудованих, там є з певного моменту божественний NDB cluster, про який розповідають круте. Там повністю синхронна майстер-майстер реплікація, дуже доступна in-memory БД… Але є один нюанс — як тільки починаєш шукати людей, які в продакшені використовують NDB cluster, то таких людей знаходиться вкрай мало.
Чим займається майстер в той момент, коли ви вирішили включити реплікацію? На майстрі відбувається досить мало додаткових рухів. Як звичайно, ми по мережі приймаємо запити, парсим їх, ганяємо транзакції, фіксуємо їх і т. д. Додатково до цього, на логічному рівні MySQL майстер починає вести binary log — файл, не зовсім текстовий, який сиплються все підряд зміни. Також майстер вміє розсилати ці логи по мережі. Все це дуже просто і, начебто, працює.
Чим займається слэйв? Зміни на слэйв краще не посилати, тому що можна потрапити в незрозуміле. У слэйва трохи більше роботи. Крім того, щоб вести один додатковий лог і за запитом розсилати його, ще є тред, який ходить до віддаленого майстру, можливо, навіть не до одного, і качає звідти binary log'і. Рішення «давайте ходити до декількох віддалених майстрам і з них качати різні логи» неоднозначно. З одного боку непогано, а з іншого виходить миттєве розбіжність. Просто фізично копіювати файли з SCP не можна, вже виходить на сервері один лог, в ньому свої позиції, локально ми їх по сітці тягнемо, складаємо в окремий лог, ще окремий тред бігає і намагається програвати ці локальні логи. Саме пекельне, на мій погляд, полягає в тому, що аж до версії 5.6 ідентифікація тієї чи іншої транзакції в балці відбувалася по імені файлу і позиції на майстра. Цікаве рішення.
Ось шлях запису, який простенький insert проходить без реплікації:


Додаток сконнектилось до сервера, поклало в таблицю і відбій.
З реплікацією виходить кілька додаткових кроків:


Додаток-письменник точно так само йде до майстра, але до того ж ці дані потрапляють в тому чи іншому вигляді в binary log, потім гойдаються по мережі в relay log, потім з relay log'а поступово реплеются (якщо нам пощастило, і слэйв не лагает, реплеются відразу) в таблицю на слэйве, після цього все доступно в читача.
Що конкретно потрапляє в binary log, залежить від налаштувань SBR/RBR/mixed. Звідки це все росте? Уявімо себе базою даних. Нам прилетів простий запит «онови одну конкретну запис» — UPDATE users SET x=123 WHERE id=456
Що записати в binary log? В принципі, все одно, насправді. Можемо коротенький запит записати, а він оновив один запис) можемо записати зміна якимось чином у тому чи іншому форматі.
Інша ситуація. Уявімо, що нам прилетів той самий запит, який сам по собі маленький, а даних змінює багато — UPDATE users SET bonus=bonus+100
Тут ефективний варіант один — писати сам запит, тому що запит — рівному 32 байта, а записів він може оновити довільну кількість— 1000, 100 000, 1 000 000, скільки завгодно… Неефективно писати змінені записи в лог.
А що станеться, якщо ми в лог помістимо такий нехитрий запит «відкиньмо всіх юзерів, які не заходили під своїм логіном давно» — UPDATE users SET disabled=1 WHERE last_login < UNIX_TIMESTAMP(NOW())-100*86400
Раптово наступає жах. Проблема в тому, що якщо среплицировать ідеально сам запит, то, по-перше, час ніколи не синхронно між двома нодами, крім цього, за рахунок того, що шлях запису такий довгий, в момент реплея цей «NOW» розійдеться-таки. Репліка раптово розходиться з майстром, і всі наступні зміни, формально кажучи, вже небезпечні, можуть призвести до чого завгодно.
Взагалі кажучи, для таких запитів, незалежно від кількості змінених даних, в ідеалі треба копіювати самі рядки. В даному конкретному випадку можна самі рядки не копіювати, а зафіксувати константу і в лог написати не «NOW», а конкретний timestamp, який був використаний майстром на момент реплікації.


Забавні факти, які випадково дізнаєшся, пірнаючи в нетрі реплікації. Причому, пірнати можна неглибоко — нариваєшся на них одразу. У випадковому порядку вони такі:
  • майстер многопоточен, а слэйв — ні. Зрозуміло, що якщо майстер наливає навантаження в чотири ядра, слэйв цю навантаження в одне ядро наливати не встигає. Все досить погано;
  • стан слэйва визначається ім'ям позиції у файлі майстра. Вдумайтеся — стан однієї ноди в кластері визначається ім'ям файлу і позицією в цьому файлі на інший ноде кластера, з якою може з будь-яких причин статися що завгодно!
  • «рятівний» RBR. Виявляється, за замовчуванням туди пишуться повні before/after row image, тобто ми змінили одну колонку в п'яти-килобайтной рядку, оп! — 10 Кб трафіку і байтів 20-40 оверхедів на цю рядок, потім оп! — їде така жирна рядок попередньої версії, оп! — їде після цього версія з новими значеннями. Адміністратори виють хором! Тим не менш, це просто офігенно з точки зору деяких збочених додатків, наприклад, зовнішніх читалок, які намагаються подцепиться до сервера MySQL, з нього витягати дані і робити з ними що-небудь, наприклад, сунути їх в повнотекстовий індекс. Наскільки це погано з точки зору адміністрування бази даних, в якій одна зміна на три байти породжує 10 Кб трафіку на гвинті, а потім 10 Кб трафіку по мережі на кожного слэйва, настільки ж це добре для будь-яких систем типу повнотекстового пошуку, як Sphinx, у яких немає локальної копії даних MySQL з нуля імплементувати немає ніякого бажання. В MySQL 5.6 схаменулися і зробили binlog_row_image (але по дефолту full, а не minimal або noblob).
Коротше кажучи, все влаштовано не хитро — палиця, мотузка, один лог, другий лог. І навіть у цьому протоколі «дитячі» хвороби досить забавні:


Для людини, який використовує реплікацію два дні, все це страшно і важко. Але, знаючи, наскільки вона нехитро влаштована, в принципі, зрозуміло, як з нею жити:
  • насамперед, не віримо дефолтів;
  • уважно дивимося на налаштування, думаємо, чого хочемо — SBR, RBR і т. д.
І краще відразу налаштувати, щоб потім не розбирати дивний фарш.
В ситуації «протух лог, розійшлася позиція, невідомо, що відбувається» є певний інструментарій — дивимося подієві и, намагаємося зрозуміти, яка транзакція вже проскочила, яка — немає, можна все це справа врятувати або відновити і т. д. Якщо GTID«и заздалегідь зуміли включити, то життя стає простіше.
Інший момент спостереження за реплікацією. Цікаво подивитися, як внутрішнє криве пристрій провокує не те, що конкуренцію, а створення додаткових продуктів. „Чарівний“ Tungsten Replicator, кажуть, добре вирішує завдання під назвою „однопотоковий слэйв — це погано“, а якщо б не вроджені складності, не було б додаткового продукту, який дозволяє користуватися цим механізмом, переливати дані в інші системи, з одного боку, та заодно вирішувати ряд проблем, вбудованих в існуючу систему, з іншого боку.
Як завжди, радити неможливо. Комусь допомагає, хтось буде сильно плюватися. Але, кажуть, є ситуації, в яких з неминучим однопоточним лагом добре справляється Tungsten. Я упевнений, є ще всякі цікаві фокуси, але внутрішній однопотоковий слэйв — це важко.
Що робити, якщо ви навіщо використовували репліки як бекап? Я вважаю, треба битися головою об стіну, бо репліка і бекап — це дві різні штуки. Тим не менше, якщо ви креативні пацани і використовуєте досить нову версію, delayed replication вас рятує, з одного боку, але з іншого боку, якщо ви не робите повноцінних бекапів, вас все одно нічого не врятує.
Далі ще один елемент креативу. Неважко уявити ситуацію, коли майстер забив логами весь 10 PB хмарний диск або забив розсилкою цих логів всю мережу, при цьому 90% цих оновлень нам не потрібні, тому що нам цікаво відтворити, наприклад, одну таблицю прицільно або одну базу прицільно, а за замовчуванням все валиться валом в бінарний лог — всі зміни по всіх базах, за всіх таблиць, по всьому. Рішення знову вражає своєю креативністю. З одного боку, є чотири настройки — {binlog|replicate}_{do|ignore}_db, які дозволяють фільтрувати на майстер — що запишеться в лог, а що проигнорируется. На слэйве, відповідно, дозволяє робити те ж саме. Тобто на майстра ми можемо відфільтрувати те, що потрапляє в binary log — в цю воронку, яка потім зливається в мережу, а на слэйве, відповідно, ми можемо поставити вхідний фільтр на те, що прилітає з мережі. Або писати на диск тільки частину даних, а потім на слэйве реплеить, знову ж таки, лише частина даних. Раптово навіть у цій нехитрій історії наступає жах, тому що комбінація — використовуємо одну БД, а апдейтим таблицю в інший БД через цікавий синтаксис — вона веде себе якось… А як конкретно вона себе поведе — невідомо, оскільки різні фільтри спрацьовують в різні моменти.
Вбудованих приємних штук під назвою „перевибори майстра, якщо він раптово здох“, треба піднімати руками. Відсутність інструментів для менеджменту кластера — це, на мою думку, добре — породжує конкуренцію, породжує створення додаткових продуктів. У самому справі, якщо б в звичайному MySQL ідеально працювала дуже кльова майстер-майстер реплікація, або хоча б автоматичне підняття після збоїв, то навіщо була потрібна всяка Galera, Регсопа/MariaDB Cluster і т. д.?
Ще трохи фокусів. Цікава реалізація реплікації, яка проста як палиця, і мотузка, без всяких перевірок, з одного боку, і без всяких інструментів, щоб приємніше менеджить кластер реплицирующегося слэйва, з іншого боку. Це погано. Але зате можна вручну ліпити з цього такі цікаві конфігурації, що порушаться всі, хто потім прийде і за вами буде це розбирати.
Конфігурація №1. Майстер-майстер «на коліні» в стилі MySQL робиться ось так:


Що лякає — скільки в світі ідіотів! Погуглите „Майстер-майстер MySQL реплікація“ — кожна друга посилання ось така. Пекло і голокост.
Фокус №2 — catch-all slave — приємнішими. Ніяких непотрібних перевірок немає з кого прилітає, кому потрапляє, і що з цим робити. За рахунок цього можна зробити забавні штуки типу слэйва, на який або прицільно зливається частина даних з купи серверів, або прицільно зливаються всі дані з усіх серверів — сервер з усіма-усіма бэкапами. Але, повторюся, реплікація є, тобто є певний базовий інструмент, який копіює таблицю А замість і все.
Ну і, нарешті, фокус №3 — підміняємо всяке. Згадуємо, що реплікація живе на логічному рівні, ніяк не пов'язане з фізичним рівнем зберігання. За рахунок цього можна вкрай цікаво чудити. Можна змінювати движок «на льоту» з незрозумілими цілями — ось true story, що, мовляв, реплікація з InnoDB баз в MyISAM таблиці просто заради того, щоб повнотекстовий пошук працював хоч якось. Є креативний фінт під назвою „зміна схеми через реплікацію“. У чому жир, розуміти відмовляюся, але бувають і такі фокуси. Ну і, є зрозумілий і цікавий режим роботи під назвою „параноїдальний апгрейд версії через реплікацію“.
У ході доповіді ми дізналися:


Тим не менш, з цим пеклом можна жити, якщо хоча б приблизно розуміти, як він влаштований.
Основний посил в тому, що:


У 2015 році на конференції HighLoad++ Junior Андрій Аксьонов прочитав нову версію своєї доповіді про пристрої реплікації в MySQL. Її ми теж розшифрували і опублікували у своєму блозі.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.