Пишемо в PostgreSQL на субсвітловому: 1 host, 1 day, 1TB

Нещодавно я розповів, як за допомогою типових рецептів збільшити продуктивність SQL-запитів «на читання» з PostgreSQL-бази. Сьогодні ж мова піде про те, як можна зробити ефективнішим запис у БД без використання будь-яких «крутилок» у конфізі — просто правильно організувавши потоки даних.

Пишемо в PostgreSQL на субсвітловому: 1 host, 1 day, 1TB

#1. Секціонування

Стаття про те, як і навіщо варто організовувати прикладне секціонування «теоретично» вже була, тут же йтиметься про практику застосування деяких підходів у рамках нашого сервісу моніторингу сотень PostgreSQL-серверів.

«Справи давно минулих днів…»

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

Часи були майже билинні, актуальними були різні варіанти PostgreSQL 9.x, тому все секціонування довелося робити «вручну» — через успадкування таблиць та тригери роутингу з динамічним EXECUTE.

Пишемо в PostgreSQL на субсвітловому: 1 host, 1 day, 1TB
Рішення, що вийшло, виявилося досить універсальним, щоб можна було подорожувати його на всі таблиці:

  • Було оголошено порожню «заголовну» батьківську таблицю, на якій описувалися всі потрібні індекси та тригери.
  • Запис з погляду клієнта проводилася в «кореневу» таблицю, а всередині за допомогою тригера роутингу BEFORE INSERT запис «фізично» вставлялася у потрібну секцію. Якщо такої ще не було – ми ловили виняток і…
  • … за допомогою CREATE TABLE ... (LIKE ... INCLUDING ...) за шаблоном батьківської таблиці створювалася секція з обмеженням на потрібну датущоб при вилученні даних читання проводилося тільки в ній.

PG10: перша спроба

Але секціонування через успадкування було історично не дуже пристосоване для роботи з активним потоком запису або великою кількістю нащадків. Наприклад, можна згадати, що алгоритм вибору потрібної секції мав квадратичну складність, Що при 100+ секціях працює, самі розумієте як…

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

Як з'ясувалося після перекопування мануалу, нативно секційована таблиця в цій версії:

  • не підтримує опис індексів
  • не підтримує на ній тригерів
  • не може бути сама нічиєю «нащадком»
  • не підтримує INSERT ... ON CONFLICT
  • не вміє породжувати секцію автоматично

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

PG10: другий шанс

Отже, ми почали вирішувати проблеми, що виникли по черзі:

  1. Оскільки тригери та ON CONFLICT нам виявилися подекуди все-таки потрібні, для їх відпрацювання зробили проміжну проксі-таблицю.
  2. Позбулися «роутингу» у тригерах — тобто від EXECUTE.
  3. Винесли окремо таблицю-шаблон з усіма індексамищоб вони навіть не були присутні на проксі-таблиці.

Пишемо в PostgreSQL на субсвітловому: 1 host, 1 day, 1TB
Нарешті, після цього, вже нативно відсекціонували основну таблицю. Створення нової секції поки що так і залишилося на совісті програми.

«Пилим» словники

Як і в будь-якій аналітичній системі, у нас теж були «факти» та «розрізи» (Словники). У нашому випадку, у цій якості виступали, наприклад, тіло «шаблону» однотипні повільні запити або текст самого запиту.

"Факти" у нас були відсекціоновані днями вже давно, тому ми спокійно видаляли застарілі секції, і вони нам не заважали (логи ж!). А ось зі словниками вийшло лихо...

Не сказати, що їх виявилося дуже багато, але приблизно на 100TB "фактів" вийшов словник на 2.5TB. З такої таблиці зручно нічого не видаляєш, не стиснеш за адекватний час, та й запис до неї поступово ставав дедалі повільнішим.

