Масштабування бази даних через шардирование і партиционирование



Масштабування бази даних через шардирование і партиционирование
Денис Іванов (2ГІС)
Всім привіт! Мене звуть Денис Іванов, і я розповім про масштабуванні баз даних через шардирование і партиционирование. Після цієї доповіді у всіх повинне з'явитися бажання щось попартицировать, пошардировать, ви зрозумієте, що це дуже просто, воно ніяк жерти не просить, працює, і все чудово.

Трохи розповім про себе — я працюю в компанії WebAPI в 2GIS-е, ми надаємо API для організацій, у нас дуже багато різних даних, 8 країн, в яких ми працюємо, 250 великих міст, 50 тис. населених пунктів. У нас досить велике навантаження — 25 млн. активних користувачів на місяць, і в середньому навантаження близько 2000 RPS йде на API. Все це розташовується в трьох датацентрах.

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

Я більшою мірою розповім про шардінг. Він буває вертикальним і горизонтальним. Також буває такий спосіб масштабування як реплікація. Доповідь "Як влаштована MySQL реплікація" Андрія Аксьонова з Sphinx про це і був. Я цю тему практично не буду висвітлювати.

Перейдемо детальніше до теми партицирования (вертикальний шардінг). Як це все виглядає?


У нас є велика таблиця, наприклад, з користувачами — у нас дуже багато користувачів. Партицирование — це коли ми одну велику таблицю поділяємо на багато маленьких за яким-небудь принципом.
З горизонтальним шардингом все приблизно так само, але при цьому у нас таблички лежать у різних базах на інших інстансах.


Єдина відмінність горизонтального масштабування від вертикального в тому, що горизонтальне масштабування буде розносити дані по різних инстансам.

Про реплікацію я не буду зупинятися, тут все дуже просто.


Перейдемо глибше до цієї теми, і я розповім практично все про партицировании на прикладі Postgres'а.
Давайте розглянемо просту табличку, напевно, практично в 99% проектів така табличка є — це новини.


У новини є ідентифікатор, є категорія, в якій ця новина розташована, є автор новини, її рейтинг і якийсь заголовок — абсолютно стандартна таблиця, нічого складного немає.

Як же цю таблицю розділити на кілька? З чого почати?

Всього потрібно буде зробити 2 дії над табличкою — це поставити у нашого шарда, наприклад, news_1, те, що вона буде успадковуватися таблицею news. News буде базовою таблицею, буде містити всю структуру, і ми, створюючи партицию, будемо вказувати, що вона успадковується нашої базової таблиці. Наследованная таблиця буде мати всі колонки батьків — тієї базової таблиці, яку ми вказали, а також вона може мати свої колонки, які ми додатково туди додамо. Вона буде повноцінною таблицею, але успадкованої від батька, і там не буде обмежень, індексів і тригерів від батьків — це дуже важливо. Якщо ви на базовій таблиці насоздаете індекси і успадкуєте її, то в успадкованої таблиці індексів, обмежень і тригерів не буде.

2-е дію, яке потрібно зробити — це поставити обмеження. Це буде перевірка, що в цю таблицю дані будуть потрапляти тільки от з таким ознакою.


В даному випадку ознака — це category_id=1, т. е. лише записи з category_id=1 будуть потрапляти в цю таблицю.
Які типи перевірок бувають для партицированных таблиць?


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

Тут потрібно детальніше зупинитися, тому що перевірка підтримує оператор BETWEEN, напевно, ви всі його знаєте.


І так просто його зробити можна. Але не можна. Можна зробити, тому що нам дозволять таке зробити, PostgreSQL підтримує таке. Як ви бачите, у нас в 1-шу партицию потрапляють дані між 100 і 200, а у 2-у — між 200 і 300. В яку з цих партіцій потрапить запис з рейтингом 200? Не відомо, як пощастить. Тому так робити не можна, потрібно вказувати суворе значення, тобто строго в 1-шу партицию будуть потрапляти значення більше 100 і менше або дорівнює 200, і в другу більше 200, але не 200, і менше або дорівнює 300.


Це обов'язково потрібно запам'ятати і так не робити, тому що ви не дізнаєтеся, в яку з партіцій дані потраплять. Потрібно чітко прописувати всі умови перевірки.

Також не варто створювати партіціі по різних полях, тобто що в 1-шу партицию у нас будуть потрапляти запису з category_id=1, а 2-ий — з рейтингом 100.


Знову ж таки, якщо нас прийде така запис, в якій category_id = 1 і рейтинг =100, то невідомо в яку з партіцій потрапить ця запис. Партицировать варто по одній ознаці, по якомусь одному полю — це дуже важливо.
Давайте розглянемо нашу партицию цілком:


Ваша партицированная таблиця буде виглядати ось так, т. е. це таблиця news_1 з ознакою, що туди будуть потрапляти записи тільки з category_id = 1, і ця таблиця буде успадкована від базової таблиці news — все дуже просто.


Ми на базову таблицю повинні додати деяке правило, щоб, коли ми будемо працювати з нашою основною таблицею news, вставка на запис з category_id = 1 потрапила саме в ту партицию, а не в основну. Ми вказуємо просте правило, називаємо його як хочемо, говоримо, що коли дані будуть вставлятися в news з category_id = 1, замість цього будемо вставляти дані в news_1. Тут теж все дуже просто: по шаблончику воно все змінюється і буде чудово працювати. Це правило створюється на базовій таблиці.


Таким чином ми заводимо потрібну нам кількість партіцій. Для прикладу я буду використовувати 2 партіціі, щоб було простіше. Тобто у нас все однаково, крім найменувань цієї таблиці і умови, за яким дані будуть туди потрапляти. Ми також заводимо відповідні правила за шаблоном на кожну з таблиць.


Давайте розглянемо приклад вставки даних:


Дані будемо вставляти як звичайно, ніби у нас звичайна велика товста таблиця, тобто ми вставляємо запис з category_id=1 з category_id=2, можемо навіть вставити дані з category_id=3.


Ось ми вибираємо дані, у нас вони всі є:


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

Ми також можемо зробити відповідні запити в певні партіціі, вказуючи наша умова, тобто category_id = 1, або входження до числа (2, 3).


Все буде чудово працювати, всі дані будуть вибиратися. Знову ж таки, незважаючи на те, що з партіціі з category_id=3 у нас немає.


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


Хоч у нас і застосовано партицирование до цієї таблиці, основна таблиця все одно існує. Вона справжня таблиця, вона може зберігати дані, і з допомогою оператора ONLY можна вибрати дані з цієї таблиці, ми можемо знайти, що цей запис тут сховалася.


Тут можна, як видно на слайді, вставляти дані безпосередньо в партицию. Можна вставляти дані з допомогою правил в основну таблицю, але можна і в саму партицию.


Якщо ми будемо вставляти дані в партицию з якимось чужорідним умовою, наприклад, з category_id = 4, то ми отримаємо помилку «сюди такі дані не можна вставляти» — це теж дуже зручно — ми просто будемо класти дані тільки в ті партіціі, які нам дійсно потрібно, і якщо у нас щось піде не так, ми на рівні бази все це отловим.


Тут приклад побільше. Можна bulk_insert використовувати, тобто вставляти кілька записів одночасно і вони всі самі розподіляться за допомогою правил потрібної партіціі. Тобто ми можемо взагалі не морочитися, просто працювати з нашою таблицею, як ми і працювали. Додаток продовжить працювати, але при цьому дані будуть потрапляти в партіціі, все це буде красиво розкладено по поличках без нашої участі.


Нагадаю, що ми можемо вибирати дані з основної таблиці, із зазначенням умови, можемо, не вказуючи це умова вибирати дані з партіціі. Як це виглядає з боку explain'а:


У нас буде Seq Scan по всій таблиці цілком, тому що туди дані можуть все одно потрапляти, і буде скан партіціі. Якщо ми будемо вказувати умови кількох категорій, то він буде сканувати лише ті таблиці, які є умови. Він не буде дивитися в інші партіціі. Так працює оптимізатор — це правильно, і так дійсно швидше.

Ми можемо подивитися, як буде виглядати explain на самій партіціі.