Начебто словник… у ньому кожен запис має бути представлений рівно один раз… і це правильно, але!.. Ніхто не заважає нам мати за окремим словником на кожен день! Так, це приносить певну надмірність, зате дозволяє:

  • писати/читати швидше за рахунок меншого розміру секції
  • споживати менше пам'яті за рахунок роботи з більш компактними індексами
  • зберігати менше даних за рахунок можливості швидкого видалення застарілих

В результаті всього комплексу заходів навантаження по CPU скоротилося на ~30%, по диску - на ~50%:

Пишемо в PostgreSQL на субсвітловому: 1 host, 1 day, 1TB
При цьому ми продовжили писати в базу те саме, просто з меншим навантаженням.

#2. Еволюція та рефакторинг БД

Отже, ми зупинилися на тому, що у нас на кожен день є своя секція із даними. Власне, CHECK (dt = '2018-10-12'::date) — і є ключ секціонування та умова потрапляння запису до конкретної секції.

Оскільки всі звіти в нашому сервісі будуються в розрізі конкретної дати, то й індекси ще з «несекціонованих часів» для них були всі типи (Сервер, Дата, шаблон плану), (Сервер, Дата, Вузол плану), (Дата, Клас помилки, Сервер), ...

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

Пишемо в PostgreSQL на субсвітловому: 1 host, 1 day, 1TB
Напрям оптимізації очевидний — просто прибираємо поле з датою з усіх індексів на секційованих таблицях. За наших обсягів виграш — порядку 1TB/тиждень!

А тепер зауважимо, що цей терабайт ще треба було якось записати. Тобто ми ще й диск повинні тепер вантажити менше! На цій картинці добре видно отриманий ефект від проведеного чищення, якому ми присвятили тиждень:

Пишемо в PostgreSQL на субсвітловому: 1 host, 1 day, 1TB

#3. «Розмазуємо» пікове навантаження

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

Наближаємо попередню картинку і бачимо, що диск у нас «качає» по навантаженню з дворазовою амплітудою між сусідніми відліками, чого явно «статистично» не повинно бути за такої кількості операцій:

Пишемо в PostgreSQL на субсвітловому: 1 host, 1 day, 1TB

Домогтися цього досить легко. У нас на моніторинг було заведено вже майже 1000 серверівкожен обробляється окремим логічним потоком, а кожен потік скидає накопичену інформацію для відправки в базу з певною періодичністю, приблизно так:

setInterval(sendToDB, interval)

Проблема тут криється в тому, що всі потоки стартують приблизно в один часТому моменти відправлення у них майже завжди збігаються «до точки». Упс №2…

На щастя, справиться це досить легко, додаванням «випадкової» розбіжності по часу:

setInterval(sendToDB, interval * (1 + 0.1 * (Math.random() - 0.5)))

#4. Кешуємо, що потрібно можна

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

Наприклад, ми зробили можливість аналізу у розрізі вузлів плану (всі ці Seq Scan on users), але відразу подумати, що вони, в масі, однакові - забули.

Ні, звичайно, в базу нічого повторно не пишеться, це відсікає тригер з INSERT ... ON CONFLICT DO NOTHING. Але до бази ці дані долітають все одно, та ще й зайве читання для перевірки конфлікту робити доводиться. Упс №3…

Різниця за кількістю записів, що відправляються в базу, до/після включення кешування — очевидна:

Пишемо в PostgreSQL на субсвітловому: 1 host, 1 day, 1TB

А це супутнє падіння навантаження на сховище:

Пишемо в PostgreSQL на субсвітловому: 1 host, 1 day, 1TB

Разом

«Терабайт-на добу» тільки звучить страшно. Якщо ви все робите правильно, то це лише 2^40 байт / 86400 секунд = ~12.5MB/sщо тримали навіть настільні IDE-гвинти. 🙂

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

Пишемо в PostgreSQL на субсвітловому: 1 host, 1 day, 1TB

Джерело: habr.com

Додати коментар або відгук