Це буде звичайна таблиця, просто Seq Scan по ній, нічого надприродного. Точно так само будуть працювати update ' и і delete'и. Ми можемо update нути основну таблицю, можемо також update ' и слати безпосередньо в партіціі. Так само і delete'и будуть працювати. На них потрібно так само відповідні правила створити, як ми створювали з insert'ом, тільки замість insert написати update або delete.

Перейдемо до такої речі як Index's

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

Як ми з цією проблемою боролися у себе. Ми створили чудову утиліту PartitionMagic, яка дозволяє автоматично управляти партициями і не морочитися з створенням індексів, тригерів з неіснуючими партициями, з якимись бяками, які можуть відбуватися. Ця утиліта open source'ва, нижче посилання. Ми цю утиліту у вигляді збереженої процедури додаємо до нас в базу, вона там лежить, не вимагає додаткових extension'ів, ніяких розширень, нічого перезбирати не потрібно, тобто ми беремо PostgreSQL, звичайну процедуру, запихаємо в базу і з нею працюємо.

Ось та ж сама таблиця, яку ми розглядали, нічого нового, все те ж саме.


Як же нам запартицировать її? А просто ось так:


Ми викликаємо процедуру, вказуємо, що таблиця буде news, і партицировать будемо category_id. І все далі буде сам працювати, нам більше нічого не треба робити. Ми так само вставляємо дані.

У нас тут три записи з category_id =1, два записи з category_id=2, і одна з category_id=3.


Після вставки дані автоматично потраплять в потрібні партіціі, ми можемо зробити селекты.


Все, партіціі вже створилися, всі дані розклалися по поличках, все чудово працює.
Які ми отримуємо за рахунок цього переваги:

  • при вставці ми автоматично створюємо партицию, якщо її ще немає;
  • підтримуємо актуальну структуру, можемо управляти просто базовою таблицею, навішуючи на неї індекси, перевірки, тригери, додавати колонки, і вони автоматично будуть потрапляти під все партіціі після виклику цієї процедури ще раз.
Ми отримуємо дійсно велика перевага в цьому. Ось ссилочка https://github.com/2gis/partition_magic. На цьому перша частина доповіді закінчена. Ми навчилися партицировать дані. Нагадаю, що партицирование застосовується на одному инстансе — це той же самий інстанси бази, де у вас лежала б велика товста таблиця, але ми її роздрібнили на дрібні частини. Ми можемо абсолютно не змінювати наше додаток — воно так само буде працювати з основною таблицею вставляємо туди дані, редагуємо, видаляємо. Так само все працює, але працює швидше. Приблизно, в середньому в 3-4 рази швидше.

Перейдемо до другої частини доповіді — це горизонтальний шардінг. Нагадаю, що горизонтальний шардінг — це коли дані ми розносимо по декількох серверів. Все це робиться досить просто, варто один раз це налаштувати, і воно буде працювати чудово. Я розповім докладніше, як це можна зробити.

Розглядати будемо таку ж структуру з двома шардами — news_1 і news_2, але це будуть різні инстансы, третім инстансом буде основна база, з якою ми будемо працювати:


Та ж сама таблиця:


Єдине, що туди треба додати, це CONSTRAINT CHECK, того, що записи будуть випадати тільки з category_id=1. Так само, як у попередньому прикладі, але це не успадкована таблиця, це буде таблиця з шардом, яку ми робимо на сервері, який буде виступати шардом з category_id=1. Це потрібно запам'ятати. Єдине, що потрібно зробити — це додати CONSTRAINT.

Ми ще можемо додатково створити індекс category_id:


Незважаючи на те, що у нас стоїть check — перевірка, PostgreSQL все одно звертається в цей шард, і шард може дуже надовго замислитися, тому що даних може бути дуже багато, а у випадку з індексом він швидко відповість, тому що в індексі нічого немає за таким запитом, тому його краще додати.

Як налаштувати шардінг на основному сервері?


Ми підключаємо EXTENSION. EXTENSION йде в Postgres'e з коробки, робиться це командою CREATE EXTENSION, називається він postgres_fdw, розшифровується як foreign data wrapper.

Далі нам потрібно завести віддалений сервер, підключити його до основного, ми називаємо його як завгодно, вказуємо, що цей сервер буде використовувати foreign data wrapper, який ми вказали.

Таким же чином можна використовувати для шарда MySql, Oracle, Mongo… Foreign data wrapper є для дуже багатьох баз даних, тобто можна окремі шарды зберігати в різних базах.

В опції ми додаємо хост, порт і ім'я бази, з якою будемо працювати, потрібно просто вказати адресу вашого сервера, порт (швидше за все, він буде стандартний) і базу, яку ми завели.

Далі ми створюємо маппінг для користувача — за цими даними основний сервер буде авторизуватися до дочірнього. Ми зазначаємо, що для сервера news_1 буде користувач postgres, з паролем postgres. І на основну базу даних він буде маппиться як наш user postgres.

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

Далі ми заводимо табличку на основному сервері:


Це буде табличка з такою ж структурою, але єдине, що буде відрізнятися — це префікс того, що це буде foreign table, тобто вона якась іноземна для нас, віддалена, і ми вказуємо, з якого сервера вона буде взята, і в опціях вказуємо схему і ім'я таблиці, яку нам потрібно взяти.

Схема по дефолту — це public, таблицю, яку ми порушили, назвали news. Точно так само ми підключаємо 2-шу таблицю до основного сервера, тобто додаємо сервер, додаємо маппінг, створюємо таблицю. Все, що залишилося — це завести нашу основну таблицю.


Це робиться з допомогою VIEW, через уявлення, ми з допомогою UNION ALL склеюємо запити з віддалених таблиць і отримуємо одну велику товсту таблицю news з віддалених серверів.

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


Ми заводимо основне правило, яке буде спрацьовувати, якщо жодна перевірка не спрацювала, щоб не відбувалося нічого. Тобто ми вказуємо DO INSTEAD NOTHING і заводимо такі ж перевірки, як ми робили раніше, але тільки з зазначенням нашого умови, тобто category_id=1 і таблицю, в яку дані замість цього будуть потрапляти.


Тобто єдина відмінність — це category_id ми будемо вказувати ім'я таблиці. Також подивимося на вставку даних.


Я спеціально виділив неіснуючі партіціі, оскільки ці дані по нашому умові не потраплять нікуди, тобто у нас вказано, що ми нічого не будемо робити, якщо не знайшлося жодного умови, тому що це VIEW, це не справжня таблиця, туди дані вставити не можна. У тому умови ми можемо написати, що дані будуть вставлятися в якусь третю таблицю, тобто ми можемо завести щось типу буфера або кошики і INSERT INTO робити в ту таблицю, щоб там накопичувалися дані, якщо раптом якісь партіцій у нас немає, і дані стали приходити, для яких немає шардов.

Вибираємо дані

Зверніть увагу на сортування ідентифікаторів — у нас спочатку виводяться всі записи з першого шарда, потім з другого. Це відбувається з-за того, що postgres ходить по VIEW послідовно. У нас вказані select'и через UNION ALL, і він саме так виконує — посилає запити на віддалені машини, збирає ці дані і склеює, і вони будуть відсортовані за тим принципом, за яким ми цю VIEW створили, за яким той сервер віддав дані.

Робимо запити, які ми робили раніше з основної таблиці, із зазначенням категорії, тоді postgres віддасть дані тільки з другого шарда, або безпосередньо звертаємося до шард.


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

Подивимося на explain.


У нас foreign scan по news_1 і foreign scan по news_2, так само, як було з партицированием, тільки замість Seq Scan-а у нас foreign scan — це віддалений скан, який виконується на іншому сервері.


Партицирование — це дійсно просто, варто лише зробити кілька дій, все налаштувати, і воно все буде чудово працювати, не буде просити їсти. Можна так само працювати з основною таблицею, як ми працювали раніше, але при цьому у нас все гарно лежить по поличках і готове до масштабування, готове до великої кількості даних. Все це працює на одному сервері, і при цьому ми одержуємо приріст продуктивності в 3-4 рази, за рахунок того, що у нас обсяг даних в таблиці скорочується, оскільки це різні таблиці.

Шардінг — лише трохи складніше партицирования, тим, що потрібно налаштовувати кожен сервер окремо, але це дає деяку перевагу в тому, що ми можемо просто нескінченна кількість серверів додавати, і все буде чудово працювати.
Джерело: Хабрахабр

0 коментарів

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